diff --git a/modules/HardwareDrivers/PowerSupplies/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/CMakeLists.txt index d8b822ea76..ef857f3437 100644 --- a/modules/HardwareDrivers/PowerSupplies/CMakeLists.txt +++ b/modules/HardwareDrivers/PowerSupplies/CMakeLists.txt @@ -1,5 +1,6 @@ ev_add_module(DPM1000) ev_add_module(Huawei_R100040Gx) +ev_add_module(Huawei_V100R023C10) ev_add_module(UUGreenPower_UR1000X0) ev_add_module(InfyPower) ev_add_module(InfyPower_BEG1K075G) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/CHANGELOG.md b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/CHANGELOG.md new file mode 100644 index 0000000000..13b8548f5a --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +## 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": , "current": }` diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/CMakeLists.txt new file mode 100644 index 0000000000..39b20a266e --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/CMakeLists.txt @@ -0,0 +1,40 @@ +# +# 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") +add_subdirectory(fusion_charger_lib) + +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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/Huawei_V100R023C10.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/Huawei_V100R023C10.cpp new file mode 100644 index 0000000000..c54d1d07ec --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/Huawei_V100R023C10.cpp @@ -0,0 +1,254 @@ +// 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(mod->p_connector_1.get()))->base; + break; + case 1: + return &(dynamic_cast(mod->p_connector_2.get()))->base; + break; + case 2: + return &(dynamic_cast(mod->p_connector_3.get()))->base; + break; + case 3: + return &(dynamic_cast(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 get_connector_bases(Huawei_V100R023C10* mod, std::uint8_t connectors_used) { + std::vector connector_bases; + for (std::uint8_t i = 0; i < connectors_used; i++) { + connector_bases.push_back(get_connector_impl(mod, i)); + } + return connector_bases; +} + +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")); + } + + // 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); + + 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 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_config, connector_configs, log); +} + +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 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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/Huawei_V100R023C10.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/Huawei_V100R023C10.hpp new file mode 100644 index 0000000000..890dd22f26 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/Huawei_V100R023C10.hpp @@ -0,0 +1,132 @@ +// 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 + +// headers for required interface implementations +#include +#include +#include +#include + +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 +// insert your custom include headers here +#include +#include +// 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; +}; + +class Huawei_V100R023C10 : public Everest::ModuleBase { +public: + Huawei_V100R023C10() = delete; + Huawei_V100R023C10(const ModuleInfo& info, std::unique_ptr p_connector_1, + std::unique_ptr p_connector_2, + std::unique_ptr p_connector_3, + std::unique_ptr p_connector_4, + std::vector> r_board_support, + std::vector> r_isolation_monitor, + std::vector> r_carside_powermeter, + std::vector> r_over_voltage_monitor, Conf& config) : + ModuleBase(info), + 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){}; + + const std::unique_ptr p_connector_1; + const std::unique_ptr p_connector_2; + const std::unique_ptr p_connector_3; + const std::unique_ptr p_connector_4; + const std::vector> r_board_support; + const std::vector> r_isolation_monitor; + const std::vector> r_carside_powermeter; + const std::vector> 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; + + std::atomic communication_fault_raised; + std::atomic psu_not_running_raised; + + std::atomic initial_hmac_acquired; + + std::vector implementations = {p_connector_1.get(), p_connector_2.get(), + p_connector_3.get(), p_connector_4.get()}; + + enum class UpstreamVoltageSource { + IMD, + OVM, + }; + // PSU upstream voltage source + UpstreamVoltageSource upstream_voltage_source; + // 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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_1/power_supply_DCImpl.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_1/power_supply_DCImpl.cpp new file mode 100644 index 0000000000..f854bd6880 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_1/power_supply_DCImpl.cpp @@ -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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_1/power_supply_DCImpl.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_1/power_supply_DCImpl.hpp new file mode 100644 index 0000000000..087d983679 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_1/power_supply_DCImpl.hpp @@ -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 + +#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& 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& 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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_2/power_supply_DCImpl.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_2/power_supply_DCImpl.cpp new file mode 100644 index 0000000000..3b4c2b542a --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_2/power_supply_DCImpl.cpp @@ -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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_2/power_supply_DCImpl.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_2/power_supply_DCImpl.hpp new file mode 100644 index 0000000000..0e7b0e40a7 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_2/power_supply_DCImpl.hpp @@ -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 + +#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& 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& 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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_3/power_supply_DCImpl.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_3/power_supply_DCImpl.cpp new file mode 100644 index 0000000000..49eca66326 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_3/power_supply_DCImpl.cpp @@ -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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_3/power_supply_DCImpl.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_3/power_supply_DCImpl.hpp new file mode 100644 index 0000000000..95ebf4b4c2 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_3/power_supply_DCImpl.hpp @@ -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 + +#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& 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& 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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_4/power_supply_DCImpl.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_4/power_supply_DCImpl.cpp new file mode 100644 index 0000000000..171feefc21 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_4/power_supply_DCImpl.cpp @@ -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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_4/power_supply_DCImpl.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_4/power_supply_DCImpl.hpp new file mode 100644 index 0000000000..1071cbd0f5 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_4/power_supply_DCImpl.hpp @@ -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 + +#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& 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& 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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_base/base.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_base/base.cpp new file mode 100644 index 0000000000..d623468b10 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_base/base.cpp @@ -0,0 +1,405 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include "base.hpp" +#include +#include + +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& 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(); + + 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; + } + + 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(); + } + }); + + 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); + }); + } + + // 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); + }); + } + + // 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; + } + + // 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); + } + }); + } +} + +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; + } + + 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; + + 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(new_caps.max_export_current_A), config.max_export_current_A); + new_caps.max_export_power_W = std::min(static_cast(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; + + 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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_base/base.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_base/base.hpp new file mode 100644 index 0000000000..48ac165d2f --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/connector_base/base.hpp @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include "../Huawei_V100R023C10.hpp" +#include +#include +#include + +#include +#include + +namespace module { + +struct EverestConnectorConfig { + int global_connector_number; + double max_export_current_A; + double max_export_power_W; + + template 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; + } +}; + +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& 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::atomic 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 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 upstream_voltage; + std::atomic output_voltage; + std::atomic output_current; + std::atomic contactor_status; + } external_provided_data; +}; + +}; // namespace module diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/doc.rst b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/doc.rst new file mode 100644 index 0000000000..fcf88e4c9b --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/doc.rst @@ -0,0 +1,83 @@ +.. _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) + +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``. + +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 \ No newline at end of file diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/.gitignore b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/.gitignore new file mode 100644 index 0000000000..7ac6434eba --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/.gitignore @@ -0,0 +1,5 @@ +.vscode/settings.json + +build/ +.venv/ +.cache/ diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/CMakeLists.txt new file mode 100644 index 0000000000..4d1eea4bc2 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.16) +project(huawei-fusion-charger) + +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +if(BUILD_TESTS) + include(CTest) +endif() + + +if(DEFINED MODULE_NAME) + set(BUILDING_IN_EVEREST ON) +else() + set(BUILDING_IN_EVEREST OFF) +endif() + + +if(NOT BUILDING_IN_EVEREST) + message(STATUS "Not building in Everest environment, fetching MQTT-C") + include(FetchContent) + FetchContent_Declare( + MQTT-C + GIT_REPOSITORY https://github.com/LiamBindle/MQTT-C + GIT_TAG v1.1.6 + ) + FetchContent_MakeAvailable(MQTT-C) +endif() + +add_subdirectory(fusion-charger-dispenser-library) +add_subdirectory(goose-lib) +add_subdirectory(huawei-fusioncharge-driver) +add_subdirectory(modbus-server) +add_subdirectory(log) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/.gitignore b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/.gitignore new file mode 100644 index 0000000000..7ac6434eba --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/.gitignore @@ -0,0 +1,5 @@ +.vscode/settings.json + +build/ +.venv/ +.cache/ diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/CMakeLists.txt new file mode 100644 index 0000000000..a84509af20 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/CMakeLists.txt @@ -0,0 +1,21 @@ +if(POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) +endif() + +file(GLOB LIB_SOURCES lib/*.cpp) +add_library(fusion_charger_dispenser STATIC ${LIB_SOURCES}) +ev_register_library_target(fusion_charger_dispenser) +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) + +option(BUILD_ACCEPTANCE_TESTS "Build acceptance tests" OFF) +if(EVEREST_CORE_BUILD_TESTING) + add_subdirectory(tests) + + if (BUILD_ACCEPTANCE_TESTS) + add_subdirectory(user-acceptance-tests) + endif() + +endif() + +add_subdirectory(power_stack_mock) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/README.md b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/README.md new file mode 100644 index 0000000000..616c393ab5 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/README.md @@ -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 \ No newline at end of file diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/callbacks.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/callbacks.hpp new file mode 100644 index 0000000000..f37526e90b --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/callbacks.hpp @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include + +using ElectronicLockStatus = + fusion_charger::modbus_driver::raw_registers::CollectedConnectorRegisters::ElectronicLockStatus; +using ContactorStatus = fusion_charger::modbus_driver::raw_registers::CollectedConnectorRegisters::ContactorStatus; + +struct ConnectorCallbacks { + std::function connector_upstream_voltage; + std::function output_voltage; + std::function output_current; + std::function contactor_status; + std::function electronic_lock_status; +}; diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/configuration.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/configuration.hpp new file mode 100644 index 0000000000..d5bef4b03e --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/configuration.hpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once +#include + +#include +#include + +#include "callbacks.hpp" +#include "fusion_charger/modbus/registers/raw.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_config; + + std::chrono::milliseconds module_placeholder_allocation_timeout; +}; + +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; +}; diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/connector.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/connector.hpp new file mode 100644 index 0000000000..7b33d7e2ff --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/connector.hpp @@ -0,0 +1,290 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include + +#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 new_state, std::optional new_mode_phase); + +public: + struct Callbacks { + // Called only when the state changes + std::optional> state_transition; + // Called only when the mode phase changes + std::optional> mode_phase_transition; + // Called when either state or mode phase changes + std::optional> 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, std::uint16_t local_connector_number, DispenserConfig dispenser_config, + logs::LogIntf log); + 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 get_hmac_key(); + + /** + * @brief Reset all stored PSU capabilities from our registers + */ + void reset_psu_capabilities(); + +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 eth_interface; + ConnectorGooseSender goose_sender; + ConnectorRegistersConfig connector_registers_config; + ConnectorRegisters connector_registers; + + std::optional rated_output_power_psu; // in kW, set by register callback + std::optional max_rated_psu_voltage; // in V, set by register callback + std::optional max_rated_psu_current; // in A, set by register callback + + 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 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(); +}; diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/connector_goose_sender.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/connector_goose_sender.hpp new file mode 100644 index 0000000000..1f3edb50ea --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/connector_goose_sender.hpp @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include +#include +#include + +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> 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 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 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 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); +}; diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/dispenser.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/dispenser.hpp new file mode 100644 index 0000000000..137580676d --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/dispenser.hpp @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "configuration.hpp" +#include "connector.hpp" +#include "connector_goose_sender.hpp" +#include "state.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 ErrorEventSet; + +class Dispenser { +private: + std::vector> connectors; + logs::LogIntf log; + + std::atomic psu_communication_state = DispenserPsuCommunicationState::UNINITIALIZED; + + DispenserConfig dispenser_config; + std::vector connector_configs; + + goose_ethernet::EthernetInterface eth_interface; + + std::atomic stop_charge_reason = StopChargeReason::INSULATION_FAULT; + + std::optional modbus_socket; + std::optional modbus_event_loop_thread; + std::optional modbus_unsolicitated_event_thread; + std::optional goose_receiver_thread; + + std::shared_ptr transport; + + std::optional> openssl_data; + std::shared_ptr protocol; + std::shared_ptr pcl; + std::optional server; + std::optional registry; + + std::optional dispenser_registers; + std::optional psu_registers; + std::optional error_registers; + + ErrorEventSet raised_errors = {}; + std::mutex raised_error_mutex; + + std::optional psu_running_mode = std::nullopt; + + const int MAX_NUMBER_OF_CONNECTORS = 4; + + // 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(); + +public: + Dispenser(DispenserConfig dispenser_config, std::vector 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 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]; + } +}; diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/state.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/state.hpp new file mode 100644 index 0000000000..0520fbeec4 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/state.hpp @@ -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, +}; diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/tls_util.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/tls_util.hpp new file mode 100644 index 0000000000..4843d3a985 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/include/tls_util.hpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include + +#include + +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 init_mutual_tls_client(int socket, MutualTlsClientConfig config); + +struct MutualTlsServerConfig { + std::string client_ca; + + std::string server_cert; + std::string server_key; +}; + +std::tuple init_mutual_tls_server(int socket, MutualTlsServerConfig config); + +void free_ssl(std::tuple ssl); + +} // namespace tls_util diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/lib/connector.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/lib/connector.cpp new file mode 100644 index 0000000000..25d185f35b --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/lib/connector.cpp @@ -0,0 +1,527 @@ +// 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 new_state, std::optional 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 lock(mutex); + return current_state; +} + +ModePhase ConnectorFSM::get_mode_phase() { + std::lock_guard lock(mutex); + return current_mode_phase; +} + +void ConnectorFSM::on_car_connected() { + std::lock_guard lock(mutex); + + if (current_state == States::CarDisconnected) { + transition(States::NoKeyYet, std::nullopt); + } +} + +void ConnectorFSM::on_car_disconnected() { + std::lock_guard lock(mutex); + + if (current_state != States::CarDisconnected) { + transition(States::CarDisconnected, std::nullopt); + } +} + +void ConnectorFSM::on_mode_phase_change(ModePhase mode_phase) { + std::lock_guard 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 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 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, std::uint16_t local_connector_number, + DispenserConfig dispenser_config, logs::LogIntf log) : + connector_config(connector_config), + local_connector_number(local_connector_number), + dispenser_config(dispenser_config), + eth_interface(std::make_shared(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; + 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) { +} + +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 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(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(); +} + +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 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 mac_address) { + this->goose_sender.on_new_mac_address(mac_address); + this->send_needed_goose_frame(); +} + +std::vector Connector::get_hmac_key() { + const std::uint8_t* hmac_key = connector_registers.hmac_key.get_value(); // pointer to private memory + return std::vector(hmac_key, hmac_key + connector_registers.hmac_key.get_size()); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/lib/connector_goose_sender.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/lib/connector_goose_sender.cpp new file mode 100644 index 0000000000..43138b2106 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/lib/connector_goose_sender.cpp @@ -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 intf, bool secure, + logs::LogIntf log) : + secure(secure), + logs(log), + goose_sender(std::chrono::milliseconds(1000), + std::vector{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 hmac_key) { + this->hmac_key = hmac_key; +} + +void ConnectorGooseSender::on_new_mac_address(std::vector 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 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(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(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); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/lib/dispenser.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/lib/dispenser.cpp new file mode 100644 index 0000000000..ec668d5fa7 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/lib/dispenser.cpp @@ -0,0 +1,471 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include "dispenser.hpp" + +#include +#include +#include + +#include + +using namespace fusion_charger::modbus_driver::raw_registers; +using namespace fusion_charger::modbus_driver; +using namespace fusion_charger::modbus_extensions; + +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::this_thread::sleep_for(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 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)); + } + + // 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 = std::make_shared( + connector_configs[local_connector_number - 1], local_connector_number, dispenser_config, log); + 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(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_socket.value()); + } + + protocol = std::make_shared(transport, 0, 0); + pcl = std::make_shared(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.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(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 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(); }); +} + +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 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(); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/lib/tls_util.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/lib/tls_util.cpp new file mode 100644 index 0000000000..17cbe18461 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/lib/tls_util.cpp @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include "tls_util.hpp" + +#include +#include +#include + +#include +#include +#include + +using namespace tls_util; + +std::tuple 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 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_free(std::get<0>(ssl)); + SSL_CTX_free(std::get<1>(ssl)); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/CMakeLists.txt new file mode 100644 index 0000000000..720524d29e --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/CMakeLists.txt @@ -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) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/include/power_stack_mock/power_stack_mock.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/include/power_stack_mock/power_stack_mock.hpp new file mode 100644 index 0000000000..8f421d4c96 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/include/power_stack_mock/power_stack_mock.hpp @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#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_config; + + std::function power_requirement_request_callback; + std::function 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 get_unsolicited_report_data(std::uint16_t start_address, std::uint16_t quantity); + std::vector read_registers(std::uint16_t start_address, std::uint16_t quantity); + void write_registers(std::uint16_t start_address, const std::vector& 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); + + std::optional + 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 + 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); + 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); + +private: + PowerStackMockConfig config; + goose_ethernet::EthernetInterface eth; + + std::unordered_map + 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> openssl_data; + std::shared_ptr transport; + std::shared_ptr protocol; + std::shared_ptr pas; + modbus_server::client::ModbusClient client; + + std::optional modbus_event_loop; + std::vector read_and_check(std::uint16_t start_address, std::uint16_t quantity); + + std::atomic running = true; + std::atomic answer_module_placeholder_allocation = true; + std::thread goose_receiver_thread; + + std::map> + last_power_requirement_requests; + std::map> power_requirement_request_counter = {}; + std::map> last_stop_charge_requests; + std::map> 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 registers); + + PowerStackMock(int client_socket, std::optional> openssl_data, + std::shared_ptr transport, PowerStackMockConfig config); +}; diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/include/power_stack_mock/util.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/include/power_stack_mock/util.hpp new file mode 100644 index 0000000000..642b13e31a --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/include/power_stack_mock/util.hpp @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once +#include + +#include +#include + +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 vec); +std::vector float_to_uint16_vec(float value); + +double uint16_vec_to_double(std::vector vec); + +std::uint32_t uint16_vec_to_uint32(std::vector vec); + +} // namespace test_utils + +} // namespace user_acceptance_tests diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/lib/power_stack_mock.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/lib/power_stack_mock.cpp new file mode 100644 index 0000000000..e677394deb --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/lib/power_stack_mock.cpp @@ -0,0 +1,555 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include "power_stack_mock/power_stack_mock.hpp" + +#include +#include +#include + +#include +#include +#include +#include + +#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> openssl_data, + std::shared_ptr transport, PowerStackMockConfig config) : + client_sock(client_socket), + openssl_data(openssl_data), + transport(transport), + protocol(std::make_shared(transport)), + pas(std::make_shared(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(ssl), + config); + } + + return new PowerStackMock(client_socket, std::nullopt, + std::make_shared(client_socket), config); +} + +std::vector 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 +parse_goose_frame(const goose_ethernet::EthernetFrame& frame, + std::optional> hmac_key = std::nullopt) { + // if hmac_key is provided, only allow secure goose frames + if (hmac_key.has_value()) { + return std::make_unique(frame, hmac_key.value()); + } + + try { + return std::make_unique(frame); + } catch (std::exception& e) { + return std::make_unique(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 frame; + try { + if (config.verify_hmac) { + frame = parse_goose_frame(packet, std::vector(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 response_frame; + if (config.enable_hmac) { + response_frame = std::make_unique(); + } else { + response_frame = std::make_unique(); + } + 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 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 guard(pdu_registers_mutex); + + for (auto& segment : segments) { + pdu_registers.insert_or_assign(segment.registers_start, segment); + } +} + +std::vector PowerStackMock::get_unsolicited_report_data(std::uint16_t start_address, + std::uint16_t quantity) { + std::lock_guard guard(pdu_registers_mutex); + + if (pdu_registers.find(start_address) == pdu_registers.end()) { + return {}; + } + + auto segment = pdu_registers.at(start_address); + + std::vector 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 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& values) { + client.write_multiple_registers(start_address, values); +} + +void PowerStackMock::set_psu_running_mode(PSURunningMode mode) { + client.write_single_register(0x2006, static_cast(mode)); +} + +void PowerStackMock::send_mac_address() { + auto address = eth.get_mac_address(); + + auto mac = std::vector(); + 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 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(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(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(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(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(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(offset_from_connector_number(local_connector_number)), + float_to_uint16_vec(power)); +} + +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 +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 +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(client.read_holding_registers( + 0x110D + static_cast(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(offset_from_connector_number(local_connector_number)), 2); + + return registers_to_float(read_result); +} + +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 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 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(offset_from_connector_number(local_connector_number)), 2)); + + auto output_current = registers_to_float(client.read_holding_registers( + 0x1109 + static_cast(offset_from_connector_number(local_connector_number)), 2)); + + auto contactors_upstream_voltage = registers_to_float(client.read_holding_registers( + 0x1113 + static_cast(offset_from_connector_number(local_connector_number)), 2)); + + auto contactors_status = (ContactorStatus)client.read_holding_registers( + 0x1154 + static_cast(offset_from_connector_number(local_connector_number)), 1)[0]; + + auto electronic_lock_status = (ElectronicLockStatus)client.read_holding_registers( + 0x1156 + static_cast(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 registers) { + std::uint16_t high_byte = registers[0]; + std::uint16_t low_byte = registers[1]; + std::uint32_t combined_bytes = (static_cast(high_byte) << 16) | low_byte; + float result = *reinterpret_cast(&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; +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/lib/util.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/lib/util.cpp new file mode 100644 index 0000000000..4887e4a16e --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/lib/util.cpp @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include "power_stack_mock/util.hpp" + +#include +#include +#include +#include +#include +#include +#include + +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 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 float_to_uint16_vec(float value) { + std::uint8_t* v = reinterpret_cast(&value); + std::uint8_t v0[4] = {v[3], v[2], v[1], v[0]}; + + std::vector v1; + v1.push_back(static_cast(v0[0] << 8 | v0[1])); + v1.push_back(static_cast(v0[2] << 8 | v0[3])); + + return v1; +} + +double uint16_vec_to_double(std::vector vec) { + std::uint8_t v0[8] = { + static_cast(vec[3] & 0xFF), static_cast(vec[3] >> 8), + static_cast(vec[2] & 0xFF), static_cast(vec[2] >> 8), + static_cast(vec[1] & 0xFF), static_cast(vec[1] >> 8), + static_cast(vec[0] & 0xFF), static_cast(vec[0] >> 8), + }; + + return *((double*)v0); +} + +std::uint32_t uint16_vec_to_uint32(std::vector vec) { + std::uint16_t v0[2] = { + static_cast(vec[1]), + static_cast(vec[0]), + }; + + return *reinterpret_cast(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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/src/main.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/src/main.cpp new file mode 100644 index 0000000000..e0f9dfed1c --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/src/main.cpp @@ -0,0 +1,315 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#define MOCK_REGULAR_ERRORS 0 + +#include + +#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 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 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 mqtt_client; + std::string base_topic; + + std::unordered_map publish_deadline; + std::unordered_map 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 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 + + 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); + } + } else { + not_sending_capabilities_counter++; + } + } + + std::array 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 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 active_mocks; // counts all active mocks +std::condition_variable mocks_cv; + +std::vector mock_threads; + +void on_socket(int socket, void* context) { + PowerStackMockConfig* config = (PowerStackMockConfig*)context; + + printf("New client\n"); + + std::lock_guard lock(mocks_mutex); + active_mocks++; + + auto mock = std::make_unique(std::unique_ptr(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 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()); + } + + printf("Waiting for connections on port %d\n", config.port); + + std::shared_ptr 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(std::make_shared(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 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; +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/src/mqtt.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/src/mqtt.cpp new file mode 100644 index 0000000000..34b063dd04 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/src/mqtt.cpp @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include "mqtt.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +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 lock(publish_queue_mutex); + publish_queue.push_back({topic, message}); +} + +void MqttClient::background_thread_fn() { + while (!stop_flag) { + std::vector queue_copy; + + { + std::lock_guard 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; +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/src/mqtt.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/src/mqtt.hpp new file mode 100644 index 0000000000..b399fc37f9 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/src/mqtt.hpp @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include +#include +#include +#include + +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 publish_queue; + + static int open_socket(std::string host, std::string port); + + void background_thread_fn(); +}; diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/src/socket_server.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/src/socket_server.cpp new file mode 100644 index 0000000000..6b3e74ebc2 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/src/socket_server.cpp @@ -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 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); + } +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/src/socket_server.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/src/socket_server.hpp new file mode 100644 index 0000000000..149b7ef82f --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/power_stack_mock/src/socket_server.hpp @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include +#include +#include + +#include +#include +#include +#include + +// Simple socket server that accepts (unlimited) connections and calls a +// callback on each accepted client. +class SocketServer { + void* context; + std::function on_client; // called when a client connects + std::thread server_thread; + int port; + int server_sock; + +public: + SocketServer(int port, void* context, std::function on_client); + ~SocketServer(); + +private: + void main(); +}; diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/tests/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/tests/CMakeLists.txt new file mode 100644 index 0000000000..6571ddf461 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/tests/CMakeLists.txt @@ -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) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/tests/connector_fsm.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/tests/connector_fsm.cpp new file mode 100644 index 0000000000..d2db040d50 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/tests/connector_fsm.cpp @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include + +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(); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/.gitignore b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/.gitignore new file mode 100644 index 0000000000..c4a02b2b13 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/.gitignore @@ -0,0 +1,2 @@ +test_certificates/** +!test_certificates/generate.sh diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/CMakeLists.txt new file mode 100644 index 0000000000..20568f924c --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/CMakeLists.txt @@ -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}) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/include/user_acceptance_tests/dispenser_test_fixture.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/include/user_acceptance_tests/dispenser_test_fixture.hpp new file mode 100644 index 0000000000..bb4d98e2df --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/include/user_acceptance_tests/dispenser_test_fixture.hpp @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include + +#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 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 connector_configs; + + std::atomic dispenser_connector_upstream_voltage; + std::atomic dispenser_output_voltage; + std::atomic dispesner_output_current; + std::atomic dispenser_contactor_status; + std::atomic dispenser_electronic_lock_status; + + ConnectorCallbacks connector_callbacks; + std::shared_ptr dispenser; // Move this below 'dispenser_config' + // + PowerStackMockConfig power_stack_mock_config; + std::shared_ptr 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(); + std::optional 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(); + std::optional 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 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 get_stop_request_counter(); + void disconnect_car(std::uint16_t local_connector_number); + + void assert_working_status(std::array expected_status); + void assert_requirement_type(std::array, 4> expected_types); + + void assert_stop_request_counter_greater_or_equal(std::array expected); +}; + +} // namespace dispenser_fixture + +} // namespace user_acceptance_tests diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/lib/dispenser_test_fixture.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/lib/dispenser_test_fixture.cpp new file mode 100644 index 0000000000..2e2a2ff5f7 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/lib/dispenser_test_fixture.cpp @@ -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 + +#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_config, connector_configs); + dispenser->start(); + power_stack_mock = std::shared_ptr(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 DispenserWithTlsTest::connector() { + return dispenser->get_connector(local_connector_number); +} + +std::optional +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 DispenserWithoutTlsTest::connector() { + return dispenser->get_connector(local_connector_number); +} + +std::optional +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 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 DispenserWithMultipleConnectors::get_stop_request_counter() { + auto result = std::array(); + 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 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, 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 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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/src/multiple_connectors.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/src/multiple_connectors.cpp new file mode 100644 index 0000000000..198b61ae85 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/src/multiple_connectors.cpp @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#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); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/src/with_tls.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/src/with_tls.cpp new file mode 100644 index 0000000000..38c93c6d3a --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/src/with_tls.cpp @@ -0,0 +1,591 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include +#include + +#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(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(); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/src/without_tls.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/src/without_tls.cpp new file mode 100644 index 0000000000..cb6efee354 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/src/without_tls.cpp @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#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); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/test_certificates/generate.sh b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/test_certificates/generate.sh new file mode 100755 index 0000000000..3267b2c150 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/fusion-charger-dispenser-library/user-acceptance-tests/test_certificates/generate.sh @@ -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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/.gitignore b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/.gitignore new file mode 100644 index 0000000000..7ac6434eba --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/.gitignore @@ -0,0 +1,5 @@ +.vscode/settings.json + +build/ +.venv/ +.cache/ diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/CMakeLists.txt new file mode 100644 index 0000000000..1122e069da --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/CMakeLists.txt @@ -0,0 +1,10 @@ +if(POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) +endif() + +if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") + set(MACOSX TRUE) +endif() + +add_subdirectory(libs) +add_subdirectory(examples) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/README.md b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/README.md new file mode 100644 index 0000000000..5e7a372411 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/README.md @@ -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-core. diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/examples/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/examples/CMakeLists.txt new file mode 100644 index 0000000000..c8641858ae --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/examples/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(ethernet_frame ethernet_frame.cpp) +target_link_libraries(ethernet_frame goose-ethernet) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/examples/ethernet_frame.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/examples/ethernet_frame.cpp new file mode 100644 index 0000000000..f953e99b59 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/examples/ethernet_frame.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include +#include +#include +#include + +int main(int argc, char** argv) { + if (argc != 2) { + fprintf(stderr, "Usage: %s \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; +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/CMakeLists.txt new file mode 100644 index 0000000000..ea961d2b2a --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(goose-ethernet) +add_subdirectory(goose) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/CMakeLists.txt new file mode 100644 index 0000000000..e912bf19e5 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/CMakeLists.txt @@ -0,0 +1,20 @@ +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}) +ev_register_library_target(goose-ethernet) +target_include_directories(goose-ethernet PUBLIC include) + +if (MACOSX) + target_link_libraries(goose-ethernet PRIVATE pcap) +endif() + + +if(EVEREST_CORE_BUILD_TESTING) + add_subdirectory(tests) +endif() diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/include/goose-ethernet/driver.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/include/goose-ethernet/driver.hpp new file mode 100644 index 0000000000..6cd4d18e26 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/include/goose-ethernet/driver.hpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include +#include + +#ifdef __APPLE__ +#include +#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> 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 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> receive_packet_raw() override; + +public: + EthernetInterface(const char* interface_name); + ~EthernetInterface(); + + const std::uint8_t* get_mac_address() const override; +}; + +}; // namespace goose_ethernet diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/include/goose-ethernet/frame.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/include/goose-ethernet/frame.hpp new file mode 100644 index 0000000000..a5a42c00b2 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/include/goose-ethernet/frame.hpp @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include +#include +#include +#include + +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 eth_802_1q_tag; // already in system byte order + std::uint16_t ethertype; + std::vector 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& data); + + /** + * @brief Serialize the Ethernet frame with header and payload, without crc + * + * @return std::vector Serialized Ethernet frame + * @throws SerializeError if the frame is invalid; e.g. payload is too + * long/short + */ + std::vector 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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/src/driver.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/src/driver.cpp new file mode 100644 index 0000000000..276734ebe6 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/src/driver.cpp @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +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 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()); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/src/driver.linux.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/src/driver.linux.cpp new file mode 100644 index 0000000000..6084317de4 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/src/driver.linux.cpp @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#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> EthernetInterface::receive_packet_raw() { + std::vector 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; +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/src/driver.mac.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/src/driver.mac.cpp new file mode 100644 index 0000000000..2de3f64062 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/src/driver.mac.cpp @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include +#include +#include + +#include +#include +#include + +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(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 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(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; +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/src/frame.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/src/frame.cpp new file mode 100644 index 0000000000..0d9f9fd1a3 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/src/frame.cpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include +#include + +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(data + 18, data + size); + } else { + eth_802_1q_tag = std::nullopt; + payload = std::vector(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& data) : EthernetFrame(data.data(), data.size()) { +} + +std::vector 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 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; +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/tests/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/tests/CMakeLists.txt new file mode 100644 index 0000000000..aeba48e1b5 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/tests/CMakeLists.txt @@ -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) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/tests/frame.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/tests/frame.cpp new file mode 100644 index 0000000000..5e40045fc8 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose-ethernet/tests/frame.cpp @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include + +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); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/CMakeLists.txt new file mode 100644 index 0000000000..decde5d682 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/CMakeLists.txt @@ -0,0 +1,12 @@ +file(GLOB_RECURSE GOOSE_SOURCES "src/*.cpp") + +find_package(OpenSSL REQUIRED) + +add_library(goose STATIC ${GOOSE_SOURCES}) +ev_register_library_target(goose) +target_include_directories(goose PUBLIC include) +target_link_libraries(goose PUBLIC goose-ethernet OpenSSL::SSL OpenSSL::Crypto Huawei::FusionCharger::LogInterface) + +if(EVEREST_CORE_BUILD_TESTING) + add_subdirectory(tests) +endif() diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/include/goose/ber.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/include/goose/ber.hpp new file mode 100644 index 0000000000..84d4418438 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/include/goose/ber.hpp @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include +#include +#include + +namespace goose { +namespace frame { +namespace ber { + +template std::vector encode_be(T value) { + std::vector result; + for (size_t i = 0; i < sizeof(T); i++) { + result.push_back((value >> (8 * (sizeof(T) - i - 1))) & 0xFF); + } + return result; +} + +template T decode_be(const std::vector& 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 value; + + BEREntry() = default; + BEREntry(std::uint8_t tag, std::vector 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* input); + + // Encode the entry into a vector of bytes and append it to the payload + void add(const BEREntry& entry); + + std::vector encode() const; +}; + +template 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& input, std::optional 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(entry.value); + tag = entry.tag; + } + + std::vector 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& input, std::optional expected_tag, + std::optional 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 encode() const { + return BEREntry(tag, std::vector(data.begin(), data.end())).encode(); + } +}; + +} // namespace ber +} // namespace frame +} // namespace goose diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/include/goose/frame.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/include/goose/frame.hpp new file mode 100644 index 0000000000..005f90bf38 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/include/goose/frame.hpp @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include +#include +#include +#include + +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& raw); + + std::vector 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 apdu_entries; + + GoosePDU() = default; + GoosePDU(const std::vector& pdu); + + std::vector 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> 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 hmac_key) const; +}; + +} // namespace frame +} // namespace goose diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/include/goose/sender.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/include/goose/sender.hpp new file mode 100644 index 0000000000..94284bb5dd --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/include/goose/sender.hpp @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#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 hmac_key; + +public: + SendPacketSecure(goose::frame::SecureGooseFrame frame, std::vector 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 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 ts; + + size_t current_ts_index = 0; + + std::optional thread; + bool stop_flag = false; + std::atomic has_new_package = false; + std::shared_ptr intf; + + std::optional> 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 ts, + std::shared_ptr 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 packet) override; + void run() override; + void start() override; + void stop() override; +}; + +}; // namespace sender +} // namespace goose diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/src/ber.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/src/ber.cpp new file mode 100644 index 0000000000..c1abfa2996 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/src/ber.cpp @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +namespace goose { +namespace frame { +namespace ber { + +BEREntry::BEREntry(std::vector* 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 BEREntry::encode() const { + std::vector 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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/src/frame.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/src/frame.cpp new file mode 100644 index 0000000000..71131dabb9 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/src/frame.cpp @@ -0,0 +1,448 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include +#include + +using namespace goose::frame; + +GooseTimestamp::GooseTimestamp(const std::vector& 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 GooseTimestamp::encode() const { + std::vector 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(seconds) * 1000 + (static_cast(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(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& 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 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 st_num_entry(root_value, 0x85); + st_num = st_num_entry.data; + + // sq_num + ber::PrimitiveBEREntry sq_num_entry(root_value, 0x86); + sq_num = sq_num_entry.data; + + // simulation + ber::PrimitiveBEREntry simulation_entry(root_value, 0x87); + simulation = simulation_entry.data; + + // conf_rev + ber::PrimitiveBEREntry conf_rev_entry(root_value, 0x88); + conf_rev = conf_rev_entry.data; + + // ndsCom + ber::PrimitiveBEREntry ndsCom_entry(root_value, 0x89); + ndsCom = ndsCom_entry.data; + + // apdu count + ber::PrimitiveBEREntry 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 GoosePDU::serialize() const { + goose::frame::ber::BEREntry root; + root.tag = 0x61; + root.value = std::vector(); + + goose::frame::ber::BEREntry go_cb_ref_entry; + go_cb_ref_entry.tag = 0x80; + go_cb_ref_entry.value = + std::vector(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(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(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{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{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(); + 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(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 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> 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 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 hmac = + std::vector(ethernet_frame.payload.data() + length + extended_length - 32, + ethernet_frame.payload.data() + length + extended_length); + + std::vector 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 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 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 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 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; +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/src/sender.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/src/sender.cpp new file mode 100644 index 0000000000..65f351aa89 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/src/sender.cpp @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +using namespace goose::sender; + +Sender::Sender(std::chrono::milliseconds t0, std::vector ts, + std::shared_ptr 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(packet)); +} + +void Sender::send(std::unique_ptr packet) { + std::lock_guard 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 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(); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/CMakeLists.txt new file mode 100644 index 0000000000..79891f6f37 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/CMakeLists.txt @@ -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") diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/ber.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/ber.cpp new file mode 100644 index 0000000000..5efe4e4301 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/ber.cpp @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include + +TEST(BEREntry, encode_small_length) { + goose::frame::ber::BEREntry entry; + entry.tag = 0x01; + entry.value = {0x03, 0x04}; + + std::vector 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 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 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 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 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 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 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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/goose_frame.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/goose_frame.cpp new file mode 100644 index 0000000000..5fa48acf02 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/goose_frame.cpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include + +#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); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/goose_pdu.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/goose_pdu.cpp new file mode 100644 index 0000000000..861dcc65c6 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/goose_pdu.cpp @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include + +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(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(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); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/goose_timestamp.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/goose_timestamp.cpp new file mode 100644 index 0000000000..bc90d28e10 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/goose_timestamp.cpp @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include +#include +#include + +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 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(now.time_since_epoch()).count(); + + auto diff = now_ms - timestamp.to_ms(); + + ASSERT_LT(abs(diff), 10); +} + +TEST(GooseTimestamp, parsing_invalid_size) { + std::vector raw = {0, 0, 0, 1, 0x80, 0, 0, 0x0a, 0xff}; + ASSERT_THROW(GooseTimestamp timestamp(raw), std::runtime_error); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/hex_to_vec.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/hex_to_vec.cpp new file mode 100644 index 0000000000..af16a55940 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/hex_to_vec.cpp @@ -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]); + } +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/hex_to_vec.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/hex_to_vec.hpp new file mode 100644 index 0000000000..0add794df0 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/hex_to_vec.hpp @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include +#include + +void hex_to_vec(const char* hex_data, std::uint8_t* data, size_t data_size); diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/secure_goose_frame.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/secure_goose_frame.cpp new file mode 100644 index 0000000000..da61e67f72 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/secure_goose_frame.cpp @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include + +#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(key, key + sizeof(key))); + + auto deserialized = goose::frame::SecureGooseFrame(serialized, std::vector(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(key, key + sizeof(key))); + + serialized.payload[serialized.payload.size() - 1]++; + ASSERT_THROW(goose::frame::SecureGooseFrame frame(serialized, std::vector(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(key, key + sizeof(key))); + + serialized.payload[65]++; // edit payload + + ASSERT_THROW(goose::frame::SecureGooseFrame frame(serialized, std::vector(key, key + sizeof(key))), + std::runtime_error); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/sender.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/sender.cpp new file mode 100644 index 0000000000..4507ba9bdd --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/goose-lib/libs/goose/tests/sender.cpp @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include + +class DummyEthernetInterface : public goose_ethernet::EthernetInterfaceIntf { + std::function send_callback; + std::function()> receive_callback; + +public: + DummyEthernetInterface(std::function send_callback, + std::function()> 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> 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(60)); + + DummyEthernetInterface intf( + [&](const std::uint8_t* packet, size_t size) { + ASSERT_EQ(dummy_frame.serialize().size(), size); + send_counter++; + }, + [&]() -> std::vector { + 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 received_times; + std::vector received_st_nums; + std::vector received_sq_nums; + + auto intf = std::make_shared( + [&](const std::uint8_t* data, size_t size) { + received_times.push_back(std::chrono::duration_cast( + 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 { 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 received_times; + std::vector received_st_nums; + std::vector received_sq_nums; + + auto intf = std::make_shared( + [&](const std::uint8_t* data, size_t size) { + received_times.push_back(std::chrono::duration_cast( + 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 { return {}; }); + + auto t0 = std::chrono::milliseconds(10); + auto ts = std::vector{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) +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/.gitignore b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/.gitignore new file mode 100644 index 0000000000..7ac6434eba --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/.gitignore @@ -0,0 +1,5 @@ +.vscode/settings.json + +build/ +.venv/ +.cache/ diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/CMakeLists.txt new file mode 100644 index 0000000000..c70246828c --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/CMakeLists.txt @@ -0,0 +1,6 @@ +if(POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) +endif() + +add_subdirectory(libs) +add_subdirectory(examples) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/README.md b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/README.md new file mode 100644 index 0000000000..bf06461352 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/README.md @@ -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-core. + +## 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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/examples/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/examples/CMakeLists.txt new file mode 100644 index 0000000000..6b83b95d74 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/examples/CMakeLists.txt @@ -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) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/examples/real_hw_first_test.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/examples/real_hw_first_test.cpp new file mode 100644 index 0000000000..6fee12c1d9 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/examples/real_hw_first_test.cpp @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 \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(sock); + auto protocol = std::make_shared(transport); + auto pcl = std::make_shared(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(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); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/examples/unsolicitated_decoder.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/examples/unsolicitated_decoder.cpp new file mode 100644 index 0000000000..50761923f5 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/examples/unsolicitated_decoder.cpp @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include +#include + +int main(int argc, char** argv) { + if (argc != 2) { + printf("Usage: %s \n", argv[0]); + return 1; + } + + std::string data = std::string(argv[1]); + std::vector 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; +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/CMakeLists.txt new file mode 100644 index 0000000000..af737a79b3 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/CMakeLists.txt @@ -0,0 +1,3 @@ +add_subdirectory(fusion_charger_modbus_extensions) +add_subdirectory(fusion_charger_modbus_driver) +add_subdirectory(fusion_charger_goose_driver) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_goose_driver/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_goose_driver/CMakeLists.txt new file mode 100644 index 0000000000..ec99683742 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_goose_driver/CMakeLists.txt @@ -0,0 +1,15 @@ +file(GLOB SOURCES "src/*.cpp") + +add_library(fusion_charger_goose_driver STATIC ${SOURCES}) +ev_register_library_target(fusion_charger_goose_driver) +target_include_directories(fusion_charger_goose_driver PUBLIC include) +target_link_libraries(fusion_charger_goose_driver PUBLIC goose) + +if(BUILD_TESTING) + 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() diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_goose_driver/include/fusion_charger/goose/driver_utils.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_goose_driver/include/fusion_charger/goose/driver_utils.hpp new file mode 100644 index 0000000000..2fb4b0317b --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_goose_driver/include/fusion_charger/goose/driver_utils.hpp @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include + +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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_goose_driver/include/fusion_charger/goose/power_request.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_goose_driver/include/fusion_charger/goose/power_request.hpp new file mode 100644 index 0000000000..1c73b78d1a --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_goose_driver/include/fusion_charger/goose/power_request.hpp @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include + +#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(requirement_type)); + pdu.apdu_entries[3] = utils::make_u16(static_cast(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(utils::expect_u16(input.apdu_entries[2])); + mode = static_cast(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(requirement_type)); + pdu.apdu_entries[3] = utils::make_u16(static_cast(result)); + pdu.apdu_entries[4] = utils::make_u16(static_cast(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(utils::expect_u16(input.apdu_entries[2])); + result = static_cast(utils::expect_u16(input.apdu_entries[3])); + mode = static_cast(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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_goose_driver/include/fusion_charger/goose/stop_charge_request.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_goose_driver/include/fusion_charger/goose/stop_charge_request.hpp new file mode 100644 index 0000000000..63f9a1eefc --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_goose_driver/include/fusion_charger/goose/stop_charge_request.hpp @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include + +#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(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(utils::expect_u16(input.apdu_entries[2])); + + return input.timestamp; + } +}; + +}; // namespace goose +}; // namespace fusion_charger diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_goose_driver/src/driver_utils.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_goose_driver/src/driver_utils.cpp new file mode 100644 index 0000000000..9653a9e082 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_goose_driver/src/driver_utils.cpp @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include +#include + +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(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(entry.value); + return *reinterpret_cast(&val_u32); +} + +}; // namespace utils +}; // namespace goose +}; // namespace fusion_charger diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_goose_driver/tests/real_world_tests.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_goose_driver/tests/real_world_tests.cpp new file mode 100644 index 0000000000..e1021b580e --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_goose_driver/tests/real_world_tests.cpp @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include +#include +#include + +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(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(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); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/CMakeLists.txt new file mode 100644 index 0000000000..46b42c861e --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/CMakeLists.txt @@ -0,0 +1,15 @@ +file(GLOB SOURCES "src/*.cpp") + +add_library(fusion_charger_modbus_driver STATIC ${SOURCES}) +ev_register_library_target(fusion_charger_modbus_driver) +target_include_directories(fusion_charger_modbus_driver PUBLIC include) +target_link_libraries(fusion_charger_modbus_driver PUBLIC modbus-server fusion_charger_modbus_extensions) + +if(BUILD_TESTING) + 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() diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/include/fusion_charger/modbus/registers/connector.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/include/fusion_charger/modbus/registers/connector.hpp new file mode 100644 index 0000000000..519dd77f12 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/include/fusion_charger/modbus/registers/connector.hpp @@ -0,0 +1,156 @@ +// 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 get_contactor_upstream_voltage; + std::function get_output_voltage; + std::function get_output_current; + std::function get_contactor_status; + std::function get_electronic_lock_status; +}; + +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 total_energy_charged; + DataProviderHolding connector_type; + // Reg 0x1105 + DataProviderHolding maximum_rated_charge_current; + DataProviderCallbacks output_voltage; + DataProviderCallbacks output_current; + DataProviderHoldingUnsolicitatedReportCallback working_status; + DataProviderHoldingUnsolicitatedReportCallback connection_status; + DataProviderHolding connector_no; // 1-12 + DataProviderCallbacks contactor_upstream_voltage; + DataProviderMemoryHolding<6> mac_address; + // "Status of contactors (DC+, DC-)" 0 off, 1 on + DataProviderCallbacksUnsolicitated contactor_status; + // unlocked = 0, locked = 1 + DataProviderCallbacksUnsolicitated electronic_lock_status; + DataProviderUnsolicitatedEvent charging_event_connector; // todo ?? + + // from dispenser + DataProviderHolding max_rated_psu_voltage; + DataProviderHolding max_rated_psu_current; + DataProviderHolding min_rated_psu_voltage; + DataProviderHolding min_rated_psu_current; + + DataProviderHolding rated_output_power_connector; + DataProviderMemoryHolding<48> hmac_key; + DataProviderHolding rated_output_power_psu; + // written by dispenser + DataProviderHolding psu_port_available; + + // alarms + DataProviderHoldingUnsolicitatedReportCallback dc_output_contact_fault; + DataProviderHoldingUnsolicitatedReportCallback 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), + output_current(config.get_output_current, utils::ignore_write), + 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), + mac_address(config.mac_address), + contactor_status(config.get_contactor_status, utils::ignore_write, utils::always_report), + electronic_lock_status(config.get_electronic_lock_status, utils::ignore_write, + 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(0, 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(offset, collected_connector_registers)); + registry.add(std::make_unique(offset, setting_connector_registers)); + registry.add(std::make_unique(offset, alarm_connector_registers)); + } +}; + +} // namespace fusion_charger::modbus_driver diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/include/fusion_charger/modbus/registers/dispenser.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/include/fusion_charger/modbus/registers/dispenser.hpp new file mode 100644 index 0000000000..efc8a6de2b --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/include/fusion_charger/modbus/registers/dispenser.hpp @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once +#include + +#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; +}; + +struct DispenserRegisters { + DataProviderHolding manufacturer; + DataProviderHolding model; + DataProviderHolding protocol_version; + DataProviderHolding hardware_version; + DataProviderStringHolding<48> software_version; + + DataProviderHolding charging_connectors_count; + DataProviderStringHolding<22> esn_dispenser; + DataProviderCallbacksUnsolicitated time_sync; + DataProviderHoldingUnsolicitatedReportCallback door_status_alarm; + DataProviderHoldingUnsolicitatedReportCallback water_alarm; + DataProviderHoldingUnsolicitatedReportCallback epo_alarm; + DataProviderHoldingUnsolicitatedReportCallback 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); }, [](std::uint32_t) {}, utils::always_report), + door_status_alarm(0, utils::always_report), + water_alarm(0, utils::always_report), + epo_alarm(0, utils::always_report), + tilt_alarm(0, 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(common_data_providers)); + registry.add(std::make_unique(collected_data_providers)); + registry.add(std::make_unique(alarm_data_providers)); + } +}; + +}; // namespace fusion_charger::modbus_driver diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/include/fusion_charger/modbus/registers/error.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/include/fusion_charger/modbus/registers/error.hpp new file mode 100644 index 0000000000..818dcf60eb --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/include/fusion_charger/modbus/registers/error.hpp @@ -0,0 +1,866 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include +#include +#include +#include + +#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(errorEvent.error_category) + << "; Subcategory: " << static_cast(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(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(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(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(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(dc_dc_charging_module_providers)); + + raw_registers::CoolingSectionRegisters::DataProviders cooling_section_providers{cooling_section.cooling_unit_1}; + registry.add(std::make_unique(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(power_distribution_module_providers)); + } + + void add_callback(std::function 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 high_voltage_door_status_sensor = + DataProviderHolding(AlarmStatus::NORMAL); + DataProviderHolding door_status_sensor = DataProviderHolding(AlarmStatus::NORMAL); + DataProviderHolding water = DataProviderHolding(AlarmStatus::NORMAL); + DataProviderHolding smoke = DataProviderHolding(AlarmStatus::NORMAL); + DataProviderHolding epo = DataProviderHolding(AlarmStatus::NORMAL); + } power_unit; + + struct { + DataProviderHolding unknown_system_type = DataProviderHolding(AlarmStatus::NORMAL); + DataProviderHolding power_detection_exception = + DataProviderHolding(AlarmStatus::NORMAL); + DataProviderHolding syncrhonization_cable_status_fault_of_energy_routing_board = + DataProviderHolding(AlarmStatus::NORMAL); + DataProviderHolding soft_start_fault = DataProviderHolding(AlarmStatus::NORMAL); + DataProviderHolding soft_start_module_communication_failure = + DataProviderHolding(AlarmStatus::NORMAL); + DataProviderHolding soft_start_module_overloaded = + DataProviderHolding(AlarmStatus::NORMAL); + DataProviderHolding soft_start_module_fault = + DataProviderHolding(AlarmStatus::NORMAL); + DataProviderHolding soft_start_module_overtemperature = + DataProviderHolding(AlarmStatus::NORMAL); + DataProviderHolding soft_start_module_undertemperature = + DataProviderHolding(AlarmStatus::NORMAL); + DataProviderHolding soft_start_module_disconnection_failure = + DataProviderHolding(AlarmStatus::NORMAL); + DataProviderHolding phase_sequence_abornmal_alarm = + DataProviderHolding(AlarmStatus::NORMAL); + DataProviderHolding power_distribution_module_communication_failure = + DataProviderHolding(AlarmStatus::NORMAL); + DataProviderHolding fault_of_insulation_resistance_to_ground = + DataProviderHolding(AlarmStatus::NORMAL); + DataProviderHolding modbus_tcp_certificate = DataProviderHolding(0); + } charging_power_unit; + + struct { + DataProviderHolding ac_branch_1 = DataProviderHolding(0); + DataProviderHolding ac_branch_2 = DataProviderHolding(0); + } ac_branch; + + struct { + DataProviderHolding rectifier_1 = DataProviderHolding(0); + DataProviderHolding rectifier_2 = DataProviderHolding(0); + DataProviderHolding rectifier_3 = DataProviderHolding(0); + DataProviderHolding rectifier_4 = DataProviderHolding(0); + DataProviderHolding rectifier_5 = DataProviderHolding(0); + DataProviderHolding rectifier_6 = DataProviderHolding(0); + } ac_dc_rectifier; + + struct { + DataProviderHolding dc_dc_module_1 = DataProviderHolding(0); + DataProviderHolding dc_dc_module_2 = DataProviderHolding(0); + DataProviderHolding dc_dc_module_3 = DataProviderHolding(0); + DataProviderHolding dc_dc_module_4 = DataProviderHolding(0); + DataProviderHolding dc_dc_module_5 = DataProviderHolding(0); + DataProviderHolding dc_dc_module_6 = DataProviderHolding(0); + DataProviderHolding dc_dc_module_7 = DataProviderHolding(0); + DataProviderHolding dc_dc_module_8 = DataProviderHolding(0); + DataProviderHolding dc_dc_module_9 = DataProviderHolding(0); + DataProviderHolding dc_dc_module_10 = DataProviderHolding(0); + DataProviderHolding dc_dc_module_11 = DataProviderHolding(0); + DataProviderHolding dc_dc_module_12 = DataProviderHolding(0); + } dc_dc_charging_module; + + struct { + DataProviderHolding cooling_unit_1 = DataProviderHolding(0); + } cooling_section; + + struct { + DataProviderHolding power_distribution_module_1 = DataProviderHolding(0); + DataProviderHolding power_distribution_module_2 = DataProviderHolding(0); + DataProviderHolding power_distribution_module_3 = DataProviderHolding(0); + DataProviderHolding power_distribution_module_4 = DataProviderHolding(0); + DataProviderHolding power_distribution_module_5 = DataProviderHolding(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 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 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 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 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 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 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 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 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/include/fusion_charger/modbus/registers/power_unit.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/include/fusion_charger/modbus/registers/power_unit.hpp new file mode 100644 index 0000000000..00dfc664fb --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/include/fusion_charger/modbus/registers/power_unit.hpp @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include "raw.hpp" + +namespace fusion_charger::modbus_driver { +using namespace modbus::registers::data_providers; + +struct PowerUnitRegisters { + using PSURunningMode = raw_registers::SettingPowerUnitRegisters::PSURunningMode; + + DataProviderHolding manufacturer; + DataProviderHolding protocol_version; + DataProviderHolding hardware_version; + DataProviderStringHolding<48> software_version; + DataProviderStringHolding<32> esn_control_board; + + DataProviderHolding psu_running_mode; + DataProviderMemoryHolding<6> psu_mac; + DataProviderHolding ac_input_voltage_a; + DataProviderHolding ac_input_voltage_b; + DataProviderHolding ac_input_voltage_c; + DataProviderHolding ac_input_current_a; + DataProviderHolding ac_input_current_b; + DataProviderHolding ac_input_current_c; + DataProviderHolding total_historic_input_energy; + + PowerUnitRegisters() : + manufacturer(0), + protocol_version(0), + hardware_version(0), + software_version(""), + esn_control_board(""), + psu_running_mode(PSURunningMode::STARTING_UP), // default value + ac_input_voltage_a(0.0), + ac_input_voltage_b(0.0), + ac_input_voltage_c(0.0), + ac_input_current_a(0.0), + ac_input_current_b(0.0), + ac_input_current_c(0.0), + total_historic_input_energy(0.0) { + } + + void add_to_registry(modbus::registers::registry::ComplexRegisterRegistry& registry) { + raw_registers::CommonPowerUnitRegisters::DataProviders common_data_providers{ + manufacturer, protocol_version, hardware_version, software_version, esn_control_board}; + + raw_registers::SettingPowerUnitRegisters::DataProviders setting_data_providers{ + psu_running_mode, psu_mac, + ac_input_voltage_a, ac_input_voltage_b, + ac_input_voltage_c, ac_input_current_a, + ac_input_current_b, ac_input_current_c, + total_historic_input_energy}; + + registry.add(std::make_unique(common_data_providers)); + + registry.add(std::make_unique(setting_data_providers)); + } +}; + +}; // namespace fusion_charger::modbus_driver diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/include/fusion_charger/modbus/registers/raw.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/include/fusion_charger/modbus/registers/raw.hpp new file mode 100644 index 0000000000..f4a9491f50 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/include/fusion_charger/modbus/registers/raw.hpp @@ -0,0 +1,469 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include +#include +#include +#include + +namespace fusion_charger { +namespace modbus_driver { +namespace raw_registers { + +using namespace modbus::registers::data_providers; +using namespace modbus::registers::complex_registers; +using namespace modbus::registers::converters; +using namespace modbus::registers::registry; +using namespace modbus_extensions; + +class CommonDispenserRegisters : public ComplexRegisterSubregistry { +public: + struct DataProviders { + DataProvider& manufacturer; + DataProvider& model; + DataProvider& protocol_version; + DataProvider& hardware_version; + DataProviderString<48>& software_version; + }; + + CommonDispenserRegisters(const DataProviders& data_providers) { + // clang-format off + this->add(new ElementaryRegister(0x0000, data_providers.manufacturer, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x0001, data_providers.model, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x0002, data_providers.protocol_version, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x0004, data_providers.hardware_version, ConverterABCD::instance())); + this->add(new StringRegister <48> (0x0013, data_providers.software_version, ConverterIdentity::instance())); + // clang-format on + } +}; + +class CommonPowerUnitRegisters : public ComplexRegisterSubregistry { +public: + struct DataProviders { + DataProvider& manufacturer; + DataProvider& protocol_version; + DataProvider& hardware_version; + DataProviderString<48>& software_version; + DataProviderString<32>& esn_control_board; + }; + + CommonPowerUnitRegisters(const DataProviders& data_providers) { + // clang-format off + this->add(new ElementaryRegister(0x0100, data_providers.manufacturer, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x0101, data_providers.protocol_version, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x0102, data_providers.hardware_version, ConverterABCD::instance())); + this->add(new StringRegister <48> (0x0103, data_providers.software_version, ConverterIdentity::instance())); + this->add(new StringRegister <32> (0x011B, data_providers.esn_control_board, ConverterIdentity::instance())); + // clang-format on + }; +}; + +class CollectedDispenserRegisters : public ComplexRegisterSubregistry { +public: + struct DataProviders { + DataProvider& charging_connectors_count; + DataProviderString<22>& esn_dispenser; + DataProviderUnsolicitated& time_sync; // todo: what should this name be? The docs have a + // really long name! + }; + + CollectedDispenserRegisters(const DataProviders& data_providers) { + // clang-format off + this->add(new ElementaryRegister(0x1015, data_providers.charging_connectors_count, ConverterABCD::instance())); + this->add(new StringRegister <22> (0x1016, data_providers.esn_dispenser, ConverterIdentity::instance())); + this->add(new ElementaryRegister(0x1024, data_providers.time_sync, ConverterABCD::instance())); + // clang-format on + } +}; + +enum class ConnectorOffset : std::uint16_t { + CONNECTOR_1_OFFSET = 0x0000, + CONNECTOR_2_OFFSET = 0x0C00, + CONNECTOR_3_OFFSET = 0x0D00, + CONNECTOR_4_OFFSET = 0x0E00, +}; + +static ConnectorOffset offset_from_connector_number(std::uint16_t connector_number) { + switch (connector_number) { + case 1: + return ConnectorOffset::CONNECTOR_1_OFFSET; + case 2: + return ConnectorOffset::CONNECTOR_2_OFFSET; + case 3: + return ConnectorOffset::CONNECTOR_3_OFFSET; + case 4: + return ConnectorOffset::CONNECTOR_4_OFFSET; + default: + throw std::runtime_error("Invalid connector number"); + } +} + +enum class ConnectorType : std::uint16_t { + CCS1 = 0x0001, + CCS2 = 0x0002, + CHAdeMO = 0x0003, + GB = 0x0004, +}; + +enum class WorkingStatus : std::uint16_t { + STANDBY = 0, + STANDBY_WITH_CONNECTOR_INSERTED = 1, + CHARGING = 3, + CHARGING_COMPLETE = 4, + FAULT = 5, + DISPENSER_UPGRADE = 7, + CHARGING_STARTING = 8, +}; + +enum class ConnectionStatus : std::uint16_t { + NOT_CONNECTED = 0, + SEMI_CONNECTED = 1, // not compatible with ccs2 + FULL_CONNECTED = 2, +}; + +enum class PsuOutputPortAvailability : std::uint16_t { + NOT_AVAILABLE = 0, + AVAILABLE = 1, +}; + +std::string working_status_to_string(const WorkingStatus& status); + +class CollectedConnectorRegisters : public ComplexRegisterSubregistry { +public: + enum class ContactorStatus : std::uint16_t { + OFF = 0, + ON = 1, + }; + enum class ElectronicLockStatus : std::uint16_t { + UNLOCKED = 0, + LOCKED = 1, + }; + enum class ChargingEventConnector : std::uint16_t { + START_TO_STOP = 0, + STOP_TO_START = 1, + }; + + struct DataProviders { + DataProvider& total_energy_charged; + DataProvider& connector_type; + DataProvider& maximum_rated_charge_current; + DataProvider& output_voltage; + DataProvider& output_current; + DataProviderUnsolicitated& working_status; + DataProviderUnsolicitated& connection_status; + DataProvider& connector_no; // 1-12 + DataProvider& contactor_upstream_voltage; + DataProviderMemory<6>& mac_address; + // 0 off, 1 on + DataProviderUnsolicitated& contactor_status; + DataProviderUnsolicitated& electronic_lock_status; + DataProviderUnsolicitated& charging_event_connector; // todo ?? + }; + + CollectedConnectorRegisters(ConnectorOffset connector, const DataProviders& data_providers) { + std::uint16_t offset = static_cast(connector); + // clang-format off + this->add(new ElementaryRegister (0x1100 + offset, data_providers.total_energy_charged, ConverterABCD::instance())); + this->add(new ElementaryRegister (0x1104 + offset, data_providers.connector_type, ConverterABCD::instance())); + this->add(new ElementaryRegister (0x1105 + offset, data_providers.maximum_rated_charge_current, ConverterABCD::instance())); + this->add(new ElementaryRegister (0x1107 + offset, data_providers.output_voltage, ConverterABCD::instance())); + this->add(new ElementaryRegister (0x1109 + offset, data_providers.output_current, ConverterABCD::instance())); + this->add(new ElementaryRegister (0x110B + offset, data_providers.working_status, ConverterABCD::instance())); + this->add(new ElementaryRegister (0x110D + offset, data_providers.connection_status, ConverterABCD::instance())); + this->add(new ElementaryRegister (0x110E + offset, data_providers.connector_no, ConverterABCD::instance())); + this->add(new ElementaryRegister (0x1113 + offset, data_providers.contactor_upstream_voltage, ConverterABCD::instance())); + this->add(new MemoryRegister <6> (0x114D + offset, data_providers.mac_address, ConverterIdentity::instance())); + this->add(new ElementaryRegister (0x1154 + offset, data_providers.contactor_status, ConverterABCD::instance())); + this->add(new ElementaryRegister (0x1156 + offset, data_providers.electronic_lock_status, ConverterABCD::instance())); + this->add(new ElementaryRegister (0x117E + offset, data_providers.charging_event_connector, ConverterABCD::instance())); + // clang-format on + } +}; + +class SettingPowerUnitRegisters : public ComplexRegisterSubregistry { +public: + enum class PSURunningMode : std::uint16_t { + STARTING_UP = 0, + RUNNING = 1, + FAULTY = 2, + SLEEPING = 3, + UPGRADING = 4, + }; + + static std::string psu_running_mode_to_string(const PSURunningMode& mode); + + struct DataProviders { + DataProvider& psu_running_mode; + DataProviderMemory<6>& psu_mac; + DataProvider& ac_input_voltage_a; + DataProvider& ac_input_voltage_b; + DataProvider& ac_input_voltage_c; + DataProvider& ac_input_current_a; + DataProvider& ac_input_current_b; + DataProvider& ac_input_current_c; + DataProvider& total_historic_input_energy; + }; + + SettingPowerUnitRegisters(const DataProviders& data_provider) { + // clang-format off + this->add(new ElementaryRegister(0x2006, data_provider.psu_running_mode, ConverterABCD::instance())); + this->add(new MemoryRegister <6> (0x2111, data_provider.psu_mac, ConverterIdentity::instance())); + this->add(new ElementaryRegister (0x2007, data_provider.ac_input_voltage_a, ConverterABCD::instance())); + this->add(new ElementaryRegister (0x2009, data_provider.ac_input_voltage_b, ConverterABCD::instance())); + this->add(new ElementaryRegister (0x200B, data_provider.ac_input_voltage_c, ConverterABCD::instance())); + this->add(new ElementaryRegister (0x200D, data_provider.ac_input_current_a, ConverterABCD::instance())); + this->add(new ElementaryRegister (0x200F, data_provider.ac_input_current_b, ConverterABCD::instance())); + this->add(new ElementaryRegister (0x2011, data_provider.ac_input_current_c, ConverterABCD::instance())); + this->add(new ElementaryRegister (0x2013, data_provider.total_historic_input_energy, ConverterABCD::instance())); + // clang-format on + }; +}; + +class SettingConnectorRegisters : public ComplexRegisterSubregistry { +public: + struct DataProviders { + DataProvider& max_rated_psu_voltage; + DataProvider& max_rated_psu_current; + DataProvider& min_rated_psu_voltage; + DataProvider& min_rated_psu_current; + DataProvider& rated_output_power_connector; // kW + DataProviderMemory<48>& hmac_key; + DataProvider& rated_output_power_psu; // kW + DataProvider& psu_port_available; + }; + + SettingConnectorRegisters(ConnectorOffset connector, const DataProviders& data_providers) { + std::uint16_t offset = static_cast(connector); + + // clang-format off + this->add(new ElementaryRegister (offset + 0x2100, data_providers.max_rated_psu_voltage, ConverterABCD::instance())); + this->add(new ElementaryRegister (offset + 0x2102, data_providers.max_rated_psu_current, ConverterABCD::instance())); + this->add(new ElementaryRegister (offset + 0x2105, data_providers.min_rated_psu_voltage, ConverterABCD::instance())); + this->add(new ElementaryRegister (offset + 0x2107, data_providers.min_rated_psu_current, ConverterABCD::instance())); + this->add(new ElementaryRegister (offset + 0x2109, data_providers.rated_output_power_connector, ConverterABCD::instance())); + this->add(new MemoryRegister <48> (offset + 0x2115, data_providers.hmac_key, ConverterIdentity::instance())); + this->add(new ElementaryRegister (offset + 0x212D, data_providers.rated_output_power_psu, ConverterABCD::instance())); + this->add(new ElementaryRegister(offset + 0x212F, data_providers.psu_port_available, ConverterABCD::instance())); + // clang-format on + } +}; + +struct AlarmDispenserRegisters : public ComplexRegisterSubregistry { + struct DataProviders { + DataProviderUnsolicitated& door_status_alarm; + DataProviderUnsolicitated& water_alarm; + DataProviderUnsolicitated& epo_alarm; + DataProviderUnsolicitated& tilt_alarm; + }; + + AlarmDispenserRegisters(const DataProviders& data_providers) { + // clang-format off + this->add(new ElementaryRegister(0x3001, data_providers.door_status_alarm, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x3002, data_providers.water_alarm, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x3003, data_providers.epo_alarm, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x3004, data_providers.tilt_alarm, ConverterABCD::instance())); + // clang-format on + } +}; + +struct AlarmConnectorRegisters : public ComplexRegisterSubregistry { + struct DataProviders { + DataProviderUnsolicitated& dc_output_contact_fault; + DataProviderUnsolicitated& inverse_connection_dispenser_inlet_cable; + }; + + AlarmConnectorRegisters(ConnectorOffset connector, const DataProviders& data_providers) { + std::uint16_t offset = static_cast(connector); + + // clang-format off + this->add(new ElementaryRegister(offset + 0x3105, data_providers.dc_output_contact_fault, ConverterABCD::instance())); + this->add(new ElementaryRegister(offset + 0x3115, data_providers.inverse_connection_dispenser_inlet_cable, ConverterABCD::instance())); + // clang-format on + } +}; + +enum class AlarmStatus : std::uint16_t { + NORMAL = 0, + ALARM = 1, +}; + +inline std::ostream& operator<<(std::ostream& os, const AlarmStatus& status) { + switch (status) { + case AlarmStatus::NORMAL: + os << "NORMAL"; + break; + case AlarmStatus::ALARM: + os << "ALARM"; + break; + default: + os << "UNKNOWN"; + break; + } + return os; +} + +struct AlarmPowerUnitRegisters : public ComplexRegisterSubregistry { + struct DataProviders { + DataProvider& high_voltage_door_status_sensor; + DataProvider& door_status_sensor; + DataProvider& water; + DataProvider& smoke; + DataProvider& epo; + }; + + AlarmPowerUnitRegisters(const DataProviders& data_providers) { + // clang-format off + this->add(new ElementaryRegister(0x4000, data_providers.high_voltage_door_status_sensor, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4001, data_providers.door_status_sensor, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4002, data_providers.water, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4003, data_providers.smoke, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4004, data_providers.epo, ConverterABCD::instance())); + // clang-format on + } +}; + +struct AlarmChargingPowerUnitRegisters : public ComplexRegisterSubregistry { + struct DataProviders { + DataProvider& unknown_system_type; + DataProvider& power_detection_exception; + DataProvider& syncrhonization_cable_status_fault_of_energy_routing_board; + DataProvider& soft_start_fault; + DataProvider& soft_start_module_communication_failure; + DataProvider& soft_start_module_overloaded; + DataProvider& soft_start_module_fault; + DataProvider& soft_start_module_overtemperature; + DataProvider& soft_start_module_undertemperature; + DataProvider& soft_start_module_disconnection_failure; + DataProvider& phase_sequence_abornmal_alarm; + DataProvider& power_distribution_module_communication_failure; + DataProvider& fault_of_insulation_resistance_to_ground; + DataProvider& modbus_tcp_certificate; + }; + + AlarmChargingPowerUnitRegisters(const DataProviders& data_providers) { + // clang-format off + this->add(new ElementaryRegister(0x4005, data_providers.unknown_system_type, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4006, data_providers.power_detection_exception, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4007, data_providers.syncrhonization_cable_status_fault_of_energy_routing_board, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4008, data_providers.soft_start_fault, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4009, data_providers.soft_start_module_communication_failure, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x400A, data_providers.soft_start_module_overloaded, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x400B, data_providers.soft_start_module_fault, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x400C, data_providers.soft_start_module_overtemperature, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x400D, data_providers.soft_start_module_undertemperature, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x400E, data_providers.soft_start_module_disconnection_failure, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x400F, data_providers.phase_sequence_abornmal_alarm, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4010, data_providers.power_distribution_module_communication_failure, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4011, data_providers.fault_of_insulation_resistance_to_ground, ConverterABCD::instance())); + this->add(new ElementaryRegister (0x4012, data_providers.modbus_tcp_certificate, ConverterABCD::instance())); + // clang-format on + } +}; + +struct AlarmAcBranchRegisters : public ComplexRegisterSubregistry { + struct DataProviders { + DataProvider& ac_branch_1; + DataProvider& ac_branch_2; + }; + + AlarmAcBranchRegisters(const DataProviders& data_providers) { + // clang-format off + this->add(new ElementaryRegister(0x4020, data_providers.ac_branch_1, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4022, data_providers.ac_branch_2, ConverterABCD::instance())); + // clang-format on + } +}; + +struct AlarmAcDcRectifierRegisters : public ComplexRegisterSubregistry { + struct DataProviders { + DataProvider& rectifier_1; + DataProvider& rectifier_2; + DataProvider& rectifier_3; + DataProvider& rectifier_4; + DataProvider& rectifier_5; + DataProvider& rectifier_6; + }; + + AlarmAcDcRectifierRegisters(const DataProviders& data_providers) { + // clang-format off + this->add(new ElementaryRegister(0x4040, data_providers.rectifier_1, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4042, data_providers.rectifier_2, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4044, data_providers.rectifier_3, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4046, data_providers.rectifier_4, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4048, data_providers.rectifier_5, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x404A, data_providers.rectifier_6, ConverterABCD::instance())); + // clang-format on + } +}; + +struct DcDcChargingModuleRegisters : public ComplexRegisterSubregistry { + struct DataProviders { + DataProvider& dc_dc_charging_module_1; + DataProvider& dc_dc_charging_module_2; + DataProvider& dc_dc_charging_module_3; + DataProvider& dc_dc_charging_module_4; + DataProvider& dc_dc_charging_module_5; + DataProvider& dc_dc_charging_module_6; + DataProvider& dc_dc_charging_module_7; + DataProvider& dc_dc_charging_module_8; + DataProvider& dc_dc_charging_module_9; + DataProvider& dc_dc_charging_module_10; + DataProvider& dc_dc_charging_module_11; + DataProvider& dc_dc_charging_module_12; + }; + + DcDcChargingModuleRegisters(const DataProviders& data_providers) { + // clang-format off + this->add(new ElementaryRegister(0x4070, data_providers.dc_dc_charging_module_1, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4072, data_providers.dc_dc_charging_module_2, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4074, data_providers.dc_dc_charging_module_3, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4076, data_providers.dc_dc_charging_module_4, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4078, data_providers.dc_dc_charging_module_5, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x407A, data_providers.dc_dc_charging_module_6, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x407C, data_providers.dc_dc_charging_module_7, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x407E, data_providers.dc_dc_charging_module_8, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4080, data_providers.dc_dc_charging_module_9, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4082, data_providers.dc_dc_charging_module_10, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4084, data_providers.dc_dc_charging_module_11, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x4086, data_providers.dc_dc_charging_module_12, ConverterABCD::instance())); + // clang-format on + } +}; + +struct CoolingSectionRegisters : public ComplexRegisterSubregistry { + struct DataProviders { + DataProvider& cooling_unit_1; + }; + + CoolingSectionRegisters(const DataProviders& data_providers) { + // clang-format off + this->add(new ElementaryRegister(0x40D0, data_providers.cooling_unit_1, ConverterABCD::instance())); + // clang-format on + } +}; + +struct PowerDistributionModuleRegisters : public ComplexRegisterSubregistry { + struct DataProviders { + DataProvider& power_distribution_module_1; + DataProvider& power_distribution_module_2; + DataProvider& power_distribution_module_3; + DataProvider& power_distribution_module_4; + DataProvider& power_distribution_module_5; + }; + + PowerDistributionModuleRegisters(const DataProviders& data_providers) { + // clang-format off + this->add(new ElementaryRegister(0x40E0, data_providers.power_distribution_module_1, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x40E2, data_providers.power_distribution_module_2, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x40E4, data_providers.power_distribution_module_3, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x40E6, data_providers.power_distribution_module_4, ConverterABCD::instance())); + this->add(new ElementaryRegister(0x40E8, data_providers.power_distribution_module_5, ConverterABCD::instance())); + // clang-format on + } +}; + +}; // namespace raw_registers +}; // namespace modbus_driver +}; // namespace fusion_charger diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/include/fusion_charger/modbus/registers/utils.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/include/fusion_charger/modbus/registers/utils.hpp new file mode 100644 index 0000000000..060651ab91 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/include/fusion_charger/modbus/registers/utils.hpp @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include + +namespace fusion_charger::modbus_driver { +namespace utils { + +bool always_report(); + +template void ignore_write(T) { +} + +} // namespace utils +} // namespace fusion_charger::modbus_driver diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/src/raw.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/src/raw.cpp new file mode 100644 index 0000000000..84fe145acf --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/src/raw.cpp @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +std::string fusion_charger::modbus_driver::raw_registers::working_status_to_string(const WorkingStatus& status) { + switch (status) { + case WorkingStatus::STANDBY: + return "STANDBY"; + case WorkingStatus::STANDBY_WITH_CONNECTOR_INSERTED: + return "STANDBY_WITH_CONNECTOR_INSERTED"; + case WorkingStatus::CHARGING: + return "CHARGING"; + case WorkingStatus::CHARGING_COMPLETE: + return "CHARGING_COMPLETE"; + case WorkingStatus::FAULT: + return "FAULT"; + case WorkingStatus::DISPENSER_UPGRADE: + return "DISPENSER_UPGRADE"; + case WorkingStatus::CHARGING_STARTING: + return "CHARGING_STARTING"; + default: + return "UNKNOWN"; + } +} + +std::string fusion_charger::modbus_driver::raw_registers::SettingPowerUnitRegisters::psu_running_mode_to_string( + const PSURunningMode& mode) { + switch (mode) { + case PSURunningMode::STARTING_UP: + return "STARTING_UP"; + case PSURunningMode::RUNNING: + return "RUNNING"; + case PSURunningMode::FAULTY: + return "FAULTY"; + case PSURunningMode::SLEEPING: + return "SLEEPING"; + case PSURunningMode::UPGRADING: + return "UPGRADING"; + default: + return "UNKNOWN"; + } +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/src/utils.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/src/utils.cpp new file mode 100644 index 0000000000..dd00df3add --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/src/utils.cpp @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +bool fusion_charger::modbus_driver::utils::always_report() { + return true; +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/tests/all_register_overlap.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/tests/all_register_overlap.cpp new file mode 100644 index 0000000000..5ec1afba31 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/tests/all_register_overlap.cpp @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest + +#include + +#include +#include +#include + +using namespace fusion_charger::modbus_driver; + +TEST(RegisterOverlap, all_connectors_no_overlap) { + std::uint8_t mac[6] = {0, 1, 2, 3, 4, 5}; + + PowerUnitRegisters power_unit_registers; + DispenserRegistersConfig dispenser_registers_config; + dispenser_registers_config.esn = "12345678"; + DispenserRegisters dispenser_registers(dispenser_registers_config); + + ConnectorRegistersConfig connector_register_config1; + + std::copy(std::begin(mac), std::end(mac), std::begin(connector_register_config1.mac_address)); + connector_register_config1.type = ConnectorRegisters::ConnectorType::CCS1; + connector_register_config1.global_connector_no = 1; + connector_register_config1.connector_number = 1; + connector_register_config1.max_rated_charge_current = 0.0; + connector_register_config1.rated_output_power_connector = 0.0; + connector_register_config1.get_contactor_upstream_voltage = []() { return 0.0; }; + connector_register_config1.get_output_voltage = []() { return 0.0; }; + connector_register_config1.get_output_current = []() { return 0.0; }; + ConnectorRegisters connector_registers1(connector_register_config1); + + ConnectorRegistersConfig connector_register_config2; + + std::copy(std::begin(mac), std::end(mac), std::begin(connector_register_config2.mac_address)); + connector_register_config2.type = ConnectorRegisters::ConnectorType::CCS1; + connector_register_config2.global_connector_no = 2; + connector_register_config2.connector_number = 2; + connector_register_config2.max_rated_charge_current = 0.0; + connector_register_config2.rated_output_power_connector = 0.0; + connector_register_config2.get_contactor_upstream_voltage = []() { return 0.0; }; + connector_register_config2.get_output_voltage = []() { return 0.0; }; + connector_register_config2.get_output_current = []() { return 0.0; }; + ConnectorRegisters connector_registers2(connector_register_config2); + + ConnectorRegistersConfig connector_register_config3; + + std::copy(std::begin(mac), std::end(mac), std::begin(connector_register_config3.mac_address)); + connector_register_config3.type = ConnectorRegisters::ConnectorType::CCS1; + connector_register_config3.global_connector_no = 3; + connector_register_config3.connector_number = 3; + connector_register_config3.max_rated_charge_current = 0.0; + connector_register_config3.rated_output_power_connector = 0.0; + connector_register_config3.get_contactor_upstream_voltage = []() { return 0.0; }; + connector_register_config3.get_output_voltage = []() { return 0.0; }; + connector_register_config3.get_output_current = []() { return 0.0; }; + ConnectorRegisters connector_registers3(connector_register_config3); + + ConnectorRegistersConfig connector_register_config4; + + std::copy(std::begin(mac), std::end(mac), std::begin(connector_register_config4.mac_address)); + connector_register_config4.type = ConnectorRegisters::ConnectorType::CCS1; + connector_register_config4.global_connector_no = 4; + connector_register_config4.connector_number = 4; + connector_register_config4.max_rated_charge_current = 0.0; + connector_register_config4.rated_output_power_connector = 0.0; + connector_register_config4.get_contactor_upstream_voltage = []() { return 0.0; }; + connector_register_config4.get_output_voltage = []() { return 0.0; }; + connector_register_config4.get_output_current = []() { return 0.0; }; + ConnectorRegisters connector_registers4(connector_register_config4); + + modbus::registers::registry::ComplexRegisterRegistry registry; + power_unit_registers.add_to_registry(registry); + dispenser_registers.add_to_registry(registry); + connector_registers1.add_to_registry(registry); + connector_registers2.add_to_registry(registry); + connector_registers3.add_to_registry(registry); + connector_registers4.add_to_registry(registry); + + ASSERT_NO_THROW(registry.verify_overlap()); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/tests/assumptions.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/tests/assumptions.cpp new file mode 100644 index 0000000000..7b6c31dd9f --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/tests/assumptions.cpp @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include + +using namespace modbus::registers::complex_registers; +using namespace modbus::registers::data_providers; + +TEST(Assumptions, elementary_register_with_enum_type_works) { + enum class MyEnum : std::uint16_t { + VALUE_1 = 0x1234, + VALUE_2 = 0x5678, + }; + + DataProviderHolding data_provider(MyEnum::VALUE_1); + + ElementaryRegister reg(0x0000, data_provider, modbus::registers::converters::ConverterABCD::instance()); + + EXPECT_EQ(data_provider.get_value(), MyEnum::VALUE_1); + auto read = reg.on_read(); + EXPECT_EQ(read, std::vector({0x12, 0x34})); + reg.on_write(0, {0x56, 0x78}); + EXPECT_EQ(data_provider.get_value(), MyEnum::VALUE_2); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/tests/raw_registers_overlap.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/tests/raw_registers_overlap.cpp new file mode 100644 index 0000000000..3e6f429c79 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_driver/tests/raw_registers_overlap.cpp @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include + +using namespace fusion_charger::modbus_driver::raw_registers; + +TEST(CommonDispenserRegisters, no_internal_overlap) { + DataProviderHolding manufacturer(0); + DataProviderHolding model(1); + DataProviderHolding protocol_version(2); + DataProviderHolding hardware_version(2); + DataProviderStringHolding<48> software_version; + + CommonDispenserRegisters::DataProviders data_providers{manufacturer, model, protocol_version, hardware_version, + software_version}; + + CommonDispenserRegisters dispenser_registers(data_providers); + ASSERT_NO_THROW(dispenser_registers.verify_internal_overlap()); +} + +TEST(CommonPowerUnitRegisters, no_internal_overlap) { + DataProviderHolding manufacturer(0); + DataProviderHolding protocol_version(1); + DataProviderHolding hardware_version(2); + DataProviderStringHolding<48> software_version; + DataProviderStringHolding<32> esn_control_board; + + CommonPowerUnitRegisters::DataProviders data_providers{manufacturer, protocol_version, hardware_version, + software_version, esn_control_board}; + + CommonPowerUnitRegisters power_unit_registers(data_providers); + ASSERT_NO_THROW(power_unit_registers.verify_internal_overlap()); +} + +TEST(CollectedDispenserRegisters, no_internal_overlap) { + DataProviderHolding number_of_charging_connectors(0); + DataProviderStringHolding<22> esn_dispenser; + DataProviderHoldingUnsolicitatedReportCallback time_sync(0, []() { return true; }); + + CollectedDispenserRegisters::DataProviders data_providers{number_of_charging_connectors, esn_dispenser, time_sync}; + + CollectedDispenserRegisters dispenser_registers(data_providers); + ASSERT_NO_THROW(dispenser_registers.verify_internal_overlap()); +} + +bool always_true() { + return true; +} + +TEST(CollectedConnectorRegisters, no_internal_overlap) { + DataProviderHolding total_energy_charged(0); + DataProviderHolding connector_type(ConnectorType::CCS1); + DataProviderHolding maximum_rated_charge_current(0.0); + DataProviderHolding output_voltage(0.0); + DataProviderHolding output_current(0.0); + DataProviderHoldingUnsolicitatedReportCallback working_status(WorkingStatus::STANDBY, always_true); + DataProviderHoldingUnsolicitatedReportCallback connection_status(ConnectionStatus::NOT_CONNECTED, + always_true); + DataProviderHolding connector_no(0); // 1-12 + DataProviderHolding contactor_upstream_voltage(0); + DataProviderMemoryHolding<6> mac_address; + // 0 off, 1 on + DataProviderHoldingUnsolicitatedReportCallback contactor_status( + CollectedConnectorRegisters::ContactorStatus::OFF, always_true); + DataProviderHoldingUnsolicitatedReportCallback + electronic_lock_status(CollectedConnectorRegisters::ElectronicLockStatus::UNLOCKED, always_true); + DataProviderHoldingUnsolicitatedReportCallback + charging_event_connector(CollectedConnectorRegisters::ChargingEventConnector::START_TO_STOP, always_true); + + CollectedConnectorRegisters::DataProviders data_providers{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}; + + CollectedConnectorRegisters registers(ConnectorOffset::CONNECTOR_1_OFFSET, data_providers); + + ASSERT_NO_THROW(registers.verify_internal_overlap()); +} + +TEST(SettingPowerUnitRegisters, no_internal_overlap) { + DataProviderHolding psu_running_mode( + SettingPowerUnitRegisters::PSURunningMode::FAULTY); + DataProviderMemoryHolding<6> psu_mac; + DataProviderHolding ac_input_voltage_a(0); + DataProviderHolding ac_input_voltage_b(0); + DataProviderHolding ac_input_voltage_c(0); + DataProviderHolding ac_input_current_a(0); + DataProviderHolding ac_input_current_b(0); + DataProviderHolding ac_input_current_c(0); + DataProviderHolding total_historic_input_energy(0); + + SettingPowerUnitRegisters::DataProviders data_providers{psu_running_mode, psu_mac, + ac_input_voltage_a, ac_input_voltage_b, + ac_input_voltage_c, ac_input_current_a, + ac_input_current_b, ac_input_current_c, + total_historic_input_energy}; + + SettingPowerUnitRegisters registers(data_providers); + ASSERT_NO_THROW(registers.verify_internal_overlap()); +} + +TEST(SettingConnectorRegisters, no_internal_overlap) { + DataProviderHolding max_rated_psu_voltage(0); + DataProviderHolding max_rated_psu_current(0); + DataProviderHolding min_rated_psu_voltage(0); + DataProviderHolding min_rated_psu_current(0); + DataProviderHolding rated_output_power_connector(0); + DataProviderMemoryHolding<48> hmac_key; + DataProviderHolding rated_output_power_psu(0); + DataProviderHolding psu_port_available(PsuOutputPortAvailability::NOT_AVAILABLE); + + SettingConnectorRegisters::DataProviders data_providers{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}; + + SettingConnectorRegisters registers(ConnectorOffset::CONNECTOR_1_OFFSET, data_providers); + ASSERT_NO_THROW(registers.verify_internal_overlap()); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/CMakeLists.txt new file mode 100644 index 0000000000..36a5248a8c --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/CMakeLists.txt @@ -0,0 +1,15 @@ +file(GLOB SOURCES "src/*.cpp") + +add_library(fusion_charger_modbus_extensions STATIC ${SOURCES}) +ev_register_library_target(fusion_charger_modbus_extensions) +target_include_directories(fusion_charger_modbus_extensions PUBLIC include) +target_link_libraries(fusion_charger_modbus_extensions PUBLIC modbus-server modbus-registers) + +if(BUILD_TESTING) + include(GoogleTest) + + file(GLOB TEST_SOURCES "tests/*.cpp") + add_executable(fusion_charger_modbus_extensions_test ${TEST_SOURCES}) + target_link_libraries(fusion_charger_modbus_extensions_test PRIVATE fusion_charger_modbus_extensions gtest_main) + gtest_discover_tests(fusion_charger_modbus_extensions_test) +endif() diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/include/fusion_charger/modbus/extensions/unsolicitated_registers.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/include/fusion_charger/modbus/extensions/unsolicitated_registers.hpp new file mode 100644 index 0000000000..7c1daf950e --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/include/fusion_charger/modbus/extensions/unsolicitated_registers.hpp @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include +#include + +namespace fusion_charger::modbus_extensions { + +class DataProviderExtUnsolicitated { +public: + DataProviderExtUnsolicitated() { + } + + virtual bool should_uncolicitated_report() = 0; +}; + +template +class DataProviderUnsolicitated : public DataProviderExtUnsolicitated, + public modbus::registers::data_providers::DataProvider { +public: + DataProviderUnsolicitated() : modbus::registers::data_providers::DataProvider() { + } +}; + +// example for holding register +template +class DataProviderHoldingUnsolicitated : public modbus::registers::data_providers::DataProviderHolding, + public DataProviderUnsolicitated { +public: + DataProviderHoldingUnsolicitated(T value) : modbus::registers::data_providers::DataProviderHolding(value) { + } + + void on_read(std::uint8_t* val, size_t len) override { + modbus::registers::data_providers::DataProviderHolding::on_read(val, len); + } + + void on_write(std::uint8_t* val, size_t len) override { + modbus::registers::data_providers::DataProviderHolding::on_write(val, len); + } +}; + +// Event provider; can only be read; reports once after report() was called +template class DataProviderUnsolicitatedEvent : public DataProviderHoldingUnsolicitated { +protected: + bool should_report; + +public: + DataProviderUnsolicitatedEvent(T initial) : DataProviderHoldingUnsolicitated(initial), should_report(false) { + } + + bool should_uncolicitated_report() override { + if (this->should_report) { + this->should_report = false; + return true; + } + return false; + } + + void report(T val) { + this->update_value(val); + this->should_report = true; + } +}; + +template +class DataProviderHoldingUnsolicitatedReportCallback : public DataProviderHoldingUnsolicitated { +protected: + std::function unsolicitated_report; + +public: + DataProviderHoldingUnsolicitatedReportCallback(T initial, std::function unsolicitated_report) : + DataProviderHoldingUnsolicitated(initial), unsolicitated_report(unsolicitated_report) { + } + + bool should_uncolicitated_report() override { + return unsolicitated_report(); + } +}; + +template +class DataProviderCallbacksUnsolicitated : public modbus::registers::data_providers::DataProviderCallbacks, + public DataProviderUnsolicitated { + std::function unsolicitated_report; + +public: + DataProviderCallbacksUnsolicitated(std::function read_cb, std::function write_cb, + std::function unsolicitated_report) : + modbus::registers::data_providers::DataProviderCallbacks(read_cb, write_cb), + unsolicitated_report(unsolicitated_report) { + } + + bool should_uncolicitated_report() override { + return unsolicitated_report(); + } + + void on_read(std::uint8_t* val, size_t len) override { + modbus::registers::data_providers::DataProviderCallbacks::on_read(val, len); + } + + void on_write(std::uint8_t* val, size_t len) override { + modbus::registers::data_providers::DataProviderCallbacks::on_write(val, len); + } +}; + +std::optional> +unsolicitated_report_helper(modbus::registers::complex_registers::ComplexRegisterUntyped* reg); + +}; // namespace fusion_charger::modbus_extensions diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/include/fusion_charger/modbus/extensions/unsolicitated_registry.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/include/fusion_charger/modbus/extensions/unsolicitated_registry.hpp new file mode 100644 index 0000000000..ed5fe23481 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/include/fusion_charger/modbus/extensions/unsolicitated_registry.hpp @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include + +#include "unsolicitated_registers.hpp" +#include "unsolicitated_report.hpp" + +namespace fusion_charger::modbus_extensions { + +class UnsolicitatedRegistry : public modbus::registers::registry::ComplexRegisterRegistry { +public: + UnsolicitatedRegistry() : modbus::registers::registry::ComplexRegisterRegistry() { + } + + std::optional unsolicitated_report() { + fusion_charger::modbus_extensions::UnsolicitatedReportRequest::Device req_device; + req_device.location = 0x0000; + + for (auto& reg : this->get_all_registers()) { + if (auto res = unsolicitated_report_helper(reg)) { + fusion_charger::modbus_extensions::UnsolicitatedReportRequest::Segment req_segment; + req_segment.registers_start = reg->get_start_address(); + req_segment.registers_count = reg->get_size(); + req_segment.registers = *res; + + req_device.segments.push_back(req_segment); + } + } + + if (req_device.segments.empty()) { + return std::nullopt; + } + + fusion_charger::modbus_extensions::UnsolicitatedReportRequest req; + req.response_required = false; + req.devices.push_back(req_device); + + req.defragment(); + + return req; + } +}; + +}; // namespace fusion_charger::modbus_extensions diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/include/fusion_charger/modbus/extensions/unsolicitated_report.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/include/fusion_charger/modbus/extensions/unsolicitated_report.hpp new file mode 100644 index 0000000000..72bc7425c9 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/include/fusion_charger/modbus/extensions/unsolicitated_report.hpp @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#ifndef FUSION_CHARGER_MODBUS_EXTENSIONS__DUMMY_HPP +#define FUSION_CHARGER_MODBUS_EXTENSIONS__DUMMY_HPP + +#include + +namespace fusion_charger::modbus_extensions { + +struct UnsolicitatedReportRequest : public modbus_server::pdu::SpecificPDU { + // Subtype defs: + struct Segment { + std::uint16_t registers_start; + std::uint16_t registers_count; // todo: can be derived! + std::vector registers; //! 2 bytes per register + + std::vector to_vec() const; + }; + struct Device { + std::uint16_t location; + std::vector segments; + + std::vector to_vec() const; + + // defragment segments + void defragment(); + }; + + // data members: + bool response_required; + std::vector devices; + + UnsolicitatedReportRequest() = default; + + void from_generic(const modbus_server::pdu::GenericPDU& generic) override; + modbus_server::pdu::GenericPDU to_generic() const override; + + // defragment segments + void defragment(); +}; + +struct UnsolicitatedReportResponse : public modbus_server::pdu::SpecificPDU { + bool success; + void from_generic(const modbus_server::pdu::GenericPDU& generic) override; + modbus_server::pdu::GenericPDU to_generic() const override; +}; + +} // namespace fusion_charger::modbus_extensions + +#endif diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/include/fusion_charger/modbus/extensions/unsolicitated_report_server.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/include/fusion_charger/modbus/extensions/unsolicitated_report_server.hpp new file mode 100644 index 0000000000..bdf828e0d8 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/include/fusion_charger/modbus/extensions/unsolicitated_report_server.hpp @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#ifndef FUSION_CHARGER_MODBUS_EXTENSIONS__UNSOLICITATED_REPORT_SERVER_HPP +#define FUSION_CHARGER_MODBUS_EXTENSIONS__UNSOLICITATED_REPORT_SERVER_HPP + +#include + +#include "unsolicitated_report.hpp" + +namespace fusion_charger::modbus_extensions { + +class UnsolicitatedReportBasicServer : public modbus_server::ModbusBasicServer { +public: + UnsolicitatedReportBasicServer(std::shared_ptr corr_layer, + logs::LogIntf log = logs::log_printf) : + modbus_server::ModbusBasicServer(corr_layer, log) { + } + + // todo: check throws things + /** + * @brief Send an unsolicitated report to the server + * + * @param request + * @param timeout + * @returns std::nullopt if given request doesn't request a response + * @returns UnsolicitatedReportResponse if given request requests a response + * @throws modbus_server::pdu::EncodingError if given request can not be + * encoded + * @throws modbus_server::pdu::DecodingError if the response can not be + * decoded + * @throws modbus_server::ApplicationServerError if the client returns an + * error + */ + std::optional send_unsolicitated_report(UnsolicitatedReportRequest& request, + std::chrono::milliseconds timeout); +}; + +}; // namespace fusion_charger::modbus_extensions + +#endif diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/src/unsolicitated_registers.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/src/unsolicitated_registers.cpp new file mode 100644 index 0000000000..4054b84ec1 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/src/unsolicitated_registers.cpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +namespace fusion_charger::modbus_extensions { + +std::optional> +unsolicitated_report_helper(modbus::registers::complex_registers::ComplexRegisterUntyped* reg) { + modbus::registers::data_providers::DataProviderUntyped* prov = reg->get_data_provider(); + if (auto ext = dynamic_cast(prov)) { + if (ext->should_uncolicitated_report()) { + return reg->on_read(); + } + } + + return std::nullopt; +} + +}; // namespace fusion_charger::modbus_extensions diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/src/unsolicitated_report.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/src/unsolicitated_report.cpp new file mode 100644 index 0000000000..823f0abc7c --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/src/unsolicitated_report.cpp @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include "modbus-server/frames.hpp" + +using namespace fusion_charger::modbus_extensions; +using namespace modbus_server::pdu; + +std::vector UnsolicitatedReportRequest::Segment::to_vec() const { + if (registers.size() != registers_count * 2) { + throw EncodingError("UnsolicitatedReportRequest::Segment", + "'registers' size must be equal to 'registers_count * 2'"); + } + + std::vector ret; + ret.push_back(registers_start >> 8); + ret.push_back(registers_start & 0xFF); + ret.push_back(registers_count >> 8); + ret.push_back(registers_count & 0xFF); + + ret.insert(ret.end(), registers.begin(), registers.end()); + + return ret; +} + +std::vector UnsolicitatedReportRequest::Device::to_vec() const { + std::vector ret; + ret.push_back(location >> 8); + ret.push_back(location & 0xFF); + + std::uint16_t number_of_segments = segments.size(); + ret.push_back(number_of_segments >> 8); + ret.push_back(number_of_segments & 0xFF); + + for (const auto& segment : segments) { + auto segment_vec = segment.to_vec(); + ret.insert(ret.end(), segment_vec.begin(), segment_vec.end()); + } + + return ret; +} + +void UnsolicitatedReportRequest::from_generic(const GenericPDU& generic) { + if (generic.function_code != 0x41) { + throw DecodingError(generic, "UnsolicitatedReportRequest", "Invalid function code"); + } + + if (generic.data.size() < 6) { + throw DecodingError(generic, "UnsolicitatedReportRequest", "Invalid data size (expected at least 6 bytes)"); + } + + std::uint8_t subfunction_code = generic.data[0]; + std::uint16_t data_length = generic.data[1] << 8 | generic.data[2]; + std::uint8_t reporting_type = generic.data[3]; + std::uint16_t number_of_devices = generic.data[4] << 8 | generic.data[5]; + + if (subfunction_code != 0x91) { + throw DecodingError(generic, "UnsolicitatedReportRequest", "Invalid subfunction code"); + } + + if (data_length != generic.data.size() - 3) { + throw DecodingError(generic, "UnsolicitatedReportRequest", "Invalid data length in header"); + } + + this->response_required = (reporting_type & 0x80) != 0; + + auto rest_data = std::vector(generic.data.begin() + 6, generic.data.end()); + auto devices = std::vector(); + for (size_t i = 0; i < number_of_devices; i++) { + auto device = Device(); + + if (rest_data.size() < 4) { + throw DecodingError(generic, "UnsolicitatedReportRequest", + "Invalid data size (expected at least 4 bytes for device)"); + } + + device.location = (rest_data[0] << 8) | rest_data[1]; + std::uint16_t number_of_segments = (rest_data[2] << 8) | rest_data[3]; + + rest_data.erase(rest_data.begin(), rest_data.begin() + 4); + + for (int j = 0; j < number_of_segments; j++) { + auto segment = Segment(); + + if (rest_data.size() < 6) { + throw DecodingError(generic, "UnsolicitatedReportRequest", + "Invalid data size (expected at least 6 bytes for segment)"); + } + + segment.registers_start = (rest_data[0] << 8) | rest_data[1]; + segment.registers_count = (rest_data[2] << 8) | rest_data[3]; + + rest_data.erase(rest_data.begin(), rest_data.begin() + 4); + + if (segment.registers_count * 2 > rest_data.size()) { + throw DecodingError(generic, "UnsolicitatedReportRequest", + "Invalid data size (expected at least " + + std::to_string(segment.registers_count * 2 + 4) + " bytes for segment)"); + } + + segment.registers = + std::vector(rest_data.begin(), rest_data.begin() + segment.registers_count * 2); + + rest_data.erase(rest_data.begin(), rest_data.begin() + segment.registers_count * 2); + + device.segments.push_back(segment); + } + + devices.push_back(device); + } + + this->devices = devices; +} + +GenericPDU UnsolicitatedReportRequest::to_generic() const { + // this is the "Device 1" ... "Device N" part + std::vector devices_vec; + for (const auto& device : devices) { + auto device_vec = device.to_vec(); + devices_vec.insert(devices_vec.end(), device_vec.begin(), device_vec.end()); + } + + // "Data length" in pdf + std::uint16_t size_in_header = devices_vec.size() + 1 + 2; // +1 for reporting type and +2 for number of devices + + std::uint16_t number_of_devices = devices.size(); + + GenericPDU generic; + generic.function_code = 0x41; + generic.data.push_back(0x91); // subfunction code + generic.data.push_back(size_in_header >> 8); + generic.data.push_back(size_in_header & 0xFF); + generic.data.push_back(response_required ? 0x80 : 0x00); // todo: bit 7, is this correct? or should it be 0x01? + generic.data.push_back(number_of_devices >> 8); + generic.data.push_back(number_of_devices & 0xFF); + generic.data.insert(generic.data.end(), devices_vec.begin(), devices_vec.end()); + + return generic; +} + +void UnsolicitatedReportResponse::from_generic(const GenericPDU& generic) { + if (generic.function_code != 0x41) { + throw DecodingError(generic, "UnsolicitatedReportResponse", "Invalid function code"); + } + + // todo: in the pdf there are only 5 bytes specified but the length is given + // in 16 bit??? + + if (generic.data.size() != 5) { + throw DecodingError(generic, "UnsolicitatedReportResponse", "Invalid data size (expected 5 bytes)"); + } + + if (generic.data[0] != 0x91) { + throw DecodingError(generic, "UnsolicitatedReportResponse", "Invalid subfunction code"); + } + + // todo: check if this is correct; we have no real world example of this... + // std::uint16_t size_in_header = (generic.data[1] << 8) | generic.data[2]; + // if (size_in_header != 2) { + // throw DecodingError(generic, "UnsolicitatedReportResponse", + // "Invalid data size in header (expected 1)"); + // } + + // todo: check if this is correct; we have no real world example of this... + success = generic.data[3] == 0x00 && generic.data[4] == 0x00; +} + +GenericPDU UnsolicitatedReportResponse ::to_generic() const { + GenericPDU generic; + generic.function_code = 0x41; + generic.data.push_back(0x91); + // todo: check if longer frames are possible + generic.data.push_back(0x00); + generic.data.push_back(0x01); + generic.data.push_back(0x00); + generic.data.push_back(success ? 0x00 : 0x01); + return generic; +} + +void UnsolicitatedReportRequest::defragment() { + for (auto& device : this->devices) { + device.defragment(); + } +} + +void UnsolicitatedReportRequest::Device::defragment() { + bool defragmented = false; + + while (!defragmented) { + bool round_defragmented = false; + for (size_t i = 0; i < segments.size(); i++) { + // check if another segment is directly after this one + for (size_t j = 0; j < segments.size(); j++) { + if (i == j) + continue; + + if (segments[i].registers_start + segments[i].registers_count == segments[j].registers_start) { + // merge segments + segments[i].registers_count += segments[j].registers_count; + segments[i].registers.insert(segments[i].registers.end(), segments[j].registers.begin(), + segments[j].registers.end()); + segments.erase(segments.begin() + j); + round_defragmented = true; + break; + } + } + + if (round_defragmented) + break; + } + + // if we didn't have to defragment this round, we are done + if (!round_defragmented) { + defragmented = true; + } + } +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/src/unsolicitated_report_server.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/src/unsolicitated_report_server.cpp new file mode 100644 index 0000000000..5a675a4fd9 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/src/unsolicitated_report_server.cpp @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include +#include + +using namespace fusion_charger::modbus_extensions; + +std::optional +UnsolicitatedReportBasicServer::send_unsolicitated_report(UnsolicitatedReportRequest& request, + std::chrono::milliseconds timeout) { + request.defragment(); + + if (!request.response_required) { + this->pas->request_without_response(request.to_generic()); + return std::nullopt; + } + + auto response_generic = this->pas->request_response(request.to_generic(), timeout); + + if (response_generic.function_code & 0x80) { + throw modbus_server::pdu::PDUException(response_generic); + } + + UnsolicitatedReportResponse response; + response.from_generic(response_generic); + + return response; +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/tests/unsolicitated_registry.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/tests/unsolicitated_registry.cpp new file mode 100644 index 0000000000..03bd7514b0 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/tests/unsolicitated_registry.cpp @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include +#include + +using namespace fusion_charger::modbus_extensions; +using namespace modbus::registers; +using namespace modbus::registers::data_providers; + +class TestSubregistry : public registry::ComplexRegisterSubregistry { +public: + struct DataProviders { + DataProvider& reg_holding; + DataProviderUnsolicitated& reg_unsolicitated1; + DataProviderUnsolicitated& reg_unsolicitated2; + }; + + TestSubregistry(DataProviders providers) { + // clang-format off + this->add(new complex_registers::ElementaryRegister(0x0000, providers.reg_holding, converters::ConverterABCD::instance())); + this->add(new complex_registers::ElementaryRegister(0x0001, providers.reg_unsolicitated1, converters::ConverterABCD::instance())); + this->add(new complex_registers::ElementaryRegister(0x0002, providers.reg_unsolicitated2, converters::ConverterABCD::instance())); + // clang-format on + } +}; + +TEST(UnsolicitatedRegistry, basic_positive_test) { + DataProviderHolding reg_holding(0x1234); + DataProviderUnsolicitatedEvent reg_unsolicitated1(0x5678); + DataProviderUnsolicitatedEvent reg_unsolicitated2(0x9abc); + UnsolicitatedRegistry registry; + + { + TestSubregistry::DataProviders data_providers{reg_holding, reg_unsolicitated1, reg_unsolicitated2}; + registry.add(std::make_shared(data_providers)); + } + + // nothing should report now + auto report = registry.unsolicitated_report(); + ASSERT_FALSE(report.has_value()); + + // if one wants to be reported, next report should include this + reg_unsolicitated1.report(0x1234); + report = registry.unsolicitated_report(); + ASSERT_TRUE(report.has_value()); + ASSERT_EQ(report->devices.size(), 1); + ASSERT_EQ(report->devices[0].location, 0x0000); + ASSERT_EQ(report->devices[0].segments.size(), 1); + ASSERT_EQ(report->devices[0].segments[0].registers_start, 0x0001); + ASSERT_EQ(report->devices[0].segments[0].registers_count, 0x0001); + ASSERT_EQ(report->devices[0].segments[0].registers.size(), 2); + ASSERT_EQ(report->devices[0].segments[0].registers[0], 0x12); + ASSERT_EQ(report->devices[0].segments[0].registers[1], 0x34); + + // nothing should report now + report = registry.unsolicitated_report(); + ASSERT_FALSE(report.has_value()); + + // if both want to be reported, next report should include both, defragmented + reg_unsolicitated1.report(0xdead); + reg_unsolicitated2.report(0xbeef); + report = registry.unsolicitated_report(); + ASSERT_TRUE(report.has_value()); + ASSERT_EQ(report->devices.size(), 1); + ASSERT_EQ(report->devices[0].location, 0x0000); + ASSERT_EQ(report->devices[0].segments.size(), 1); + ASSERT_EQ(report->devices[0].segments[0].registers_start, 0x0001); + ASSERT_EQ(report->devices[0].segments[0].registers_count, 0x0002); + ASSERT_EQ(report->devices[0].segments[0].registers.size(), 4); + ASSERT_EQ(report->devices[0].segments[0].registers[0], 0xde); + ASSERT_EQ(report->devices[0].segments[0].registers[1], 0xad); + ASSERT_EQ(report->devices[0].segments[0].registers[2], 0xbe); + ASSERT_EQ(report->devices[0].segments[0].registers[3], 0xef); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/tests/unsolicitated_report.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/tests/unsolicitated_report.cpp new file mode 100644 index 0000000000..3e531672a9 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/tests/unsolicitated_report.cpp @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include + +using namespace fusion_charger::modbus_extensions; + +TEST(UnsolicitatedReportRequest_Device, defragment_2_segments) { + UnsolicitatedReportRequest::Device device; + + device.segments.push_back({0, 2, {0x00, 0x01, 0x00, 0x02}}); + device.segments.push_back({2, 2, {0x00, 0x03, 0x00, 0x04}}); + + device.defragment(); + + ASSERT_EQ(device.segments.size(), 1); + ASSERT_EQ(device.segments[0].registers_start, 0); + ASSERT_EQ(device.segments[0].registers_count, 4); + ASSERT_EQ(device.segments[0].registers.size(), 8); + ASSERT_EQ(device.segments[0].registers[0], 0x00); + ASSERT_EQ(device.segments[0].registers[1], 0x01); + ASSERT_EQ(device.segments[0].registers[2], 0x00); + ASSERT_EQ(device.segments[0].registers[3], 0x02); + ASSERT_EQ(device.segments[0].registers[4], 0x00); + ASSERT_EQ(device.segments[0].registers[5], 0x03); + ASSERT_EQ(device.segments[0].registers[6], 0x00); + ASSERT_EQ(device.segments[0].registers[7], 0x04); + + device.segments.clear(); + + device.segments.push_back({7, 2, {0x00, 0x01, 0x00, 0x02}}); + device.segments.push_back({4, 3, {0x00, 0x03, 0x00, 0x04, 0xde, 0xad}}); + + device.defragment(); + ASSERT_EQ(device.segments.size(), 1); + + ASSERT_EQ(device.segments[0].registers_start, 4); + ASSERT_EQ(device.segments[0].registers_count, 5); + ASSERT_EQ(device.segments[0].registers.size(), 10); + ASSERT_EQ(device.segments[0].registers[0], 0x00); + ASSERT_EQ(device.segments[0].registers[1], 0x03); + ASSERT_EQ(device.segments[0].registers[2], 0x00); + ASSERT_EQ(device.segments[0].registers[3], 0x04); + ASSERT_EQ(device.segments[0].registers[4], 0xde); + ASSERT_EQ(device.segments[0].registers[5], 0xad); + ASSERT_EQ(device.segments[0].registers[6], 0x00); + ASSERT_EQ(device.segments[0].registers[7], 0x01); + ASSERT_EQ(device.segments[0].registers[8], 0x00); + ASSERT_EQ(device.segments[0].registers[9], 0x02); +} + +TEST(UnsolicitatedReportRequest_Device, defragment_3_segments) { + UnsolicitatedReportRequest::Device device; + + device.segments.push_back({0, 2, {0x00, 0x01, 0x00, 0x02}}); + device.segments.push_back({2, 2, {0x00, 0x03, 0x00, 0x04}}); + device.segments.push_back({4, 2, {0x00, 0x05, 0x00, 0x06}}); + + device.defragment(); + + ASSERT_EQ(device.segments.size(), 1); + ASSERT_EQ(device.segments[0].registers_start, 0); + ASSERT_EQ(device.segments[0].registers_count, 6); + ASSERT_EQ(device.segments[0].registers.size(), 12); + ASSERT_EQ(device.segments[0].registers[0], 0x00); + ASSERT_EQ(device.segments[0].registers[1], 0x01); + ASSERT_EQ(device.segments[0].registers[2], 0x00); + ASSERT_EQ(device.segments[0].registers[3], 0x02); + ASSERT_EQ(device.segments[0].registers[4], 0x00); + ASSERT_EQ(device.segments[0].registers[5], 0x03); + ASSERT_EQ(device.segments[0].registers[6], 0x00); + ASSERT_EQ(device.segments[0].registers[7], 0x04); + ASSERT_EQ(device.segments[0].registers[8], 0x00); + ASSERT_EQ(device.segments[0].registers[9], 0x05); + ASSERT_EQ(device.segments[0].registers[10], 0x00); + ASSERT_EQ(device.segments[0].registers[11], 0x06); +} + +TEST(UnsolicitatedReportRequest_Device, defragment_2_segments_in_pool_of_3) { + UnsolicitatedReportRequest::Device device; + + device.segments.push_back({0, 2, {0x00, 0x01, 0x00, 0x02}}); + device.segments.push_back({2, 2, {0x00, 0x03, 0x00, 0x04}}); + device.segments.push_back({5, 2, {0x00, 0x05, 0x00, 0x06}}); + + device.defragment(); + + ASSERT_EQ(device.segments.size(), 2); + ASSERT_EQ(device.segments[0].registers_start, 0); + ASSERT_EQ(device.segments[0].registers_count, 4); + ASSERT_EQ(device.segments[0].registers.size(), 8); + ASSERT_EQ(device.segments[0].registers[0], 0x00); + ASSERT_EQ(device.segments[0].registers[1], 0x01); + ASSERT_EQ(device.segments[0].registers[2], 0x00); + ASSERT_EQ(device.segments[0].registers[3], 0x02); + ASSERT_EQ(device.segments[0].registers[4], 0x00); + ASSERT_EQ(device.segments[0].registers[5], 0x03); + ASSERT_EQ(device.segments[0].registers[6], 0x00); + ASSERT_EQ(device.segments[0].registers[7], 0x04); + + ASSERT_EQ(device.segments[1].registers_start, 5); + ASSERT_EQ(device.segments[1].registers_count, 2); + ASSERT_EQ(device.segments[1].registers.size(), 4); + ASSERT_EQ(device.segments[1].registers[0], 0x00); + ASSERT_EQ(device.segments[1].registers[1], 0x05); + ASSERT_EQ(device.segments[1].registers[2], 0x00); + ASSERT_EQ(device.segments[1].registers[3], 0x06); +} + +TEST(UnsolicitatedReportRequest_Device, defragment_doesnt_defragment_non_defragmentable) { + UnsolicitatedReportRequest::Device device; + + device.segments.push_back({0, 2, {0x00, 0x01, 0x00, 0x02}}); + device.segments.push_back({3, 2, {0x00, 0x03, 0x00, 0x04}}); + + device.defragment(); + + ASSERT_EQ(device.segments.size(), 2); +} + +TEST(UnsolicitatedReportRequest, positive_test) { + UnsolicitatedReportRequest req; + UnsolicitatedReportRequest::Device device1234; + + device1234.location = 0x1234; + device1234.segments.push_back({0, 2, {0xba, 0xad, 0xca, 0xfe}}); + device1234.segments.push_back({2, 2, {0xbe, 0xef, 0xfe, 0xed}}); + device1234.segments.push_back({5, 1, {0xde, 0xad}}); + + req.devices.push_back(device1234); + req.response_required = true; + + auto generic = req.to_generic(); + + ASSERT_EQ(generic.function_code, 0x41); + ASSERT_EQ(generic.data[0], 0x91); + // length + ASSERT_EQ(generic.data[1], 0x00); + ASSERT_EQ(generic.data[2], 29); + // response required + ASSERT_EQ(generic.data[3], 0x80); + // number of devices + ASSERT_EQ(generic.data[4], 0x00); + ASSERT_EQ(generic.data[5], 0x01); + // device 1 + ASSERT_EQ(generic.data[6], 0x12); + ASSERT_EQ(generic.data[7], 0x34); + // segment count + ASSERT_EQ(generic.data[8], 0x00); + ASSERT_EQ(generic.data[9], 0x03); + // segment 1 + ASSERT_EQ(generic.data[10], 0x00); + ASSERT_EQ(generic.data[11], 0x00); + ASSERT_EQ(generic.data[12], 0x00); + ASSERT_EQ(generic.data[13], 0x02); + ASSERT_EQ(generic.data[14], 0xba); + ASSERT_EQ(generic.data[15], 0xad); + ASSERT_EQ(generic.data[16], 0xca); + ASSERT_EQ(generic.data[17], 0xfe); + // segment 2 + ASSERT_EQ(generic.data[18], 0x00); + ASSERT_EQ(generic.data[19], 0x02); + ASSERT_EQ(generic.data[20], 0x00); + ASSERT_EQ(generic.data[21], 0x02); + ASSERT_EQ(generic.data[22], 0xbe); + ASSERT_EQ(generic.data[23], 0xef); + ASSERT_EQ(generic.data[24], 0xfe); + ASSERT_EQ(generic.data[25], 0xed); + // segment 3 + ASSERT_EQ(generic.data[26], 0x00); + ASSERT_EQ(generic.data[27], 0x05); + ASSERT_EQ(generic.data[28], 0x00); + ASSERT_EQ(generic.data[29], 0x01); + ASSERT_EQ(generic.data[30], 0xde); + ASSERT_EQ(generic.data[31], 0xad); +} + +TEST(UnsolicitatedReportRequest, positive_test_defragmented) { + UnsolicitatedReportRequest req; + UnsolicitatedReportRequest::Device device1234; + + device1234.location = 0x1234; + device1234.segments.push_back({0, 2, {0xba, 0xad, 0xca, 0xfe}}); + device1234.segments.push_back({2, 2, {0xbe, 0xef, 0xfe, 0xed}}); + device1234.segments.push_back({5, 1, {0xde, 0xad}}); + + req.devices.push_back(device1234); + req.response_required = true; + req.defragment(); + + auto generic = req.to_generic(); + + ASSERT_EQ(generic.function_code, 0x41); + ASSERT_EQ(generic.data[0], 0x91); + // length + ASSERT_EQ(generic.data[1], 0x00); + ASSERT_EQ(generic.data[2], 25); + // response required + ASSERT_EQ(generic.data[3], 0x80); + // number of devices + ASSERT_EQ(generic.data[4], 0x00); + ASSERT_EQ(generic.data[5], 0x01); + // device 1 + ASSERT_EQ(generic.data[6], 0x12); + ASSERT_EQ(generic.data[7], 0x34); + // segment count + ASSERT_EQ(generic.data[8], 0x00); + ASSERT_EQ(generic.data[9], 0x02); + // segment 1+2 + ASSERT_EQ(generic.data[10], 0x00); + ASSERT_EQ(generic.data[11], 0x00); + ASSERT_EQ(generic.data[12], 0x00); + ASSERT_EQ(generic.data[13], 0x04); + ASSERT_EQ(generic.data[14], 0xba); + ASSERT_EQ(generic.data[15], 0xad); + ASSERT_EQ(generic.data[16], 0xca); + ASSERT_EQ(generic.data[17], 0xfe); + ASSERT_EQ(generic.data[18], 0xbe); + ASSERT_EQ(generic.data[19], 0xef); + ASSERT_EQ(generic.data[20], 0xfe); + ASSERT_EQ(generic.data[21], 0xed); + // segment 3 + ASSERT_EQ(generic.data[22], 0x00); + ASSERT_EQ(generic.data[23], 0x05); + ASSERT_EQ(generic.data[24], 0x00); + ASSERT_EQ(generic.data[25], 0x01); + ASSERT_EQ(generic.data[26], 0xde); + ASSERT_EQ(generic.data[27], 0xad); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/tests/unsolicitated_report_registers.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/tests/unsolicitated_report_registers.cpp new file mode 100644 index 0000000000..34bf151582 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/tests/unsolicitated_report_registers.cpp @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include + +using namespace fusion_charger::modbus_extensions; + +TEST(DataProviderHoldingUnsolicitated, basic_positive_test) { + bool unsolicitated_report = true; + + DataProviderHoldingUnsolicitatedReportCallback data_provider( + 0x1234, [&unsolicitated_report]() { return unsolicitated_report; }); + modbus::registers::data_providers::DataProviderHolding& data_provider_base = data_provider; + modbus::registers::complex_registers::ElementaryRegister reg( + 0x1234, data_provider_base, modbus::registers::converters::ConverterABCD::instance()); + + // check initial value + auto val = reg.on_read(); + ASSERT_EQ(val[0], 0x12); + ASSERT_EQ(val[1], 0x34); + + auto report = unsolicitated_report_helper(®); + ASSERT_TRUE(report.has_value()); + ASSERT_EQ(report.value().size(), 2); + ASSERT_EQ(report.value()[0], 0x12); + ASSERT_EQ(report.value()[1], 0x34); + + unsolicitated_report = false; + report = unsolicitated_report_helper(®); + ASSERT_FALSE(report.has_value()); + + unsolicitated_report = true; + reg.on_write(0, {0x56, 0x78}); + + val = reg.on_read(); + ASSERT_EQ(val[0], 0x56); + ASSERT_EQ(val[1], 0x78); + + report = unsolicitated_report_helper(®); + ASSERT_TRUE(report.has_value()); + ASSERT_EQ(report.value().size(), 2); + ASSERT_EQ(report.value()[0], 0x56); + ASSERT_EQ(report.value()[1], 0x78); +} + +TEST(DataProviderUnsolicitatedEvent, basic_positive_test) { + bool unsolicitated_report = true; + + DataProviderUnsolicitatedEvent data_provider(0x1234); + modbus::registers::data_providers::DataProviderHolding& data_provider_base = data_provider; + modbus::registers::complex_registers::ElementaryRegister reg( + 0x1234, data_provider_base, modbus::registers::converters::ConverterABCD::instance()); + + // read always works + auto val = reg.on_read(); + ASSERT_EQ(val[0], 0x12); + ASSERT_EQ(val[1], 0x34); + + // should not report yet + auto report = unsolicitated_report_helper(®); + ASSERT_FALSE(report.has_value()); + + report = unsolicitated_report_helper(®); + ASSERT_FALSE(report.has_value()); + + // report + data_provider.report(0x5678); + + // read should work + val = reg.on_read(); + ASSERT_EQ(val[0], 0x56); + ASSERT_EQ(val[1], 0x78); + + // should report once + report = unsolicitated_report_helper(®); + ASSERT_TRUE(report.has_value()); + ASSERT_EQ(report.value().size(), 2); + ASSERT_EQ(report.value()[0], 0x56); + ASSERT_EQ(report.value()[1], 0x78); + + // should not report again + report = unsolicitated_report_helper(®); + ASSERT_FALSE(report.has_value()); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/tests/unsolicitated_report_server.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/tests/unsolicitated_report_server.cpp new file mode 100644 index 0000000000..bcdcd5bacf --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/huawei-fusioncharge-driver/libs/fusion_charger_modbus_extensions/tests/unsolicitated_report_server.cpp @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include + +using namespace fusion_charger::modbus_extensions; + +class DummyPDUCorrelationLayer : public modbus_server::PDUCorrelationLayerIntf { + std::vector next_answer; + std::vector last_request; + +public: + DummyPDUCorrelationLayer() = default; + + void blocking_poll() override { + } + bool poll() override { + return false; + } + + modbus_server::pdu::GenericPDU request_response(const modbus_server::pdu::GenericPDU& request, + std::chrono::milliseconds timeout) override { + last_request.push_back(request); + + if (next_answer.empty()) { + throw std::runtime_error("No answer available"); + } + auto answer = next_answer.front(); + next_answer.erase(next_answer.begin()); + return answer; + } + + void request_without_response(const modbus_server::pdu::GenericPDU& request) override { + last_request.push_back(request); + } + + void add_next_answer(const modbus_server::pdu::GenericPDU& answer) { + next_answer.push_back(answer); + } + + std::optional call_on_pdu(const modbus_server::pdu::GenericPDU& pdu) { + return this->on_pdu.value()(pdu); + } + + modbus_server::pdu::GenericPDU get_last_request() { + if (last_request.empty()) { + throw std::runtime_error("No request available"); + } + auto request = last_request.front(); + last_request.erase(last_request.begin()); + return request; + } +}; + +TEST(UnsolicitatedReportBasicServer, send_unsolicitated_report_no_response) { + auto corr_layer = std::make_shared(); + UnsolicitatedReportBasicServer server(corr_layer); + UnsolicitatedReportRequest request; + request.devices.push_back({0x1234, {{0x5678, 0x0001, {0x00, 0x01}}}}); + request.response_required = false; + + auto before_time = std::chrono::system_clock().now(); + auto response = server.send_unsolicitated_report(request, std::chrono::milliseconds(100)); + auto after_time = std::chrono::system_clock().now(); + + ASSERT_FALSE(response.has_value()); + + // Sending should be fast as no response is expected + ASSERT_TRUE(after_time - before_time < std::chrono::milliseconds(10)); + + auto last_request = corr_layer->get_last_request(); + ASSERT_EQ(last_request.function_code, 0x41); + ASSERT_EQ(last_request.data[0], 0x91); + // len + ASSERT_EQ(last_request.data[1], 0x00); + ASSERT_EQ(last_request.data[2], 13); + // no response + ASSERT_EQ(last_request.data[3], 0x00); + // devices + ASSERT_EQ(last_request.data[4], 0x00); + ASSERT_EQ(last_request.data[5], 0x01); + // location + ASSERT_EQ(last_request.data[6], 0x12); + ASSERT_EQ(last_request.data[7], 0x34); + // segments + ASSERT_EQ(last_request.data[8], 0x00); + ASSERT_EQ(last_request.data[9], 0x01); + // start + ASSERT_EQ(last_request.data[10], 0x56); + ASSERT_EQ(last_request.data[11], 0x78); + // more doesn't need to be checked... +} + +TEST(UnsolicitatedReportBasicServer, send_unsolicitated_report_with_response) { + auto corr_layer = std::make_shared(); + UnsolicitatedReportBasicServer server(corr_layer); + + UnsolicitatedReportRequest request; + request.devices.push_back({0x1234, {{0x5678, 0x0001, {0x00, 0x01}}}}); + request.response_required = true; + + { + UnsolicitatedReportResponse response; + response.success = true; + + corr_layer->add_next_answer(response.to_generic()); + } + + auto before_time = std::chrono::system_clock().now(); + auto response = server.send_unsolicitated_report(request, std::chrono::milliseconds(100)); + auto after_time = std::chrono::system_clock().now(); + + // Sending should be no longer than timeout + ASSERT_TRUE(after_time - before_time < std::chrono::milliseconds(100)); + + auto last_request = corr_layer->get_last_request(); + ASSERT_EQ(last_request.function_code, 0x41); + ASSERT_EQ(last_request.data[0], 0x91); + // len + ASSERT_EQ(last_request.data[1], 0x00); + ASSERT_EQ(last_request.data[2], 13); + // with response + ASSERT_EQ(last_request.data[3], 0x80); + // more doesn't need to be checked... + + ASSERT_TRUE(response.has_value()); + ASSERT_TRUE(response.value().success); +} + +TEST(UnsolicitatedReportBasicServer, sent_unsolicitated_report_is_defragmented) { + auto corr_layer = std::make_shared(); + UnsolicitatedReportBasicServer server(corr_layer); + UnsolicitatedReportRequest request; + request.devices.push_back({0x1234, + { + {0x5678, 0x0001, {0xde, 0xad}}, + {0x5679, 0x0001, {0xbe, 0xef}}, + }}); + request.response_required = false; + + auto before_time = std::chrono::system_clock().now(); + auto response = server.send_unsolicitated_report(request, std::chrono::milliseconds(100)); + auto after_time = std::chrono::system_clock().now(); + + ASSERT_FALSE(response.has_value()); + + // Sending should be fast as no response is expected + ASSERT_TRUE(after_time - before_time < std::chrono::milliseconds(10)); + + auto last_request = corr_layer->get_last_request(); + ASSERT_EQ(last_request.function_code, 0x41); + ASSERT_EQ(last_request.data.size(), 18); + ASSERT_EQ(last_request.data[0], 0x91); + // len + ASSERT_EQ(last_request.data[1], 0x00); + ASSERT_EQ(last_request.data[2], 15); + // no response + ASSERT_EQ(last_request.data[3], 0x00); + // devices + ASSERT_EQ(last_request.data[4], 0x00); + ASSERT_EQ(last_request.data[5], 0x01); + // location + ASSERT_EQ(last_request.data[6], 0x12); + ASSERT_EQ(last_request.data[7], 0x34); + // segments, only one segment should be sent + ASSERT_EQ(last_request.data[8], 0x00); + ASSERT_EQ(last_request.data[9], 0x01); + // start + ASSERT_EQ(last_request.data[10], 0x56); + ASSERT_EQ(last_request.data[11], 0x78); + // count + ASSERT_EQ(last_request.data[12], 0x00); + ASSERT_EQ(last_request.data[13], 0x02); + // data + ASSERT_EQ(last_request.data[14], 0xde); + ASSERT_EQ(last_request.data[15], 0xad); + ASSERT_EQ(last_request.data[16], 0xbe); + ASSERT_EQ(last_request.data[17], 0xef); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/log/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/log/CMakeLists.txt new file mode 100644 index 0000000000..3d32afc8c4 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/log/CMakeLists.txt @@ -0,0 +1,5 @@ +# add_library(huawei-fusion-charger-log-interface-lib STATIC logs.cpp) +add_library(huawei-fusion-charger-log-interface-lib logs.cpp) +target_include_directories(huawei-fusion-charger-log-interface-lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + +add_library(Huawei::FusionCharger::LogInterface ALIAS huawei-fusion-charger-log-interface-lib) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/log/logs.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/log/logs.cpp new file mode 100644 index 0000000000..a5586ac94c --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/log/logs.cpp @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include "logs/logs.hpp" + +namespace logs { + +logs::LogIntf log_printf{ + logs::LogFun([](const std::string& message) { printf("ERROR: %s\n", message.c_str()); }), + logs::LogFun([](const std::string& message) { printf("WARNING: %s\n", message.c_str()); }), + logs::LogFun([](const std::string& message) { printf("INFO: %s\n", message.c_str()); }), + logs::LogFun([](const std::string& message) { printf("DEBUG: %s\n", message.c_str()); }), + .verbose = logs::LogFun([](const std::string& message) { printf("VERBOSE: %s\n", message.c_str()); }), +}; + +}; // namespace logs diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/log/logs/logs.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/log/logs/logs.hpp new file mode 100644 index 0000000000..3e0600293f --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/log/logs/logs.hpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include + +namespace logs { + +class LogFun { + std::function fn; + +public: + LogFun(std::function fn) : fn(fn) { + } + + void operator<<(const std::string& message) { + fn(message); + } +}; + +struct LogIntf { + LogFun error; + LogFun warning; + LogFun info; + LogFun debug; + LogFun verbose; +}; + +extern LogIntf log_printf; + +}; // namespace logs diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/.gitignore b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/.gitignore new file mode 100644 index 0000000000..7ac6434eba --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/.gitignore @@ -0,0 +1,5 @@ +.vscode/settings.json + +build/ +.venv/ +.cache/ diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/CMakeLists.txt new file mode 100644 index 0000000000..4c78a1d035 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/CMakeLists.txt @@ -0,0 +1,7 @@ +if(POLICY CMP0135) + cmake_policy(SET CMP0135 NEW) +endif() + +add_subdirectory(libs) +add_subdirectory(examples) + diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/README.md b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/README.md new file mode 100644 index 0000000000..6f545df725 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/README.md @@ -0,0 +1,39 @@ +# Modbus Server / Client library + +## Build and test + +This library is built and tested as part of the build process of everest-core. + +## Run examples + +### Modbus basic server + +Note: you need an modbus tcp client for this example + +```bash +cd build/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server +./examples/dummy_basic_server +``` + +### SSL Modbus server + client + +#### Setup SSL certificates + +```bash +cd certs +./generate.sh +``` + +#### Run server + +```bash +cd build +./examples/dummy_basic_ssl_server +``` + +#### Run client + +```bash +cd build +./examples/dummy_basic_ssl_client +``` diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/certs/.gitignore b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/certs/.gitignore new file mode 100644 index 0000000000..2d807a077b --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/certs/.gitignore @@ -0,0 +1,2 @@ +*.pem +*.srl diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/certs/generate.sh b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/certs/generate.sh new file mode 100755 index 0000000000..4570cad0c4 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/certs/generate.sh @@ -0,0 +1,10 @@ +openssl genrsa -out ca.key.pem 2048 +openssl genrsa -out server.key.pem 2048 +openssl genrsa -out client.key.pem 2048 +openssl req -new -x509 -days 1000 -key ca.key.pem -out ca.crt.pem -subj "/C=DE/O=Frickly Systems GmbH/CN=The one and only Root CA" + +openssl req -new -key server.key.pem -out server.csr.pem -subj "/C=DE/O=Frickly Systems GmbH/CN=localhost" +openssl req -new -key client.key.pem -out client.csr.pem -subj "/C=DE/O=Frickly Systems GmbH/CN=client" + +openssl x509 -req -in server.csr.pem -out server.crt.pem -CA ca.crt.pem -CAkey ca.key.pem -CAcreateserial -days 1000 +openssl x509 -req -in client.csr.pem -out client.crt.pem -CA ca.crt.pem -CAkey ca.key.pem -CAcreateserial -days 1000 diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/examples/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/examples/CMakeLists.txt new file mode 100644 index 0000000000..504941a0ad --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/examples/CMakeLists.txt @@ -0,0 +1,16 @@ +add_executable(dummy_basic_server dummy_basic_server.cpp) +target_link_libraries(dummy_basic_server PRIVATE modbus-server) + +add_executable(dummy_extended_server dummy_extended_server.cpp) +target_link_libraries(dummy_extended_server PRIVATE modbus-server) + + +add_executable(dummy_basic_ssl_server dummy_basic_ssl_server.cpp) +target_link_libraries(dummy_basic_ssl_server PRIVATE modbus-server modbus-ssl) + +add_executable(dummy_basic_ssl_client dummy_basic_ssl_client.cpp) +target_link_libraries(dummy_basic_ssl_client PRIVATE modbus-client modbus-ssl) + + +add_executable(mtls_basic_script mtls_basic_script.cpp) +target_link_libraries(mtls_basic_script modbus-ssl) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/examples/dummy_basic_server.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/examples/dummy_basic_server.cpp new file mode 100644 index 0000000000..fe2c85b1fd --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/examples/dummy_basic_server.cpp @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace modbus_server; + +int main() { + printf("Hello wold\n"); + + int sock = socket(AF_INET, SOCK_STREAM, 0); + struct sockaddr_in serv_addr; + serv_addr.sin_family = AF_INET; + serv_addr.sin_addr.s_addr = INADDR_ANY; + serv_addr.sin_port = htons(502); + 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"); + } + + while (true) { + int client_sock = accept(sock, NULL, NULL); + + auto transport = std::make_shared(client_sock); + auto protocol = std::make_shared(transport); + auto pas = std::make_shared(protocol); + + ModbusBasicServer server(pas); + + server.set_read_holding_registers_request_cb([](const pdu::ReadHoldingRegistersRequest& req) { + std::vector data; + + if (req.register_start > 0x1000) { + throw ApplicationServerError(pdu::PDUExceptionCode::ILLEGAL_DATA_ADDRESS); + } + + for (int i = 0; i < req.register_count; i++) { + data.push_back(0x00); + data.push_back(0x01); + } + + pdu::ReadHoldingRegistersResponse resp(req, data); + return resp; + }); + + try { + while (1) { + pas->blocking_poll(); + } + } catch (transport_exceptions::ConnectionClosedException e) { + printf("Transport exception: %s\n", e.what()); + printf("Accepting new connection\n"); + } + + close(client_sock); + } +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/examples/dummy_basic_ssl_client.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/examples/dummy_basic_ssl_client.cpp new file mode 100644 index 0000000000..004e742948 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/examples/dummy_basic_ssl_client.cpp @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include +#include +#include +#include +#include + +#include +#include +#include + +int main() { + 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"); + } + + SSL_CTX_use_certificate_file(ctx, "../certs/ca.crt.pem", SSL_FILETYPE_PEM); + SSL_CTX_load_verify_locations(ctx, "../certs/ca.crt.pem", NULL); + SSL_CTX_use_certificate_file(ctx, "../certs/client.crt.pem", SSL_FILETYPE_PEM); + SSL_CTX_use_PrivateKey_file(ctx, "../certs/client.key.pem", SSL_FILETYPE_PEM); + + if (!SSL_CTX_check_private_key(ctx)) { + throw std::runtime_error("Private key invalid"); + } + + int sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock < 0) { + throw std::runtime_error("Socket not opened"); + } + struct sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + addr.sin_port = htons(802); + + printf("Connecting...\n"); + if (connect(sock, (struct sockaddr*)&addr, sizeof(addr))) { + throw std::runtime_error("Could not connect"); + }; + printf("TCP Connected\n"); + + SSL* ssl = SSL_new(ctx); + + SSL_set_fd(ssl, sock); + + if (SSL_connect(ssl) != 1) { + throw std::runtime_error("Could not connect to server"); + } + + printf("TLS Connected\n"); + + auto transport = std::make_shared(ssl); + auto protocol = std::make_shared(transport); + auto pas = std::make_shared(protocol); + + modbus_server::client::ModbusClient client = modbus_server::client::ModbusClient(pas); + + auto thread = std::thread([&pas]() { + try { + while (true) { + pas->blocking_poll(); + } + } catch (const std::exception& e) { + } + }); + + printf("Reading 16 registers...\n"); + auto data = client.read_holding_registers(0x0000, 0x0010); + printf("...done, result is:\n"); + for (auto& d : data) { + printf(" 0x%04x\n", d); + } + + printf("Reading another 2 registers...\n"); + data = client.read_holding_registers(0x0010, 0x0002); + printf("...done, result is:\n"); + for (auto& d : data) { + printf(" 0x%04x\n", d); + } + + printf("Closing connection\n"); + + SSL_shutdown(ssl); + SSL_free(ssl); + + close(sock); + + thread.join(); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/examples/dummy_basic_ssl_server.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/examples/dummy_basic_ssl_server.cpp new file mode 100644 index 0000000000..3b71c0ece2 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/examples/dummy_basic_ssl_server.cpp @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +// for wireshark tls decryption set SSLKEYLOGFILE to a file and run the program. +// Concurrently run wireshark and in the preferences>protocols>tls set the +// keylog file to the same file. + +static void keylog(const SSL* ssl, const char* line) { + char* file_name = getenv("SSLKEYLOGFILE"); + if (file_name == NULL) { + return; + } + + int file = open(file_name, O_WRONLY | O_APPEND); + if (file < 0) { + printf("Could not open keylog %d\n", errno); + return; + } + + int err = write(file, line, strlen(line)); + if (err < 0) { + printf("Could not write keylog %d", errno); + close(file); + return; + } + + write(file, "\n", 1); + + close(file); +} + +int main() { + 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"); + } + + SSL_CTX_set_keylog_callback(ctx, keylog); + + SSL_CTX_use_certificate_file(ctx, "../certs/ca.crt.pem", SSL_FILETYPE_PEM); + SSL_CTX_load_verify_locations(ctx, "../certs/ca.crt.pem", NULL); + SSL_CTX_use_certificate_file(ctx, "../certs/server.crt.pem", SSL_FILETYPE_PEM); + SSL_CTX_use_PrivateKey_file(ctx, "../certs/server.key.pem", SSL_FILETYPE_PEM); + + 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 | SSL_VERIFY_CLIENT_ONCE, NULL); + SSL_CTX_set_verify_depth(ctx, 10); + + int sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock < 0) { + throw std::runtime_error("Socket not opened"); + } + struct sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(802); + + if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + throw std::runtime_error("Could not bind"); + } + + if (listen(sock, 5) < 0) { + throw std::runtime_error("Could not listen"); + } + + while (1) { + printf("Waiting for client\n"); + + int client_sock = accept(sock, 0, 0); + SSL* ssl = SSL_new(ctx); + + SSL_set_fd(ssl, client_sock); + + int ret = SSL_accept(ssl); + if (ret != 1) { + printf("TLS Connection failed\n"); + SSL_free(ssl); + close(client_sock); + continue; + } + + printf("TLS Connected successfully\n"); + + auto transport = std::make_shared(ssl); + auto protocol = std::make_shared(transport); + auto pas = std::make_shared(protocol); + + modbus_server::ModbusBasicServer client = modbus_server::ModbusBasicServer(pas); + + client.set_read_holding_registers_request_cb([](modbus_server::pdu::ReadHoldingRegistersRequest request) { + std::vector data(request.register_count * 2); + + for (size_t i = 0; i < request.register_count * 2; i++) { + data[i] = i; + } + + return modbus_server::pdu::ReadHoldingRegistersResponse(request, data); + }); + + printf("Running server\n"); + try { + while (1) { + pas->blocking_poll(); + } + } catch (modbus_server::transport_exceptions::ConnectionClosedException e) { + printf("Connection closed by peer\n"); + + SSL_shutdown(ssl); + } catch (modbus_ssl::OpenSSLTransportException e) { + printf("Exception: %s\n", e.what()); + + // see man SSL_GET_ERROR(3ssl) + if (e.get_openssl_error() != SSL_ERROR_SSL) { + SSL_shutdown(ssl); + } + } catch (std::exception e) { + printf("Other Exception: %s\n", e.what()); + SSL_shutdown(ssl); + } + + SSL_free(ssl); + close(client_sock); + } +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/examples/dummy_extended_server.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/examples/dummy_extended_server.cpp new file mode 100644 index 0000000000..b971989f1d --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/examples/dummy_extended_server.cpp @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +struct CustomPDURequest : public modbus_server::pdu::SpecificPDU { + std::uint8_t data[2]; + +public: + void from_generic(const modbus_server::pdu::GenericPDU& input) override { + if (input.function_code != 0x41) { + throw modbus_server::pdu::DecodingError(input, "CustomPDURequest", "Invalid function code"); + } + + if (input.data.size() != 2) { + throw modbus_server::pdu::DecodingError(input, "CustomPDURequest", "Invalid data size"); + } + + this->data[0] = input.data[0]; + this->data[1] = input.data[1]; + } + + modbus_server::pdu::GenericPDU to_generic() const override { + modbus_server::pdu::GenericPDU output; + output.function_code = 0x41; + output.data.push_back(data[0]); + output.data.push_back(data[1]); + return output; + } +}; +struct CustomPDUResponse : public modbus_server::pdu::SpecificPDU { + std::uint8_t data[3]; + +public: + void from_generic(const modbus_server::pdu::GenericPDU& input) override { + if (input.function_code != 0x41) { + throw modbus_server::pdu::DecodingError(input, "CustomPDUResponse", "Invalid function code"); + } + + if (input.data.size() != 3) { + throw modbus_server::pdu::DecodingError(input, "CustomPDUResponse", "Invalid data size"); + } + + this->data[0] = input.data[0]; + this->data[1] = input.data[1]; + this->data[2] = input.data[2]; + } + + modbus_server::pdu::GenericPDU to_generic() const override { + modbus_server::pdu::GenericPDU output; + output.function_code = 0x41; + output.data.push_back(data[0]); + output.data.push_back(data[1]); + output.data.push_back(data[2]); + return output; + } +}; + +class CustomServer : public modbus_server::ModbusBasicServer { +protected: + std::optional> custom_pdu_cb; + +public: + CustomServer(std::shared_ptr pal) : modbus_server::ModbusBasicServer(pal) { + } + + void set_custom_pdu_cb( + modbus_server::ModbusBasicServer::AlwaysRespondingPDUHandler fn) { + custom_pdu_cb = fn; + } + +protected: + std::optional on_pdu(const modbus_server::pdu::GenericPDU& input) override { + switch (input.function_code) { + case 0x41: { + if (custom_pdu_cb.has_value()) { + CustomPDURequest req; + req.from_generic(input); + auto resp = custom_pdu_cb.value()(req); + return resp.to_generic(); + } + } + } + + return modbus_server::ModbusBasicServer::on_pdu(input); + } +}; + +int main() { + printf("Hello wold\n"); + + int sock = socket(AF_INET, SOCK_STREAM, 0); + struct sockaddr_in serv_addr; + serv_addr.sin_family = AF_INET; + serv_addr.sin_addr.s_addr = INADDR_ANY; + serv_addr.sin_port = htons(502); + 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"); + } + + while (true) { + int client_sock = accept(sock, NULL, NULL); + + auto transport = std::make_shared(client_sock); + auto protocol = std::make_shared(transport); + auto pas = std::make_shared(protocol); + + CustomServer server(pas); + + server.set_read_holding_registers_request_cb([](const modbus_server::pdu::ReadHoldingRegistersRequest& req) { + std::vector data; + + for (int i = 0; i < req.register_count; i++) { + data.push_back(0x00); + data.push_back(i); + } + + modbus_server::pdu::ReadHoldingRegistersResponse resp(req, data); + return resp; + }); + + server.set_custom_pdu_cb([](const CustomPDURequest& req) { + CustomPDUResponse resp; + resp.data[0] = req.data[0]; + resp.data[1] = req.data[1]; + resp.data[2] = 0x42; + return resp; + }); + + try { + while (1) { + pas->blocking_poll(); + } + } catch (modbus_server::transport_exceptions::ConnectionClosedException e) { + printf("Transport exception: %s\n", e.what()); + printf("Accepting new connection\n"); + } + + close(client_sock); + } +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/examples/mtls_basic_script.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/examples/mtls_basic_script.cpp new file mode 100644 index 0000000000..327184ba29 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/examples/mtls_basic_script.cpp @@ -0,0 +1,243 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +static void client(); +static void server(); + +#define ASS_NOT_NULL(exp) \ + { \ + if (!exp) { \ + printf(#exp " is NULL!\n"); \ + exit(EXIT_FAILURE); \ + } \ + } + +#define ASS_POS(exp) \ + { \ + if (exp <= 0) { \ + printf(#exp " is not positive\n"); \ + exit(EXIT_FAILURE); \ + } \ + } + +#define ASS_LIN_SOCK(exp) \ + if (exp < 0) { \ + printf(#exp "Returned something negative\n"); \ + exit(EXIT_FAILURE); \ + } + +#define ASS_SSL(ssl, exp) \ + { \ + int ret = exp; \ + if (ret <= 0) { \ + printf(#exp " returned an error (%d): %s\n", SSL_get_error(ssl, ret), \ + ERR_error_string(SSL_get_error(ssl, ret), NULL)); \ + SSL_shutdown(ssl); \ + SSL_free(ssl); \ + exit(EXIT_FAILURE); \ + } \ + } + +int main(int argc, char** argv) { + if (argc != 2) { + printf("Usage: %s [client|server]\n", argv[0]); + return 1; + } + + SSL_load_error_strings(); + SSL_library_init(); + OpenSSL_add_all_algorithms(); + + switch (argv[1][0]) { + case 'c': + client(); + break; + case 's': + server(); + break; + } + return 0; +} + +#define CA_BASE_NAME "../testdata/ca" +#define CLIENT_BASE_NAME "../testdata/client" +#define SERVER_BASE_NAME "../testdata/server" +#define KEYF ".key.pem" +#define CERTF ".crt.pem" + +static void set_ca(SSL_CTX* ctx) { + ASS_POS(SSL_CTX_use_certificate_chain_file(ctx, CA_BASE_NAME CERTF)); + ASS_POS(SSL_CTX_load_verify_locations(ctx, CA_BASE_NAME CERTF, NULL)); +} + +static void keylog(const SSL* ssl, const char* line) { + char* file_name = getenv("SSLKEYLOGFILE"); + if (file_name == NULL) { + return; + } + + int file = open(file_name, O_WRONLY | O_APPEND); + if (file < 0) { + printf("Could not open keylog %d\n", errno); + return; + } + + int err = write(file, line, strlen(line)); + if (err < 0) { + printf("Could not write keylog %d", errno); + close(file); + return; + } + + write(file, "\n", 1); + + close(file); +} + +static void server() { + SSL_CTX* ctx = SSL_CTX_new(TLS_server_method()); + ASS_NOT_NULL(ctx); + + SSL_CTX_set_keylog_callback(ctx, keylog); + + set_ca(ctx); + ASS_POS(SSL_CTX_use_certificate_file(ctx, SERVER_BASE_NAME CERTF, SSL_FILETYPE_PEM)); + ASS_POS(SSL_CTX_use_PrivateKey_file(ctx, SERVER_BASE_NAME KEYF, SSL_FILETYPE_PEM)); + + int ret = SSL_CTX_check_private_key(ctx); + if (ret != 1) { + ERR_print_errors_fp(stderr); + exit(EXIT_FAILURE); + } + + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT | SSL_VERIFY_CLIENT_ONCE, NULL); + SSL_CTX_set_verify_depth(ctx, 10); + + int sock = socket(AF_INET, SOCK_STREAM, 0); + ASS_LIN_SOCK(sock); + + struct sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = INADDR_ANY; + addr.sin_port = htons(8008); + + ASS_LIN_SOCK(bind(sock, (struct sockaddr*)&addr, sizeof(addr))); + ASS_LIN_SOCK(listen(sock, 5)); + printf("Waiting for client\n"); + int client_sock = accept(sock, 0, 0); + ASS_LIN_SOCK(client_sock); + close(sock); // server sock not needed anymore yay + + printf("TCP Connected\n"); + + auto ssl = SSL_new(ctx); + ASS_NOT_NULL(ssl); + + ASS_POS(SSL_set_fd(ssl, client_sock)); + + ASS_SSL(ssl, SSL_accept(ssl)); + printf("TLS Connected\n"); + + if (SSL_get_verify_result(ssl) != X509_V_OK) { + printf("Error: SSL verification failed.\n"); + SSL_free(ssl); + close(sock); + return; + } + + printf("SSL connection using %s \n", SSL_get_cipher(ssl)); + + auto client_cert = SSL_get_peer_certificate(ssl); + if (client_cert == NULL) { + printf("Error: Could not get client's certificate.\n"); + } else { + printf("Successfully loaded client's certificate\n"); + } + X509_free(client_cert); + + printf("reading 13 bytes...\n"); + + char buf[13]; + int r = SSL_read(ssl, buf, sizeof(buf)); + if (r) { + printf("%s", buf); + } else { + printf("Could not read lol"); + } + + SSL_shutdown(ssl); + SSL_free(ssl); + + close(client_sock); +} + +static void client() { + SSL_CTX* ctx = SSL_CTX_new(TLS_client_method()); + ASS_NOT_NULL(ctx); + + set_ca(ctx); + ASS_POS(SSL_CTX_use_certificate_file(ctx, CLIENT_BASE_NAME CERTF, SSL_FILETYPE_PEM)); + ASS_POS(SSL_CTX_use_PrivateKey_file(ctx, CLIENT_BASE_NAME KEYF, SSL_FILETYPE_PEM)); + + if (!SSL_CTX_check_private_key(ctx)) { + fprintf(stderr, "Private key invalid\n"); + exit(EXIT_FAILURE); + } + + int sock = socket(AF_INET, SOCK_STREAM, 0); + struct sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = inet_addr("127.0.0.1"); + addr.sin_port = htons(8008); + + printf("Connecting...\n"); + ASS_LIN_SOCK(connect(sock, (struct sockaddr*)&addr, sizeof(addr))); + printf("TCP Connected\n"); + + SSL* ssl = SSL_new(ctx); + ASS_NOT_NULL(ssl); + + ASS_POS(SSL_set_fd(ssl, sock)); + + ASS_SSL(ssl, SSL_connect(ssl)); + printf("TLS Connected\n"); + + if (SSL_get_verify_result(ssl) != X509_V_OK) { + printf("Error: SSL verification failed.\n"); + SSL_free(ssl); + close(sock); + return; + } + + auto client_cert = SSL_get_peer_certificate(ssl); + if (client_cert == NULL) { + printf("Error: Could not get a certificate from server.\n"); + } else { + printf("Successfully loaded server certificate\n"); + } + X509_free(client_cert); + + const char* reply = "Client I am\n"; + SSL_write(ssl, reply, strlen(reply) + 1); + + sleep(1); + + SSL_shutdown(ssl); + SSL_free(ssl); + close(sock); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/CMakeLists.txt new file mode 100644 index 0000000000..fb343a7c8a --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/CMakeLists.txt @@ -0,0 +1,5 @@ +add_subdirectory(base) +add_subdirectory(server) +add_subdirectory(client) +add_subdirectory(ssl) +add_subdirectory(registers) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/CMakeLists.txt new file mode 100644 index 0000000000..130ce33e70 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/CMakeLists.txt @@ -0,0 +1,10 @@ +file(GLOB_RECURSE MODBUS_BASE_SOURCES "src/*.cpp") + +# Create modbus-base library +add_library( + modbus-base + STATIC + ${MODBUS_BASE_SOURCES} +) +target_include_directories(modbus-base PUBLIC include) +ev_register_library_target(modbus-base) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/frames.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/frames.hpp new file mode 100644 index 0000000000..de3b6b08ff --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/frames.hpp @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#ifndef MODBUS_SERVER__FRAMES_HPP +#define MODBUS_SERVER__FRAMES_HPP + +#include +#include +#include +#include + +namespace modbus_server { +namespace pdu { + +/** + * @brief Generic Modbus PDU container, storing the function code and the raw + * data. Can be decoded into a \c SpecificPDU + */ +struct GenericPDU { + std::uint8_t function_code; + std::vector data; + + // Empty constructor + GenericPDU(); + + /** + * @brief Construct a new GenericPDU using given data + * + * @param data_with_function_code The pdu data prepended by the uint8 function + * code + */ + GenericPDU(const std::vector& data_with_function_code); + + /** + * @brief Construct a new GenericPDU from a function code and the separate + * payload data + * + * @param function_code the function code + * @param data the payload data, not including the function code + */ + GenericPDU(std::uint8_t function_code, const std::vector& data); + + /** + * @brief Convert the PDU to a raw data buffer, including the function code at + * the beginning + * + * @return std::vector the raw data buffer + */ + std::vector to_vector() const; + + /** + * @brief Serialize the PDU to a human readable string. Useful for debugging + * + * @return std::string the string representation of the PDU + */ + std::string to_string() const; +}; + +/** + * @brief An exception thrown when a \c SpecificPDU cannot be encoded to a + * \c GenericPDU + * + */ +class EncodingError : public std::runtime_error { +public: + EncodingError(const std::string& encode_class, const std::string& msg); +}; + +/** + * @brief An exception thrown when a \c GenericPDU cannot be decoded to a + * \c SpecificPDU . Also stores the original data that caused the error + * + */ +class DecodingError : public std::runtime_error { +protected: + GenericPDU original_data; + +public: + DecodingError(const GenericPDU& original_data, const std::string& decode_class, const std::string& msg); + + const GenericPDU& get_original_data() const; +}; + +/** + * @brief An interface to convert a \c GenericPDU from and to a specific Modbus + * PDU, e.g. an \c ReadHoldingRegistersRequest + * + */ +class SpecificPDU { +public: + virtual ~SpecificPDU() = default; + + /** + * @brief Populate the \c SpecificPDU from a \c GenericPDU + * + * @param generic the \c GenericPDU to decode + * @throw DecodingError if the \c GenericPDU cannot be decoded + */ + virtual void from_generic(const GenericPDU& generic) = 0; + + /** + * @brief Convert the \c SpecificPDU to a \c GenericPDU + * + * @return GenericPDU the \c GenericPDU representation of the \c SpecificPDU + * @throw EncodingError if the \c SpecificPDU cannot be encoded (most likely + * due to invalid data) + */ + virtual GenericPDU to_generic() const = 0; +}; + +enum class PDUExceptionCode : std::uint8_t { + ILLEGAL_FUNCTION = 0x01, + ILLEGAL_DATA_ADDRESS = 0x02, + ILLEGAL_DATA_VALUE = 0x03, + SERVER_DEVICE_FAILURE = 0x04, + ACKNOWLEDGE = 0x05, + SERVER_DEVICE_BUSY = 0x06, + MEMORY_PARITY_ERROR = 0x08, + GATEWAY_PATH_UNAVAILABLE = 0x0A, + GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND = 0x0B, +}; + +std::string exception_code_to_string(PDUExceptionCode code); + +/** + * @brief A \c SpecificPDU representing an Exception frame with an original + * function code and an exception code + * + */ +class ErrorPDU : public SpecificPDU { +public: + std::uint8_t function_code; + std::uint8_t exception_code; + + ErrorPDU(); + ErrorPDU(std::uint8_t function_code, std::uint8_t exception_code); + + void from_generic(const pdu::GenericPDU& generic) override; + GenericPDU to_generic() const override; +}; + +/** + * @brief Exception representing an received \c ErrorPDU without a specific + * function code. Encouraged use primarily for Clients. + */ +class PDUException : public std::exception { +protected: + std::uint8_t exception_code; + std::string message; + +public: + PDUException(const GenericPDU& pdu); + PDUException(PDUExceptionCode exception_code); + std::uint8_t get_exception_code() const; + + const char* what() const noexcept override; +}; + +} // namespace pdu + +} // namespace modbus_server + +#endif diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/frames/read_holding_registers.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/frames/read_holding_registers.hpp new file mode 100644 index 0000000000..a5ded6ee6c --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/frames/read_holding_registers.hpp @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#ifndef MODBUS_SERVER__FRAMES__READ_HOLDING_REGISTERS_HPP +#define MODBUS_SERVER__FRAMES__READ_HOLDING_REGISTERS_HPP + +#include "../frames.hpp" + +namespace modbus_server { +namespace pdu { + +struct ReadHoldingRegistersRequest : public SpecificPDU { + std::uint16_t register_start; + std::uint16_t register_count; + + ReadHoldingRegistersRequest() = default; + ~ReadHoldingRegistersRequest() override = default; + + void from_generic(const GenericPDU& generic) override; + GenericPDU to_generic() const override; +}; + +struct ReadHoldingRegistersResponse : public SpecificPDU { + std::uint16_t register_count; + std::vector register_data; + + ReadHoldingRegistersResponse(const ReadHoldingRegistersRequest& req, std::vector data) : + register_count(req.register_count), register_data(data) { + } + ReadHoldingRegistersResponse() = default; + ~ReadHoldingRegistersResponse() override = default; + + void from_generic(const GenericPDU& generic) override; + GenericPDU to_generic() const override; + + /** + * @brief Get big-endian register data, as an alternative to \c register_data + * + * @return std::vector the register data from big-endian format + * (converted to system endianness) + */ + std::vector get_register_data() const; +}; + +} // namespace pdu +} // namespace modbus_server + +#endif diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/frames/write_multiple_registers.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/frames/write_multiple_registers.hpp new file mode 100644 index 0000000000..c4a1b21e1f --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/frames/write_multiple_registers.hpp @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#ifndef MODBUS_SERVER__FRAMES__WRITE_MULTIPLE_REGISTERS_HPP +#define MODBUS_SERVER__FRAMES__WRITE_MULTIPLE_REGISTERS_HPP + +#include "../frames.hpp" + +namespace modbus_server { +namespace pdu { + +/** + * @brief Write multiple registers in the Modbus server + * + */ +struct WriteMultipleRegistersRequest : public SpecificPDU { + std::uint16_t register_start; + std::uint16_t register_count; + std::vector register_data; + + WriteMultipleRegistersRequest() = default; + ~WriteMultipleRegistersRequest() override = default; + + void from_generic(const GenericPDU& generic) override; + GenericPDU to_generic() const override; +}; + +/** + * @brief The response to a \c WriteMultipleRegistersRequest + * + */ +struct WriteMultipleRegistersResponse : public SpecificPDU { + std::uint16_t register_start; + std::uint16_t register_count; + + WriteMultipleRegistersResponse() = default; + // constructor for server side + WriteMultipleRegistersResponse(const WriteMultipleRegistersRequest& req) : + register_start(req.register_start), register_count(req.register_count) { + } + WriteMultipleRegistersResponse(std::uint16_t register_start, std::uint16_t register_count) : + register_start(register_start), register_count(register_count) { + } + ~WriteMultipleRegistersResponse() override = default; + + void from_generic(const GenericPDU& generic) override; + GenericPDU to_generic() const override; +}; + +} // namespace pdu +} // namespace modbus_server + +#endif diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/frames/write_single_register.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/frames/write_single_register.hpp new file mode 100644 index 0000000000..c725e7b636 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/frames/write_single_register.hpp @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#ifndef MODBUS_SERVER__FRAMES__WRITE_SINGLE_REGISTER_HPP +#define MODBUS_SERVER__FRAMES__WRITE_SINGLE_REGISTER_HPP + +#include "../frames.hpp" + +namespace modbus_server { +namespace pdu { + +/** + * @brief Write a single register in the Modbus server + * @note request and response are the same (format-wise) + */ +struct WriteSingleRegister : public SpecificPDU { + std::uint16_t register_address; + std::uint16_t register_value; + + WriteSingleRegister() = default; + ~WriteSingleRegister() override = default; + + void from_generic(const GenericPDU& generic) override; + GenericPDU to_generic() const override; +}; + +struct WriteSingleRegisterRequest : public WriteSingleRegister { + WriteSingleRegisterRequest() = default; +}; +struct WriteSingleRegisterResponse : public WriteSingleRegister { + WriteSingleRegisterResponse() = default; + WriteSingleRegisterResponse(const WriteSingleRegisterRequest& req); +}; + +} // namespace pdu +} // namespace modbus_server + +#endif diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/pdu_correlation.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/pdu_correlation.hpp new file mode 100644 index 0000000000..ea23a5b1e3 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/pdu_correlation.hpp @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#ifndef MODBUS_SERVER__PDU_CORRELATION_HPP +#define MODBUS_SERVER__PDU_CORRELATION_HPP + +#include +#include +#include +#include +#include +#include + +#include "transport_protocol.hpp" + +namespace modbus_server { + +class PDUCorrelationLayerIntf { +public: + using On_PDU_Callback_t = std::function(const pdu::GenericPDU&)>; + +protected: + std::optional on_pdu; + +public: + PDUCorrelationLayerIntf() = default; + virtual ~PDUCorrelationLayerIntf() = default; + + /** + * @brief Read all incoming data and correlate it with the requests that are + * in transit. This method should be called regularly to ensure that all PDUs + * are correlated. + * + * @note Note that without running this function regularly, nothing will be + * received, thus \c request_response will not work correctly. + * + */ + virtual void blocking_poll() = 0; + + virtual bool poll() = 0; + + /** + * @brief Send a request and wait for a corresponding response with the same + * context and function code (masked with 0x7f to be able to receive error + * responses). + * + * @param request The request to send + * @param timeout The timeout to wait for the response. + * @return pdu::GenericPDU The response to the request + * @throws std::runtime_error If the timeout is reached + */ + virtual pdu::GenericPDU request_response(const pdu::GenericPDU& request, std::chrono::milliseconds timeout) = 0; + + /** + * @brief Send a request without waiting for a response. + * + * @param request The request to send + */ + virtual void request_without_response(const pdu::GenericPDU& request) = 0; + + /** + * @brief Set the callback for when a PDU is received that is not part of a + * request_response call. + */ + void set_on_pdu(On_PDU_Callback_t on_pdu) { + this->on_pdu = on_pdu; + } +}; + +class PDUCorrelationLayer : public PDUCorrelationLayerIntf { +protected: + std::shared_ptr protocol; + + struct ListeningForResponseEntry { + ModbusProtocol::Context context; + std::uint8_t function_code; + std::optional pdu; + }; + + std::vector listening_for_response; + + std::mutex listening_for_response_mutex; + std::condition_variable listening_for_response_cv; + + void on_poll_data(modbus_server::ModbusProtocol::Context context, pdu::GenericPDU pdu); + +public: + PDUCorrelationLayer(std::shared_ptr protocol) : protocol(protocol) { + } + + void blocking_poll() override; + bool poll() override; + pdu::GenericPDU request_response(const pdu::GenericPDU& request, std::chrono::milliseconds timeout) override; + void request_without_response(const pdu::GenericPDU& request) override; +}; + +}; // namespace modbus_server + +#endif diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/transport.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/transport.hpp new file mode 100644 index 0000000000..d4b765130d --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/transport.hpp @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#ifndef MODBUS_SERVER__TRANSPORT_HPP +#define MODBUS_SERVER__TRANSPORT_HPP + +#include +#include +#include +#include + +#include "frames.hpp" + +namespace modbus_server { + +namespace transport_exceptions { + +class ConnectionClosedException : public std::runtime_error { +public: + ConnectionClosedException() : std::runtime_error("Connection closed") { + } + ConnectionClosedException(const std::string& details) : std::runtime_error("Connection closed: " + details) { + } +}; + +} // namespace transport_exceptions + +/** + * @brief The transport layer for modbus communication. Must implement reading + * and writing. + */ +class ModbusTransport { +public: + virtual ~ModbusTransport() = default; + + /** + * @brief Read a number of bytes from the transport layer. The number of bytes + * to read is specified by the \c count parameter + * + * @param count The number of bytes to read + * @return std::vector The read bytes + * @throws ConnectionClosedException if the connection is closed + */ + virtual std::vector read_bytes(size_t count) = 0; + + virtual std::optional> try_read_bytes(size_t count) = 0; + + /** + * @brief Write a number of bytes to the transport layer. The buffer to write + * is specified by the \c bytes parameter + * + * @param bytes The bytes to write + * @throws ConnectionClosedException if the connection is closed + */ + virtual void write_bytes(const std::vector& bytes) = 0; +}; + +/** + * @brief The transport layer implementation for a socket connection using recv + * and send + * + */ +class ModbusSocketTransport : public ModbusTransport { +protected: + int socket; + +public: + /** + * @brief Create a new ModbusSocketTransport using a socket file descriptor + * + * @param socket The socket file descriptor, as returned by \c socket() + */ + ModbusSocketTransport(int socket); + virtual ~ModbusSocketTransport() = default; + + std::vector read_bytes(size_t count) override; + std::optional> try_read_bytes(size_t count) override; + void write_bytes(const std::vector& bytes) override; +}; + +} // namespace modbus_server + +#endif diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/transport_protocol.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/transport_protocol.hpp new file mode 100644 index 0000000000..ba2acbd595 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/include/modbus-server/transport_protocol.hpp @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#ifndef MODBUS_SERVER__TRANSPORT_PROTOCOL_HPP +#define MODBUS_SERVER__TRANSPORT_PROTOCOL_HPP + +#include +#include + +#include "transport.hpp" + +namespace modbus_server { + +/** + * @brief The modbus protocol layer, uses a transport layer to send and receive + * data. Splits the data into Modbus PDUs and a corresponding context for this + * message. + * + * The context is used to store information about the message, such as the + * transaction id for Modbus TCP. + * + * The context is then used to correlate the response with the request by + * another higher layer. + * + */ +class ModbusProtocol { +public: + struct Context { + std::vector data; + + bool operator==(const Context& other) const; + bool operator!=(const Context& other) const; + }; + +protected: + std::shared_ptr transport; + +public: + ModbusProtocol(std::shared_ptr transport); + + /** + * @brief If not responding to a request, e.g. when sending a request, use + * this method to create a new context for sending a message. + * + * @return Context The new context to be used for sending a message + */ + virtual Context new_send_context() = 0; + + /** + * @brief Receive a PDU via the transport layer in a blocking manner. The + * context of the message is also returned, which can then be used to either + * correlate received response to its request or received request to the + * response. + * + * @return std::tuple The context and the received + * PDU as a tuple + */ + + virtual std::tuple receive_blocking() = 0; + + virtual std::optional> try_receive() = 0; + /** + * @brief Send a PDU via the transport layer and block until the message is + * sent. If you need to send a message without already having a context, use + * \c new_send_context to create a new context (e.g. when sending a request). + * If answering to a request use the context of the request to answer. + * + * @param pdu The PDU to send + * @param context The context of the message + */ + virtual void send_blocking(const pdu::GenericPDU& pdu, const Context& context) = 0; +}; + +/** + * @brief A modbus protocol implementation for Modbus TCP. + * + */ +class ModbusTCPProtocol : public ModbusProtocol { +protected: + std::uint16_t sending_unit_id = 0xFF; // default 0xFF + std::uint16_t current_transaction_id; + struct ModbusTCPContext { + std::uint16_t transaction_id; + std::uint16_t protocol_id; + std::uint8_t unit_id; + + ModbusTCPContext(); + ModbusTCPContext(const Context& context); + Context to_context(); + }; + +public: + /** + * @brief Create a new ModbusTCPProtocol using a transport layer + * + * @param transport the transport layer to use + */ + ModbusTCPProtocol(std::shared_ptr transport); + /** + * @brief Create a new ModbusTCPProtocol using a transport layer, a unit id + * for sending and an initial transaction id for sending + * + * @param transport the transport layer to use + * @param unit_id the unit id to use for sending + * @param transaction_id the initial transaction id to use for sending + */ + ModbusTCPProtocol(std::shared_ptr transport, std::uint16_t unit_id, std::uint16_t transaction_id); + + Context new_send_context() override; + std::tuple receive_blocking() override; + std::optional> try_receive() override; + void send_blocking(const pdu::GenericPDU& pdu, const Context& context) override; +}; + +} // namespace modbus_server + +#endif diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/frames.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/frames.cpp new file mode 100644 index 0000000000..742e6e6b37 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/frames.cpp @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +using namespace modbus_server::pdu; + +GenericPDU::GenericPDU(const std::vector& data_with_function_code) : data() { + if (data_with_function_code.size() < 1) { + throw EncodingError("GenericPDU", "Data size must be at least 1"); + } + this->data.insert(this->data.end(), data_with_function_code.begin() + 1, data_with_function_code.end()); + function_code = data_with_function_code[0]; +} + +GenericPDU::GenericPDU() : function_code(0), data() { +} + +GenericPDU::GenericPDU(std::uint8_t function_code, const std::vector& data) : + function_code(function_code), data(data) { +} + +std::vector GenericPDU::to_vector() const { + std::vector ret; + ret.push_back(function_code); + ret.insert(ret.end(), data.begin(), data.end()); + return ret; +} + +std::string GenericPDU::to_string() const { + std::string ret; + + ret += "GenericPDU(fn_code: " + std::to_string(function_code) + ", data: ["; + + size_t data_size = data.size(); + for (int i = 0; i < data_size; i++) { + ret += std::to_string(data[i]); + if (i != data_size - 1) { + ret += ", "; + } + } + + ret += "])"; + return ret; +} + +void ErrorPDU::from_generic(const pdu::GenericPDU& generic) { + if ((generic.function_code & 0x80) == 0) { + throw DecodingError(generic, "ErrorPDU", "Not an error PDU"); + } + + if (generic.data.size() != 1) { + throw DecodingError(generic, "ErrorPDU", "Invalid data size"); + } + + function_code = generic.function_code & 0x7f; + exception_code = generic.data[0]; +} + +GenericPDU ErrorPDU::to_generic() const { + return GenericPDU({(std::uint8_t)(function_code | 0x80), exception_code}); +} + +EncodingError::EncodingError(const std::string& encode_class, const std::string& msg) : + std::runtime_error("Could not encode from " + encode_class + ": " + msg) { +} + +DecodingError::DecodingError(const GenericPDU& original_data, const std::string& decode_class, const std::string& msg) : + std::runtime_error("Could not decode to " + decode_class + ": " + msg), original_data(original_data) { +} +const GenericPDU& DecodingError::get_original_data() const { + return original_data; +} + +ErrorPDU::ErrorPDU() : function_code(0), exception_code(0) { +} + +ErrorPDU::ErrorPDU(std::uint8_t function_code, std::uint8_t exception_code) : + function_code(function_code & 0x7f), exception_code(exception_code) { +} + +std::string modbus_server::pdu::exception_code_to_string(PDUExceptionCode code) { + switch (code) { + case PDUExceptionCode::ILLEGAL_FUNCTION: + return "ILLEGAL_FUNCTION"; + case PDUExceptionCode::ILLEGAL_DATA_ADDRESS: + return "ILLEGAL_DATA_ADDRESS"; + case PDUExceptionCode::ILLEGAL_DATA_VALUE: + return "ILLEGAL_DATA_VALUE"; + case PDUExceptionCode::SERVER_DEVICE_FAILURE: + return "SERVER_DEVICE_FAILURE"; + case PDUExceptionCode::ACKNOWLEDGE: + return "ACKNOWLEDGE"; + case PDUExceptionCode::SERVER_DEVICE_BUSY: + return "SERVER_DEVICE_BUSY"; + case PDUExceptionCode::MEMORY_PARITY_ERROR: + return "MEMORY_PARITY_ERROR"; + case PDUExceptionCode::GATEWAY_PATH_UNAVAILABLE: + return "GATEWAY_PATH_UNAVAILABLE"; + case PDUExceptionCode::GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND: + return "GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND"; + default: + return "Unknown (" + std::to_string((std::uint8_t)code) + ")"; + } +} + +PDUException::PDUException(const GenericPDU& pdu) { + if (pdu.data.size() != 1) { + throw DecodingError(pdu, "PDUException", "Invalid data size"); + } + this->exception_code = pdu.data[0]; + + this->message = "PDUException: " + exception_code_to_string((PDUExceptionCode)this->exception_code); +} + +PDUException::PDUException(PDUExceptionCode exception_code) : + exception_code((std::uint8_t)exception_code), message("PDUException: " + exception_code_to_string(exception_code)) { +} + +const char* PDUException::what() const noexcept { + return message.c_str(); +}; + +std::uint8_t PDUException::get_exception_code() const { + return exception_code; +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/frames/read_holding_registers.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/frames/read_holding_registers.cpp new file mode 100644 index 0000000000..c74d0e16a3 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/frames/read_holding_registers.cpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +using namespace modbus_server::pdu; + +void ReadHoldingRegistersRequest::from_generic(const GenericPDU& generic) { + if (generic.function_code != 0x03) { + throw DecodingError(generic, "ReadHoldingRegisterRequest", "Invalid function code"); + } + + if (generic.data.size() != 4) { + throw DecodingError(generic, "ReadHoldingRegisterRequest", "Invalid data size"); + } + + register_start = (generic.data[0] << 8) | generic.data[1]; + register_count = (generic.data[2] << 8) | generic.data[3]; + + if (register_count == 0) { + throw DecodingError(generic, "ReadHoldingRegisterRequest", "Register count cannot be zero"); + } + + if (register_count > 125) { + throw DecodingError(generic, "ReadHoldingRegisterRequest", "Register count too big"); + } +} + +GenericPDU ReadHoldingRegistersRequest::to_generic() const { + GenericPDU generic; + + generic.function_code = 0x03; + + generic.data.push_back(register_start >> 8); + generic.data.push_back(register_start & 0xFF); + generic.data.push_back(register_count >> 8); + generic.data.push_back(register_count & 0xFF); + + return generic; +} + +void ReadHoldingRegistersResponse::from_generic(const GenericPDU& generic) { + if (generic.function_code != 0x03) { + throw DecodingError(generic, "ReadHoldingRegisterResponse", "Invalid function code"); + } + + if (generic.data.size() < 1) { + throw DecodingError(generic, "ReadHoldingRegisterResponse", "Invalid data size"); + } + + register_count = generic.data[0] / 2; + + if (generic.data.size() != register_count * 2 + 1) { + throw DecodingError(generic, "ReadHoldingRegisterResponse", "Invalid data size"); + } + + register_data = std::vector(generic.data.begin() + 1, generic.data.end()); +} + +GenericPDU ReadHoldingRegistersResponse::to_generic() const { + if (register_count > 125) { + throw EncodingError("ReadHoldingRegistersResponse", "Register count too big"); + } + + if (register_data.size() != register_count * 2) { + throw EncodingError("ReadHoldingRegistersResponse", "Data size (" + std::to_string(register_data.size()) + + ") does not match register count derived size (" + + std::to_string(register_count * 2) + ")"); + } + + GenericPDU generic; + + generic.function_code = 0x03; + generic.data.push_back(register_count * 2); + + generic.data.insert(generic.data.end(), register_data.begin(), register_data.end()); + + return generic; +} + +std::vector ReadHoldingRegistersResponse::get_register_data() const { + std::vector ret; + for (int i = 0; i < register_data.size(); i += 2) { + ret.push_back((register_data[i] << 8) | register_data[i + 1]); + } + return ret; +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/frames/write_multiple_registers.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/frames/write_multiple_registers.cpp new file mode 100644 index 0000000000..1f6832e5a9 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/frames/write_multiple_registers.cpp @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +using namespace modbus_server::pdu; + +void WriteMultipleRegistersRequest::from_generic(const GenericPDU& generic) { + if (generic.function_code != 0x10) { + throw DecodingError(generic, "WriteMultipleRegistersRequest", "Invalid function code"); + } + + if (generic.data.size() < 5) { + throw DecodingError(generic, "WriteMultipleRegistersRequest", "Invalid data size"); + } + + register_start = (generic.data[0] << 8) | generic.data[1]; + register_count = (generic.data[2] << 8) | generic.data[3]; + std::uint8_t byte_count = generic.data[4]; + + if (register_count > 0x007b) { + throw DecodingError(generic, "WriteMultipleRegistersRequest", + "Register count too big, maximum allowed is 123 (0x007b)"); + } + + if (byte_count != register_count * 2) { + throw DecodingError(generic, "WriteMultipleRegistersRequest", "Byte count does not match register count"); + } + + if (generic.data.size() != 5 + register_count * 2) { + throw DecodingError(generic, "WriteMultipleRegistersRequest", "Invalid data size"); + } + + register_data = std::vector(generic.data.begin() + 5, generic.data.end()); +} + +GenericPDU WriteMultipleRegistersRequest::to_generic() const { + if (register_count > 0x007b) { + throw EncodingError("WriteMultipleRegistersRequest", "Register count too big, maximum allowed is 123 (0x007b)"); + } + + if (register_data.size() != register_count * 2) { + throw EncodingError("WriteMultipleRegistersRequest", "Byte count does not match register count"); + } + + GenericPDU generic; + generic.function_code = 0x10; + generic.data.push_back(register_start >> 8); + generic.data.push_back(register_start & 0xFF); + generic.data.push_back(register_count >> 8); + generic.data.push_back(register_count & 0xFF); + generic.data.push_back(register_count * 2); + generic.data.insert(generic.data.end(), register_data.begin(), register_data.end()); + + return generic; +} + +void WriteMultipleRegistersResponse::from_generic(const GenericPDU& generic) { + if (generic.function_code != 0x10) { + throw DecodingError(generic, "WriteMultipleRegistersResponse", "Invalid function code"); + } + + if (generic.data.size() != 4) { + throw DecodingError(generic, "WriteMultipleRegistersResponse", "Invalid data size"); + } + + register_start = (generic.data[0] << 8) | generic.data[1]; + register_count = (generic.data[2] << 8) | generic.data[3]; +} + +GenericPDU WriteMultipleRegistersResponse::to_generic() const { + GenericPDU generic; + + generic.function_code = 0x10; + + generic.data.push_back(register_start >> 8); + generic.data.push_back(register_start & 0xFF); + generic.data.push_back(register_count >> 8); + generic.data.push_back(register_count & 0xFF); + + return generic; +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/frames/write_single_register.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/frames/write_single_register.cpp new file mode 100644 index 0000000000..9ef3b72e2c --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/frames/write_single_register.cpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +using namespace modbus_server::pdu; + +void WriteSingleRegister::from_generic(const GenericPDU& generic) { + if (generic.function_code != 0x06) { + throw DecodingError(generic, "WriteSingleRegisterRequest", "Invalid function code"); + } + + if (generic.data.size() != 4) { + throw DecodingError(generic, "WriteSingleRegisterRequest", "Invalid data size"); + } + + register_address = (generic.data[0] << 8) | generic.data[1]; + register_value = (generic.data[2] << 8) | generic.data[3]; +} + +GenericPDU WriteSingleRegister::to_generic() const { + GenericPDU generic; + + generic.function_code = 0x06; + + generic.data.push_back(register_address >> 8); + generic.data.push_back(register_address & 0xFF); + generic.data.push_back(register_value >> 8); + generic.data.push_back(register_value & 0xFF); + + return generic; +} + +WriteSingleRegisterResponse::WriteSingleRegisterResponse(const WriteSingleRegisterRequest& req) : + WriteSingleRegister(req) { +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/modbus_protocol.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/modbus_protocol.cpp new file mode 100644 index 0000000000..3c5ddc17c9 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/modbus_protocol.cpp @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +using namespace modbus_server; + +ModbusProtocol::ModbusProtocol(std::shared_ptr transport) : transport(transport) { +} + +bool ModbusProtocol::Context::operator==(const ModbusProtocol::Context& other) const { + return this->data == other.data; +} + +bool ModbusProtocol::Context::operator!=(const ModbusProtocol::Context& other) const { + return this->data != other.data; +} + +ModbusTCPProtocol::ModbusTCPContext::ModbusTCPContext(const ModbusProtocol::Context& context) { + this->transaction_id = context.data[0] << 8 | context.data[1]; + this->protocol_id = context.data[2] << 8 | context.data[3]; + this->unit_id = context.data[4]; +} + +ModbusTCPProtocol::ModbusTCPContext::ModbusTCPContext() : transaction_id(0), protocol_id(0), unit_id(0) { +} + +ModbusProtocol::Context ModbusTCPProtocol::ModbusTCPContext::to_context() { + ModbusProtocol::Context context; + context.data.resize(5); + context.data[0] = this->transaction_id >> 8; + context.data[1] = this->transaction_id & 0xFF; + context.data[2] = this->protocol_id >> 8; + context.data[3] = this->protocol_id & 0xFF; + context.data[4] = this->unit_id; + return context; +} + +ModbusTCPProtocol::ModbusTCPProtocol(std::shared_ptr transport) : + ModbusProtocol(transport), current_transaction_id(0) { +} + +ModbusTCPProtocol::ModbusTCPProtocol(std::shared_ptr transport, std::uint16_t unit_id, + std::uint16_t transaction_id) : + ModbusProtocol(transport), sending_unit_id(unit_id), current_transaction_id(transaction_id) { +} + +ModbusTCPProtocol::Context ModbusTCPProtocol::new_send_context() { + ModbusTCPContext context; + context.transaction_id = this->current_transaction_id++; + context.protocol_id = 0; + context.unit_id = sending_unit_id; + return context.to_context(); +} + +std::tuple ModbusTCPProtocol::receive_blocking() { + auto header = this->transport->read_bytes(7); + + std::uint16_t transaction_id = header[0] << 8 | header[1]; + std::uint16_t protocol_id = header[2] << 8 | header[3]; + std::uint16_t length = header[4] << 8 | header[5]; + std::uint8_t unit_id = header[6]; + + auto data = this->transport->read_bytes(length - 1); + + ModbusTCPContext context; + context.transaction_id = transaction_id; + context.protocol_id = protocol_id; + context.unit_id = unit_id; + + return {context.to_context(), pdu::GenericPDU(data)}; +} + +std::optional> +modbus_server::ModbusTCPProtocol::try_receive() { + auto header_opt = this->transport->try_read_bytes(7); + if (!header_opt.has_value()) { + return std::nullopt; + } + auto header = header_opt.value(); + + std::uint16_t transaction_id = header[0] << 8 | header[1]; + std::uint16_t protocol_id = header[2] << 8 | header[3]; + std::uint16_t length = header[4] << 8 | header[5]; + std::uint8_t unit_id = header[6]; + + if (length <= 1) { + return std::nullopt; // first of all: nothing to be read, secondly: the + // length is invalid + } + + auto data = this->transport->read_bytes(length - 1); + + if (data.size() != length - 1) { + throw std::runtime_error("Transport returned less data than requested"); + } + + ModbusTCPContext context; + context.transaction_id = transaction_id; + context.protocol_id = protocol_id; + context.unit_id = unit_id; + + std::pair result = {context.to_context(), + pdu::GenericPDU(data)}; + + return std::optional(result); +} +void ModbusTCPProtocol::send_blocking(const pdu::GenericPDU& pdu, const ModbusProtocol::Context& context) { + auto pdu_data = pdu.to_vector(); + std::uint16_t size_in_header = pdu_data.size() + 1; // +1 for the unit id in the header + + auto context_parsed = ModbusTCPContext(context); + + std::vector frame(7); + frame[0] = context_parsed.transaction_id >> 8; + frame[1] = context_parsed.transaction_id & 0xFF; + frame[2] = context_parsed.protocol_id >> 8; + frame[3] = context_parsed.protocol_id & 0xFF; + frame[4] = size_in_header >> 8; + frame[5] = size_in_header & 0xFF; + frame[6] = context_parsed.unit_id; + + frame.insert(frame.end(), pdu_data.begin(), pdu_data.end()); + + this->transport->write_bytes(frame); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/pdu_correlation.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/pdu_correlation.cpp new file mode 100644 index 0000000000..3865b5dc35 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/pdu_correlation.cpp @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest + +#include + +using namespace modbus_server; + +void modbus_server::PDUCorrelationLayer::on_poll_data(modbus_server::ModbusProtocol::Context context, + pdu::GenericPDU pdu) { + { + std::lock_guard lock(this->listening_for_response_mutex); + for (auto& entry : this->listening_for_response) { + if (entry.context != context) { + continue; + } + + if ((entry.function_code & 0x7f) != (pdu.function_code & 0x7f)) { + continue; + } + + entry.pdu = pdu; + this->listening_for_response_cv.notify_all(); + return; + } + } + + if (!this->on_pdu.has_value()) { + return; + } + + auto response = this->on_pdu.value()(pdu); + if (response.has_value()) { + this->protocol->send_blocking(response.value(), context); + } +} + +void PDUCorrelationLayer::blocking_poll() { + auto [context, pdu] = this->protocol->receive_blocking(); + on_poll_data(context, pdu); +} + +bool modbus_server::PDUCorrelationLayer::poll() { + std::optional> data = + this->protocol->try_receive(); + + if (!data.has_value()) { + return false; + } + + on_poll_data(data.value().first, data.value().second); + return true; +} + +pdu::GenericPDU PDUCorrelationLayer::request_response(const pdu::GenericPDU& request, + std::chrono::milliseconds timeout) { + auto context = this->protocol->new_send_context(); + + { + std::lock_guard lock(this->listening_for_response_mutex); + this->listening_for_response.push_back({context, request.function_code, std::nullopt}); + } + + this->protocol->send_blocking(request, context); + + auto end_time = std::chrono::steady_clock::now() + timeout; + + while (std::chrono::steady_clock::now() < end_time) { + auto rest_timeout = end_time - std::chrono::steady_clock::now(); + + std::unique_lock lock(this->listening_for_response_mutex); + this->listening_for_response_cv.wait_for(lock, rest_timeout); + + // check if we have a response + ssize_t index = -1; + for (ssize_t i = 0; i < this->listening_for_response.size(); i++) { + if (this->listening_for_response[i].context == context) { + index = i; + } + } + + if (index == -1) { + throw std::runtime_error("context is gone"); + } + + // if we have a response, erase the entry and return the response + if (this->listening_for_response[index].pdu.has_value()) { + auto ret = this->listening_for_response[index].pdu.value(); + this->listening_for_response.erase(this->listening_for_response.begin() + index); + + return ret; + } + + // if we don't have a response, check if we have timed out and wait again + } + + throw std::runtime_error("timeout"); +} + +void PDUCorrelationLayer::request_without_response(const pdu::GenericPDU& request) { + auto context = this->protocol->new_send_context(); + this->protocol->send_blocking(request, context); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/socket_transport.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/socket_transport.cpp new file mode 100644 index 0000000000..08d105b1c9 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/src/socket_transport.cpp @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include +#include + +#include + +#include "poll.h" + +using namespace modbus_server; + +ModbusSocketTransport::ModbusSocketTransport(int socket) : socket(socket) { +} + +std::vector ModbusSocketTransport::read_bytes(size_t count) { + std::vector buf(count); + size_t read = 0; + while (read < count) { + ssize_t err = recv(this->socket, buf.data() + read, count - read, 0); + read += err; + + if (err == 0) { + throw transport_exceptions::ConnectionClosedException("Socket closed"); + } + + // todo: build custom errors for this kind of error + if (err < 0) { + throw std::runtime_error("Failed to read bytes, got " + std::to_string(count) + + " bytes, err: " + std::to_string(err) + "errno: " + std::to_string(errno)); + } + } + + return buf; +} + +std::optional> modbus_server::ModbusSocketTransport::try_read_bytes(size_t count) { + struct pollfd pfd[1]; + pfd[0].fd = this->socket; + 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 modbus data, errno: " + std::to_string(error)); + } + + if (result_code == 0) { + return std::nullopt; + } + + return read_bytes(count); +} +void ModbusSocketTransport::write_bytes(const std::vector& bytes) { + int err = send(this->socket, bytes.data(), bytes.size(), 0); + + // todo: build custom errors for this kind of error + if (err < 0) { + throw std::runtime_error("Failed to write bytes, got " + std::to_string(bytes.size()) + + " bytes, err: " + std::to_string(err) + "errno: " + std::to_string(errno)); + } +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/CMakeLists.txt new file mode 100644 index 0000000000..72bf67faca --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/CMakeLists.txt @@ -0,0 +1,8 @@ +include(GoogleTest) + +file(GLOB_RECURSE MODBUS_SERVER_TESTS_SOURCES "*.cpp") + +add_executable(modbus-base-tests ${MODBUS_SERVER_TESTS_SOURCES}) +target_link_libraries(modbus-base-tests PRIVATE modbus-base gtest_main) + +gtest_discover_tests(modbus-base-tests) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/dummy_modbus_transport.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/dummy_modbus_transport.hpp new file mode 100644 index 0000000000..4b9309f0e7 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/dummy_modbus_transport.hpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#ifndef DUMMY_MODBUS_TRANSPORT_HPP_ +#define DUMMY_MODBUS_TRANSPORT_HPP_ + +#include +#include + +// Dummy ModbusTransport for testing purposes +class DummyModbusTransport : public modbus_server::ModbusTransport { +private: + std::vector incoming_data; + std::vector outgoing_data; + +public: + std::optional> try_read_bytes(size_t count) override { + return std::nullopt; + }; + + std::vector read_bytes(size_t count) override { + if (incoming_data.size() < count) { + throw std::runtime_error("DummyModbusTransport: not enough data to read"); + } + std::vector bytes(incoming_data.begin(), incoming_data.begin() + count); + incoming_data.erase(incoming_data.begin(), incoming_data.begin() + count); + return bytes; + } + + void write_bytes(const std::vector& bytes) override { + outgoing_data.insert(outgoing_data.end(), bytes.begin(), bytes.end()); + } + + void add_incoming_data(const std::vector& data) { + incoming_data.insert(incoming_data.end(), data.begin(), data.end()); + } + std::vector get_outgoing_data() { + auto copy = outgoing_data; + outgoing_data.clear(); + return copy; + } +}; + +#endif diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/dummy_modbus_transport.test.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/dummy_modbus_transport.test.cpp new file mode 100644 index 0000000000..b4b59e7885 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/dummy_modbus_transport.test.cpp @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include "dummy_modbus_transport.hpp" + +#include + +#include +#include + +TEST(DummyModbusTransport, dummy_works) { + DummyModbusTransport transport; + transport.add_incoming_data({0x01, 0x02, 0x03}); + auto read = transport.read_bytes(3); + ASSERT_EQ(read.size(), 3); + ASSERT_EQ(read[0], 0x01); + ASSERT_EQ(read[1], 0x02); + ASSERT_EQ(read[2], 0x03); + + transport.write_bytes({0x05, 0x06}); + + auto write = transport.get_outgoing_data(); + ASSERT_EQ(write.size(), 2); + ASSERT_EQ(write[0], 0x05); + ASSERT_EQ(write[1], 0x06); +} + +TEST(DummyModbusTransport, read_bytes_empty) { + DummyModbusTransport transport; + ASSERT_THROW(transport.read_bytes(1), std::runtime_error); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/edge.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/edge.cpp new file mode 100644 index 0000000000..bad0099586 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/edge.cpp @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include + +using namespace modbus_server::pdu; + +TEST(GenericPDU, to_vector_edge_case_1_works) { + GenericPDU pdu(0xab, {}); + + std::vector expected = {0xab}; + ASSERT_EQ(pdu.to_vector(), expected); +} + +TEST(GenericPDU, to_vector_edge_case_2_works) { + std::vector data = {}; + + ASSERT_ANY_THROW(GenericPDU a(data)); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/frames.test.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/frames.test.cpp new file mode 100644 index 0000000000..9e0c515bbe --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/frames.test.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include + +using namespace modbus_server::pdu; + +TEST(GenericPDU, constructor_splits_data_correctly) { + std::vector data = {0x03, 0x00, 0x01}; + + modbus_server::pdu::GenericPDU request(data); + + ASSERT_EQ(request.function_code, 0x03); + ASSERT_EQ(request.data.size(), 2); + ASSERT_EQ(request.data[0], 0x00); + ASSERT_EQ(request.data[1], 0x01); +} + +TEST(GenericPDU, to_vector_works) { + GenericPDU pdu(0xab, {0xcd, 0xef}); + + std::vector expected = {0xab, 0xcd, 0xef}; + ASSERT_EQ(pdu.to_vector(), expected); +} + +TEST(GenericPDU, to_string_works) { + GenericPDU pdu(0xab, {0xcd, 0xef}); + + std::string expected = "GenericPDU(fn_code: 171, data: [205, 239])"; + ASSERT_EQ(pdu.to_string(), expected); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/frames/read_holding_registers.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/frames/read_holding_registers.cpp new file mode 100644 index 0000000000..e1165792f9 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/frames/read_holding_registers.cpp @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include + +TEST(ReadHoldingRegistersRequest, from_generic_splits_correctly) { + std::vector data = {0x03, 0xca, 0xfe, 0x00, 0x02}; + + modbus_server::pdu::GenericPDU generic(data); + modbus_server::pdu::ReadHoldingRegistersRequest request; + + ASSERT_NO_THROW(request.from_generic(generic)); + ASSERT_EQ(request.register_start, 0xcafe); + ASSERT_EQ(request.register_count, 0x0002); +} + +TEST(ReadHoldingRegistersRequest, from_generic_size_0) { + std::vector data = {0x04, 0x00, 0x01, 0x00, 0x00}; + + modbus_server::pdu::GenericPDU generic(data); + modbus_server::pdu::ReadHoldingRegistersRequest request; + + ASSERT_THROW(request.from_generic(generic), modbus_server::pdu::DecodingError); +} + +TEST(ReadHoldingRegistersRequest, from_generic_invalid_function_code) { + std::vector data = {0x04, 0x00, 0x01, 0x00, 0x02}; + + modbus_server::pdu::GenericPDU generic(data); + modbus_server::pdu::ReadHoldingRegistersRequest request; + + ASSERT_THROW(request.from_generic(generic), modbus_server::pdu::DecodingError); +} + +TEST(ReadHoldingRegistersRequest, from_generic_invalid_data_size_5_bytes) { + std::vector data = {0x03, 0x00, 0x01, 0x00, 0x02, 0xff}; + + modbus_server::pdu::GenericPDU generic(data); + modbus_server::pdu::ReadHoldingRegistersRequest request; + + ASSERT_THROW(request.from_generic(generic), modbus_server::pdu::DecodingError); +} +TEST(ReadHoldingRegistersRequest, from_generic_invalid_data_size_0_bytes) { + std::vector data = {0x03}; + + modbus_server::pdu::GenericPDU generic(data); + modbus_server::pdu::ReadHoldingRegistersRequest request; + + ASSERT_THROW(request.from_generic(generic), modbus_server::pdu::DecodingError); +} + +TEST(ReadHoldingRegistersRequest, from_generic_invalid_register_count) { + std::vector data = {0x03, 0x00, 0x01, 0x00, 0x80}; + + modbus_server::pdu::GenericPDU generic(data); + modbus_server::pdu::ReadHoldingRegistersRequest request; + + ASSERT_THROW(request.from_generic(generic), modbus_server::pdu::DecodingError); +} + +TEST(ReadHoldingRegistersResponse, constructor_copies_register_count_from_request) { + modbus_server::pdu::GenericPDU req_generic(0x03, {0xde, 0xad, 0x00, 0x02}); + modbus_server::pdu::ReadHoldingRegistersRequest req; + + ASSERT_NO_THROW(req.from_generic(req_generic)); + + modbus_server::pdu::ReadHoldingRegistersResponse resp(req, {0xca, 0xfe, 0xbe, 0xef}); + + ASSERT_EQ(resp.register_count, req.register_count); +} + +TEST(ReadHoldingRegistersResponse, to_generic_mapping_works_2_registers) { + modbus_server::pdu::GenericPDU req_generic(0x03, {0xde, 0xad, 0x00, 0x02}); + modbus_server::pdu::ReadHoldingRegistersRequest req; + + ASSERT_NO_THROW(req.from_generic(req_generic)); + + modbus_server::pdu::ReadHoldingRegistersResponse resp(req, {0xca, 0xfe, 0xbe, 0xef}); + + modbus_server::pdu::GenericPDU gen = resp.to_generic(); + + ASSERT_EQ(gen.function_code, 0x03); + ASSERT_EQ(gen.data.size(), 5); + ASSERT_EQ(gen.data[0], 0x04); + ASSERT_EQ(gen.data[1], 0xca); + ASSERT_EQ(gen.data[2], 0xfe); + ASSERT_EQ(gen.data[3], 0xbe); + ASSERT_EQ(gen.data[4], 0xef); +} + +TEST(ReadHoldingRegistersResponse, to_generic_mapping_works_3_register) { + modbus_server::pdu::GenericPDU req_generic(0x03, {0xde, 0xad, 0x00, 0x03}); + modbus_server::pdu::ReadHoldingRegistersRequest req; + + ASSERT_NO_THROW(req.from_generic(req_generic)); + + modbus_server::pdu::ReadHoldingRegistersResponse resp(req, {0xca, 0xfe, 0x13, 0x37, 0xaf, 0xfe}); + + modbus_server::pdu::GenericPDU gen = resp.to_generic(); + + ASSERT_EQ(gen.function_code, 0x03); + ASSERT_EQ(gen.data.size(), 7); + ASSERT_EQ(gen.data[0], 0x06); + ASSERT_EQ(gen.data[1], 0xca); + ASSERT_EQ(gen.data[2], 0xfe); + ASSERT_EQ(gen.data[3], 0x13); + ASSERT_EQ(gen.data[4], 0x37); + ASSERT_EQ(gen.data[5], 0xaf); + ASSERT_EQ(gen.data[6], 0xfe); +} + +TEST(ReadHoldingRegistersResponse, to_generic_too_much_register_data) { + modbus_server::pdu::GenericPDU req_generic(0x03, {0xde, 0xad, 0x00, 0x02}); + modbus_server::pdu::ReadHoldingRegistersRequest req; + + ASSERT_NO_THROW(req.from_generic(req_generic)); + + modbus_server::pdu::ReadHoldingRegistersResponse resp(req, {0xca, 0xfe, 0xbe, 0xef, 0xca}); + + ASSERT_THROW(resp.to_generic(), modbus_server::pdu::EncodingError); +} + +TEST(ReadHoldingRegistersResponse, to_generic_too_large_register_count) { + const std::uint8_t register_count = 0x80; + modbus_server::pdu::GenericPDU req_generic(0x03, {0xde, 0xad, 0x00, 0x12}); + modbus_server::pdu::ReadHoldingRegistersRequest req; + + ASSERT_NO_THROW(req.from_generic(req_generic)); + req.register_count = register_count; + + std::vector reg_data; + for (int i = 0; i < register_count; i++) { + reg_data.push_back(0x00); + reg_data.push_back(0x01); + } + + modbus_server::pdu::ReadHoldingRegistersResponse resp(req, reg_data); + + modbus_server::pdu::GenericPDU gen; + ASSERT_THROW(resp.to_generic(), modbus_server::pdu::EncodingError); +} + +TEST(ReadHoldingRegistersResponse, from_generic_exception_contains_original) { + modbus_server::pdu::GenericPDU req_generic(0x04, {0x00}); // invalid everything + modbus_server::pdu::ReadHoldingRegistersRequest req; + + ASSERT_THROW(req.from_generic(req_generic), modbus_server::pdu::DecodingError); + + try { + req.from_generic(req_generic); + } catch (modbus_server::pdu::DecodingError e) { + auto original = e.get_original_data(); + ASSERT_EQ(original.function_code, 0x04); + ASSERT_EQ(original.data.size(), 1); + ASSERT_EQ(original.data[0], 0x00); + } +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/frames/write_multiple_registers.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/frames/write_multiple_registers.cpp new file mode 100644 index 0000000000..2a3699f924 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/frames/write_multiple_registers.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include + +using namespace modbus_server::pdu; + +TEST(WriteMultipleRegistersRequest, to_generic_works_correctly) { + WriteMultipleRegistersRequest request; + + request.register_start = 0xdead; + request.register_count = 0x0002; + request.register_data = {0xbe, 0xef, 0xca, 0xfe}; + + GenericPDU generic = request.to_generic(); + + std::vector expected = {0xde, 0xad, 0x00, 0x02, 0x04, 0xbe, 0xef, 0xca, 0xfe}; + ASSERT_EQ(generic.function_code, 0x10); + ASSERT_EQ(generic.data, expected); +} + +TEST(WriteMultipleRegistersRequest, from_generic_works_correctly) { + std::vector data = {0x10, 0xde, 0xad, 0x00, 0x02, 0x04, 0xbe, 0xef, 0xca, 0xfe}; + + GenericPDU generic(data); + WriteMultipleRegistersRequest request; + + ASSERT_NO_THROW(request.from_generic(generic)); + ASSERT_EQ(request.register_start, 0xdead); + ASSERT_EQ(request.register_count, 0x0002); + ASSERT_EQ(request.register_data, std::vector({0xbe, 0xef, 0xca, 0xfe})); +} + +TEST(WriteMultipleRegistersResponse, to_generic_works_correctly) { + WriteMultipleRegistersResponse response; + + response.register_start = 0xdead; + response.register_count = 0x0002; + + GenericPDU generic = response.to_generic(); + + std::vector expected = {0xde, 0xad, 0x00, 0x02}; + ASSERT_EQ(generic.function_code, 0x10); + ASSERT_EQ(generic.data, expected); +} + +TEST(WriteMultipleRegistersResponse, from_generic_works_correctly) { + std::vector data = {0x10, 0xde, 0xad, 0x00, 0x02}; + + GenericPDU generic(data); + WriteMultipleRegistersResponse response; + + ASSERT_NO_THROW(response.from_generic(generic)); + ASSERT_EQ(response.register_start, 0xdead); + ASSERT_EQ(response.register_count, 0x0002); +} + +// todo: more tests diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/frames/write_single_registers.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/frames/write_single_registers.cpp new file mode 100644 index 0000000000..85f70e2e00 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/frames/write_single_registers.cpp @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include + +using namespace modbus_server::pdu; + +// Assert that WriteSingleRegisterRequest and WriteSingleRegisterResponse are +// derived from WriteSingleRegister +static_assert(std::is_base_of::value); +static_assert(std::is_base_of::value); + +TEST(WriteSingleRegister, to_generic_works_correctly) { + WriteSingleRegisterRequest request; + + request.register_address = 0xdead; + request.register_value = 0xbeef; + + GenericPDU generic = request.to_generic(); + + std::vector expected = {0xde, 0xad, 0xbe, 0xef}; + ASSERT_EQ(generic.function_code, 0x06); + ASSERT_EQ(generic.data, expected); +} + +TEST(WriteSingleRegister, from_generic_works_correctly) { + std::vector data = {0x06, 0xde, 0xad, 0xbe, 0xef}; + + GenericPDU generic(data); + WriteSingleRegisterRequest request; + + ASSERT_NO_THROW(request.from_generic(generic)); + ASSERT_EQ(request.register_address, 0xdead); + ASSERT_EQ(request.register_value, 0xbeef); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/modbus_tcp_proto.test.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/modbus_tcp_proto.test.cpp new file mode 100644 index 0000000000..0e3a3ec24a --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/modbus_tcp_proto.test.cpp @@ -0,0 +1,202 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include +#include + +#include "dummy_modbus_transport.hpp" + +using namespace modbus_server; + +#define ASSERT_EQ_VECTOR(a, b) \ + ASSERT_EQ(a.size(), b.size()); \ + for (size_t i = 0; i < a.size(); i++) { \ + ASSERT_EQ(a[i], b[i]); \ + } + +// Assert that 2 vectors are either different in size or have at least one +// different element +#define ASSERT_NE_VECTOR(a, b) \ + if (a.size() == b.size()) { \ + bool diff = false; \ + for (size_t i = 0; i < a.size(); i++) { \ + if (a[i] != b[i]) { \ + diff = true; \ + break; \ + } \ + } \ + ASSERT_TRUE(diff); \ + } else { \ + ASSERT_TRUE(true); \ + } + +TEST(ModbusTCPProtocol, client_scenario) { + auto transport = std::make_shared(); + ModbusTCPProtocol protocol = ModbusTCPProtocol(transport, 0x01, 0x0000); + + auto send_context = protocol.new_send_context(); + protocol.send_blocking(pdu::GenericPDU(0xab, {0xde, 0xad, 0xca}), send_context); + + auto written = transport->get_outgoing_data(); + ASSERT_EQ(written.size(), 7 + 4); // header + pdu + // transaction id should be 0 + ASSERT_EQ(written[0], 0); + ASSERT_EQ(written[1], 0); + // protocol id should be 0 + ASSERT_EQ(written[2], 0); + ASSERT_EQ(written[3], 0); + // length should be 5 + ASSERT_EQ(written[4], 0); + ASSERT_EQ(written[5], 5); + + ASSERT_EQ(written[7], 0xab); + ASSERT_EQ(written[8], 0xde); + ASSERT_EQ(written[9], 0xad); + ASSERT_EQ(written[10], 0xca); + + std::vector response = { + 0x00, 0x00, // transaction id + 0x00, 0x00, // protocol id + 0x00, 0x05, // length + 0x01, // unit id + 0xde, 0xad, 0xbe, 0xef, + }; + transport->add_incoming_data(response); + + auto [received_context, received_pdu] = protocol.receive_blocking(); + ASSERT_EQ(received_pdu.function_code, 0xde); + ASSERT_EQ(received_pdu.data.size(), 3); + ASSERT_EQ(received_pdu.data[0], 0xad); + ASSERT_EQ(received_pdu.data[1], 0xbe); + ASSERT_EQ(received_pdu.data[2], 0xef); + + // check that context are the same + ASSERT_EQ(send_context, received_context); +} + +TEST(ModbusTCPProtocol, client_scenario_different_unit_ids) { + auto transport = std::make_shared(); + ModbusTCPProtocol protocol = ModbusTCPProtocol(transport, 0xde, 0x0000); + + auto send_context = protocol.new_send_context(); + protocol.send_blocking(pdu::GenericPDU(0xab, {0xde, 0xad, 0xca}), send_context); + + auto written = transport->get_outgoing_data(); + ASSERT_EQ(written.size(), 7 + 4); // header + pdu + // transaction id should be 0 + ASSERT_EQ(written[0], 0); + ASSERT_EQ(written[1], 0); + // unit id should be 0xde + ASSERT_EQ(written[6], 0xde); + + std::vector response = { + 0x00, 0x00, // transaction id + 0x00, 0x00, // protocol id + 0x00, 0x05, // length + 0x0f, // unit id (different) + 0xca, 0xfe, 0xbe, 0xef, + }; + transport->add_incoming_data(response); + + auto [received_context, received_pdu] = protocol.receive_blocking(); + ASSERT_EQ(received_pdu.function_code, 0xca); + ASSERT_EQ(received_pdu.data.size(), 3); + ASSERT_EQ(received_pdu.data[0], 0xfe); + ASSERT_EQ(received_pdu.data[1], 0xbe); + ASSERT_EQ(received_pdu.data[2], 0xef); + + // check that context are the same + ASSERT_NE(send_context.data, received_context.data); +} + +TEST(ModbusTCPProtocol, new_send_context_generates_different_contexts) { + auto transport = std::make_shared(); + ModbusTCPProtocol protocol = ModbusTCPProtocol(transport); + + auto context1 = protocol.new_send_context(); + auto context2 = protocol.new_send_context(); + auto context3 = protocol.new_send_context(); + + ASSERT_NE_VECTOR(context1.data, context2.data); + ASSERT_NE_VECTOR(context2.data, context3.data); + ASSERT_NE_VECTOR(context1.data, context3.data); +} + +TEST(ModbusTCPProtocol, server_scenario) { + auto transport = std::make_shared(); + ModbusTCPProtocol protocol = ModbusTCPProtocol(transport); + + std::vector request = { + 0xca, 0xfe, // transaction id + 0x00, 0x00, // protocol id + 0x00, 0x07, // length + 0x01, // unit id + 0xde, // function code + 0xad, 0xbe, 0xef, 0x00, 0x01, + }; + transport->add_incoming_data(request); + + auto [received_context, received_pdu] = protocol.receive_blocking(); + ASSERT_EQ(received_pdu.function_code, 0xde); + ASSERT_EQ(received_pdu.data.size(), 5); + ASSERT_EQ(received_pdu.data[0], 0xad); + ASSERT_EQ(received_pdu.data[1], 0xbe); + ASSERT_EQ(received_pdu.data[2], 0xef); + ASSERT_EQ(received_pdu.data[3], 0x00); + ASSERT_EQ(received_pdu.data[4], 0x01); + + pdu::GenericPDU response(0xab, {0xca, 0xfe, 0xba, 0xbe, 0x00}); + protocol.send_blocking(response, received_context); + + auto written = transport->get_outgoing_data(); + ASSERT_EQ(written.size(), 7 + 6); // header + pdu + // transaction id should be 0xcafe + ASSERT_EQ(written[0], 0xca); + ASSERT_EQ(written[1], 0xfe); + // protocol id should be 0 + ASSERT_EQ(written[2], 0); + ASSERT_EQ(written[3], 0); + // length should be 8 + ASSERT_EQ(written[4], 0); + ASSERT_EQ(written[5], 7); + + ASSERT_EQ(written[7], 0xab); + ASSERT_EQ(written[8], 0xca); + ASSERT_EQ(written[9], 0xfe); + ASSERT_EQ(written[10], 0xba); + ASSERT_EQ(written[11], 0xbe); + ASSERT_EQ(written[12], 0x00); +} + +TEST(ModbusTCPProtocol, initial_transaction_id) { + auto transport = std::make_shared(); + ModbusTCPProtocol protocol = ModbusTCPProtocol(transport, 0xab, 0xc0de); + + auto context = protocol.new_send_context(); + protocol.send_blocking(pdu::GenericPDU(0xde, {0xad, 0xca, 0xfe}), context); + + auto written = transport->get_outgoing_data(); + ASSERT_EQ(written.size(), 7 + 4); // header + pdu + // transaction id should be 0xc0de + ASSERT_EQ(written[0], 0xc0); + ASSERT_EQ(written[1], 0xde); + // protocol id should be 0 + ASSERT_EQ(written[2], 0); + ASSERT_EQ(written[3], 0); + // length should be 5 + ASSERT_EQ(written[4], 0); + ASSERT_EQ(written[5], 5); + // unit id should be 0xab + ASSERT_EQ(written[6], 0xab); +} + +TEST(ModbusTCPProtocol, zero_length) { + auto transport = std::make_shared(); + ModbusTCPProtocol protocol = ModbusTCPProtocol(transport, 0xab, 0xc0de); + + transport->add_incoming_data({0xc0, 0xde, 0, 0, 0, 0, 0xab}); // empty pdu + auto a = protocol.try_receive(); + + ASSERT_EQ(a.has_value(), false); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/pdu_correlation.test.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/pdu_correlation.test.cpp new file mode 100644 index 0000000000..a8fc04ac64 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/pdu_correlation.test.cpp @@ -0,0 +1,354 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include +#include +#include + +#include "dummy_modbus_transport.hpp" + +using namespace modbus_server; + +TEST(PDUCorrelationLayer, check_tcp_protocol_works_expectedly) { + auto transport = std::make_shared(); + ModbusTCPProtocol protocol(transport, 0x01, 0x0000); + + // check that first send context has transaction id 0 + auto context = protocol.new_send_context(); + protocol.send_blocking(pdu::GenericPDU(0x01, {0x02}), context); + auto written = transport->get_outgoing_data(); + ASSERT_EQ(written.size(), 9); + ASSERT_EQ(written[0], 0x00); + ASSERT_EQ(written[1], 0x00); + + // check that second send context has transaction id 1 + context = protocol.new_send_context(); + protocol.send_blocking(pdu::GenericPDU(0x03, {0x04}), context); + written = transport->get_outgoing_data(); + ASSERT_EQ(written.size(), 9); + ASSERT_EQ(written[0], 0x00); + ASSERT_EQ(written[1], 0x01); +} + +TEST(PDUCorrelationLayer, request_response_single) { + auto transport = std::make_shared(); + auto protocol = std::make_shared(transport, 0x01, 0x0000); + PDUCorrelationLayer pal(protocol); + + pdu::GenericPDU request = pdu::GenericPDU(0xab, {0x01}); + + // answer + transport->add_incoming_data({ + 0x00, 0x00, // transaction id + 0x00, 0x00, // protocol id + 0x00, 0x05, // length + 0x01, // unit id + 0xab, // function code + 0x00, 0x01, 0x02 // data + }); + + auto thread = std::thread([&pal]() { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + pal.blocking_poll(); + }); + + auto response = pal.request_response(request, std::chrono::milliseconds(1000)); + ASSERT_EQ(response.function_code, 0xab); + ASSERT_EQ(response.data.size(), 3); + ASSERT_EQ(response.data[0], 0x00); + ASSERT_EQ(response.data[1], 0x01); + ASSERT_EQ(response.data[2], 0x02); + + thread.join(); +} + +TEST(PDUCorrelationLayer, request_response_parallel_out_of_order) { + /* + * What is tested here: + * at time 5ms: request 1 is sent + * at time 10ms: request 2 is sent + * at time 20ms: poll is called, response 2 is received + * at time 40ms: poll is called, response 1 is received + * + * Expected result: + * response 2 is received before response 1 because it was sent first + * response 1 contains response data to request 1 + * response 2 contains response data to request 2 + */ + auto transport = std::make_shared(); + auto protocol = std::make_shared(transport, 0x01, 0x0000); + PDUCorrelationLayer pal(protocol); + + pdu::GenericPDU request1 = pdu::GenericPDU(0xab, {0x01}); + pdu::GenericPDU request2 = pdu::GenericPDU(0xcd, {0x02}); + + // answer for request2 (transaction id 1) + transport->add_incoming_data({ + 0x00, 0x01, // transaction id + 0x00, 0x00, // protocol id + 0x00, 0x05, // length + 0x01, // unit id + 0xcd, // function code + 0x00, 0x01, 0x02 // data + }); + + // answer for request1 (transaction id 0) + transport->add_incoming_data({ + 0x00, 0x00, // transaction id + 0x00, 0x00, // protocol id + 0x00, 0x05, // length + 0x01, // unit id + 0xab, // function code + 0x00, 0x01, 0x02 // data + }); + + auto thread = std::thread([&pal]() { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + pal.blocking_poll(); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + pal.blocking_poll(); + }); + + pdu::GenericPDU response1, response2; + std::chrono::steady_clock::time_point response1_time, response2_time; + + auto thread_req_1 = std::thread([&pal, &request1, &response1, &response1_time]() { + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + response1 = pal.request_response(request1, std::chrono::milliseconds(1000)); + response1_time = std::chrono::steady_clock::now(); + }); + + auto thread_req_2 = std::thread([&pal, &request2, &response2, &response2_time]() { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + response2 = pal.request_response(request2, std::chrono::milliseconds(1000)); + response2_time = std::chrono::steady_clock::now(); + }); + + thread_req_1.join(); + thread_req_2.join(); + thread.join(); + + ASSERT_EQ(response1.function_code, 0xab); + ASSERT_EQ(response1.data.size(), 3); + ASSERT_EQ(response1.data[0], 0x00); + ASSERT_EQ(response1.data[1], 0x01); + ASSERT_EQ(response1.data[2], 0x02); + + ASSERT_EQ(response2.function_code, 0xcd); + ASSERT_EQ(response2.data.size(), 3); + ASSERT_EQ(response2.data[0], 0x00); + ASSERT_EQ(response2.data[1], 0x01); + ASSERT_EQ(response2.data[2], 0x02); + + ASSERT_TRUE(response2_time < response1_time); +} + +TEST(PDUCorrelationLayer, timeout) { + auto transport = std::make_shared(); + auto protocol = std::make_shared(transport); + PDUCorrelationLayer pal(protocol); + + pdu::GenericPDU request = pdu::GenericPDU(0xab, {0x01}); + + ASSERT_THROW(pal.request_response(request, std::chrono::milliseconds(100)), std::runtime_error); +} + +TEST(PDUCorrelationLayer, send_requestless) { + auto transport = std::make_shared(); + auto protocol = std::make_shared(transport); + PDUCorrelationLayer pal(protocol); + + pal.request_without_response(pdu::GenericPDU(0x01, {0x02})); + + auto written = transport->get_outgoing_data(); + + ASSERT_EQ(written.size(), 9); + // check pdu function code and data + ASSERT_EQ(written[7], 0x01); + ASSERT_EQ(written[8], 0x02); +} + +TEST(PDUCorrelationLayer, on_pdu_callback) { + auto transport = std::make_shared(); + auto protocol = std::make_shared(transport, 0x01, 0x0000); + PDUCorrelationLayer pal(protocol); + + pal.set_on_pdu([](const pdu::GenericPDU& pdu) { return pdu::GenericPDU(pdu.function_code, {0x03, 0x04, 0x05}); }); + + transport->add_incoming_data({ + 0xde, 0xad, // transaction id + 0x00, 0x00, // protocol id + 0x00, 0x05, // length + 0x01, // unit id + 0xef, // function code + 0x00, 0x01, 0x02 // data + }); + + pal.blocking_poll(); + + auto written = transport->get_outgoing_data(); + + ASSERT_EQ(written.size(), 11); + ASSERT_EQ(written[0], 0xde); + ASSERT_EQ(written[1], 0xad); + + ASSERT_EQ(written[7], 0xef); + ASSERT_EQ(written[8], 0x03); + ASSERT_EQ(written[9], 0x04); + ASSERT_EQ(written[10], 0x05); +} + +TEST(PDUCorrelationLayer, on_pdu_callback_ignores_request_response) { + auto transport = std::make_shared(); + auto protocol = std::make_shared(transport, 0x01, 0x0000); + PDUCorrelationLayer pal(protocol); + + std::uint8_t callback_calls = 0; + + pal.set_on_pdu([&callback_calls](const pdu::GenericPDU& pdu) { + callback_calls++; + return std::nullopt; + }); + + pdu::GenericPDU request = pdu::GenericPDU(0xef, {0x01}); + + // answer + transport->add_incoming_data({ + 0x00, 0x00, // transaction id + 0x00, 0x00, // protocol id + 0x00, 0x05, // length + 0x01, // unit id + 0xef, // function code + 0x00, 0x01, 0x02 // data + }); + + auto thread = std::thread([&pal]() { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + pal.blocking_poll(); + }); + + auto response = pal.request_response(request, std::chrono::milliseconds(1000)); + ASSERT_EQ(response.function_code, 0xef); + ASSERT_EQ(response.data.size(), 3); + ASSERT_EQ(response.data[0], 0x00); + ASSERT_EQ(response.data[1], 0x01); + ASSERT_EQ(response.data[2], 0x02); + + thread.join(); + + ASSERT_EQ(callback_calls, 0); +} + +TEST(PDUCorrelationLayer, given_no_pdu_callback_ignores_all_incoming) { + auto transport = std::make_shared(); + auto protocol = std::make_shared(transport, 0x01, 0x0000); + PDUCorrelationLayer pal(protocol); + + // some random packet + transport->add_incoming_data({ + 0x00, 0x00, // transaction id + 0x00, 0x00, // protocol id + 0x00, 0x05, // length + 0x01, // unit id + 0xef, // function code + 0x00, 0x01, 0x02 // data + }); + + pal.blocking_poll(); + + // we are still alive, nice + ASSERT_TRUE(true); +} + +TEST(PDUCorrelationLayer, error_response) { + auto transport = std::make_shared(); + auto protocol = std::make_shared(transport, 0x01, 0x0000); + PDUCorrelationLayer pal(protocol); + + transport->add_incoming_data({ + 0x00, 0x00, // transaction id + 0x00, 0x00, // protocol id + 0x00, 0x03, // length + 0x01, // unit id + 0x83, // function code + 0x02 // exception code + }); + + auto thread = std::thread([&pal]() { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + pal.blocking_poll(); + }); + + auto request = pdu::GenericPDU(0x03, {0x02}); + auto response = pal.request_response(request, std::chrono::milliseconds(1000)); + ASSERT_EQ(response.function_code, 0x83); + ASSERT_EQ(response.data.size(), 1); + ASSERT_EQ(response.data[0], 0x02); + + thread.join(); +} + +TEST(PDUCorrelationLayer, error_response_different_function_code) { + auto transport = std::make_shared(); + auto protocol = std::make_shared(transport, 0x01, 0x0000); + PDUCorrelationLayer pal(protocol); + + int callback_calls = 0; + pal.set_on_pdu([&callback_calls](const pdu::GenericPDU& pdu) { + callback_calls++; + return std::nullopt; + }); + + transport->add_incoming_data({ + 0x00, 0x00, // transaction id + 0x00, 0x00, // protocol id + 0x00, 0x03, // length + 0x01, // unit id + 0x83, // function code + 0x02 // exception code + }); + + auto thread = std::thread([&pal]() { + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + pal.blocking_poll(); + }); + + auto request = pdu::GenericPDU(0x02, {0x02}); + ASSERT_THROW(pal.request_response(request, std::chrono::milliseconds(100)), std::runtime_error); + + thread.join(); + + ASSERT_EQ(callback_calls, 1); +} + +TEST(PDUCorrelationLayer, timeout_works_accurate) { + auto transport = std::make_shared(); + auto protocol = std::make_shared(transport, 0x01, 0x0000); + auto pal = std::make_shared(protocol); + + auto thread = std::thread([&pal, &transport]() { + // 4 random packets, after 10ms, 20ms, 30ms, 40ms + for (std::uint8_t i = 0; i < 4; i++) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + transport->add_incoming_data({ + 0xff, i, // transaction id + 0x00, 0x00, // protocol id + 0x00, 0x03, // length + 0x01, // unit id + 0x05, // function code + 0x02 // exception code + }); + pal->blocking_poll(); + } + }); + + auto request = pdu::GenericPDU(0x01, {0x02}); + auto time_before = std::chrono::steady_clock::now(); + ASSERT_THROW(pal->request_response(request, std::chrono::milliseconds(50)), std::runtime_error); + auto time_after = std::chrono::steady_clock::now(); + + ASSERT_TRUE(time_after - time_before < std::chrono::milliseconds(60)); // 10ms tolerance + + thread.join(); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/pdu_exception.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/pdu_exception.cpp new file mode 100644 index 0000000000..2cd26f6cc5 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/pdu_exception.cpp @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include + +using namespace modbus_server::pdu; + +TEST(PDUExceptionCodes, to_string) { + EXPECT_EQ(exception_code_to_string(PDUExceptionCode::ILLEGAL_FUNCTION), "ILLEGAL_FUNCTION"); + EXPECT_EQ(exception_code_to_string(PDUExceptionCode::ILLEGAL_DATA_ADDRESS), "ILLEGAL_DATA_ADDRESS"); + EXPECT_EQ(exception_code_to_string(PDUExceptionCode::ILLEGAL_DATA_VALUE), "ILLEGAL_DATA_VALUE"); + EXPECT_EQ(exception_code_to_string(PDUExceptionCode::SERVER_DEVICE_FAILURE), "SERVER_DEVICE_FAILURE"); + EXPECT_EQ(exception_code_to_string(PDUExceptionCode::ACKNOWLEDGE), "ACKNOWLEDGE"); + EXPECT_EQ(exception_code_to_string(PDUExceptionCode::SERVER_DEVICE_BUSY), "SERVER_DEVICE_BUSY"); + EXPECT_EQ(exception_code_to_string(PDUExceptionCode::MEMORY_PARITY_ERROR), "MEMORY_PARITY_ERROR"); + EXPECT_EQ(exception_code_to_string(PDUExceptionCode::GATEWAY_PATH_UNAVAILABLE), "GATEWAY_PATH_UNAVAILABLE"); + EXPECT_EQ(exception_code_to_string(PDUExceptionCode::GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND), + "GATEWAY_TARGET_DEVICE_FAILED_TO_RESPOND"); +} + +TEST(PDUException, from_pdu_what) { + GenericPDU pdu; + pdu.function_code = 0x80 | 0x03; + pdu.data = {0x01}; + + PDUException ex(pdu); + EXPECT_STREQ(ex.what(), "PDUException: ILLEGAL_FUNCTION"); +} + +TEST(PDUException, from_code_what) { + PDUException ex(PDUExceptionCode::ILLEGAL_DATA_ADDRESS); + EXPECT_STREQ(ex.what(), "PDUException: ILLEGAL_DATA_ADDRESS"); +} + +TEST(PDUException, get_exception_code) { + PDUException ex(PDUExceptionCode::ILLEGAL_DATA_ADDRESS); + EXPECT_EQ(ex.get_exception_code(), (std::uint8_t)PDUExceptionCode::ILLEGAL_DATA_ADDRESS); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/socket_transport.test.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/socket_transport.test.cpp new file mode 100644 index 0000000000..a006d8465f --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/base/tests/socket_transport.test.cpp @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include +#include + +#include +#include + +TEST(SocketTransport, read_works) { + int fds[2]; + int err = socketpair(AF_UNIX, SOCK_STREAM, 0, fds); + ASSERT_EQ(err, 0); + + modbus_server::ModbusSocketTransport transport(fds[0]); + + std::vector data = {0x01, 0x02, 0x03, 0x04}; + err = write(fds[1], data.data(), data.size()); + ASSERT_EQ(err, data.size()); + + std::vector read_data = transport.read_bytes(data.size()); + for (size_t i = 0; i < data.size(); i++) { + ASSERT_EQ(data[i], read_data[i]); + } + + close(fds[1]); + close(fds[0]); +} + +TEST(SocketTransport, fragmented_read_works) { + int fds[2]; + int err = socketpair(AF_UNIX, SOCK_STREAM, 0, fds); + ASSERT_EQ(err, 0); + + modbus_server::ModbusSocketTransport transport(fds[0]); + + std::vector data = {0x01, 0x02, 0x03, 0x04}; + + auto thread = std::thread([&fds, &data]() { + int err; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + err = write(fds[1], data.data(), 2); + ASSERT_EQ(err, 2); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + err = write(fds[1], data.data() + 2, 2); + ASSERT_EQ(err, 2); + }); + + std::vector read_data = transport.read_bytes(data.size()); + ASSERT_EQ(read_data.size(), data.size()); + for (size_t i = 0; i < data.size(); i++) { + ASSERT_EQ(data[i], read_data[i]); + } + + close(fds[1]); + close(fds[0]); + + thread.join(); +} + +TEST(SocketTransport, write_works) { + int fds[2]; + int err = socketpair(AF_UNIX, SOCK_STREAM, 0, fds); + ASSERT_EQ(err, 0); + + modbus_server::ModbusSocketTransport transport(fds[0]); + + std::vector data = {0x01, 0x02, 0x03, 0x04}; + transport.write_bytes(data); + + std::vector read_data(data.size()); + err = read(fds[1], read_data.data(), read_data.size()); + ASSERT_EQ(err, data.size()); + + for (size_t i = 0; i < data.size(); i++) { + ASSERT_EQ(data[i], read_data[i]); + } +} + +TEST(SocketTransport, closed_socket_read_throws_err) { + int fds[2]; + int err = socketpair(AF_UNIX, SOCK_STREAM, 0, fds); + ASSERT_EQ(err, 0); + + modbus_server::ModbusSocketTransport transport(fds[0]); + + close(fds[1]); + + ASSERT_THROW(transport.read_bytes(1), modbus_server::transport_exceptions::ConnectionClosedException); + + close(fds[0]); +} + +TEST(SocketTransport, not_existing_socket_read_throws_err) { + int fds[2]; + int err = socketpair(AF_UNIX, SOCK_STREAM, 0, fds); + ASSERT_EQ(err, 0); + + modbus_server::ModbusSocketTransport transport(fds[0]); + + close(fds[1]); + close(fds[0]); + + // socket closed on both ends should throw runtime_error + ASSERT_THROW(transport.read_bytes(1), std::runtime_error); +} + +TEST(SocketTransport, closed_socket_write_throws_err) { + int fds[2]; + int err = socketpair(AF_UNIX, SOCK_STREAM, 0, fds); + ASSERT_EQ(err, 0); + + modbus_server::ModbusSocketTransport transport(fds[0]); + + close(fds[0]); + close(fds[1]); + + ASSERT_THROW(transport.write_bytes({0x01}), std::runtime_error); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/client/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/client/CMakeLists.txt new file mode 100644 index 0000000000..ecde3f8185 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/client/CMakeLists.txt @@ -0,0 +1,11 @@ +file(GLOB_RECURSE MODBUS_CLIENT_SOURCES "src/*.cpp") + +# Create modbus-client library +add_library( + modbus-client + STATIC + ${MODBUS_CLIENT_SOURCES} +) +target_include_directories(modbus-client PUBLIC include) +target_link_libraries(modbus-client PUBLIC modbus-base) +ev_register_library_target(modbus-client) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/client/include/modbus-server/client.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/client/include/modbus-server/client.hpp new file mode 100644 index 0000000000..ffeb5dab7a --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/client/include/modbus-server/client.hpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#ifndef MODBUS_SERVER__CLIENT_HPP +#define MODBUS_SERVER__CLIENT_HPP + +#include +#include + +namespace modbus_server { +namespace client { + +class ModbusClient { + std::shared_ptr pas; + +protected: + /** + * @brief Utility function; if given response is an error, throw \c + * PDUException + * + * @param response The response to check + * @throws PDUException if the response is an error + */ + void handle_error_response(const pdu::GenericPDU& response); + +public: + ModbusClient(std::shared_ptr pas); + std::vector read_holding_registers(std::uint16_t start_address, std::uint16_t quantity); + void write_single_register(std::uint16_t address, std::uint16_t value); + void write_multiple_registers(std::uint16_t start_address, const std::vector& values); +}; + +} // namespace client +} // namespace modbus_server + +#endif diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/client/src/client.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/client/src/client.cpp new file mode 100644 index 0000000000..4bc186b8d5 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/client/src/client.cpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include +#include +#include +#include +#include + +using namespace modbus_server::client; +using namespace modbus_server; + +ModbusClient::ModbusClient(std::shared_ptr pas) : pas(pas) { +} + +void ModbusClient::handle_error_response(const pdu::GenericPDU& response) { + if ((response.function_code & 0x80) != 0) { + throw pdu::PDUException(response); + } +} + +std::vector ModbusClient::read_holding_registers(std::uint16_t start_address, std::uint16_t quantity) { + pdu::ReadHoldingRegistersRequest request; + request.register_start = start_address; + request.register_count = quantity; + + pdu::GenericPDU generic = request.to_generic(); + + auto response = pas->request_response(generic, std::chrono::seconds(5)); + handle_error_response(response); + + pdu::ReadHoldingRegistersResponse response_data; + response_data.from_generic(response); + + return response_data.get_register_data(); +} + +void ModbusClient::write_single_register(std::uint16_t address, std::uint16_t value) { + pdu::WriteSingleRegisterRequest request; + request.register_address = address; + request.register_value = value; + + pdu::GenericPDU generic = request.to_generic(); + + auto resp = pas->request_response(generic, std::chrono::seconds(5)); + handle_error_response(resp); +} + +void ModbusClient::write_multiple_registers(std::uint16_t start_address, const std::vector& values) { + pdu::WriteMultipleRegistersRequest request; + request.register_start = start_address; + request.register_count = values.size(); + request.register_data.reserve(values.size() * 2); + + for (auto value : values) { + request.register_data.push_back(value >> 8); + request.register_data.push_back(value & 0xFF); + } + + pdu::GenericPDU generic = request.to_generic(); + + auto resp = pas->request_response(generic, std::chrono::seconds(5)); + handle_error_response(resp); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/client/tests/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/client/tests/CMakeLists.txt new file mode 100644 index 0000000000..b0a575d003 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/client/tests/CMakeLists.txt @@ -0,0 +1,8 @@ +include(GoogleTest) + +file(GLOB_RECURSE MODBUS_SERVER_TESTS_SOURCES "*.cpp") + +add_executable(modbus-client-tests ${MODBUS_SERVER_TESTS_SOURCES}) +target_link_libraries(modbus-client-tests PRIVATE modbus-client gtest_main) + +gtest_discover_tests(modbus-client-tests) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/client/tests/client.test.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/client/tests/client.test.cpp new file mode 100644 index 0000000000..276e6a82d6 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/client/tests/client.test.cpp @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include +#include + +#include "dummy_pas.hpp" + +using namespace modbus_server; +using namespace modbus_server::client; + +TEST(ModbusClient, read_holding_registers) { + auto correlation_layer = std::make_shared(); + ModbusClient client(correlation_layer); + + correlation_layer->add_next_answer(pdu::GenericPDU(0x03, {0x04, 0x01, 0x02, 0x03, 0x04})); + + auto response = client.read_holding_registers(0x01, 0x02); + ASSERT_EQ(response.size(), 2); + ASSERT_EQ(response[0], 0x0102); + ASSERT_EQ(response[1], 0x0304); +} + +TEST(ModbusClient, write_single_register) { + auto correlation_layer = std::make_shared(); + ModbusClient client(correlation_layer); + + correlation_layer->add_next_answer(pdu::GenericPDU(0x06, {0x01, 0x02, 0x03, 0x04})); + + client.write_single_register(0x0102, 0x0304); + + auto request = correlation_layer->get_last_request(); + ASSERT_EQ(request.function_code, 0x06); + ASSERT_EQ(request.data.size(), 4); + ASSERT_EQ(request.data[0], 0x01); + ASSERT_EQ(request.data[1], 0x02); + ASSERT_EQ(request.data[2], 0x03); + ASSERT_EQ(request.data[3], 0x04); +} + +TEST(ModbusClient, write_multiple_registers) { + auto correlation_layer = std::make_shared(); + ModbusClient client(correlation_layer); + + ASSERT_NO_THROW( + correlation_layer->add_next_answer(pdu::WriteMultipleRegistersResponse(0xfeed, 0x0002).to_generic())); + + client.write_multiple_registers(0xfeed, {0xc0de, 0xbeef}); + + auto request = correlation_layer->get_last_request(); + auto request_parsed = pdu::WriteMultipleRegistersRequest(); + ASSERT_NO_THROW(request_parsed.from_generic(request)); + + ASSERT_EQ(request_parsed.register_start, 0xfeed); + ASSERT_EQ(request_parsed.register_count, 2); + ASSERT_EQ(request_parsed.register_data[0], 0xc0); + ASSERT_EQ(request_parsed.register_data[1], 0xde); + ASSERT_EQ(request_parsed.register_data[2], 0xbe); + ASSERT_EQ(request_parsed.register_data[3], 0xef); +} + +TEST(ModbusClient, read_holding_registers_error_response) { + auto correlation_layer = std::make_shared(); + ModbusClient client(correlation_layer); + + correlation_layer->add_next_answer(pdu::ErrorPDU(0x03, 0x02).to_generic()); + + ASSERT_THROW(client.read_holding_registers(0x01, 0x02), pdu::PDUException); +} + +TEST(ModbusClient, write_single_register_error_response) { + auto correlation_layer = std::make_shared(); + ModbusClient client(correlation_layer); + + correlation_layer->add_next_answer(pdu::ErrorPDU(0x06, 0x02).to_generic()); + + ASSERT_THROW(client.write_single_register(0x0102, 0x0304), pdu::PDUException); +} + +TEST(ModbusClient, write_multiple_registers_error_response) { + auto correlation_layer = std::make_shared(); + ModbusClient client(correlation_layer); + + correlation_layer->add_next_answer(pdu::ErrorPDU(0x10, 0x02).to_generic()); + + ASSERT_THROW(client.write_multiple_registers(0xfeed, {0xc0de, 0xbeef}), pdu::PDUException); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/client/tests/dummy_pas.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/client/tests/dummy_pas.hpp new file mode 100644 index 0000000000..9a4fa82ad5 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/client/tests/dummy_pas.hpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#ifndef MODBUS_TESTS__DUMMY_PAS_HPP +#define MODBUS_TESTS__DUMMY_PAS_HPP + +#include + +class DummyPDUCorrelationLayer : public modbus_server::PDUCorrelationLayerIntf { + std::vector next_answer; + std::vector last_request; + +public: + DummyPDUCorrelationLayer() = default; + + bool poll() override { + return false; + }; + void blocking_poll() override { + } + + modbus_server::pdu::GenericPDU request_response(const modbus_server::pdu::GenericPDU& request, + std::chrono::milliseconds timeout) override { + printf("next A\n"); + last_request.push_back(request); + + printf("next B\n"); + if (next_answer.empty()) { + throw std::runtime_error("No answer available"); + } + auto answer = next_answer.front(); + next_answer.erase(next_answer.begin()); + printf("Debug\n"); + return answer; + } + + void request_without_response(const modbus_server::pdu::GenericPDU& request) override { + last_request.push_back(request); + } + + void add_next_answer(const modbus_server::pdu::GenericPDU& answer) { + next_answer.push_back(answer); + } + + std::optional call_on_pdu(const modbus_server::pdu::GenericPDU& pdu) { + return this->on_pdu.value()(pdu); + } + + modbus_server::pdu::GenericPDU get_last_request() { + if (last_request.empty()) { + throw std::runtime_error("No request available"); + } + auto request = last_request.front(); + last_request.erase(last_request.begin()); + return request; + } +}; + +#endif diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/CMakeLists.txt new file mode 100644 index 0000000000..cd0c36e695 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/CMakeLists.txt @@ -0,0 +1,15 @@ +include(GoogleTest) + +file(GLOB SOURCES "src/*.cpp") + +add_library(modbus-registers STATIC ${SOURCES}) +ev_register_library_target(modbus-registers) +target_include_directories(modbus-registers PUBLIC include) +target_link_libraries(modbus-registers PUBLIC modbus-server) + +if(EVEREST_CORE_BUILD_TESTING) + file(GLOB TEST_SOURCES "tests/*.cpp") + add_executable(modbus-registers-tests ${TEST_SOURCES}) + target_link_libraries(modbus-registers-tests PRIVATE modbus-registers GTest::gtest_main) + gtest_discover_tests(modbus-registers-tests) +endif() diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/include/modbus-registers/converter.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/include/modbus-registers/converter.hpp new file mode 100644 index 0000000000..95f8a00a04 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/include/modbus-registers/converter.hpp @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include + +namespace modbus { +namespace registers { +namespace converters { + +/** + * @brief Converters to be used by registers to convert between system types + * (e.g. std::uint32_t) and + * + */ +class Converter { +public: + /** + * @brief Convert from register mapped endianness to system endianness + * + * @param in Input buffer (mapped endianness) + * @param out Output buffer (system endianness) + * @param len Length of the buffers + */ + virtual void net_to_sys(const std::uint8_t* in, std::uint8_t* out, size_t len) const = 0; + + /** + * @brief Convert from system endianness to register mapped endianness + * + * @param in Input buffer (system endianness) + * @param out Output buffer (mapped endianness) + * @param len Length of the buffers + */ + virtual void sys_to_net(const std::uint8_t* in, std::uint8_t* out, size_t len) const = 0; +}; + +/** + * @brief Converts outer and inner Big Endian (ABCD; where A is the most + * significant byte) to system endianness. + * @note This is the default for 16-bit modbus registers. + * + * @example std::uint32_t sys_to_net: 0x12345678 -> 0x12, 0x34, 0x56, 0x78; + * net_to_sys: 0x12, 0x34, 0x56, 0x78 -> 0x12345678 + */ +class ConverterABCD : public Converter { +public: + void net_to_sys(const std::uint8_t* in, std::uint8_t* out, size_t len) const override { +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + for (size_t i = 0; i < len; i++) { + out[i] = in[len - i - 1]; + } +#else + memcpy(out, in, len); +#endif + } + + void sys_to_net(const std::uint8_t* in, std::uint8_t* out, size_t len) const override { +#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ + for (size_t i = 0; i < len; i++) { + out[i] = in[len - i - 1]; + } + } +#else + memcpy(out, in, len); +#endif + + /** + * @brief Singleton instance, as everything is stateless + * + * @return const ConverterABCD& Singleton instance + */ + static const ConverterABCD& instance() { + static ConverterABCD instance; + return instance; + } +}; + +/** + * @brief Converter that does not perform any conversion, useful for string + * registers. + * @example std::uint8_t[] sys_to_net: 0x12, 0x34, 0x56, 0x78 -> 0x12, 0x34, 0x56, + * 0x78; net_to_sys: 0x12, 0x34, 0x56, 0x78 -> 0x12, 0x34, 0x56, 0x78 + */ +class ConverterIdentity : public Converter { +public: + void net_to_sys(const std::uint8_t* in, std::uint8_t* out, size_t len) const override { + memcpy(out, in, len); + } + + void sys_to_net(const std::uint8_t* in, std::uint8_t* out, size_t len) const override { + memcpy(out, in, len); + } + + /** + * @brief Singleton instance, as everything is stateless + * + * @return const ConverterABCD& Singleton instance + */ + static const ConverterIdentity& instance() { + static ConverterIdentity instance; + return instance; + } +}; + +}; // namespace converters +}; // namespace registers +}; // namespace modbus diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/include/modbus-registers/data_provider.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/include/modbus-registers/data_provider.hpp new file mode 100644 index 0000000000..58255363cd --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/include/modbus-registers/data_provider.hpp @@ -0,0 +1,341 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include +#include +#include + +namespace modbus { +namespace registers { +namespace data_providers { + +/** + * @brief An untyped \c DataProvider for Registers. This is only a helper + * class. + * + */ +class DataProviderUntyped { +public: + virtual ~DataProviderUntyped() { + } +}; + +/** + * @brief A typed DataProvider for Registers. This is a interface + * + * @tparam T the provided data type. String types are generally handled by the + * sub-interface \c DataProviderString + */ +template class DataProvider : public DataProviderUntyped { +public: + /** + * @brief to be called when the register is read from, may also be called if a + * partial write is happening. + * + * @param value The raw value of the register, already converted to system + * endianness (can be safely cast to T*) + * @param len The length of the value, normally sizeof(T) + */ + virtual void on_read(std::uint8_t* value, size_t len) = 0; + /** + * @brief to be called when the register is written to, always called with the + * full data + * + * @param value The raw value of the register, in system endianness, to be + * converted later by \c Converter + * @param len The length of the value, normally sizeof(T) + */ + virtual void on_write(std::uint8_t* value, size_t len) = 0; +}; + +/** + * @brief A typed DataProvider for Registers, dedicated to storing strings. + * + * @tparam string_length The maximum length of the string (without null + * terminator) + */ +template class DataProviderString : public DataProvider {}; + +/** + * @brief A holding implementation for \c DataProviderString that stores the + * data internally without callbacks. + * + * @tparam string_length The maximum length of the string (without null + * terminator) + */ +template class DataProviderStringHolding : public DataProviderString { +private: + static void strlcpy(char* dst, const char* src, size_t size) { + strncpy(dst, src, size - 1); + dst[size - 1] = 0; + } + +protected: + char value[string_length + 1]; + std::vector> callbacks; + + void notify_callbacks() { + for (auto& callback : callbacks) { + callback(value); + } + } + +public: + DataProviderStringHolding() { + memset(value, 0, string_length); + } + DataProviderStringHolding(const char* value) { + memset(this->value, 0, string_length + 1); + strlcpy((char*)this->value, (char*)value, string_length + 1); + } + + /** + * @brief Add a write callback to be called when the value is written to + * + * @param callback The callback to be called + */ + void add_write_callback(std::function callback) { + callbacks.push_back(callback); + } + + void on_read(std::uint8_t* value, size_t len) override { + memset(value, 0, len); + //! note: we are using a strncpy here because we do not want null + //! termination in the returned string + strncpy((char*)value, (char*)this->value, std::min(string_length, len)); + // todo: more tests + } + + void on_write(std::uint8_t* value, size_t len) override { + strlcpy((char*)this->value, (char*)value, std::min(string_length + 1, len + 1)); + // todo: more tests + + notify_callbacks(); + } + + /** + * @brief Get the current value + * + * @return const char* The current value. Note that this pointer is pointing + * to private memory + */ + const char* get_value() const { + return value; + } + + /** + * @brief Update the current value. If given string is longer than the maximum + * length, it will be truncated. + * + * @param value The new value. Has to live past the function call. + */ + void update_value(const char* value) { + memset(this->value, 0, string_length + 1); + strlcpy(this->value, value, string_length + 1); + } +}; + +// todo: imporive read callback stuff + +/** + * @brief A typed DataProvider for Registers, dedicated to storing arrays. + * + * @tparam array_size The size of the array + */ +template class DataProviderMemory : public DataProvider {}; + +/** + * @brief A holding implementation for \c DataProviderMemory that stores the + * data internally without callbacks. + * + * @tparam array_size The size of the array + */ +template class DataProviderMemoryHolding : public DataProviderMemory { +protected: + std::uint8_t value[array_size]; + std::vector> callbacks; + std::vector> read_callbacks; + + void notify_callbacks() { + for (auto& callback : callbacks) { + callback(value); + } + } + + void notify_read_callbacks() { + for (auto& callback : read_callbacks) { + callback(); + } + } + +public: + DataProviderMemoryHolding() { + memset(value, 0, array_size); + } + DataProviderMemoryHolding(const std::uint8_t* value) { + memcpy(this->value, value, array_size); + } + DataProviderMemoryHolding(const std::vector& value) { + memcpy(this->value, value.data(), std::min(array_size, value.size())); + } + + /** + * @brief Add a write callback to be called when the value is written to + * + * @param callback The callback to be called + */ + void add_write_callback(std::function callback) { + callbacks.push_back(callback); + } + + void add_read_callback(std::function callback) { + read_callbacks.push_back(callback); + } + + void on_read(std::uint8_t* value, size_t len) override { + memcpy(value, this->value, std::min(array_size, len)); + + notify_read_callbacks(); + } + + void on_write(std::uint8_t* value, size_t len) override { + memcpy(this->value, value, std::min(array_size, len)); + + notify_callbacks(); + } + + /** + * @brief Get the current value + * + * @return std::uint8_t* The current value. Note that this pointer is pointing to + * private memory + */ + const std::uint8_t* get_value() { + return value; + } + + /** + * @brief Get the size of the array + * + * @return size_t The size of the array + */ + size_t get_size() { + return array_size; + } + + /** + * @brief Update the current value + * + * @param value The new value. Has to live past the function call. + */ + void update_value(const std::uint8_t* value) { + memcpy(this->value, value, array_size); + } +}; + +/** + * @brief A holding implementation for \c DataProvider that stores Elementary + * data types internally without callbacks. + * + * @tparam T The data type to be stored + */ +template class DataProviderHolding : public DataProvider { + static_assert(std::is_pointer::value == false); + +protected: + T value; + std::vector> write_callbacks; + std::vector> read_callbacks; + + void notify_write_callbacks() { + for (auto& callback : write_callbacks) { + callback(value); + } + } + + void notify_read_callbacks() { + for (auto& callback : read_callbacks) { + callback(); + } + } + +public: + /** + * @brief Construct a new \c DataProviderHolding object with an initial value + * + * @param value The initial value + */ + DataProviderHolding(T value) : value(value) { + } + + /** + * @brief Add a write callback to be called when the value is written to + * + * @param callback The callback to be called + */ + void add_write_callback(std::function callback) { + write_callbacks.push_back(callback); + } + + void add_read_callback(std::function callback) { + read_callbacks.push_back(callback); + } + + void on_read(std::uint8_t* val, size_t len) override { + memcpy(val, &value, std::min(sizeof(T), len)); + + notify_read_callbacks(); + } + + void on_write(std::uint8_t* val, size_t len) override { + memcpy(&value, val, std::min(sizeof(T), len)); + + notify_write_callbacks(); + } + + /** + * @brief Get the current value + * + * @return T The current value + */ + T get_value() const { + return value; + } + + /** + * @brief Update the current value + * + * @param value The new value + */ + void update_value(T value) { + this->value = value; + } +}; + +template class DataProviderCallbacks : public DataProvider { +protected: + std::function read_callback; + std::function write_callback; + +public: + DataProviderCallbacks(std::function read_callback, std::function write_callback) : + read_callback(read_callback), write_callback(write_callback) { + } + + void on_read(std::uint8_t* val, size_t len) override { + T value = read_callback(); + memcpy(val, &value, std::min(sizeof(T), len)); + } + + void on_write(std::uint8_t* val, size_t len) override { + T value; + memcpy(&value, val, std::min(sizeof(T), len)); + write_callback(value); + } +}; + +}; // namespace data_providers +}; // namespace registers +}; // namespace modbus diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/include/modbus-registers/registers.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/include/modbus-registers/registers.hpp new file mode 100644 index 0000000000..c8256fcf63 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/include/modbus-registers/registers.hpp @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#if __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__ +#warning "This code is only tested on little endian systems; see tests" +#endif + +#include +#include +#include +#include +#include +#include + +#include "converter.hpp" +#include "data_provider.hpp" + +namespace modbus { +namespace registers { +namespace complex_registers { + +/** + * @brief The base class for all complex registers. Used primarily to be + * storable in a vector. + * + */ +class ComplexRegisterUntyped { +protected: + const std::uint16_t start_address; + const std::uint16_t size; + +public: + ComplexRegisterUntyped(std::uint16_t start_address, std::uint16_t size); + virtual ~ComplexRegisterUntyped() = default; + + /** + * @brief Called when the register is (partially) written to, to be used by a + * modbus server. (Most likely indirectly through a + * \c registry::ComplexRegisterRegistry ) + * + * @warning The offset is in 16-bit words, not in bytes! + * @note Note that the data length can be any size, but the offset is always + * maximum \c size - 1 + * + * @param offset The offset in the register, in 16-bit words; (e.g. offset 1 = + * 2 byte offset) + * @param data The data to be written to the register, may be derived from a + * std::uint16_t vector in big-endian order (0x1234 -> {0x12, 0x34})) + */ + virtual void on_write(std::uint16_t offset, const std::vector& data) = 0; + + /** + * @brief Called when the register is (partially) read from, to be used by a + * + * @return std::vector The complete register data in big-endian order + * (can be converted to std::uint16_t vector in big-endian order ({0x12, 0x34} -> + * 0x1234)). The length of the vector should always be double of \c size + */ + virtual std::vector on_read() = 0; + + /** + * @brief Get the start address of the register in the modbus address space + * + * @return std::uint16_t The start address (0x0000 - 0xFFFF) + */ + std::uint16_t get_start_address() const; + /** + * @brief Get the size of the register in 16-bit words, to be used in + * conjunction with \c get_start_address to determine if a register is in the + * range of a (read or write) request + * + * @return std::uint16_t The size of the register in 16-bit words + */ + std::uint16_t get_size() const; + + /** + * @brief Get the \c DataProviderUntyped associated with the register. This is + * primarily used for extended functionality like unsolicitated reportings + * (not standard modbus) + * + * @return DataProviderUntyped* The data provider associated with the register + */ + virtual data_providers::DataProviderUntyped* get_data_provider() const = 0; +}; + +/** + * @brief The typed version and implementation of \c ComplexRegisterUntyped. + * This is typed for the (currently) sole purpose of type indication for the + * associated \c DataProvider + * + * @tparam provider_type The type of the associated \c DataProvider + * @tparam buffer_size The size of the buffer in bytes (not 16-bit words!). Must + * be a multiple of 2 (statically asserted!) + */ +template class ComplexRegister : public ComplexRegisterUntyped { + static_assert(buffer_size > 0); + static_assert(buffer_size % 2 == 0); + +protected: + data_providers::DataProvider& data_provider; + const converters::Converter& converter; + +public: + ComplexRegister(std::uint16_t start_address, data_providers::DataProvider& data_provider, + const converters::Converter& converter) : + ComplexRegisterUntyped(start_address, buffer_size / 2), data_provider(data_provider), converter(converter) { + } + + virtual ~ComplexRegister() = default; + + void on_write(std::uint16_t offset, const std::vector& data) override { + size_t byte_offset = offset * 2; + + std::uint8_t sys_buffer[buffer_size]; + std::uint8_t net_buffer[buffer_size]; + + // read the current value and convert it to network byte order + data_provider.on_read(sys_buffer, buffer_size); + converter.sys_to_net(sys_buffer, net_buffer, buffer_size); + + // write the new data to the buffer + memcpy(net_buffer + byte_offset, data.data(), std::min(data.size(), sizeof(net_buffer) - byte_offset)); + + // convert the buffer back to system byte order and write it to the provider + converter.net_to_sys(net_buffer, sys_buffer, buffer_size); + data_provider.on_write(sys_buffer, buffer_size); + } + + std::vector on_read() override { + std::vector net_buffer(buffer_size); + + std::uint8_t sys_buffer[buffer_size]; + data_provider.on_read(sys_buffer, buffer_size); + converter.sys_to_net(sys_buffer, net_buffer.data(), buffer_size); + + return net_buffer; + } + + data_providers::DataProviderUntyped* get_data_provider() const override { + return &data_provider; + } +}; + +/** + * @brief A register for holding (human readable) strings. The string is stored + * in the associated \c DataProviderString and can be read and written to. A + * converter can be provided though in most cases the + * \c converters::ConverterIdentity should be used. (1:1 + * mapping) + * + * @note Note that the string is not null terminated when read via modbus, if + * the provided string is longer or equal to than the register size + * + * @tparam string_length The maximum length of the string (without null + * terminator) + */ +template class StringRegister : public ComplexRegister { +public: + StringRegister(std::uint16_t start_address, data_providers::DataProviderString& data_provider, + const converters::Converter& converter) : + ComplexRegister(start_address, data_provider, converter) { + } + + virtual ~StringRegister() = default; +}; + +/** + * @brief A register for holding uint8 arrays. The array is stored + * in the associated \c DataProviderMemory and can be read and written to. A + * converter can be provided though in most cases the + * \c converters::ConverterIdentity should be used. (1:1 mapping) + * + * @tparam string_length The size of the array + */ +template class MemoryRegister : public ComplexRegister { +public: + MemoryRegister(std::uint16_t start_address, data_providers::DataProviderMemory& data_provider, + const converters::Converter& converter) : + ComplexRegister(start_address, data_provider, converter) { + } + + virtual ~MemoryRegister() = default; +}; + +/** + * @brief A register for holding elementary types (e.g. (u)int{16,32,64}_t, + * float, double) + * + * @tparam T The type of the register value, is used to derive the size of the + * complex register and the associated \c DataProvider + */ +template class ElementaryRegister : public ComplexRegister { + static_assert(std::is_pointer::value == false); + +public: + ElementaryRegister(std::uint16_t start_address, data_providers::DataProvider& data_provider, + const converters::Converter& converter) : + ComplexRegister(start_address, data_provider, converter) { + } + + virtual ~ElementaryRegister() = default; +}; + +}; // namespace complex_registers +}; // namespace registers +}; // namespace modbus diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/include/modbus-registers/registry.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/include/modbus-registers/registry.hpp new file mode 100644 index 0000000000..27e34c9890 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/include/modbus-registers/registry.hpp @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#pragma once + +#include + +#include "registers.hpp" + +namespace modbus { +namespace registers { +namespace registry { + +/** + * @brief A subregistry, to be extended by the user to create a registry + * container for a specific set of registers. A server should use the + * \c ComplexRegisterRegistry to write and read data from the registers. + * + */ +class ComplexRegisterSubregistry { +protected: + std::vector> registers; + +public: + ComplexRegisterSubregistry(); + + /** + * @brief Add a register to the subregistry, the register must be allocated on + * the heap + * + * @warning The subregistry will take ownership of the register and delete it + * when the subregistry is deleted thus the register must be allocated on the + * heap + * + * @param reg The register to be added + */ + void add(complex_registers::ComplexRegisterUntyped* reg); + + /** + * @brief Verify that no registers overlap in the address space for this + * specific subregistry + * + * @throw std::runtime_error if registers overlap + */ + void verify_internal_overlap(); + + /** + * @brief Get the registers in the subregistry + * + * @return std::vector The registers in the + * subregistry. The pointers are owned by the subregistry and live as long as + * the subregistry lives + */ + std::vector get_registers(); +}; + +/** + * @brief A per-server global registry for complex registers. The registry + * contains \c ComplexRegisterSubregistry shared pointers that contain the + * actual registers. + * + * The Registry's main purpose is to map incoming writes and reads to the + * correct registers + * + */ +class ComplexRegisterRegistry { +protected: + std::vector> registries; + + /** + * @brief Get a vector of all currently available registers in all + * subregistries combined + * + * @note This is a internal helper function + * + * @return std::vector all + * registers + */ + std::vector get_all_registers(); + +public: + /** + * @brief Verify that no registers overlap in the address space + * + * @throw std::runtime_error if registers overlap + */ + void verify_overlap(); + + /** + * @brief Write data to the registers, maps received data to the correct + * registers + * + * @param address The start address of the data + * @param data The data to be written to the registers (if derived from uint16 + * registers must be in big-endian order). Must also be a multiple of 2 bytes + * + * @throw std::runtime_error if data size is not a multiple of 2 bytes + */ + void on_write(std::uint16_t address, const std::vector& data); + + /** + * @brief Read data from the registers, maps the register data to the + * requested read region + * + * @param address The start address of the read region + * @param size The size of the read region in 16-bit words + * @return std::vector The data read from the registers in big-endian + * (can be converted to uint16 registers in big-endian order) + */ + std::vector on_read(std::uint16_t address, std::uint16_t size); + + /** + * @brief Add a subregistry to the registry + * + * @param registry the shared pointer to the subregistry + */ + void add(std::shared_ptr registry); +}; + +} // namespace registry +} // namespace registers +} // namespace modbus diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/src/registers.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/src/registers.cpp new file mode 100644 index 0000000000..29e65a828f --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/src/registers.cpp @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +using namespace modbus::registers::complex_registers; + +ComplexRegisterUntyped::ComplexRegisterUntyped(std::uint16_t start_address, std::uint16_t size) : + start_address(start_address), size(size) { +} + +std::uint16_t ComplexRegisterUntyped::get_start_address() const { + return start_address; +} + +std::uint16_t ComplexRegisterUntyped::get_size() const { + return size; +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/src/registry.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/src/registry.cpp new file mode 100644 index 0000000000..c9e37f1bd8 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/src/registry.cpp @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +using namespace modbus::registers::complex_registers; +using namespace modbus::registers::registry; + +ComplexRegisterSubregistry::ComplexRegisterSubregistry() : registers() { +} + +void ComplexRegisterSubregistry::add(ComplexRegisterUntyped* reg) { + registers.push_back(std::unique_ptr(reg)); +} + +void ComplexRegisterSubregistry::verify_internal_overlap() { + std::vector used_addresses; + for (auto& reg : registers) { + for (std::uint16_t i = 0; i < reg->get_size(); ++i) { + std::uint16_t address = reg->get_start_address() + i; + if (std::find(used_addresses.begin(), used_addresses.end(), address) != used_addresses.end()) { + throw std::runtime_error("Overlapping register addresses"); + } + used_addresses.push_back(address); + } + } +} + +std::vector ComplexRegisterSubregistry::get_registers() { + std::vector result; + for (auto& reg : registers) { + result.push_back(reg.get()); + } + return result; +} + +void ComplexRegisterRegistry::verify_overlap() { + std::vector used_addresses; + + for (auto& reg : get_all_registers()) { + for (std::uint16_t i = 0; i < reg->get_size(); ++i) { + std::uint16_t address = reg->get_start_address() + i; + if (std::find(used_addresses.begin(), used_addresses.end(), address) != used_addresses.end()) { + throw std::runtime_error("Overlapping register addresses"); + } + used_addresses.push_back(address); + } + } +} + +void ComplexRegisterRegistry::on_write(std::uint16_t address, const std::vector& data) { + if (data.size() % 2 != 0) { + throw std::runtime_error("Data size must be a multiple of 2 bytes"); + } + + for (auto& reg : get_all_registers()) { + // unit: 2b + std::uint16_t start_address = reg->get_start_address(); + // unit: 2b + std::uint16_t end_address = start_address + reg->get_size() - 1; + // unit: 2b + std::uint16_t data_size = data.size() / 2; + + // check overlap with write region + if (address + data_size <= start_address || address > end_address) { // todo: check this condition + continue; + } + + // unit 2b + std::int32_t vector_offset = address - start_address; + if (vector_offset < 0) { + auto data_offset = std::vector(data.begin() - vector_offset * 2, data.end()); + reg->on_write(0, data_offset); + } else { + reg->on_write(vector_offset, data); + } + } +} + +std::vector ComplexRegisterRegistry::on_read(std::uint16_t address, std::uint16_t size) { + if (size == 0) { + return {}; + } + + std::vector result(size * 2, 0); + std::vector is_empty(size * 2, true); + + std::uint32_t read_start_address_b = address * 2; + std::uint32_t read_size_b = size * 2; + + for (auto& reg : get_all_registers()) { + // unit: 2b + std::uint16_t start_address = reg->get_start_address(); + // unit: 2b + std::uint16_t end_address = start_address + reg->get_size() - 1; + + // check overlap with read region + if (address + size <= start_address || address > end_address) { + continue; + } + + std::uint32_t data_start_address_b = start_address * 2; + std::uint32_t data_end_address_b = end_address * 2; + + std::vector data = reg->on_read(); + + // delete data before read_start_address_b + if (read_start_address_b > data_start_address_b) { + data.erase(data.begin(), data.begin() + (read_start_address_b - data_start_address_b)); + data_start_address_b = read_start_address_b; + } + + // delete data after read_end_address_b + if (read_start_address_b + read_size_b < data_end_address_b) { + data.erase(data.begin() + (read_start_address_b + read_size_b - data_start_address_b), data.end()); + data_end_address_b = read_start_address_b + read_size_b; + } + + // copy data to result + std::int32_t result_offset = data_start_address_b - read_start_address_b; + for (std::uint32_t i = 0; i < data.size(); ++i) { + if (is_empty[i + result_offset]) { + result[i + result_offset] = data[i]; + is_empty[i + result_offset] = false; + } + } + } + + // note: empty bytes are already filled with 0 by default (see initializer of + // result) + + return result; +} + +void ComplexRegisterRegistry::add(std::shared_ptr registry) { + this->registries.push_back(std::move(registry)); +} + +std::vector ComplexRegisterRegistry::get_all_registers() { + std::vector result; + for (auto& reg : registries) { + for (auto& subreg : reg->get_registers()) { + result.push_back(subreg); + } + } + return result; +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/tests/converter.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/tests/converter.cpp new file mode 100644 index 0000000000..c99ea24c53 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/tests/converter.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include + +using namespace modbus::registers::converters; + +TEST(ConverterABCD, NetToSys) { + ConverterABCD converter; + std::uint8_t in[] = {0x12, 0x34, 0x56, 0x78}; + std::uint8_t out[sizeof(in)]; + converter.net_to_sys(in, out, sizeof(in)); + EXPECT_EQ(out[0], 0x78); + EXPECT_EQ(out[1], 0x56); + EXPECT_EQ(out[2], 0x34); + EXPECT_EQ(out[3], 0x12); +} + +TEST(ConverterABCD, SysToNet) { + ConverterABCD converter; + std::uint8_t in[] = {0x78, 0x56, 0x34, 0x12}; + std::uint8_t out[sizeof(in)]; + converter.sys_to_net(in, out, sizeof(in)); + EXPECT_EQ(out[0], 0x12); + EXPECT_EQ(out[1], 0x34); + EXPECT_EQ(out[2], 0x56); + EXPECT_EQ(out[3], 0x78); +} + +TEST(ConverterIdentity, NetToSys) { + ConverterIdentity converter; + std::uint8_t in[] = {0x12, 0x34, 0x56, 0x78}; + std::uint8_t out[sizeof(in)]; + converter.net_to_sys(in, out, sizeof(in)); + EXPECT_EQ(out[0], 0x12); + EXPECT_EQ(out[1], 0x34); + EXPECT_EQ(out[2], 0x56); + EXPECT_EQ(out[3], 0x78); +} + +TEST(ConverterIdentity, SysToNet) { + ConverterIdentity converter; + std::uint8_t in[] = {0x12, 0x34, 0x56, 0x78}; + std::uint8_t out[sizeof(in)]; + converter.sys_to_net(in, out, sizeof(in)); + EXPECT_EQ(out[0], 0x12); + EXPECT_EQ(out[1], 0x34); + EXPECT_EQ(out[2], 0x56); + EXPECT_EQ(out[3], 0x78); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/tests/data_provider_callback.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/tests/data_provider_callback.cpp new file mode 100644 index 0000000000..488929e0c6 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/tests/data_provider_callback.cpp @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include +#include +#include + +using namespace modbus::registers; + +TEST(DataProviderCallbacks, uint32_combined_works) { + std::uint32_t value = 0; + data_providers::DataProviderCallbacks provider( + [&value]() { return value; }, [&value](std::uint32_t new_value) { value = new_value; }); + + std::uint8_t sys_buffer[4] = {0, 0, 0, 0}; + std::uint8_t net_buffer[4] = {0, 0, 0, 0}; + provider.on_read(sys_buffer, 4); + + converters::ConverterABCD::instance().sys_to_net(sys_buffer, net_buffer, 4); + EXPECT_EQ(net_buffer[0], 0); + EXPECT_EQ(net_buffer[1], 0); + EXPECT_EQ(net_buffer[2], 0); + EXPECT_EQ(net_buffer[3], 0); + + net_buffer[0] = 0x12; + net_buffer[1] = 0x34; + net_buffer[2] = 0x56; + net_buffer[3] = 0x78; + converters::ConverterABCD::instance().net_to_sys(net_buffer, sys_buffer, 4); + provider.on_write(sys_buffer, 4); + EXPECT_EQ(value, 0x12345678); +} + +TEST(DataProviderCallbacks, uint32_elemetary_register_rw) { + std::uint32_t callback_call_count = 0; + data_providers::DataProviderCallbacks provider([]() { return 0xdeadbeef; }, + [&callback_call_count](std::uint32_t new_value) { + callback_call_count++; + EXPECT_EQ(new_value, 0x12345678); + }); + + complex_registers::ElementaryRegister reg(0, provider, converters::ConverterABCD::instance()); + + reg.on_write(0, {0x12, 0x34, 0x56, 0x78}); + EXPECT_EQ(callback_call_count, 1); + + std::vector buffer = reg.on_read(); + EXPECT_EQ(buffer.size(), 4); + EXPECT_EQ(buffer[0], 0xde); + EXPECT_EQ(buffer[1], 0xad); + EXPECT_EQ(buffer[2], 0xbe); + EXPECT_EQ(buffer[3], 0xef); + + EXPECT_EQ(callback_call_count, 1); // should not have changed +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/tests/memory_register.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/tests/memory_register.cpp new file mode 100644 index 0000000000..5427abac81 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/tests/memory_register.cpp @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include +#include + +using namespace modbus::registers::converters; +using namespace modbus::registers::complex_registers; +using namespace modbus::registers::data_providers; + +TEST(MemoryRegister, read_normal) { + DataProviderMemoryHolding<4> provider; + MemoryRegister<4> reg(0, provider, ConverterIdentity::instance()); + + std::uint8_t val[4] = {0xde, 0xad, 0xbe, 0xef}; + provider.update_value(val); + + auto read_val = reg.on_read(); + EXPECT_EQ(read_val.size(), 4); + EXPECT_EQ(read_val[0], 0xde); + EXPECT_EQ(read_val[1], 0xad); + EXPECT_EQ(read_val[2], 0xbe); + EXPECT_EQ(read_val[3], 0xef); +} + +TEST(MemoryRegister, write_normal) { + DataProviderMemoryHolding<4> provider; + MemoryRegister<4> reg(0, provider, ConverterIdentity::instance()); + + reg.on_write(0, {0xde, 0xad, 0xbe, 0xef}); + auto val = provider.get_value(); + EXPECT_EQ(val[0], 0xde); + EXPECT_EQ(val[1], 0xad); + EXPECT_EQ(val[2], 0xbe); + EXPECT_EQ(val[3], 0xef); +} + +TEST(MemoryRegister, write_callback) { + DataProviderMemoryHolding<4> provider; + MemoryRegister<4> reg(0, provider, ConverterIdentity::instance()); + + std::uint8_t write_count = 0; + provider.add_write_callback([&write_count](const std::uint8_t* val) { write_count++; }); + + ASSERT_EQ(write_count, 0); + + reg.on_write(0, {0xde, 0xad, 0xbe, 0xef}); + + EXPECT_EQ(write_count, 1); +} + +TEST(MemoryRegister, vector_constructors) { + DataProviderMemoryHolding<4> provider({0xde, 0xad, 0xbe, 0xef}); + MemoryRegister<4> reg(0, provider, ConverterIdentity::instance()); + + auto read_val = reg.on_read(); + EXPECT_EQ(read_val.size(), 4); + EXPECT_EQ(read_val[0], 0xde); + EXPECT_EQ(read_val[1], 0xad); + EXPECT_EQ(read_val[2], 0xbe); + EXPECT_EQ(read_val[3], 0xef); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/tests/registry.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/tests/registry.cpp new file mode 100644 index 0000000000..9bd7cb86e8 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/tests/registry.cpp @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include + +using namespace modbus::registers::complex_registers; +using namespace modbus::registers::registry; +using namespace modbus::registers::data_providers; +using namespace modbus::registers::converters; + +TEST(Registry, write_cases) { + DataProviderHolding provider(0); + DataProviderHolding provider2(0); + + auto subregistry = std::make_shared(); + subregistry->add(new ElementaryRegister(0x0010, provider, ConverterABCD::instance())); + subregistry->add(new ElementaryRegister(0x0012, provider2, ConverterABCD::instance())); + + EXPECT_NO_THROW(subregistry->verify_internal_overlap()); + + ComplexRegisterRegistry registry; + registry.add(subregistry); + EXPECT_NO_THROW(registry.verify_overlap()); + + // [ ][ ] + // |--| + registry.on_write(0x0010, {0x12, 0x34, 0x56, 0x78}); + EXPECT_EQ(provider.get_value(), 0x12345678); + + // [ ] + // || + registry.on_write(0x0011, {0xde, 0xad}); + EXPECT_EQ(provider.get_value(), 0x1234dead); + + // [ ] + // |--| + registry.on_write(0x000F, {0xbe, 0xef, 0xca, 0xfe}); + EXPECT_EQ(provider.get_value(), 0xcafedead); + + // [ ][ ] + // |--| + registry.on_write(0x0012, {0x01, 0x02, 0x03, 0x04}); + EXPECT_EQ(provider2.get_value(), 0x0102030400000000); + + // [ ][ ] + // |------| + registry.on_write(0x000F, {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}); + EXPECT_EQ(provider.get_value(), 0x03040506); + EXPECT_EQ(provider2.get_value(), 0x0708030400000000); + + // [ ][ ] + // |------| + registry.on_write(0x0012, {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}); + EXPECT_EQ(provider.get_value(), 0x03040506); + EXPECT_EQ(provider2.get_value(), 0x0102030405060708); +} + +TEST(Registry, read_cases) { + DataProviderHolding provider(0x0010); + DataProviderHolding provider2(0x0012); + + auto subregistry = std::make_shared(); + subregistry->add(new ElementaryRegister(0x0010, provider, ConverterABCD::instance())); + subregistry->add(new ElementaryRegister(0x0012, provider2, ConverterABCD::instance())); + + EXPECT_NO_THROW(subregistry->verify_internal_overlap()); + + ComplexRegisterRegistry registry; + registry.add(subregistry); + EXPECT_NO_THROW(registry.verify_overlap()); + + provider.update_value(0x12345678); + provider2.update_value(0x0102030405060708); + + auto data = registry.on_read(0x0010, 0); + EXPECT_EQ(data.size(), 0); + + // [ ][ ] + // |--| + data = registry.on_read(0x0012, 2); + EXPECT_EQ(data.size(), 4); + EXPECT_EQ(data[0], 0x01); + EXPECT_EQ(data[1], 0x02); + EXPECT_EQ(data[2], 0x03); + EXPECT_EQ(data[3], 0x04); + + // [ ][ ] + // |------| + data = registry.on_read(0x0012, 4); + EXPECT_EQ(data.size(), 8); + EXPECT_EQ(data[0], 0x01); + EXPECT_EQ(data[1], 0x02); + EXPECT_EQ(data[2], 0x03); + EXPECT_EQ(data[3], 0x04); + EXPECT_EQ(data[4], 0x05); + EXPECT_EQ(data[5], 0x06); + EXPECT_EQ(data[6], 0x07); + EXPECT_EQ(data[7], 0x08); + + // [ ][ ] + // |--| + data = registry.on_read(0x0011, 2); + EXPECT_EQ(data.size(), 4); + EXPECT_EQ(data[0], 0x56); + EXPECT_EQ(data[1], 0x78); + EXPECT_EQ(data[2], 0x01); + EXPECT_EQ(data[3], 0x02); + + // [ ][ ] + // |--| + data = registry.on_read(0x0010, 2); + EXPECT_EQ(data.size(), 4); + EXPECT_EQ(data[0], 0x12); + EXPECT_EQ(data[1], 0x34); + EXPECT_EQ(data[2], 0x56); + EXPECT_EQ(data[3], 0x78); + + // [ ][ ] + // |----------| + data = registry.on_read(0x0010, 6); + EXPECT_EQ(data.size(), 12); + EXPECT_EQ(data[0], 0x12); + EXPECT_EQ(data[1], 0x34); + EXPECT_EQ(data[2], 0x56); + EXPECT_EQ(data[3], 0x78); + EXPECT_EQ(data[4], 0x01); + EXPECT_EQ(data[5], 0x02); + EXPECT_EQ(data[6], 0x03); + EXPECT_EQ(data[7], 0x04); + EXPECT_EQ(data[8], 0x05); + EXPECT_EQ(data[9], 0x06); + EXPECT_EQ(data[10], 0x07); + EXPECT_EQ(data[11], 0x08); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/tests/string_register.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/tests/string_register.cpp new file mode 100644 index 0000000000..be87a64fea --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/tests/string_register.cpp @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include +#include + +using namespace modbus::registers::converters; +using namespace modbus::registers::complex_registers; +using namespace modbus::registers::data_providers; + +TEST(StringRegister, register_start_register_size) { + ConverterIdentity converter; + DataProviderStringHolding<8> provider; + StringRegister<8> reg(0xbeef, provider, converter); + + EXPECT_EQ(reg.get_start_address(), 0xbeef); + EXPECT_EQ(reg.get_size(), 4); +} + +TEST(StringRegister, write_basic) { + ConverterIdentity converter; + DataProviderStringHolding<8> provider; + StringRegister<8> reg(0, provider, converter); + + reg.on_write(0, {0x54, 0x65, 0x6C, 0x65, 0}); + EXPECT_STREQ(provider.get_value(), "Tele"); +} + +TEST(StringRegister, write_too_long) { + ConverterIdentity converter; + DataProviderStringHolding<4> provider; + StringRegister<4> reg(0, provider, converter); + + reg.on_write(0, {0x54, 0x65, 0x6C, 0x65, 0x54, 0x65, 0x6C, 0x65}); + EXPECT_STREQ(provider.get_value(), "Tele"); +} + +TEST(StringRegister, write_exact_with_termination) { + ConverterIdentity converter; + DataProviderStringHolding<4> provider; + StringRegister<4> reg(0, provider, converter); + + reg.on_write(0, {0x54, 0x65, 0x6C, 0x65, 0}); + EXPECT_STREQ(provider.get_value(), "Tele"); +} + +// todo: is this wanted (auto-termination)? +TEST(StringRegister, write_exact_without_termination) { + ConverterIdentity converter; + DataProviderStringHolding<4> provider; + StringRegister<4> reg(0, provider, converter); + + reg.on_write(0, {0x54, 0x65, 0x6C, 0x65}); + EXPECT_STREQ(provider.get_value(), "Tele"); +} + +TEST(StringRegister, offset_write_in_range) { + ConverterIdentity converter; + DataProviderStringHolding<8> provider; + StringRegister<8> reg(0, provider, converter); + + reg.on_write(0, {0x54, 0x65, 0x6C, 0x65, 0}); + EXPECT_STREQ(provider.get_value(), "Tele"); + // note: the offset is in registers (2 bytes) + reg.on_write(2, {0x54, 0x65, 0x6C, 0x65, 0}); + EXPECT_STREQ(provider.get_value(), "TeleTele"); +} + +TEST(StringRegister, offset_write_too_long) { + ConverterIdentity converter; + DataProviderStringHolding<10> provider; + StringRegister<10> reg(0, provider, converter); + + reg.on_write(0, {0x54, 0x65, 0x6C, 0x65, 0}); + EXPECT_STREQ(provider.get_value(), "Tele"); + // note: the offset is in registers (2 bytes) + reg.on_write(2, {0x54, 0x65, 0x6C, 0x65, 0x74, 0x75, 0x62, 0x62, 0x79}); + EXPECT_STREQ(provider.get_value(), "TeleTeletu"); +} + +TEST(StringRegister, offset_write_exact) { + ConverterIdentity converter; + DataProviderStringHolding<8> provider; + StringRegister<8> reg(0, provider, converter); + + reg.on_write(0, {0x54, 0x65, 0x6C, 0x65}); + EXPECT_STREQ(provider.get_value(), "Tele"); + // note: the offset is in registers (2 bytes) + reg.on_write(2, {0x54, 0x65, 0x6C, 0x65}); + EXPECT_STREQ(provider.get_value(), "TeleTele"); +} + +TEST(StringRegister, read_basic) { + ConverterIdentity converter; + DataProviderStringHolding<8> provider; + StringRegister<8> reg(0, provider, converter); + + provider.update_value("Hello"); + auto data = reg.on_read(); + EXPECT_EQ(data.size(), 8); + EXPECT_EQ(data[0], 0x48); + EXPECT_EQ(data[1], 0x65); + EXPECT_EQ(data[2], 0x6C); + EXPECT_EQ(data[3], 0x6C); + EXPECT_EQ(data[4], 0x6F); + EXPECT_EQ(data[5], 0); + EXPECT_EQ(data[6], 0); + EXPECT_EQ(data[7], 0); +} + +TEST(StringRegister, read_too_long_has_no_termination) { + ConverterIdentity converter; + DataProviderStringHolding<8> provider; + StringRegister<8> reg(0, provider, converter); + + provider.update_value("Hello World"); + auto data = reg.on_read(); + EXPECT_EQ(data.size(), 8); + EXPECT_EQ(data[0], 0x48); + EXPECT_EQ(data[1], 0x65); + EXPECT_EQ(data[2], 0x6C); + EXPECT_EQ(data[3], 0x6C); + EXPECT_EQ(data[4], 0x6F); + EXPECT_EQ(data[5], 0x20); + EXPECT_EQ(data[6], 0x57); + EXPECT_EQ(data[7], 0x6F); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/tests/u32_register.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/tests/u32_register.cpp new file mode 100644 index 0000000000..aea462ca30 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/registers/tests/u32_register.cpp @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include +#include + +using namespace modbus::registers::converters; +using namespace modbus::registers::complex_registers; +using namespace modbus::registers::data_providers; + +// u32_ABCD tests +TEST(ElementaryRegister, register_start_register_size) { + ConverterIdentity converter; + DataProviderHolding provider(0); + ElementaryRegister reg(0xc0de, provider, converter); + + EXPECT_EQ(reg.get_start_address(), 0xc0de); + EXPECT_EQ(reg.get_size(), 2); + + DataProviderHolding provider2(0); + ElementaryRegister reg2(0xdead, provider2, converter); + + EXPECT_EQ(reg2.get_start_address(), 0xdead); + EXPECT_EQ(reg2.get_size(), 4); +} + +TEST(ElementaryRegister, u32_ABCD_write_exact) { + DataProviderHolding provider(0); + ElementaryRegister reg(0, provider, ConverterABCD::instance()); + + reg.on_write(0, {0x12, 0x34, 0x56, 0x78}); + EXPECT_EQ(provider.get_value(), 0x12345678); +} + +TEST(ElementaryRegister, u32_ABCD_write_too_long) { + DataProviderHolding provider(0); + ElementaryRegister reg(0, provider, ConverterABCD::instance()); + + reg.on_write(0, {0x12, 0x34, 0x56, 0x78, 0x12}); + EXPECT_EQ(provider.get_value(), 0x12345678); +} + +TEST(ElementaryRegister, u32_ABCD_write_too_short) { + DataProviderHolding provider(0); + ElementaryRegister reg(0, provider, ConverterABCD::instance()); + + reg.on_write(0, { + 0x12, + 0x34, + }); + EXPECT_EQ(provider.get_value(), 0x12340000); +} + +TEST(ElementaryRegister, u32_ABCD_write_offset_basic) { + DataProviderHolding provider(0); + ElementaryRegister reg(0, provider, ConverterABCD::instance()); + + reg.on_write(0, {0x12, 0x34, 0x56, 0x78}); + EXPECT_EQ(provider.get_value(), 0x12345678); + // note: the offset is in registers (2 bytes) + reg.on_write(1, {0xde, 0xad}); + EXPECT_EQ(provider.get_value(), 0x1234dead); +} + +TEST(ElementaryRegister, u32_ABCD_write_offset_too_long) { + DataProviderHolding provider(0); + ElementaryRegister reg(0, provider, ConverterABCD::instance()); + + reg.on_write(0, {0x12, 0x34, 0x56, 0x78}); + EXPECT_EQ(provider.get_value(), 0x12345678); + // note: the offset is in registers (2 bytes) + reg.on_write(1, {0xde, 0xad, 0xbe, 0xef}); + EXPECT_EQ(provider.get_value(), 0x1234dead); +} + +// float_ABCD + +TEST(ElementaryRegister, float_ABCD_write_exact) { + DataProviderHolding provider(0); + ElementaryRegister reg(0, provider, ConverterABCD::instance()); + + reg.on_write(0, {0x41, 0x48, 0x00, 0x00}); + EXPECT_FLOAT_EQ(provider.get_value(), 12.5); + reg.on_write(0, {0x40, 0x49, 0x06, 0x25}); + EXPECT_FLOAT_EQ(provider.get_value(), 3.141); +} + +TEST(ElementaryRegister, float_ABCD_write_too_long) { + DataProviderHolding provider(0); + ElementaryRegister reg(0, provider, ConverterABCD::instance()); + + reg.on_write(0, {0x41, 0x48, 0x00, 0x00, 0x41}); + EXPECT_FLOAT_EQ(provider.get_value(), 12.5); +} + +// i64_ABCD tests + +TEST(ElementaryRegister, i64_ABCD_write_exact) { + DataProviderHolding provider(0); + ElementaryRegister reg(0, provider, ConverterABCD::instance()); + + reg.on_write(0, {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}); + EXPECT_EQ(provider.get_value(), 0x123456789abcdef0); +} + +TEST(ElementaryRegister, i64_ABCD_2_complement_write_exact) { + DataProviderHolding provider(0); + ElementaryRegister reg(0, provider, ConverterABCD::instance()); + + reg.on_write(0, {0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}); + EXPECT_EQ(provider.get_value(), 0x123456789abcdef0); + reg.on_write(0, {0xed, 0xcb, 0xa9, 0x87, 0x65, 0x43, 0x21, 0x10}); + EXPECT_EQ(provider.get_value(), -0x123456789abcdef0); +} + +// todo: tests for other converters diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/CMakeLists.txt new file mode 100644 index 0000000000..7662e4890c --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/CMakeLists.txt @@ -0,0 +1,10 @@ +file(GLOB_RECURSE MODBUS_SERVER_SOURCES "src/*.cpp") + +add_library( + modbus-server + STATIC + ${MODBUS_SERVER_SOURCES} +) +target_include_directories(modbus-server PUBLIC include) +target_link_libraries(modbus-server PUBLIC modbus-base Huawei::FusionCharger::LogInterface) +ev_register_library_target(modbus-server) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/include/modbus-server/modbus_basic_server.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/include/modbus-server/modbus_basic_server.hpp new file mode 100644 index 0000000000..ee164d2d5c --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/include/modbus-server/modbus_basic_server.hpp @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#ifndef MODBUS_SERVER__MODBUS_BASIC_SERVER_HPP +#define MODBUS_SERVER__MODBUS_BASIC_SERVER_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace modbus_server { + +class ModbusBasicServer { +public: + // An alias for a function that takes a request PDU and always returns a + // corresponding response PDU + template using AlwaysRespondingPDUHandler = std::function; + +protected: + std::shared_ptr pas; + + logs::LogIntf log; + + std::optional> + read_registers_request; + std::optional> + write_multiple_registers_request; + std::optional> + write_single_register_request; + +public: + ModbusBasicServer(std::shared_ptr pas, logs::LogIntf log = logs::log_printf); + + void set_read_holding_registers_request_cb( + AlwaysRespondingPDUHandler fn); + void set_write_multiple_registers_request_cb( + AlwaysRespondingPDUHandler fn); + void set_write_single_register_request_cb( + AlwaysRespondingPDUHandler fn); + +protected: + /** + * @brief Calls \c on_pdu and catches all exceptions. If \c on_pdu returns an + * \c std::nullopt this function will do so too. If \c on_pdu throws an + * exception, this function will catch it and return an error PDU that fits + * the exception best. + * + * @details The mapping currently works as follows: pdu::DecodingError -> + * ILLEGAL_DATA_VALUE; pdu::EncodingError -> SERVER_DEVICE_FAILURE; + * ApplicationServerError -> calls to_pdu on exception object; any other + * exception -> SERVER_DEVICE_FAILURE + * + * @param input the incoming PDU + * @return std::optional the return value of \c on_pdu or an + * pdu::ErrorPDU if an exception was thrown + */ + std::optional on_pdu_error_handled(const pdu::GenericPDU& input); + + /** + * @brief Incoming PDU handler, called by the \c PDUCorrelationLayer. Can be + * extended by subclasses but \c ModbusBasicServer::on_pdu should always be + * called at the end. + * + * This function can also throw exceptions which will all be caught by the + * wrapper function \c on_pdu_error_handled + * + * @param input the incoming PDU + * @return std::optional the corresponding response PDU, if a + * answer is available; returns an ILLEGAL_FUNCTION error PDU if the functino + * code is unknown or a corresponding handler is not set + * + * @throws modbus_server::pdu::DecodingError if the incoming PDU could not be + * decoded (most likely client's fault) + * @throws modbus_server::pdu::EncodingError if the response PDU could not be + * encoded (most likely application server's fault) + * @throws modbus_server::ApplicationServerError if the application wants to + * send an error PDU as answer + */ + virtual std::optional on_pdu(const pdu::GenericPDU& input); +}; + +} // namespace modbus_server + +#endif diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/include/modbus-server/server_exception.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/include/modbus-server/server_exception.hpp new file mode 100644 index 0000000000..de7deea73b --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/include/modbus-server/server_exception.hpp @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#ifndef MODBUS_SERVER__GENERIC_SERVER_HPP +#define MODBUS_SERVER__GENERIC_SERVER_HPP + +#include +#include + +namespace modbus_server { + +class ApplicationServerError : public std::exception { +private: + std::uint8_t exception_code; + std::vector other_data; // generally not used but is theoretically possible + +public: + ApplicationServerError(pdu::PDUExceptionCode exception_code) : + exception_code(static_cast(exception_code)){}; + + ApplicationServerError(std::uint8_t exception_code, const std::vector& other_data = {}) : + exception_code(exception_code), other_data(other_data){}; + + const char* what() const noexcept override { + return "ApplicationServerError"; + } + + pdu::GenericPDU to_pdu(std::uint8_t original_function_code) const { + std::vector data; + data.push_back(exception_code); + data.insert(data.end(), other_data.begin(), other_data.end()); + return pdu::GenericPDU(0x80 | original_function_code, data); + } +}; + +}; // namespace modbus_server + +#endif diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/src/server.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/src/server.cpp new file mode 100644 index 0000000000..ea70f2cc0a --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/src/server.cpp @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include +#include + +using namespace modbus_server; + +ModbusBasicServer::ModbusBasicServer(std::shared_ptr pas, logs::LogIntf log) : + pas(pas), log(log) { + pas->set_on_pdu(std::bind(&ModbusBasicServer::on_pdu_error_handled, this, std::placeholders::_1)); +} + +void ModbusBasicServer::set_read_holding_registers_request_cb( + AlwaysRespondingPDUHandler fn) { + read_registers_request = fn; +} + +void ModbusBasicServer::set_write_multiple_registers_request_cb( + AlwaysRespondingPDUHandler fn) { + write_multiple_registers_request = fn; +} + +void ModbusBasicServer::set_write_single_register_request_cb( + AlwaysRespondingPDUHandler fn) { + write_single_register_request = fn; +} + +std::optional ModbusBasicServer::on_pdu_error_handled(const pdu::GenericPDU& input) { + //! don't forget to change doxygen comment if exception-pdu mapping changes + try { + return this->on_pdu(input); + } catch (pdu::DecodingError e) { + log.error << "Decoding error: " + std::string(e.what()); + log.verbose << "\tOriginal data: " + e.get_original_data().to_string(); + + return pdu::ErrorPDU(input.function_code, (std::uint8_t)pdu::PDUExceptionCode::ILLEGAL_DATA_VALUE).to_generic(); + } catch (pdu::EncodingError e) { + log.error << "Encoding error: " + std::string(e.what()); + + // fallthrough to SERVER_DEVICE_FAILURE + } catch (ApplicationServerError e) { + return e.to_pdu(input.function_code); + } catch (std::exception e) { + log.error << "Unknown exception: " + std::string(e.what()); + + // fallthrough to SERVER_DEVICE_FAILURE + } + + return pdu::ErrorPDU(input.function_code, (std::uint8_t)pdu::PDUExceptionCode::SERVER_DEVICE_FAILURE).to_generic(); +} + +std::optional ModbusBasicServer::on_pdu(const pdu::GenericPDU& input) { + switch (input.function_code) { + case 0x03: { + if (!read_registers_request.has_value()) { + return pdu::ErrorPDU(input.function_code, (std::uint8_t)pdu::PDUExceptionCode::ILLEGAL_FUNCTION) + .to_generic(); + } + + pdu::ReadHoldingRegistersRequest request; + request.from_generic(input); + auto response = this->read_registers_request.value()(request); + return response.to_generic(); + } + + case 0x10: { + if (!write_multiple_registers_request.has_value()) { + return pdu::ErrorPDU(input.function_code, (std::uint8_t)pdu::PDUExceptionCode::ILLEGAL_FUNCTION) + .to_generic(); + } + + pdu::WriteMultipleRegistersRequest request; + request.from_generic(input); + auto response = this->write_multiple_registers_request.value()(request); + return response.to_generic(); + } + + case 0x06: { + if (!write_single_register_request.has_value()) { + return pdu::ErrorPDU(input.function_code, (std::uint8_t)pdu::PDUExceptionCode::ILLEGAL_FUNCTION) + .to_generic(); + } + + pdu::WriteSingleRegisterRequest request; + request.from_generic(input); + auto response = this->write_single_register_request.value()(request); + return response.to_generic(); + } + } + + log.verbose << "Server: Unknown function code: " + std::to_string(input.function_code); + + // todo: determine if we should just ignore the PDU or send this error + return pdu::ErrorPDU(input.function_code, (std::uint8_t)pdu::PDUExceptionCode::ILLEGAL_FUNCTION).to_generic(); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/tests/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/tests/CMakeLists.txt new file mode 100644 index 0000000000..870dbcc092 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/tests/CMakeLists.txt @@ -0,0 +1,8 @@ +include(GoogleTest) + +file(GLOB_RECURSE MODBUS_SERVER_TESTS_SOURCES "*.cpp") + +add_executable(modbus-server-tests ${MODBUS_SERVER_TESTS_SOURCES}) +target_link_libraries(modbus-server-tests PRIVATE modbus-server gtest_main) + +gtest_discover_tests(modbus-server-tests) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/tests/dummy_pas.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/tests/dummy_pas.hpp new file mode 100644 index 0000000000..f68ee5ed83 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/tests/dummy_pas.hpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#ifndef MODBUS_TESTS__DUMMY_PAS_HPP +#define MODBUS_TESTS__DUMMY_PAS_HPP + +#include + +class DummyPDUCorrelationLayer : public modbus_server::PDUCorrelationLayerIntf { + std::vector next_answer; + std::vector last_request; + +public: + DummyPDUCorrelationLayer() = default; + + void blocking_poll() override { + } + bool poll() override { + return false; + }; + + modbus_server::pdu::GenericPDU request_response(const modbus_server::pdu::GenericPDU& request, + std::chrono::milliseconds timeout) override { + last_request.push_back(request); + + if (next_answer.empty()) { + throw std::runtime_error("No answer available"); + } + auto answer = next_answer.front(); + next_answer.erase(next_answer.begin()); + return answer; + } + + void request_without_response(const modbus_server::pdu::GenericPDU& request) override { + last_request.push_back(request); + } + + void add_next_answer(const modbus_server::pdu::GenericPDU& answer) { + next_answer.push_back(answer); + } + + std::optional call_on_pdu(const modbus_server::pdu::GenericPDU& pdu) { + return this->on_pdu.value()(pdu); + } + + modbus_server::pdu::GenericPDU get_last_request() { + if (last_request.empty()) { + throw std::runtime_error("No request available"); + } + auto request = last_request.front(); + last_request.erase(last_request.begin()); + return request; + } +}; + +#endif diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/tests/server.test.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/tests/server.test.cpp new file mode 100644 index 0000000000..5f78993bf9 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/server/tests/server.test.cpp @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include + +#include +#include + +#include "dummy_pas.hpp" + +using namespace modbus_server; + +TEST(ModbusBasicServer, read_holding_registers_test) { + auto correlation_layer = std::make_shared(); + ModbusBasicServer server(correlation_layer); + + std::uint8_t callback_called = 0; + + server.set_read_holding_registers_request_cb([&callback_called](const pdu::ReadHoldingRegistersRequest& request) { + callback_called++; + return pdu::ReadHoldingRegistersResponse(request, {0x01, 0x02, 0x03, 0x04}); + }); + + pdu::ReadHoldingRegistersRequest req; + req.register_start = 0x1234; + req.register_count = 0x0002; + + std::optional response = correlation_layer->call_on_pdu(req.to_generic()); + + ASSERT_TRUE(response.has_value()); + pdu::ReadHoldingRegistersResponse response_parsed; + ASSERT_NO_THROW(response_parsed.from_generic(response.value())); + + ASSERT_EQ(response_parsed.register_count, 2); + ASSERT_EQ(response_parsed.register_data[0], 0x01); + ASSERT_EQ(response_parsed.register_data[1], 0x02); + ASSERT_EQ(response_parsed.register_data[2], 0x03); + ASSERT_EQ(response_parsed.register_data[3], 0x04); + + ASSERT_EQ(callback_called, 1); +} + +TEST(ModbusBasicServer, write_single_register_test) { + auto correlation_layer = std::make_shared(); + ModbusBasicServer server(correlation_layer); + + std::uint8_t callback_called = 0; + + server.set_write_single_register_request_cb([&callback_called](const pdu::WriteSingleRegisterRequest& request) { + callback_called++; + return pdu::WriteSingleRegisterResponse(request); + }); + + pdu::WriteSingleRegisterRequest req; + req.register_address = 0x1234; + req.register_value = 0x5678; + + std::optional response = correlation_layer->call_on_pdu(req.to_generic()); + + ASSERT_TRUE(response.has_value()); + pdu::WriteSingleRegisterResponse response_parsed; + ASSERT_NO_THROW(response_parsed.from_generic(response.value())); + + ASSERT_EQ(response_parsed.register_address, 0x1234); + ASSERT_EQ(response_parsed.register_value, 0x5678); + + ASSERT_EQ(callback_called, 1); +} + +TEST(ModbusBasicServer, write_multiple_registers_test) { + auto correlation_layer = std::make_shared(); + ModbusBasicServer server(correlation_layer); + + std::uint8_t callback_called = 0; + + server.set_write_multiple_registers_request_cb( + [&callback_called](const pdu::WriteMultipleRegistersRequest& request) { + callback_called++; + return pdu::WriteMultipleRegistersResponse(request); + }); + + pdu::WriteMultipleRegistersRequest req; + req.register_start = 0x1234; + req.register_count = 0x0002; + req.register_data = {0x01, 0x02, 0x03, 0x04}; + + std::optional response = correlation_layer->call_on_pdu(req.to_generic()); + + ASSERT_TRUE(response.has_value()); + pdu::WriteMultipleRegistersResponse response_parsed; + ASSERT_NO_THROW(response_parsed.from_generic(response.value())); + + ASSERT_EQ(response_parsed.register_start, 0x1234); + ASSERT_EQ(response_parsed.register_count, 0x0002); + + ASSERT_EQ(callback_called, 1); +} + +TEST(ModbusBasicServer, invalid_request_sends_error_pdu) { + auto correlation_layer = std::make_shared(); + ModbusBasicServer server(correlation_layer); + + std::uint8_t callback_called = 0; + + server.set_read_holding_registers_request_cb([&callback_called](const pdu::ReadHoldingRegistersRequest& request) { + callback_called++; + return pdu::ReadHoldingRegistersResponse(request, {0x01, 0x02, 0x03, 0x04}); + }); + + pdu::GenericPDU req; + req.function_code = 0x03; + req.data = {0x01, 0x02, 0x03}; + + std::optional response = correlation_layer->call_on_pdu(req); + + ASSERT_TRUE(response.has_value()); + pdu::ErrorPDU response_parsed; + ASSERT_NO_THROW(response_parsed.from_generic(response.value())); + ASSERT_EQ(response_parsed.function_code, 0x03); + + ASSERT_EQ(callback_called, 0); +} + +TEST(ModbusBasicServer, not_serializable_response_sends_error_pdu) { + auto correlation_layer = std::make_shared(); + ModbusBasicServer server(correlation_layer); + + std::uint8_t callback_called = 0; + + server.set_read_holding_registers_request_cb([&callback_called](const pdu::ReadHoldingRegistersRequest& request) { + callback_called++; + return pdu::ReadHoldingRegistersResponse(request, + {0x01, 0x02, 0x03}); // not serializable because of missing 4th byte + }); + + pdu::ReadHoldingRegistersRequest req; + req.register_start = 0x1234; + req.register_count = 0x0002; + + std::optional response = correlation_layer->call_on_pdu(req.to_generic()); + + ASSERT_TRUE(response.has_value()); + pdu::ErrorPDU response_parsed; + ASSERT_NO_THROW(response_parsed.from_generic(response.value())); + ASSERT_EQ(response_parsed.function_code, 0x03); + + ASSERT_EQ(callback_called, 1); +} + +TEST(ModbusBasicServer, application_server_error_sends_error_pdu) { + auto correlation_layer = std::make_shared(); + ModbusBasicServer server(correlation_layer); + + std::uint8_t callback_called = 0; + + server.set_read_holding_registers_request_cb([&callback_called](const pdu::ReadHoldingRegistersRequest& request) { + callback_called++; + throw ApplicationServerError(pdu::PDUExceptionCode::GATEWAY_PATH_UNAVAILABLE); + + return pdu::ReadHoldingRegistersResponse(request, {}); // unreachable + }); + + pdu::ReadHoldingRegistersRequest req; + req.register_start = 0x1234; + req.register_count = 0x0002; + + std::optional response = correlation_layer->call_on_pdu(req.to_generic()); + + ASSERT_TRUE(response.has_value()); + pdu::ErrorPDU response_parsed; + ASSERT_NO_THROW(response_parsed.from_generic(response.value())); + ASSERT_EQ(response_parsed.function_code, 0x03); + ASSERT_EQ(response_parsed.exception_code, (std::uint8_t)pdu::PDUExceptionCode::GATEWAY_PATH_UNAVAILABLE); + + ASSERT_EQ(callback_called, 1); +} + +TEST(ModbusBasicServer, not_registered_cb_sends_illegal_function) { + auto correlation_layer = std::make_shared(); + ModbusBasicServer server(correlation_layer); + + std::uint8_t callback_called = 0; + + pdu::ReadHoldingRegistersRequest req; + req.register_start = 0x1234; + req.register_count = 0x0002; + + std::optional response = correlation_layer->call_on_pdu(req.to_generic()); + + ASSERT_TRUE(response.has_value()); + pdu::ErrorPDU response_parsed; + ASSERT_NO_THROW(response_parsed.from_generic(response.value())); + ASSERT_EQ(response_parsed.function_code, 0x03); + ASSERT_EQ(response_parsed.exception_code, (std::uint8_t)pdu::PDUExceptionCode::ILLEGAL_FUNCTION); +} + +TEST(ModbusBasicServer, unknown_function_code_sends_illegal_function) { + auto correlation_layer = std::make_shared(); + ModbusBasicServer server(correlation_layer); + + std::uint8_t callback_called = 0; + + pdu::GenericPDU req(0x7f, {0x00}); + + std::optional response = correlation_layer->call_on_pdu(req); + + ASSERT_TRUE(response.has_value()); + pdu::ErrorPDU response_parsed; + ASSERT_NO_THROW(response_parsed.from_generic(response.value())); + ASSERT_EQ(response_parsed.function_code, 0x7f); + ASSERT_EQ(response_parsed.exception_code, (std::uint8_t)pdu::PDUExceptionCode::ILLEGAL_FUNCTION); +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/ssl/CMakeLists.txt b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/ssl/CMakeLists.txt new file mode 100644 index 0000000000..8996c635df --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/ssl/CMakeLists.txt @@ -0,0 +1,11 @@ +find_package(OpenSSL REQUIRED) + +file(GLOB_RECURSE MODBUS_SSL_SOURCES "src/*.cpp") + +add_library(modbus-ssl STATIC ${MODBUS_SSL_SOURCES}) +ev_register_library_target(modbus-ssl) + +target_link_libraries( + modbus-ssl PUBLIC modbus-base OpenSSL::SSL OpenSSL::Crypto +) +target_include_directories(modbus-ssl PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/ssl/include/modbus-ssl/openssl_transport.hpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/ssl/include/modbus-ssl/openssl_transport.hpp new file mode 100644 index 0000000000..417f875357 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/ssl/include/modbus-ssl/openssl_transport.hpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#ifndef MODBUS_SSL_M_HPP +#define MODBUS_SSL_M_HPP +#include + +#include +#include + +namespace modbus_ssl { + +class OpenSSLTransport : public modbus_server::ModbusTransport { +public: + OpenSSLTransport(SSL* ssl); + ~OpenSSLTransport(); + + std::vector read_bytes(size_t count) override; + std::optional> try_read_bytes(size_t count) override; + + void write_bytes(const std::vector& bytes) override; + +private: + SSL* ssl; + std::mutex mutex; +}; + +class OpenSSLTransportException : public std::runtime_error { +protected: + int openssl_error; + +public: + OpenSSLTransportException(const std::string& message, int openssl_error) : + std::runtime_error(message), openssl_error(openssl_error) { + } + + int get_openssl_error() { + return openssl_error; + } +}; + +}; // namespace modbus_ssl + +#endif diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/ssl/src/openssl_transport.cpp b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/ssl/src/openssl_transport.cpp new file mode 100644 index 0000000000..e1ea289756 --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/fusion_charger_lib/modbus-server/libs/ssl/src/openssl_transport.cpp @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest +#include +#include + +#include +#include +#include + +using namespace modbus_ssl; + +OpenSSLTransport::OpenSSLTransport(SSL* ssl) : ssl(ssl) { +} +OpenSSLTransport::~OpenSSLTransport() { +} + +std::vector OpenSSLTransport::read_bytes(size_t count) { + std::vector buffer(count); + size_t read = 0; + + while (read < count) { + int ret; + int err; + { + auto lock = std::lock_guard(mutex); + ret = SSL_read(ssl, buffer.data() + read, count - read); + + if (ret <= 0) { + err = SSL_get_error(ssl, ret); + } + } + + // auto thread_id = std::this_thread::get_id(); + // printf("SSL READ with ID: 0x%X\n", thread_id); + + if (ret <= 0) { + // int err = SSL_get_error(ssl, ret); + + // "The operation did not complete and can be retried later." + if (err == SSL_ERROR_WANT_READ) { + std::this_thread::yield(); + // std::this_thread::sleep_for(std::chrono::microseconds(10)); + continue; + } + + if (err == SSL_ERROR_WANT_WRITE) { + std::this_thread::yield(); + // std::this_thread::sleep_for(std::chrono::microseconds(10)); + continue; + } + + if (err == SSL_ERROR_ZERO_RETURN) { + throw modbus_server::transport_exceptions::ConnectionClosedException(); + } + + throw OpenSSLTransportException("SSL_read failed with error " + std::string(ERR_error_string(err, NULL)), + err); + } + read += ret; + } + + return buffer; +} + +std::optional> OpenSSLTransport::try_read_bytes(size_t count) { + std::vector buffer(count); + size_t read = 0; + + auto lock = std::lock_guard(mutex); + + // First try to peek the data + int ret = SSL_peek(ssl, buffer.data(), count); + + if (ret <= 0) { + int err = SSL_get_error(ssl, ret); + + // "The operation did not complete and can be retried later." + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + return std::nullopt; // let openssl do its thing next time + } + + if (err == SSL_ERROR_ZERO_RETURN) { + throw modbus_server::transport_exceptions::ConnectionClosedException(); + } + + throw OpenSSLTransportException("SSL_peek failed with error " + std::string(ERR_error_string(err, NULL)), err); + } + + if (static_cast(ret) < count) { + return std::nullopt; // not enough data + } + + // enough data? read it! + ret = SSL_read(ssl, buffer.data(), count); + if (ret <= 0) { + int err = SSL_get_error(ssl, ret); + + // "The operation did not complete and can be retried later." + if (err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE) { + return std::nullopt; + } + + if (err == SSL_ERROR_ZERO_RETURN) { + throw modbus_server::transport_exceptions::ConnectionClosedException(); + } + + throw OpenSSLTransportException("SSL_read failed with error " + std::string(ERR_error_string(err, NULL)), err); + } + + if (ret != count) { + throw std::runtime_error("SSL_read returned less data than requested"); + } + + return buffer; +} + +void OpenSSLTransport::write_bytes(const std::vector& bytes) { + size_t written = 0; + + while (written < bytes.size()) { + auto lock = std::lock_guard(mutex); + + int ret = SSL_write(ssl, bytes.data() + written, bytes.size() - written); + if (ret <= 0) { + throw std::runtime_error("SSL_write failed with error " + + std::string(ERR_error_string(SSL_get_error(ssl, ret), NULL))); + } + written += ret; + } +} diff --git a/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/manifest.yaml b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/manifest.yaml new file mode 100644 index 0000000000..a41d7915da --- /dev/null +++ b/modules/HardwareDrivers/PowerSupplies/Huawei_V100R023C10/manifest.yaml @@ -0,0 +1,194 @@ +description: >- + Driver for the Huawei FusionCharge V100R023C10 DC Power Unit. + Currently the driver only supports a single connector per dispenser (per host). +config: + ethernet_interface: + description: Ethernet interface name to use for communication with the power supply. + type: string + default: eth0 + psu_ip: + description: IP address of the power supply. + type: string + default: "192.168.11.1" + psu_port: + description: Modbus port of the power supply. + type: integer + default: 502 + tls_enabled: + description: | + Enable TLS encryption for the connection to the power supply. + If set to true, the psu_ca, the client_cert and client_key must be provided. + type: boolean + default: false + psu_ca_cert: + description: | + Path to the CA certificate file for the power supply. + If not provided, the connection will not be encrypted. + type: string + default: "" + client_cert: + description: | + Path to the client certificate file for the power supply. + If not provided, the connection will not be encrypted. + type: string + default: "" + client_key: + description: | + Path to the client key file for the power supply. + If not provided, the connection will not be encrypted. + type: string + default: "" + module_placeholder_allocation_timeout_s: + description: | + Timeout in seconds for the allocation of a module placeholder. + type: integer + default: 5 + esn: + description: | + Electronic Serial Number of the dispenser. + Required to identify the dispenser in case there are multiple dispensers. + type: string + default: "0000000000000000" + HACK_publish_requested_voltage_current: + description: | + Just publish the requested voltage and current to the power supply. + This is a hack useful for testing in a SIL environment. + type: boolean + default: false + HACK_use_ovm_while_cable_check: + description: | + Use the over voltage monitor while the cable check is running instead of the powermeter, e.g. when the powermeter is too slow. + type: boolean + default: false + send_secure_goose: + description: | + Send secure GOOSE frames to the power supply. + If set to false, the GOOSE frames are sent without a signature. If set to true, the outgoing GOOSE frames are signed. + Note: This does not affect receiving GOOSE frames. + type: boolean + default: true + allow_insecure_goose: + description: | + Allow receiving insecure GOOSE frames from the power supply. + If set to false, the received GOOSE frames must be signed (the signature is only checked if verify_secure_goose is set to true). + If set to true, the received GOOSE frames are not checked for a signature. + Note: If verify_secure_goose is set to true, this setting is ignored. + type: boolean + default: false + verify_secure_goose: + description: | + Verify the received GOOSE frames. + If set to false, the received GOOSE frames are not verified. + If set to true, the received GOOSE frames must be secure and the signature must be valid. + Note: If this is set to true the allow_insecure_goose setting is ignored + type: boolean + default: true + upstream_voltage_source: + description: | + The PSU upstream voltage source to use + type: string + enum: + - IMD + - OVM + default: IMD +provides: + connector_1: + description: Power supply interface for the first connector. + interface: power_supply_DC + config: + global_connector_number: + description: | + Number of the connector. + Required to identify the connector. + type: integer + max_export_current_A: + description: >- + Maximum current that the connector can deliver. + type: number + default: 0 + max_export_power_W: + description: >- + Maximum power that the connector can deliver. + type: number + default: 0 + connector_2: + description: Power supply interface for the second connector. + interface: power_supply_DC + config: + global_connector_number: + description: | + Number of the connector. + Required to identify the connector. + type: integer + default: -1 + max_export_current_A: + description: >- + Maximum current that the connector can deliver. + type: number + default: 0 + max_export_power_W: + description: >- + Maximum power that the connector can deliver. + type: number + default: 0 + connector_3: + description: Power supply interface for the third connector. + interface: power_supply_DC + config: + global_connector_number: + description: | + Number of the connector. + Required to identify the connector. + type: integer + default: -1 + max_export_current_A: + description: >- + Maximum current that the connector can deliver. + type: number + default: 0 + max_export_power_W: + description: >- + Maximum power that the connector can deliver. + type: number + default: 0 + connector_4: + description: Power supply interface for the fourth connector. + interface: power_supply_DC + config: + global_connector_number: + description: | + Number of the connector. + Required to identify the connector. + type: integer + default: -1 + max_export_current_A: + description: >- + Maximum current that the connector can deliver. + type: number + default: 0 + max_export_power_W: + description: >- + Maximum power that the connector can deliver. + type: number + default: 0 +requires: + board_support: + interface: evse_board_support + min_connections: 1 + max_connections: 4 + isolation_monitor: + interface: isolation_monitor + min_connections: 0 + max_connections: 4 + carside_powermeter: + interface: powermeter + min_connections: 0 + max_connections: 4 + over_voltage_monitor: + interface: over_voltage_monitor + min_connections: 0 + max_connections: 4 +metadata: + license: https://opensource.org/licenses/Apache-2.0 + authors: + - Frickly Systems GmbH