diff --git a/config/config-sil-dc-sae-v2g.yaml b/config/config-sil-dc-sae-v2g.yaml index 0e56b1238..7820d7d5e 100644 --- a/config/config-sil-dc-sae-v2g.yaml +++ b/config/config-sil-dc-sae-v2g.yaml @@ -57,7 +57,7 @@ active_modules: slac: module: JsSlacSimulator imd: - module: JsIMDSimulator + module: IMDSimulator car_simulator: module: JsCarSimulator config_module: diff --git a/config/config-sil-dc-sae-v2h.yaml b/config/config-sil-dc-sae-v2h.yaml index 476a25a75..9817ab4a4 100644 --- a/config/config-sil-dc-sae-v2h.yaml +++ b/config/config-sil-dc-sae-v2h.yaml @@ -57,7 +57,7 @@ active_modules: slac: module: JsSlacSimulator imd: - module: JsIMDSimulator + module: IMDSimulator car_simulator: module: JsCarSimulator config_module: diff --git a/config/config-sil-dc.yaml b/config/config-sil-dc.yaml index 07566523c..14f3a6eb9 100644 --- a/config/config-sil-dc.yaml +++ b/config/config-sil-dc.yaml @@ -54,7 +54,7 @@ active_modules: slac: module: JsSlacSimulator imd: - module: JsIMDSimulator + module: IMDSimulator car_simulator: module: JsCarSimulator config_module: diff --git a/config/config-sil-ocpp201.yaml b/config/config-sil-ocpp201.yaml index 91c080d1a..b3b74c4e2 100644 --- a/config/config-sil-ocpp201.yaml +++ b/config/config-sil-ocpp201.yaml @@ -120,15 +120,15 @@ active_modules: implementation_id: evse - module_id: evse_manager_2 implementation_id: evse + auth: + - module_id: auth + implementation_id: main system: - module_id: system implementation_id: main security: - module_id: evse_security implementation_id: main - kvs: - - module_id: persistent_store - implementation_id: main persistent_store: module: PersistentStore evse_security: diff --git a/config/config-sil-two-evse-dc.yaml b/config/config-sil-two-evse-dc.yaml index 90f284590..eeb50b186 100644 --- a/config/config-sil-two-evse-dc.yaml +++ b/config/config-sil-two-evse-dc.yaml @@ -78,7 +78,7 @@ active_modules: powersupply_dc: module: JsDCSupplySimulator imd: - module: JsIMDSimulator + module: IMDSimulator car_simulator_1: module: JsCarSimulator config_module: diff --git a/dependencies.yaml b/dependencies.yaml index ea94e81cd..c6a8afca5 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -41,7 +41,7 @@ libcurl: # OCPP libocpp: git: https://github.com/EVerest/libocpp.git - git_tag: 990e3bc + git_tag: 84b604a # Josev Josev: git: https://github.com/EVerest/ext-switchev-iso15118.git @@ -66,4 +66,4 @@ everest-utils: # setting it here can be misleading since it does not affect the version being used libevse-security: git: https://github.com/EVerest/libevse-security.git - git_tag: v0.4.0 + git_tag: 1604c8b diff --git a/lib/staging/ocpp/evse_security_ocpp.cpp b/lib/staging/ocpp/evse_security_ocpp.cpp index 926726b40..43e95b816 100644 --- a/lib/staging/ocpp/evse_security_ocpp.cpp +++ b/lib/staging/ocpp/evse_security_ocpp.cpp @@ -92,6 +92,11 @@ std::optional EvseSecurity::get_key_pair(const ocpp::CertificateS } } +bool EvseSecurity::update_certificate_links(const ocpp::CertificateSigningUseEnum& certificate_type) { + // TODO: Implement if required + return false; +} + std::string EvseSecurity::get_verify_file(const ocpp::CaCertificateType& certificate_type) { return this->r_security.call_get_verify_file(conversions::from_ocpp(certificate_type)); } diff --git a/lib/staging/ocpp/evse_security_ocpp.hpp b/lib/staging/ocpp/evse_security_ocpp.hpp index d451c1cbc..6a14c3168 100644 --- a/lib/staging/ocpp/evse_security_ocpp.hpp +++ b/lib/staging/ocpp/evse_security_ocpp.hpp @@ -35,6 +35,7 @@ class EvseSecurity : public ocpp::EvseSecurity { const std::string& country, const std::string& organization, const std::string& common, bool use_tpm) override; std::optional get_key_pair(const ocpp::CertificateSigningUseEnum& certificate_type) override; + bool update_certificate_links(const ocpp::CertificateSigningUseEnum& certificate_type) override; std::string get_verify_file(const ocpp::CaCertificateType& certificate_type) override; int get_leaf_expiry_days_count(const ocpp::CertificateSigningUseEnum& certificate_type) override; }; diff --git a/modules/API/API.cpp b/modules/API/API.cpp index 02cb39b10..21fd9ae22 100644 --- a/modules/API/API.cpp +++ b/modules/API/API.cpp @@ -9,7 +9,7 @@ namespace module { static const auto NOTIFICATION_PERIOD = std::chrono::seconds(1); SessionInfo::SessionInfo() : - state("Unknown"), + state(State::Unknown), start_energy_import_wh(0), end_energy_import_wh(0), latest_total_w(0), @@ -19,9 +19,9 @@ SessionInfo::SessionInfo() : this->end_time_point = this->start_time_point; } -bool SessionInfo::is_state_charging(const std::string& current_state) { - if (current_state == "AuthRequired" || current_state == "Charging" || current_state == "ChargingPausedEV" || - current_state == "ChargingPausedEVSE") { +bool SessionInfo::is_state_charging(const SessionInfo::State current_state) { + if (current_state == State::AuthRequired || current_state == State::Charging || + current_state == State::ChargingPausedEV || current_state == State::ChargingPausedEVSE) { return true; } return false; @@ -29,8 +29,9 @@ bool SessionInfo::is_state_charging(const std::string& current_state) { void SessionInfo::reset() { std::lock_guard lock(this->session_info_mutex); - this->state = "Unknown"; - this->state_info = ""; + this->state = State::Unknown; + this->active_permanent_faults.clear(); + this->active_errors.clear(); this->start_energy_import_wh = 0; this->end_energy_import_wh = 0; this->start_energy_export_wh = 0; @@ -65,43 +66,80 @@ types::energy::ExternalLimits get_external_limits(const std::string& data, bool return external_limits; } -void SessionInfo::update_state(const std::string& event, const std::string& state_info) { +static void remove_error_from_list(std::vector& list, const std::string& error_type) { + list.erase(std::remove_if(list.begin(), list.end(), + [error_type](const module::SessionInfo::Error& err) { return err.type == error_type; }), + list.end()); +} + +void SessionInfo::update_state(const types::evse_manager::SessionEventEnum event, const SessionInfo::Error& error) { std::lock_guard lock(this->session_info_mutex); + using Event = types::evse_manager::SessionEventEnum; + + if (event == Event::Enabled) { + this->state = State::Unplugged; + } else if (event == Event::Disabled) { + this->state = State::Disabled; + } else if (event == Event::SessionStarted) { + this->state = State::Preparing; + } else if (event == Event::ReservationStart) { + this->state = State::Reserved; + } else if (event == Event::ReservationEnd) { + this->state = State::Unplugged; + } else if (event == Event::AuthRequired) { + this->state = State::AuthRequired; + } else if (event == Event::WaitingForEnergy) { + this->state = State::WaitingForEnergy; + } else if (event == Event::TransactionStarted) { + this->state = State::Preparing; + } else if (event == Event::ChargingPausedEV) { + this->state = State::ChargingPausedEV; + } else if (event == Event::ChargingPausedEVSE) { + this->state = State::ChargingPausedEVSE; + } else if (event == Event::ChargingStarted) { + this->state = State::Charging; + } else if (event == Event::ChargingResumed) { + this->state = State::Charging; + } else if (event == Event::TransactionFinished) { + this->state = State::Finished; + } else if (event == Event::SessionFinished) { + this->state = State::Unplugged; + } else if (event == Event::PermanentFault) { + this->active_permanent_faults.push_back(error); + } else if (event == Event::Error) { + this->active_errors.push_back(error); + } else if (event == Event::PermanentFaultCleared or event == Event::ErrorCleared) { + remove_error_from_list(this->active_permanent_faults, error.type); + } else if (event == Event::AllErrorsCleared) { + this->active_permanent_faults.clear(); + this->active_errors.clear(); + } +} - this->state_info = state_info; - if (event == "Enabled") { - this->state = "Unplugged"; - } else if (event == "Disabled") { - this->state = "Disabled"; - } else if (event == "SessionStarted") { - this->state = "Preparing"; - } else if (event == "ReservationStart") { - this->state = "Reserved"; - } else if (event == "ReservationEnd") { - this->state = "Unplugged"; - } else if (event == "AuthRequired") { - this->state = "AuthRequired"; - } else if (event == "WaitingForEnergy") { - this->state = "WaitingForEnergy"; - } else if (event == "TransactionStarted") { - this->state = "Preparing"; - } else if (event == "ChargingPausedEV") { - this->state = "ChargingPausedEV"; - } else if (event == "ChargingPausedEVSE") { - this->state = "ChargingPausedEVSE"; - } else if (event == "ChargingStarted") { - this->state = "Charging"; - } else if (event == "ChargingResumed") { - this->state = "Charging"; - } else if (event == "TransactionFinished") { - this->state = "Finished"; - } else if (event == "SessionFinished") { - this->state = "Unplugged"; - } else if (event == "Error") { - this->state = "Error"; - } else if (event == "PermanentFault") { - this->state = "PermanentFault"; +std::string SessionInfo::state_to_string(SessionInfo::State s) { + switch (s) { + case SessionInfo::State::Unplugged: + return "Unplugged"; + case SessionInfo::State::Disabled: + return "Disabled"; + case SessionInfo::State::Preparing: + return "Preparing"; + case SessionInfo::State::Reserved: + return "Reserved"; + case SessionInfo::State::AuthRequired: + return "AuthRequired"; + case SessionInfo::State::WaitingForEnergy: + return "WaitingForEnergy"; + case SessionInfo::State::ChargingPausedEV: + return "ChargingPausedEV"; + case SessionInfo::State::ChargingPausedEVSE: + return "ChargingPausedEVSE"; + case SessionInfo::State::Charging: + return "Charging"; + case SessionInfo::State::Finished: + return "Finished"; } + return "Unknown"; } void SessionInfo::set_start_energy_import_wh(int32_t start_energy_import_wh) { @@ -152,6 +190,10 @@ void SessionInfo::set_latest_total_w(double latest_total_w) { this->latest_total_w = latest_total_w; } +static void to_json(json& j, const SessionInfo::Error& e) { + j = json{{"type", e.type}, {"description", e.description}, {"severity", e.severity}}; +} + SessionInfo::operator std::string() { std::lock_guard lock(this->session_info_mutex); @@ -165,8 +207,9 @@ SessionInfo::operator std::string() { auto charging_duration_s = std::chrono::duration_cast(this->end_time_point - this->start_time_point); - json session_info = json::object({{"state", this->state}, - {"state_info", this->state_info}, + json session_info = json::object({{"state", state_to_string(this->state)}, + {"active_permanent_faults", this->active_permanent_faults}, + {"active_errors", this->active_errors}, {"charged_energy_wh", charged_energy_wh}, {"discharged_energy_wh", discharged_energy_wh}, {"latest_total_w", this->latest_total_w}, @@ -252,12 +295,15 @@ void API::init() { evse->subscribe_session_event( [this, var_session_info, var_logging_path, &session_info](types::evse_manager::SessionEvent session_event) { - auto event = types::evse_manager::session_event_enum_to_string(session_event.event); - std::string state_info = ""; + SessionInfo::Error error; if (session_event.error.has_value()) { - state_info = types::evse_manager::error_enum_to_string(session_event.error.value().error_code); + error.type = types::evse_manager::error_enum_to_string(session_event.error.value().error_code); + error.description = session_event.error.value().error_description; + error.severity = + types::evse_manager::error_severity_to_string(session_event.error.value().error_severity); } - session_info->update_state(event, state_info); + + session_info->update_state(session_event.event, error); if (session_event.event == types::evse_manager::SessionEventEnum::SessionStarted) { if (session_event.session_started.has_value()) { diff --git a/modules/API/API.hpp b/modules/API/API.hpp index cb2fc1ece..fafbf147f 100644 --- a/modules/API/API.hpp +++ b/modules/API/API.hpp @@ -34,31 +34,22 @@ namespace module { class LimitDecimalPlaces; class SessionInfo { -private: - std::mutex session_info_mutex; - - std::string state; ///< Latest state of the EVSE - std::string state_info; ///< Additional information of this state - int32_t start_energy_import_wh; ///< Energy reading (import) at the beginning of this charging session in Wh - int32_t end_energy_import_wh; ///< Energy reading (import) at the end of this charging session in Wh - int32_t start_energy_export_wh; ///< Energy reading (export) at the beginning of this charging session in Wh - int32_t end_energy_export_wh; ///< Energy reading (export) at the end of this charging session in Wh - std::chrono::time_point start_time_point; ///< Start of the charging session - std::chrono::time_point end_time_point; ///< End of the charging session - double latest_total_w; ///< Latest total power reading in W - - bool is_state_charging(const std::string& current_state); - public: SessionInfo(); + struct Error { + std::string type; + std::string description; + std::string severity; + }; + bool start_energy_export_wh_was_set{ false}; ///< Indicate if start export energy value (optional) has been received or not bool end_energy_export_wh_was_set{ false}; ///< Indicate if end export energy value (optional) has been received or not void reset(); - void update_state(const std::string& event, const std::string& state_info); + void update_state(const types::evse_manager::SessionEventEnum event, const SessionInfo::Error& error); void set_start_energy_import_wh(int32_t start_energy_import_wh); void set_end_energy_import_wh(int32_t end_energy_import_wh); void set_latest_energy_import_wh(int32_t latest_energy_wh); @@ -69,6 +60,37 @@ class SessionInfo { /// \brief Converts this struct into a serialized json object operator std::string(); + +private: + std::mutex session_info_mutex; + + std::vector active_permanent_faults; ///< Array of currently active permanent faults that prevent charging + std::vector active_errors; ///< Array of currently active errors that do not prevent charging + int32_t start_energy_import_wh; ///< Energy reading (import) at the beginning of this charging session in Wh + int32_t end_energy_import_wh; ///< Energy reading (import) at the end of this charging session in Wh + int32_t start_energy_export_wh; ///< Energy reading (export) at the beginning of this charging session in Wh + int32_t end_energy_export_wh; ///< Energy reading (export) at the end of this charging session in Wh + std::chrono::time_point start_time_point; ///< Start of the charging session + std::chrono::time_point end_time_point; ///< End of the charging session + double latest_total_w; ///< Latest total power reading in W + + enum class State { + Unknown, + Unplugged, + Disabled, + Preparing, + Reserved, + AuthRequired, + WaitingForEnergy, + ChargingPausedEV, + ChargingPausedEVSE, + Charging, + Finished + } state; + + bool is_state_charging(const SessionInfo::State current_state); + + std::string state_to_string(State s); }; } // namespace module // ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 diff --git a/modules/API/README.md b/modules/API/README.md index 1c8af8952..7bef91468 100644 --- a/modules/API/README.md +++ b/modules/API/README.md @@ -44,10 +44,37 @@ This variable is published every second and contains a json object with informat "discharged_energy_wh": 0, "latest_total_w": 0.0, "state": "Unplugged", - "state_info": "" + "active_permanent_faults": [], + "active_errors": [] } ``` +Example with permanent faults being active: + +```json +{ + "active_errors": [], + "active_permanent_faults": [ + { + "description": "The control pilot voltage is out of range.", + "severity": "High", + "type": "MREC14PilotFault" + }, + { + "description": "The vehicle is in an invalid mode for charging (Reported by IEC stack)", + "severity": "High", + "type": "MREC10InvalidVehicleMode" + } + ], + "charged_energy_wh": 0, + "charging_duration_s": 0, + "datetime": "2024-01-15T14:58:15.172Z", + "discharged_energy_wh": 0, + "latest_total_w": 0, + "state": "Preparing" +} +``` + - **charged_energy_wh** contains the charged energy in Wh - **charging_duration_s** contains the duration of the current charging session in seconds - **datetime** contains a string representation of the current UTC datetime in RFC3339 format @@ -64,20 +91,44 @@ This variable is published every second and contains a json object with informat - ChargingPausedEV - ChargingPausedEVSE - Finished - - Error - - PermanentFault - -- **state_info** contains additional information for the current state, at the moment this is only set to a meaningful value in the Error state. Here it can have the following values: - - Car - - CarDiodeFault - - Relais - - RCD +- **active_permanent_faults** array of all active errors that are permanent faults (i.e. that block charging). If anything is set here it should be shown as an error to the user instead of showing the current state: + - RCD_Selftest + - RCD_DC + - RCD_AC + - VendorError + - VendorWarning + - ConnectorLockCapNotCharged + - ConnectorLockUnexpectedOpen + - ConnectorLockUnexpectedClose + - ConnectorLockFailedLock + - ConnectorLockFailedUnlock + - MREC1ConnectorLockFailure + - MREC2GroundFailure + - MREC3HighTemperature + - MREC4OverCurrentFailure + - MREC5OverVoltage + - MREC6UnderVoltage + - MREC8EmergencyStop + - MREC10InvalidVehicleMode + - MREC14PilotFault + - MREC15PowerLoss + - MREC17EVSEContactorFault + - MREC18CableOverTempDerate + - MREC19CableOverTempStop + - MREC20PartialInsertion + - MREC23ProximityFault + - MREC24ConnectorVoltageHigh + - MREC25BrokenLatch + - MREC26CutCable + - DiodeFault - VentilationNotAvailable - - OverCurrent - - Internal - - SLAC - - HLC + - BrownOut + - EnergyManagement + - PermanentFault + - PowermeterTransactionStartFailed + +- **active_errors** array of all active errors that do not block charging. This could be shown to the user but the current state should still be shown as it does not interfere with charging. The enum is the same as for active_permanent_faults. ### everest_api/evse_manager/var/limits This variable is published every second and contains a json object with information relating to the current limits of this EVSE. diff --git a/modules/EnergyManager/Market.cpp b/modules/EnergyManager/Market.cpp index c3fc75253..8b1ac6b59 100644 --- a/modules/EnergyManager/Market.cpp +++ b/modules/EnergyManager/Market.cpp @@ -268,7 +268,7 @@ void Market::get_list_of_evses(std::vector& list) { list.push_back(this); } - for (auto child : _children) { + for (auto& child : _children) { child.get_list_of_evses(list); } } diff --git a/modules/EvseManager/Charger.cpp b/modules/EvseManager/Charger.cpp index c29876eb3..5b995c2ea 100644 --- a/modules/EvseManager/Charger.cpp +++ b/modules/EvseManager/Charger.cpp @@ -47,7 +47,7 @@ Charger::Charger(const std::unique_ptr& bsp, const std::unique_ hlc_use_5percent_current_session = false; // Register callbacks for errors/error clearings - error_handling->signal_error.connect([this](const types::evse_manager::ErrorEnum e, const bool prevent_charging) { + error_handling->signal_error.connect([this](const types::evse_manager::Error e, const bool prevent_charging) { std::scoped_lock lock(stateMutex); if (prevent_charging) { error_prevent_charging_flag = true; diff --git a/modules/EvseManager/ErrorHandling.cpp b/modules/EvseManager/ErrorHandling.cpp index 9ab87fa97..ab2e8f3ae 100644 --- a/modules/EvseManager/ErrorHandling.cpp +++ b/modules/EvseManager/ErrorHandling.cpp @@ -5,6 +5,18 @@ namespace module { +static types::evse_manager::Error_severity to_evse_manager_severity(Everest::error::Severity s) { + switch (s) { + case Everest::error::Severity::High: + return types::evse_manager::Error_severity::High; + case Everest::error::Severity::Medium: + return types::evse_manager::Error_severity::Medium; + case Everest::error::Severity::Low: + return types::evse_manager::Error_severity::Low; + } + return types::evse_manager::Error_severity::Low; +} + ErrorHandling::ErrorHandling(const std::unique_ptr& _r_bsp, const std::vector>& _r_hlc, const std::vector>& _r_connector_lock, @@ -20,17 +32,35 @@ ErrorHandling::ErrorHandling(const std::unique_ptr& _r_b r_bsp->subscribe_all_errors( [this](const Everest::error::Error& error) { types::evse_manager::ErrorEnum evse_error{types::evse_manager::ErrorEnum::VendorWarning}; + types::evse_manager::Error output_error; + output_error.error_description = error.description; + output_error.error_severity = to_evse_manager_severity(error.severity); + if (modify_error_bsp(error, true, evse_error)) { // signal to charger a new error has been set that prevents charging - signal_error(evse_error, true); + output_error.error_code = evse_error; + signal_error(output_error, true); } else { // signal an error that does not prevent charging - signal_error(evse_error, false); + output_error.error_code = evse_error; + signal_error(output_error, false); } }, [this](const Everest::error::Error& error) { types::evse_manager::ErrorEnum evse_error{types::evse_manager::ErrorEnum::VendorWarning}; - modify_error_bsp(error, false, evse_error); + types::evse_manager::Error output_error; + output_error.error_description = error.description; + output_error.error_severity = to_evse_manager_severity(error.severity); + + if (modify_error_bsp(error, false, evse_error)) { + // signal to charger an error has been cleared that prevents charging + output_error.error_code = evse_error; + signal_error_cleared(output_error, true); + } else { + // signal an error cleared that does not prevent charging + output_error.error_code = evse_error; + signal_error_cleared(output_error, false); + } if (active_errors.all_cleared()) { // signal to charger that all errors are cleared now @@ -47,17 +77,35 @@ ErrorHandling::ErrorHandling(const std::unique_ptr& _r_b r_connector_lock[0]->subscribe_all_errors( [this](const Everest::error::Error& error) { types::evse_manager::ErrorEnum evse_error{types::evse_manager::ErrorEnum::VendorWarning}; + types::evse_manager::Error output_error; + output_error.error_description = error.description; + output_error.error_severity = to_evse_manager_severity(error.severity); + if (modify_error_connector_lock(error, true, evse_error)) { // signal to charger a new error has been set that prevents charging - signal_error(evse_error, true); + output_error.error_code = evse_error; + signal_error(output_error, true); } else { // signal an error that does not prevent charging - signal_error(evse_error, false); + output_error.error_code = evse_error; + signal_error(output_error, false); } }, [this](const Everest::error::Error& error) { types::evse_manager::ErrorEnum evse_error{types::evse_manager::ErrorEnum::VendorWarning}; - modify_error_connector_lock(error, false, evse_error); + types::evse_manager::Error output_error; + output_error.error_description = error.description; + output_error.error_severity = to_evse_manager_severity(error.severity); + + if (modify_error_connector_lock(error, false, evse_error)) { + // signal to charger an error has been cleared that prevents charging + output_error.error_code = evse_error; + signal_error_cleared(output_error, true); + } else { + // signal an error cleared that does not prevent charging + output_error.error_code = evse_error; + signal_error_cleared(output_error, false); + } if (active_errors.all_cleared()) { // signal to charger that all errors are cleared now @@ -75,17 +123,35 @@ ErrorHandling::ErrorHandling(const std::unique_ptr& _r_b r_ac_rcd[0]->subscribe_all_errors( [this](const Everest::error::Error& error) { types::evse_manager::ErrorEnum evse_error{types::evse_manager::ErrorEnum::VendorWarning}; + types::evse_manager::Error output_error; + output_error.error_description = error.description; + output_error.error_severity = to_evse_manager_severity(error.severity); + if (modify_error_ac_rcd(error, true, evse_error)) { // signal to charger a new error has been set that prevents charging - signal_error(evse_error, true); + output_error.error_code = evse_error; + signal_error(output_error, true); } else { // signal an error that does not prevent charging - signal_error(evse_error, false); + output_error.error_code = evse_error; + signal_error(output_error, false); } }, [this](const Everest::error::Error& error) { types::evse_manager::ErrorEnum evse_error{types::evse_manager::ErrorEnum::VendorWarning}; - modify_error_ac_rcd(error, false, evse_error); + types::evse_manager::Error output_error; + output_error.error_description = error.description; + output_error.error_severity = to_evse_manager_severity(error.severity); + + if (modify_error_ac_rcd(error, false, evse_error)) { + // signal to charger an error has been cleared that prevents charging + output_error.error_code = evse_error; + signal_error_cleared(output_error, true); + } else { + // signal an error cleared that does not prevent charging + output_error.error_code = evse_error; + signal_error_cleared(output_error, false); + } if (active_errors.all_cleared()) { // signal to charger that all errors are cleared now @@ -106,7 +172,11 @@ void ErrorHandling::raise_overcurrent_error(const std::string& description) { if (modify_error_evse_manager("evse_manager/MREC4OverCurrentFailure", true, evse_error)) { // signal to charger a new error has been set - signal_error(evse_error, true); + types::evse_manager::Error output_error; + output_error.error_description = description; + output_error.error_severity = types::evse_manager::Error_severity::High; + output_error.error_code = types::evse_manager::ErrorEnum::MREC4OverCurrentFailure; + signal_error(output_error, true); }; } @@ -114,8 +184,20 @@ void ErrorHandling::clear_overcurrent_error() { // clear externally p_evse->request_clear_all_evse_manager_MREC4OverCurrentFailure(); types::evse_manager::ErrorEnum evse_error{types::evse_manager::ErrorEnum::VendorWarning}; - - modify_error_evse_manager("evse_manager/MREC4OverCurrentFailure", false, evse_error); + types::evse_manager::Error output_error; + output_error.error_description = ""; + output_error.error_severity = types::evse_manager::Error_severity::High; + output_error.error_code = types::evse_manager::ErrorEnum::MREC4OverCurrentFailure; + + if (modify_error_evse_manager("evse_manager/MREC4OverCurrentFailure", false, evse_error)) { + // signal to charger an error has been cleared that prevents charging + output_error.error_code = evse_error; + signal_error_cleared(output_error, true); + } else { + // signal an error cleared that does not prevent charging + output_error.error_code = evse_error; + signal_error_cleared(output_error, false); + } if (active_errors.all_cleared()) { // signal to charger that all errors are cleared now @@ -134,7 +216,11 @@ void ErrorHandling::raise_internal_error(const std::string& description) { if (modify_error_evse_manager("evse_manager/Internal", true, evse_error)) { // signal to charger a new error has been set - signal_error(evse_error, true); + types::evse_manager::Error output_error; + output_error.error_description = description; + output_error.error_severity = types::evse_manager::Error_severity::High; + output_error.error_code = types::evse_manager::ErrorEnum::VendorError; + signal_error(output_error, true); }; } @@ -142,8 +228,20 @@ void ErrorHandling::clear_internal_error() { // clear externally p_evse->request_clear_all_evse_manager_Internal(); types::evse_manager::ErrorEnum evse_error{types::evse_manager::ErrorEnum::VendorWarning}; - - modify_error_evse_manager("evse_manager/Internal", false, evse_error); + types::evse_manager::Error output_error; + output_error.error_description = ""; + output_error.error_severity = types::evse_manager::Error_severity::High; + output_error.error_code = types::evse_manager::ErrorEnum::VendorError; + + if (modify_error_evse_manager("evse_manager/Internal", false, evse_error)) { + // signal to charger an error has been cleared that prevents charging + output_error.error_code = evse_error; + signal_error_cleared(output_error, true); + } else { + // signal an error cleared that does not prevent charging + output_error.error_code = evse_error; + signal_error_cleared(output_error, false); + } if (active_errors.all_cleared()) { // signal to charger that all errors are cleared now @@ -162,7 +260,11 @@ void ErrorHandling::raise_powermeter_transaction_start_failed_error(const std::s if (modify_error_evse_manager("evse_manager/PowermeterTransactionStartFailed", true, evse_error)) { // signal to charger a new error has been set - signal_error(evse_error, true); + types::evse_manager::Error output_error; + output_error.error_description = description; + output_error.error_severity = types::evse_manager::Error_severity::High; + output_error.error_code = types::evse_manager::ErrorEnum::PowermeterTransactionStartFailed; + signal_error(output_error, true); }; } @@ -170,8 +272,20 @@ void ErrorHandling::clear_powermeter_transaction_start_failed_error() { // clear externally p_evse->request_clear_all_evse_manager_PowermeterTransactionStartFailed(); types::evse_manager::ErrorEnum evse_error{types::evse_manager::ErrorEnum::VendorWarning}; - - modify_error_evse_manager("evse_manager/PowermeterTransactionStartFailed", false, evse_error); + types::evse_manager::Error output_error; + output_error.error_description = ""; + output_error.error_severity = types::evse_manager::Error_severity::High; + output_error.error_code = types::evse_manager::ErrorEnum::VendorError; + + if (modify_error_evse_manager("evse_manager/PowermeterTransactionStartFailed", false, evse_error)) { + // signal to charger an error has been cleared that prevents charging + output_error.error_code = evse_error; + signal_error_cleared(output_error, true); + } else { + // signal an error cleared that does not prevent charging + output_error.error_code = evse_error; + signal_error_cleared(output_error, false); + } if (active_errors.all_cleared()) { // signal to charger that all errors are cleared now diff --git a/modules/EvseManager/ErrorHandling.hpp b/modules/EvseManager/ErrorHandling.hpp index dfc6e5b5a..ce2ff5927 100644 --- a/modules/EvseManager/ErrorHandling.hpp +++ b/modules/EvseManager/ErrorHandling.hpp @@ -110,8 +110,11 @@ class ErrorHandling { const std::vector>& r_ac_rcd, const std::unique_ptr& _p_evse); - // Signal for internal events type - sigslot::signal signal_error; + // Signal that one error has been raised. Bool argument is true if it preventing charging. + sigslot::signal signal_error; + // Signal that one error has been cleared. Bool argument is true if it was preventing charging. + sigslot::signal signal_error_cleared; + // Signal that all errors are cleared (both those preventing charging and not) sigslot::signal<> signal_all_errors_cleared; void raise_overcurrent_error(const std::string& description); diff --git a/modules/EvseManager/evse/evse_managerImpl.cpp b/modules/EvseManager/evse/evse_managerImpl.cpp index 5561a5d69..73590e2d2 100644 --- a/modules/EvseManager/evse/evse_managerImpl.cpp +++ b/modules/EvseManager/evse/evse_managerImpl.cpp @@ -97,19 +97,31 @@ void evse_managerImpl::set_session_uuid() { void evse_managerImpl::ready() { // Register callbacks for errors/permanent faults - mod->error_handling->signal_error.connect( - [this](const types::evse_manager::ErrorEnum e, const bool prevent_charging) { + mod->error_handling->signal_error.connect([this](const types::evse_manager::Error e, const bool prevent_charging) { + types::evse_manager::SessionEvent se; + + se.error = e; + se.uuid = session_uuid; + + if (prevent_charging) { + se.event = types::evse_manager::SessionEventEnum::PermanentFault; + } else { + se.event = types::evse_manager::SessionEventEnum::Error; + } + publish_session_event(se); + }); + + mod->error_handling->signal_error_cleared.connect( + [this](const types::evse_manager::Error e, const bool prevent_charging) { types::evse_manager::SessionEvent se; - types::evse_manager::Error error; - error.error_code = e; - se.error = error; + se.error = e; se.uuid = session_uuid; if (prevent_charging) { - se.event = types::evse_manager::SessionEventEnum::PermanentFault; + se.event = types::evse_manager::SessionEventEnum::PermanentFaultCleared; } else { - se.event = types::evse_manager::SessionEventEnum::Error; + se.event = types::evse_manager::SessionEventEnum::ErrorCleared; } publish_session_event(se); }); diff --git a/modules/EvseSecurity/conversions.cpp b/modules/EvseSecurity/conversions.cpp index f07f3ef03..7a58a9fb5 100644 --- a/modules/EvseSecurity/conversions.cpp +++ b/modules/EvseSecurity/conversions.cpp @@ -190,6 +190,7 @@ evse_security::KeyPair from_everest(types::evse_security::KeyPair other) { evse_security::KeyPair lhs; lhs.key = other.key; lhs.certificate = other.certificate; + lhs.certificate_single = other.certificate_single; lhs.password = other.password; return lhs; } @@ -393,6 +394,7 @@ types::evse_security::KeyPair to_everest(evse_security::KeyPair other) { types::evse_security::KeyPair lhs; lhs.key = other.key; lhs.certificate = other.certificate; + lhs.certificate_single = other.certificate_single; lhs.password = other.password; return lhs; } diff --git a/modules/OCPP/OCPP.cpp b/modules/OCPP/OCPP.cpp index ffcdbd1fa..6e5659156 100644 --- a/modules/OCPP/OCPP.cpp +++ b/modules/OCPP/OCPP.cpp @@ -116,6 +116,18 @@ static ErrorInfo get_error_info(const std::optional } } +ocpp::SessionStartedReason get_session_started_reason(const types::evse_manager::StartSessionReason reason) { + switch (reason) { + case types::evse_manager::StartSessionReason::EVConnected: + return ocpp::SessionStartedReason::EVConnected; + case types::evse_manager::StartSessionReason::Authorized: + return ocpp::SessionStartedReason::Authorized; + default: + throw std::out_of_range( + "Could not convert types::evse_manager::StartSessionReason to ocpp::SessionStartedReason"); + } +} + void create_empty_user_config(const fs::path& user_config_path) { if (fs::exists(user_config_path.parent_path())) { std::ofstream fs(user_config_path.c_str()); @@ -242,9 +254,9 @@ void OCPP::process_session_event(int32_t evse_id, const types::evse_manager::Ses << "Received SessionStarted"; // ev side disconnect auto session_started = session_event.session_started.value(); - this->charge_point->on_session_started( - ocpp_connector_id, session_event.uuid, - types::evse_manager::start_session_reason_to_string(session_started.reason), session_started.logging_path); + this->charge_point->on_session_started(ocpp_connector_id, session_event.uuid, + get_session_started_reason(session_started.reason), + session_started.logging_path); } else if (event == "SessionFinished") { EVLOG_debug << "Connector#" << ocpp_connector_id << ": " << "Received SessionFinished"; diff --git a/modules/OCPP201/OCPP201.cpp b/modules/OCPP201/OCPP201.cpp index 39911dde4..69b175f3a 100644 --- a/modules/OCPP201/OCPP201.cpp +++ b/modules/OCPP201/OCPP201.cpp @@ -12,8 +12,6 @@ namespace module { const std::string INIT_SQL = "init_core.sql"; const std::string CERTS_DIR = "certs"; -const std::string KVS_OCPP201_INOPERATIVE_KEY_PREFIX = "OCPP201_INOPERATIVE_"; - namespace fs = std::filesystem; ocpp::v201::FirmwareStatusEnum get_firmware_status_notification(const types::system::FirmwareUpdateStatusEnum status) { @@ -496,10 +494,31 @@ ocpp::v201::IdToken get_id_token(const types::authorization::ProvidedIdToken& pr id_token.idToken = provided_id_token.id_token; if (provided_id_token.id_token_type.has_value()) { id_token.type = get_id_token_enum(provided_id_token.id_token_type.value()); + } else { + id_token.type = ocpp::v201::IdTokenEnum::Local; } return id_token; } +TxStartPoint get_tx_start_point(const std::string& tx_start_point_string) { + if (tx_start_point_string == "ParkingBayOccupancy") { + return TxStartPoint::ParkingBayOccupancy; + } else if (tx_start_point_string == "EVConnected") { + return TxStartPoint::EVConnected; + } else if (tx_start_point_string == "Authorized") { + return TxStartPoint::Authorized; + } else if (tx_start_point_string == "PowerPathClosed") { + return TxStartPoint::PowerPathClosed; + } else if (tx_start_point_string == "EnergyTransfer") { + return TxStartPoint::EnergyTransfer; + } else if (tx_start_point_string == "DataSigned") { + return TxStartPoint::DataSigned; + } + + // default to PowerPathClosed for now + return TxStartPoint::PowerPathClosed; +} + void OCPP201::init_evse_ready_map() { std::lock_guard lk(this->evse_ready_mutex); for (size_t evse_id = 1; evse_id <= this->r_evse_manager.size(); evse_id++) { @@ -507,52 +526,32 @@ void OCPP201::init_evse_ready_map() { } } -void OCPP201::init_evses() { - - if (this->r_kvs->call_exists(KVS_OCPP201_INOPERATIVE_KEY_PREFIX + "0")) { - this->cs_operational_status = ocpp::v201::OperationalStatusEnum::Inoperative; - } else { - this->cs_operational_status = ocpp::v201::OperationalStatusEnum::Operative; - } - +std::map OCPP201::get_connector_structure() { + std::map evse_connector_structure; int evse_id = 1; for (const auto& evse : this->r_evse_manager) { - int connector_id = 1; auto _evse = evse->call_get_evse(); + int32_t num_connectors = _evse.connectors.size(); + if (_evse.id != evse_id) { throw std::runtime_error("Configured evse_id(s) must start with 1 counting upwards"); } - - if (_evse.connectors.size() == 0) { - _evse.connectors.push_back({1}); - } - - Evse module_evse; - module_evse.evse_id = evse_id; - - if (this->r_kvs->call_exists(KVS_OCPP201_INOPERATIVE_KEY_PREFIX + std::to_string(evse_id))) { - module_evse.operational_state = ocpp::v201::OperationalStatusEnum::Inoperative; - } else { - module_evse.operational_state = ocpp::v201::OperationalStatusEnum::Operative; - } - - for (const auto& connector : _evse.connectors) { - if (connector.id != connector_id) { - throw std::runtime_error("Configured connector_id(s) must start with 1 counting upwards"); - } - - auto connector_id_str = std::to_string(evse_id) + "." + std::to_string(connector_id); - if (this->r_kvs->call_exists(KVS_OCPP201_INOPERATIVE_KEY_PREFIX + connector_id_str)) { - module_evse.connectors[connector_id] = ocpp::v201::OperationalStatusEnum::Inoperative; - } else { - module_evse.connectors[connector_id] = ocpp::v201::OperationalStatusEnum::Operative; + if (num_connectors > 0) { + int connector_id = 1; + for (const auto& connector : _evse.connectors) { + if (connector.id != connector_id) { + throw std::runtime_error("Configured connector_id(s) must start with 1 counting upwards"); + } + connector_id++; } - connector_id++; + } else { + num_connectors = 1; } - this->evses[_evse.id] = module_evse; + evse_connector_structure[evse_id] = num_connectors; evse_id++; } + return evse_connector_structure; } bool OCPP201::all_evse_ready() { @@ -565,30 +564,6 @@ bool OCPP201::all_evse_ready() { return true; } -void OCPP201::set_connector_operational_status(const ocpp::v201::OperationalStatusEnum operational_status, - const int32_t evse_id, const int32_t connector_id, const bool persist) { - if (operational_status == ocpp::v201::OperationalStatusEnum::Operative) { - this->evses.at(evse_id).connectors.at(connector_id) = ocpp::v201::OperationalStatusEnum::Operative; - auto connector_id_str = std::to_string(evse_id) + "." + std::to_string(connector_id); - this->r_kvs->call_delete(KVS_OCPP201_INOPERATIVE_KEY_PREFIX + connector_id_str); - } else if (persist) { - this->evses.at(evse_id).connectors.at(connector_id) = ocpp::v201::OperationalStatusEnum::Inoperative; - auto connector_id_str = std::to_string(evse_id) + "." + std::to_string(connector_id); - this->r_kvs->call_store(KVS_OCPP201_INOPERATIVE_KEY_PREFIX + connector_id_str, true); - } -} - -void OCPP201::set_evse_operational_status(const ocpp::v201::OperationalStatusEnum operational_status, - const int32_t evse_id, const bool persist) { - if (operational_status == ocpp::v201::OperationalStatusEnum::Operative) { - this->evses.at(evse_id).operational_state = ocpp::v201::OperationalStatusEnum::Operative; - this->r_kvs->call_delete(KVS_OCPP201_INOPERATIVE_KEY_PREFIX + std::to_string(evse_id)); - } else if (persist) { - this->evses.at(evse_id).operational_state = ocpp::v201::OperationalStatusEnum::Inoperative; - this->r_kvs->call_store(KVS_OCPP201_INOPERATIVE_KEY_PREFIX + std::to_string(evse_id), true); - } -} - void OCPP201::init() { invoke_init(*p_main); invoke_init(*p_auth_provider); @@ -634,7 +609,6 @@ void OCPP201::ready() { invoke_ready(*p_auth_validator); this->ocpp_share_path = this->info.paths.share; - this->cs_operational_status = ocpp::v201::OperationalStatusEnum::Operative; const auto device_model_database_path = [&]() { const auto config_device_model_path = fs::path(this->config.DeviceModelDatabasePath); @@ -692,49 +666,18 @@ void OCPP201::ready() { } }; - callbacks.change_availability_callback = [this](const ocpp::v201::ChangeAvailabilityRequest& request, - const bool persist) { - if (request.evse.has_value()) { - auto evse_id = request.evse.value().id; - auto connector_id = request.evse.value().connectorId; - if (request.operationalStatus == ocpp::v201::OperationalStatusEnum::Operative) { - // change to operative - if (connector_id.has_value()) { - // connector is addressed - this->set_connector_operational_status(ocpp::v201::OperationalStatusEnum::Operative, evse_id, - connector_id.value(), persist); - } else { - // EVSE is addressed - this->set_evse_operational_status(ocpp::v201::OperationalStatusEnum::Operative, evse_id, persist); + callbacks.connector_effective_operative_status_changed_callback = + [this](const int32_t evse_id, const int32_t connector_id, const ocpp::v201::OperationalStatusEnum new_status) { + if (new_status == ocpp::v201::OperationalStatusEnum::Operative) { + if (this->r_evse_manager.at(evse_id - 1)->call_enable(connector_id)) { + this->charge_point->on_enabled(evse_id, connector_id); } - this->r_evse_manager.at(evse_id - 1)->call_enable(request.evse.value().connectorId.value_or(0)); } else { - // change to inoperative - if (connector_id.has_value()) { - // connector is addressed - this->set_connector_operational_status(ocpp::v201::OperationalStatusEnum::Inoperative, evse_id, - connector_id.value(), persist); - } else { - // EVSE is addressed - this->set_evse_operational_status(ocpp::v201::OperationalStatusEnum::Inoperative, evse_id, persist); - } - this->r_evse_manager.at(evse_id - 1)->call_disable(request.evse.value().connectorId.value_or(0)); - } - } else { - // whole charging station is adressed - this->cs_operational_status = request.operationalStatus; - for (size_t evse_id = 1; evse_id <= this->r_evse_manager.size(); evse_id++) { - if (this->cs_operational_status == ocpp::v201::OperationalStatusEnum::Operative and - this->evses.at(evse_id).operational_state == ocpp::v201::OperationalStatusEnum::Operative) { - this->r_evse_manager.at(evse_id - 1)->call_enable(0); - this->r_kvs->call_delete(KVS_OCPP201_INOPERATIVE_KEY_PREFIX + "0"); - } else if (request.operationalStatus == ocpp::v201::OperationalStatusEnum::Inoperative) { - this->r_evse_manager.at(evse_id - 1)->call_disable(0); - this->r_kvs->call_store(KVS_OCPP201_INOPERATIVE_KEY_PREFIX + "0", true); + if (this->r_evse_manager.at(evse_id - 1)->call_disable(connector_id)) { + this->charge_point->on_unavailable(evse_id, connector_id); } } - } - }; + }; callbacks.remote_start_transaction_callback = [this](const ocpp::v201::RequestStartTransactionRequest& request, const bool authorize_remote_start) { @@ -799,6 +742,20 @@ void OCPP201::ready() { return get_update_firmware_response(response); }; + callbacks.variable_changed_callback = [this](const ocpp::v201::SetVariableData& set_variable_data) { + if (set_variable_data.component.name == "TxCtrlr" and + set_variable_data.variable.name == "EVConnectionTimeOut") { + try { + auto ev_connection_timeout = std::stoi(set_variable_data.attributeValue.get()); + this->r_auth->call_set_connection_timeout(ev_connection_timeout); + } catch (const std::exception& e) { + EVLOG_error << "Could not parse EVConnectionTimeOut and did not set it in Auth module, error: " + << e.what(); + return; + } + } + }; + callbacks.validate_network_profile_callback = [this](const int32_t configuration_slot, const ocpp::v201::NetworkConnectionProfile& network_connection_profile) { @@ -839,18 +796,31 @@ void OCPP201::ready() { const auto sql_init_path = this->ocpp_share_path / INIT_SQL; - this->init_evses(); - - std::map evse_connector_structure; - for (const auto [evse_id, evse] : this->evses) { - evse_connector_structure[evse_id] = evse.connectors.size(); - } - + std::map evse_connector_structure = this->get_connector_structure(); this->charge_point = std::make_unique( evse_connector_structure, device_model_database_path, this->ocpp_share_path.string(), this->config.CoreDatabasePath, sql_init_path.string(), this->config.MessageLogPath, std::make_shared(*this->r_security), callbacks); + const auto tx_start_point_request_value_response = this->charge_point->request_value( + ocpp::v201::Component{"TxCtrlr"}, ocpp::v201::Variable{"TxStartPoint"}, ocpp::v201::AttributeEnum::Actual); + if (tx_start_point_request_value_response.status == ocpp::v201::GetVariableStatusEnum::Accepted and + tx_start_point_request_value_response.value.has_value()) { + auto tx_start_point_string = tx_start_point_request_value_response.value.value(); + this->tx_start_point = get_tx_start_point(tx_start_point_string); + EVLOG_info << "TxStartPoint from device model: " << tx_start_point_string; + } else { + this->tx_start_point = TxStartPoint::PowerPathClosed; + } + + const auto ev_connection_timeout_request_value_response = this->charge_point->request_value( + ocpp::v201::Component{"TxCtrlr"}, ocpp::v201::Variable{"EVConnectionTimeOut"}, + ocpp::v201::AttributeEnum::Actual); + if (ev_connection_timeout_request_value_response.status == ocpp::v201::GetVariableStatusEnum::Accepted and + ev_connection_timeout_request_value_response.value.has_value()) { + this->r_auth->call_set_connection_timeout(ev_connection_timeout_request_value_response.value.value()); + } + if (this->config.EnableExternalWebsocketControl) { const std::string connect_topic = "everest_api/ocpp/cmd/connect"; this->mqtt.subscribe(connect_topic, @@ -865,10 +835,27 @@ void OCPP201::ready() { for (const auto& evse : this->r_evse_manager) { evse->subscribe_session_event([this, evse_id](types::evse_manager::SessionEvent session_event) { const auto connector_id = session_event.connector_id.value_or(1); + const auto evse_connector = std::make_pair(evse_id, connector_id); switch (session_event.event) { case types::evse_manager::SessionEventEnum::SessionStarted: { - this->session_started_reason = session_event.session_started.value().reason; - this->charge_point->on_session_started(evse_id, connector_id); + if (!session_event.session_started.has_value()) { + this->session_started_reasons[evse_connector] = + types::evse_manager::StartSessionReason::EVConnected; + } else { + this->session_started_reasons[evse_connector] = session_event.session_started.value().reason; + } + + switch (this->tx_start_point) { + case TxStartPoint::EVConnected: + [[fallthrough]]; + case TxStartPoint::Authorized: + [[fallthrough]]; + case TxStartPoint::PowerPathClosed: + [[fallthrough]]; + case TxStartPoint::EnergyTransfer: + this->charge_point->on_session_started(evse_id, connector_id); + break; + } break; } case types::evse_manager::SessionEventEnum::SessionFinished: { @@ -891,7 +878,8 @@ void OCPP201::ready() { auto trigger_reason = ocpp::v201::TriggerReasonEnum::Authorized; // if session started reason was Authorized, Transaction is started because of EV plug in event - if (this->session_started_reason == types::evse_manager::StartSessionReason::Authorized) { + if (this->session_started_reasons[evse_connector] == + types::evse_manager::StartSessionReason::Authorized) { trigger_reason = ocpp::v201::TriggerReasonEnum::CablePluggedIn; } @@ -899,11 +887,18 @@ void OCPP201::ready() { trigger_reason = ocpp::v201::TriggerReasonEnum::RemoteStart; } - this->charge_point->on_transaction_started( - evse_id, connector_id, session_id, timestamp, trigger_reason, meter_value, id_token, std::nullopt, - reservation_id, remote_start_id, - ocpp::v201::ChargingStateEnum::EVConnected); // FIXME(piet): add proper groupIdToken + - // ChargingStateEnum + if (this->tx_start_point == TxStartPoint::EnergyTransfer) { + this->transaction_starts[evse_connector].emplace(TransactionStart{ + evse_id, connector_id, session_id, timestamp, trigger_reason, meter_value, id_token, + std::nullopt, reservation_id, remote_start_id, ocpp::v201::ChargingStateEnum::Charging}); + } else { + this->charge_point->on_transaction_started( + evse_id, connector_id, session_id, timestamp, trigger_reason, meter_value, id_token, + std::nullopt, reservation_id, remote_start_id, + ocpp::v201::ChargingStateEnum::EVConnected); // FIXME(piet): add proper groupIdToken + + // ChargingStateEnum + } + break; } case types::evse_manager::SessionEventEnum::TransactionFinished: { @@ -930,6 +925,21 @@ void OCPP201::ready() { break; } case types::evse_manager::SessionEventEnum::ChargingStarted: { + if (this->tx_start_point == TxStartPoint::EnergyTransfer) { + if (this->transaction_starts[evse_connector].has_value()) { + auto transaction_start = this->transaction_starts[evse_connector].value(); + this->charge_point->on_transaction_started( + transaction_start.evse_id, transaction_start.connector_id, transaction_start.session_id, + transaction_start.timestamp, transaction_start.trigger_reason, + transaction_start.meter_start, transaction_start.id_token, transaction_start.group_id_token, + transaction_start.reservation_id, transaction_start.remote_start_id, + transaction_start.charging_state); + this->transaction_starts[evse_connector].reset(); + } else { + EVLOG_error + << "ChargingStarted with TxStartPoint EnergyTransfer but no TransactionStart was available"; + } + } this->charge_point->on_charging_state_changed(evse_id, ocpp::v201::ChargingStateEnum::Charging); break; } @@ -946,28 +956,12 @@ void OCPP201::ready() { break; } case types::evse_manager::SessionEventEnum::Disabled: { - if (session_event.connector_id.has_value()) { - this->charge_point->on_unavailable(evse_id, connector_id); - } else { - for (size_t index = 1; index <= this->evses.at(evse_id).connectors.size(); index++) { - this->charge_point->on_unavailable(evse_id, index); - } - } + this->charge_point->on_unavailable(evse_id, connector_id); break; } case types::evse_manager::SessionEventEnum::Enabled: { - if (session_event.connector_id.has_value()) { - if (this->evses.at(evse_id).operational_state == ocpp::v201::OperationalStatusEnum::Operative) { - this->charge_point->on_operative(evse_id, connector_id); - } - } else { - for (size_t index = 1; index <= this->evses.at(evse_id).connectors.size(); index++) { - if (this->evses.at(evse_id).connectors.at(index) == - ocpp::v201::OperationalStatusEnum::Operative) { - this->charge_point->on_operative(evse_id, index); - } - } - } + // A single connector was enabled + this->charge_point->on_enabled(evse_id, connector_id); break; } } @@ -1011,19 +1005,8 @@ void OCPP201::ready() { while (!this->all_evse_ready()) { this->evse_ready_cv.wait(lk); } - - // align state machine based on operational status - for (const auto& [evse_id, evse] : this->evses) { - if (evse.operational_state == ocpp::v201::OperationalStatusEnum::Inoperative or - this->cs_operational_status == ocpp::v201::OperationalStatusEnum::Inoperative) { - this->r_evse_manager.at(evse_id - 1)->call_disable(0); - } - for (const auto [connector_id, operational_state] : evse.connectors) { - if (operational_state == ocpp::v201::OperationalStatusEnum::Inoperative) { - this->r_evse_manager.at(evse_id - 1)->call_disable(connector_id); - } - } - } + // In case (for some reason) EvseManager ready signals are sent after this point, this will prevent a hang + lk.unlock(); const auto boot_reason = get_boot_reason(this->r_system->call_get_boot_reason()); this->charge_point->set_message_queue_resume_delay(std::chrono::seconds(this->config.MessageQueueResumeDelay)); diff --git a/modules/OCPP201/OCPP201.hpp b/modules/OCPP201/OCPP201.hpp index 1aec647d9..b802987c5 100644 --- a/modules/OCPP201/OCPP201.hpp +++ b/modules/OCPP201/OCPP201.hpp @@ -17,23 +17,39 @@ #include // headers for required interface implementations +#include #include #include -#include #include #include // ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 // insert your custom include headers here +#include + #include -struct Evse { - uint16_t evse_id; - ocpp::v201::OperationalStatusEnum operational_state; - std::map connectors; - ocpp::v201::OperationalStatusEnum get_connector_state(uint16_t connector_id) { - return connectors.at(connector_id); - } +enum class TxStartPoint { + ParkingBayOccupancy, + EVConnected, + Authorized, + PowerPathClosed, + EnergyTransfer, + DataSigned +}; + +struct TransactionStart { + int32_t evse_id; + int32_t connector_id; + std::string session_id; + ocpp::DateTime timestamp; + ocpp::v201::TriggerReasonEnum trigger_reason; + ocpp::v201::MeterValue meter_start; + ocpp::v201::IdToken id_token; + std::optional group_id_token; + std::optional reservation_id; + std::optional remote_start_id; + ocpp::v201::ChargingStateEnum charging_state; }; // ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 @@ -57,8 +73,9 @@ class OCPP201 : public Everest::ModuleBase { std::unique_ptr p_auth_provider, std::unique_ptr p_data_transfer, std::vector> r_evse_manager, std::unique_ptr r_system, - std::unique_ptr r_security, std::unique_ptr r_kvs, - std::vector> r_data_transfer, Conf& config) : + std::unique_ptr r_security, + std::vector> r_data_transfer, std::unique_ptr r_auth, + Conf& config) : ModuleBase(info), mqtt(mqtt_provider), p_main(std::move(p_main)), @@ -68,8 +85,8 @@ class OCPP201 : public Everest::ModuleBase { r_evse_manager(std::move(r_evse_manager)), r_system(std::move(r_system)), r_security(std::move(r_security)), - r_kvs(std::move(r_kvs)), r_data_transfer(std::move(r_data_transfer)), + r_auth(std::move(r_auth)), config(config){}; Everest::MqttProvider& mqtt; @@ -80,8 +97,8 @@ class OCPP201 : public Everest::ModuleBase { const std::vector> r_evse_manager; const std::unique_ptr r_system; const std::unique_ptr r_security; - const std::unique_ptr r_kvs; const std::vector> r_data_transfer; + const std::unique_ptr r_auth; const Conf& config; // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 @@ -101,26 +118,22 @@ class OCPP201 : public Everest::ModuleBase { // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 // insert your private definitions here - types::evse_manager::StartSessionReason session_started_reason; // keep track of this to be able to report correct - // trigger reason in TransactionStarted event - std::filesystem::path ocpp_share_path; + // track the session started reasons for every EVSE+Connector combination to be able to report correct trigger + // reason in TransactionStarted event + std::map, types::evse_manager::StartSessionReason> session_started_reasons; + std::map, std::optional> transaction_starts; - // holds operational states of EVSE - ocpp::v201::OperationalStatusEnum cs_operational_status; - std::map evses; + TxStartPoint tx_start_point; + + std::filesystem::path ocpp_share_path; // key represents evse_id, value indicates if ready std::map evse_ready_map; std::mutex evse_ready_mutex; std::condition_variable evse_ready_cv; void init_evse_ready_map(); - void init_evses(); bool all_evse_ready(); - - void set_connector_operational_status(const ocpp::v201::OperationalStatusEnum operational_status, - const int32_t evse_id, const int32_t connector_id, const bool persist); - void set_evse_operational_status(const ocpp::v201::OperationalStatusEnum operational_status, const int32_t evse_id, - const bool persist); + std::map get_connector_structure(); // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 }; diff --git a/modules/OCPP201/manifest.yaml b/modules/OCPP201/manifest.yaml index 29cca370e..904a70b4a 100644 --- a/modules/OCPP201/manifest.yaml +++ b/modules/OCPP201/manifest.yaml @@ -54,14 +54,14 @@ requires: interface: evse_security min_connections: 1 max_connections: 1 - kvs: - interface: kvs - min_connections: 1 - max_connections: 1 data_transfer: interface: ocpp_data_transfer min_connections: 0 max_connections: 1 + auth: + interface: auth + min_connections: 1 + max_connections: 1 enable_external_mqtt: true metadata: license: https://opensource.org/licenses/Apache-2.0 diff --git a/modules/Setup/CMakeLists.txt b/modules/Setup/CMakeLists.txt index c37f428f2..e7c48acb0 100644 --- a/modules/Setup/CMakeLists.txt +++ b/modules/Setup/CMakeLists.txt @@ -9,7 +9,11 @@ ev_setup_cpp_module() # ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 # insert your custom targets and additional config variables here -# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 +target_sources(${MODULE_NAME} + PRIVATE + "RunApplication.cpp" + "WiFiSetup.cpp" +)# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 target_sources(${MODULE_NAME} PRIVATE diff --git a/modules/Setup/README.md b/modules/Setup/README.md index 068d82473..f88b7eaba 100644 --- a/modules/Setup/README.md +++ b/modules/Setup/README.md @@ -138,9 +138,29 @@ To add a wifi network a payload with the following format must be published to t { "interface": "wlan0", "ssid": "Example", - "psk": "a_valid_pre_shared_key" + "psk": "20fcb529dee0aad11b0568f553942850d06e4c4531c0d75b35345d580b300f78" } ``` +The PSK field can represent the passphrase instead using escaped quotes: +```json +{ + "interface": "wlan0", + "ssid": "Example", + "psk": "\"A_valid_passphrase\"" +} +``` +For open WiFi networks the psk must be an empty string `"psk": ""`. + +For hidden networks an optional item is needed: +```json +{ + "interface": "wlan0", + "ssid": "Example", + "psk": "\"A_valid_passphrase\"", + "hidden": true +} +``` +When `hidden` is not supplied then it is assumed to be false. ### everest_api/setup/cmd/enable_network To enable a wifi network a payload with the following format must be published to this topic: diff --git a/modules/Setup/RunApplication.cpp b/modules/Setup/RunApplication.cpp new file mode 100644 index 000000000..4f7366ff3 --- /dev/null +++ b/modules/Setup/RunApplication.cpp @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include "RunApplication.hpp" + +#include +#include + +#include + +namespace module { + +CmdOutput run_application(const std::string& name, std::vector args) { + // search_path requires basename and not a full path + boost::filesystem::path path = name; + + if (path.is_relative()) { + path = boost::process::search_path(name); + } + + if (path.empty()) { + EVLOG_debug << fmt::format("The application '{}' could not be found", name); + return CmdOutput{"", {}, 1}; + } + + boost::process::ipstream stream; + boost::process::child cmd(path, boost::process::args(args), boost::process::std_out > stream); + std::string output; + std::vector split_output; + std::string temp; + while (std::getline(stream, temp)) { + output += temp + "\n"; + split_output.push_back(temp); + } + cmd.wait(); + return CmdOutput{output, split_output, cmd.exit_code()}; +} + +} // namespace module diff --git a/modules/Setup/RunApplication.hpp b/modules/Setup/RunApplication.hpp new file mode 100644 index 000000000..dbd37bf82 --- /dev/null +++ b/modules/Setup/RunApplication.hpp @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef RUNAPPLICATION_HPP +#define RUNAPPLICATION_HPP + +#include +#include + +namespace module { + +struct CmdOutput { + std::string output; + std::vector split_output; + int exit_code; +}; + +CmdOutput run_application(const std::string& name, std::vector args); + +} // namespace module + +#endif // RUNAPPLICATION_HPP diff --git a/modules/Setup/Setup.cpp b/modules/Setup/Setup.cpp index a6f4c8f63..022843626 100644 --- a/modules/Setup/Setup.cpp +++ b/modules/Setup/Setup.cpp @@ -1,18 +1,20 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2022 - 2022 Pionix GmbH and Contributors to EVerest #include "Setup.hpp" +#include "RunApplication.hpp" #include #include #include #include -#include - #include namespace module { +// set WifiConfigureClass to the class to use for configuring WiFi +typedef WpaCliSetup WifiConfigureClass; + void to_json(json& j, const NetworkDeviceInfo& k) { j = json::object({{"interface", k.interface}, {"wireless", k.wireless}, @@ -24,32 +26,20 @@ void to_json(json& j, const NetworkDeviceInfo& k) { {"link_type", k.link_type}}); } -void to_json(json& j, const WifiInfo& k) { - auto flags_array = json::array(); - flags_array = k.flags; - j = json::object({{"bssid", k.bssid}, - {"ssid", k.ssid}, - {"frequency", k.frequency}, - {"signal_level", k.signal_level}, - {"flags", flags_array}}); -} - void to_json(json& j, const WifiCredentials& k) { - j = json::object({{"interface", k.interface}, {"ssid", k.ssid}, {"psk", k.psk}}); + j = json::object({{"interface", k.interface}, {"ssid", k.ssid}, {"psk", k.psk}, {"hidden", k.hidden}}); } void from_json(const json& j, WifiCredentials& k) { k.interface = j.at("interface"); k.ssid = j.at("ssid"); k.psk = j.at("psk"); -} - -void to_json(json& j, const WifiList& k) { - j = json::object({{"interface", k.interface}, - {"network_id", k.network_id}, - {"ssid", k.ssid}, - {"connected", k.connected}, - {"signal_level", k.signal_level}}); + k.hidden = false; + // optional item + auto it = j.find("hidden"); + if ((it != j.end() && *it)) { + k.hidden = true; + } } void to_json(json& j, const InterfaceAndNetworkId& k) { @@ -73,6 +63,27 @@ void to_json(json& j, const ApplicationInfo& k) { {"release_metadata_file", k.release_metadata_file}}); } +//------------------------------------------------------------------------------ +// JSON conversion for WifiConfigureClass types +static void to_json(json& j, const WifiConfigureClass::WifiNetworkStatus& k) { + j = json::object({{"interface", k.interface}, + {"network_id", k.network_id}, + {"ssid", k.ssid}, + {"connected", k.connected}, + {"signal_level", k.signal_level}}); +} + +static void to_json(json& j, const WifiConfigureClass::WifiScan& k) { + auto flags_array = json::array(); + flags_array = k.flags; + j = json::object({{"bssid", k.bssid}, + {"ssid", k.ssid}, + {"frequency", k.frequency}, + {"signal_level", k.signal_level}, + {"flags", flags_array}}); +} + +//------------------------------------------------------------------------------ void Setup::init() { invoke_init(*p_main); @@ -142,37 +153,46 @@ void Setup::ready() { std::string add_network_cmd = this->cmd_base + "add_network"; this->mqtt.subscribe(add_network_cmd, [this](const std::string& data) { WifiCredentials wifi_credentials = json::parse(data); - this->add_and_enable_network(wifi_credentials); - this->save_config(wifi_credentials.interface); + WifiConfigureClass wifi; + this->add_and_enable_network(wifi_credentials.interface, wifi_credentials.ssid, wifi_credentials.psk, + wifi_credentials.hidden); + wifi.save_config(wifi_credentials.interface); this->publish_configured_networks(); }); std::string enable_network_cmd = this->cmd_base + "enable_network"; this->mqtt.subscribe(enable_network_cmd, [this](const std::string& data) { - InterfaceAndNetworkId wifi = json::parse(data); - this->enable_network(wifi.interface, wifi.network_id); + InterfaceAndNetworkId wifi_details = json::parse(data); + WifiConfigureClass wifi; + wifi.enable_network(wifi_details.interface, wifi_details.network_id); + wifi.save_config(wifi_details.interface); this->publish_configured_networks(); }); std::string disable_network_cmd = this->cmd_base + "disable_network"; this->mqtt.subscribe(disable_network_cmd, [this](const std::string& data) { - InterfaceAndNetworkId wifi = json::parse(data); - this->disable_network(wifi.interface, wifi.network_id); + InterfaceAndNetworkId wifi_details = json::parse(data); + WifiConfigureClass wifi; + wifi.disable_network(wifi_details.interface, wifi_details.network_id); + wifi.save_config(wifi_details.interface); this->publish_configured_networks(); }); std::string select_network_cmd = this->cmd_base + "select_network"; this->mqtt.subscribe(select_network_cmd, [this](const std::string& data) { - InterfaceAndNetworkId wifi = json::parse(data); - this->select_network(wifi.interface, wifi.network_id); + InterfaceAndNetworkId wifi_details = json::parse(data); + WifiConfigureClass wifi; + wifi.select_network(wifi_details.interface, wifi_details.network_id); + wifi.save_config(wifi_details.interface); this->publish_configured_networks(); }); std::string remove_network_cmd = this->cmd_base + "remove_network"; this->mqtt.subscribe(remove_network_cmd, [this](const std::string& data) { - InterfaceAndNetworkId wifi = json::parse(data); - this->remove_network(wifi.interface, wifi.network_id); - this->save_config(wifi.interface); + InterfaceAndNetworkId wifi_details = json::parse(data); + WifiConfigureClass wifi; + wifi.remove_network(wifi_details.interface, wifi_details.network_id); + wifi.save_config(wifi_details.interface); this->publish_configured_networks(); }); @@ -244,7 +264,7 @@ void Setup::publish_hostname() { void Setup::publish_ap_state() { std::string ap_state_var = this->var_base + "ap_state"; - auto hostapd_enabled_output = this->run_application("systemctl", {"is-active", "--quiet", "hostapd"}); + auto hostapd_enabled_output = run_application("systemctl", {"is-active", "--quiet", "hostapd"}); if (hostapd_enabled_output.exit_code == 0) { this->ap_state = "enabled"; } else { @@ -398,7 +418,7 @@ std::vector Setup::get_network_devices() { std::string virtual_type_file = this->read_type_file(virtual_type_path); if (virtual_type_file == type_file) { // assume it's a vpn, but check ip link - auto ip_output = this->run_application("ip", {"--json", "-details", "link", "show", interface}); + auto ip_output = run_application("ip", {"--json", "-details", "link", "show", interface}); if (ip_output.exit_code != 0) { continue; } @@ -421,7 +441,7 @@ std::vector Setup::get_network_devices() { } void Setup::populate_rfkill_status(std::vector& device_info) { - auto rfkill_output = this->run_application("rfkill", {"--json"}); + auto rfkill_output = run_application("rfkill", {"--json"}); if (rfkill_output.exit_code != 0) { return; } @@ -459,7 +479,7 @@ bool Setup::rfkill_unblock(std::string rfkill_id) { return false; } - auto rfkill_output = this->run_application("rfkill", {"unblock", rfkill_id}); + auto rfkill_output = run_application("rfkill", {"unblock", rfkill_id}); if (rfkill_output.exit_code != 0) { return false; } @@ -485,7 +505,7 @@ bool Setup::rfkill_block(std::string rfkill_id) { return false; } - auto rfkill_output = this->run_application("rfkill", {"block", rfkill_id}); + auto rfkill_output = run_application("rfkill", {"block", rfkill_id}); if (rfkill_output.exit_code != 0) { return false; } @@ -493,102 +513,14 @@ bool Setup::rfkill_block(std::string rfkill_id) { return true; } -bool Setup::is_wifi_interface(std::string interface) { - // TODO: maybe cache these results for some time? - auto network_devices = this->get_network_devices(); - - for (auto device : network_devices) { - if (device.interface == interface && device.wireless) { - return true; - } - } - - return false; -} - -std::vector Setup::list_configured_networks(std::string interface) { - if (!this->is_wifi_interface(interface)) { - return {}; - } - - // use wpa_cli to query wifi info - auto wpa_cli_list_networks_output = this->run_application("wpa_cli", {"-i", interface, "list_networks"}); - if (wpa_cli_list_networks_output.exit_code != 0) { - return {}; - } - - std::vector configured_networks; - std::map wpa_cli_status_map; - - auto wpa_cli_status_output = this->run_application("wpa_cli", {"-i", interface, "status"}); - if (wpa_cli_status_output.exit_code == 0) { - for (auto wpa_cli_status_it = wpa_cli_status_output.split_output.begin(); - wpa_cli_status_it != wpa_cli_status_output.split_output.end(); ++wpa_cli_status_it) { - std::vector wpa_cli_status_columns; - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) - boost::split(wpa_cli_status_columns, *wpa_cli_status_it, boost::is_any_of("=")); - if (wpa_cli_status_columns.size() == 2) { - wpa_cli_status_map[wpa_cli_status_columns.at(0)] = wpa_cli_status_columns.at(1); - } - } - } - std::map wpa_cli_signal_poll_map; - auto wpa_cli_signal_poll_output = this->run_application("wpa_cli", {"-i", interface, "signal_poll"}); - if (wpa_cli_signal_poll_output.exit_code == 0) { - - for (auto wpa_cli_signal_poll_it = wpa_cli_signal_poll_output.split_output.begin(); - wpa_cli_signal_poll_it != wpa_cli_signal_poll_output.split_output.end(); ++wpa_cli_signal_poll_it) { - std::vector wpa_cli_signal_poll_columns; - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) - boost::split(wpa_cli_signal_poll_columns, *wpa_cli_signal_poll_it, boost::is_any_of("=")); - if (wpa_cli_signal_poll_columns.size() == 2) { - wpa_cli_signal_poll_map[wpa_cli_signal_poll_columns.at(0)] = wpa_cli_signal_poll_columns.at(1); - } - } - } - - auto list_networks_results = wpa_cli_list_networks_output.split_output; - if (list_networks_results.size() >= 2) { - // skip header - for (auto list_networks_results_it = std::next(list_networks_results.begin()); - list_networks_results_it != list_networks_results.end(); ++list_networks_results_it) { - std::vector list_networks_results_columns; - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) - boost::split(list_networks_results_columns, *list_networks_results_it, boost::is_any_of("\t")); - - WifiList wifi_list; - wifi_list.interface = interface; - wifi_list.network_id = std::stoi(list_networks_results_columns.at(0)); - wifi_list.ssid = list_networks_results_columns.at(1); - wifi_list.connected = false; - wifi_list.signal_level = -100; // -100 dBm is the minimum for wifi - - if (wpa_cli_status_map.count("id") && wpa_cli_status_map.count("ssid") && - wpa_cli_status_map.count("wpa_state")) { - if (wpa_cli_status_map.at("id") == list_networks_results_columns.at(0) && - wpa_cli_status_map.at("ssid") == wifi_list.ssid && - wpa_cli_status_map.at("wpa_state") == "COMPLETED") { - wifi_list.connected = true; - if (wpa_cli_signal_poll_map.count("RSSI")) { - wifi_list.signal_level = std::stoi(wpa_cli_signal_poll_map.at("RSSI")); - } - } - } - - configured_networks.push_back(wifi_list); - } - } - - return configured_networks; -} - void Setup::publish_configured_networks() { auto network_devices = this->get_network_devices(); for (auto device : network_devices) { if (!device.wireless) { continue; } - auto network_list = this->list_configured_networks(device.interface); + WifiConfigureClass wifi; + auto network_list = wifi.list_networks_status(device.interface); std::string network_list_var = this->var_base + "configured_networks"; json configured_networks_json = json::array(); configured_networks_json = network_list; @@ -596,193 +528,56 @@ void Setup::publish_configured_networks() { } } -int Setup::add_network(std::string interface) { - if (!this->is_wifi_interface(interface)) { - return -1; - } - - auto wpa_cli_output = this->run_application("wpa_cli", {"-i", interface, "add_network"}); - if (wpa_cli_output.exit_code != 0) { - return -1; - } +bool Setup::add_and_enable_network(const std::string& interface, const std::string& ssid, const std::string& psk, + bool hidden) { + WifiConfigureClass wifi; - if (wpa_cli_output.split_output.size() != 1) { - return -1; - } - return std::stoi(wpa_cli_output.split_output.at(0)); -} - -bool Setup::add_and_enable_network(WifiCredentials wifi_credentials) { - return this->add_and_enable_network(wifi_credentials.interface, wifi_credentials.ssid, wifi_credentials.psk); -} - -bool Setup::add_and_enable_network(std::string interface, std::string ssid, std::string psk) { - if (interface.empty()) { + std::string net_if = interface; + if (net_if.empty()) { EVLOG_warning << "Attempting to add a network without an interface, attempting to use the first one"; auto network_devices = this->get_network_devices(); for (auto device : network_devices) { if (device.wireless) { - interface = device.interface; + net_if = device.interface; break; } } } - if (!this->is_wifi_interface(interface)) { - return false; - } - - auto network_id = this->add_network(interface); - if (network_id == -1) { - return false; - } - - if (!this->set_network(interface, network_id, ssid, psk)) { - return false; - } - - return this->enable_network(interface, network_id); -} - -bool Setup::set_network(std::string interface, int network_id, std::string ssid, std::string psk) { - if (!this->is_wifi_interface(interface)) { - return false; - } - - auto network_id_string = std::to_string(network_id); - - auto ssid_parameter = "\"" + ssid + "\""; - - auto wpa_cli_set_ssid_output = - this->run_application("wpa_cli", {"-i", interface, "set_network", network_id_string, "ssid", ssid_parameter}); - if (wpa_cli_set_ssid_output.exit_code != 0) { - return false; - } - - auto wpa_cli_set_psk_output = - this->run_application("wpa_cli", {"-i", interface, "set_network", network_id_string, "psk", psk}); - - if (wpa_cli_set_psk_output.exit_code != 0) { - return false; - } - - return true; -} - -bool Setup::enable_network(std::string interface, int network_id) { - if (!this->is_wifi_interface(interface)) { - return false; - } - - auto network_id_string = std::to_string(network_id); - - auto wpa_cli_enable_network_output = - this->run_application("wpa_cli", {"-i", interface, "enable_network", network_id_string}); - if (wpa_cli_enable_network_output.exit_code != 0) { - return false; - } - - return true; -} - -bool Setup::disable_network(std::string interface, int network_id) { - if (!this->is_wifi_interface(interface)) { - return false; - } - - auto network_id_string = std::to_string(network_id); - - auto wpa_cli_disable_network_output = - this->run_application("wpa_cli", {"-i", interface, "disable_network", network_id_string}); - if (wpa_cli_disable_network_output.exit_code != 0) { - return false; - } - - return true; -} - -bool Setup::select_network(std::string interface, int network_id) { - if (!this->is_wifi_interface(interface)) { - return false; - } - - auto network_id_string = std::to_string(network_id); - - auto wpa_cli_disable_network_output = - this->run_application("wpa_cli", {"-i", interface, "select_network", network_id_string}); - if (wpa_cli_disable_network_output.exit_code != 0) { - return false; - } - - return true; -} - -bool Setup::remove_network(std::string interface, int network_id) { - if (!this->is_wifi_interface(interface)) { - return false; - } - - auto network_id_string = std::to_string(network_id); - - auto wpa_cli_remove_network_output = - this->run_application("wpa_cli", {"-i", interface, "remove_network", network_id_string}); - if (wpa_cli_remove_network_output.exit_code != 0) { - return false; - } - - return true; -} - -bool Setup::remove_networks(std::string interface) { - if (!this->is_wifi_interface(interface)) { - return false; - } - - auto networks = this->list_configured_networks(interface); - - bool success = true; - for (auto network : networks) { - auto network_id_string = std::to_string(network.network_id); - - auto wpa_cli_remove_network_output = - this->run_application("wpa_cli", {"-i", interface, "remove_network", network_id_string}); - if (wpa_cli_remove_network_output.exit_code != 0) { - success = false; - } - } - - return success; + auto network_id = wifi.add_network(net_if); + bool bResult = network_id != -1; + bResult = bResult && wifi.set_network(net_if, network_id, ssid, psk, hidden); + bResult = bResult && wifi.enable_network(net_if, network_id); + return bResult; } bool Setup::remove_all_networks() { auto network_devices = this->get_network_devices(); - bool success = true; + std::uint32_t remove_fail = 0; + for (auto device : network_devices) { if (!device.wireless) { continue; } - if (!this->remove_networks(device.interface)) { - success = false; - } - this->save_config(device.interface); - } + WifiConfigureClass wifi; + auto networks = wifi.list_networks(device.interface); - return success; -} + for (auto network : networks) { + if (!wifi.remove_network(device.interface, network.network_id)) { + remove_fail++; + } + } -bool Setup::save_config(std::string interface) { - bool success = true; - auto wpa_cli_save_config_output = this->run_application("wpa_cli", {"-i", interface, "save_config"}); - if (wpa_cli_save_config_output.exit_code != 0) { - success = false; + wifi.save_config(device.interface); } - return success; + + return remove_fail == 0; } bool Setup::reboot() { bool success = true; - auto reboot_output = this->run_application("systemctl", {"reboot"}); + auto reboot_output = run_application("systemctl", {"reboot"}); if (reboot_output.exit_code != 0) { success = false; } @@ -791,7 +586,7 @@ bool Setup::reboot() { bool Setup::is_online() { bool success = true; - auto reboot_output = this->run_application("ping", {"-c", "1", this->config.online_check_host}); + auto reboot_output = run_application("ping", {"-c", "1", this->config.online_check_host}); if (reboot_output.exit_code != 0) { success = false; } @@ -810,20 +605,20 @@ void Setup::check_online_status() { void Setup::enable_ap() { bool success = true; - auto wpa_cli_output = this->run_application("wpa_cli", {"-i", this->config.ap_interface, "disconnect"}); + auto wpa_cli_output = run_application("wpa_cli", {"-i", this->config.ap_interface, "disconnect"}); if (wpa_cli_output.exit_code != 0) { EVLOG_error << "Could not disconnect from wireless LAN"; } - auto start_hostapd_output = this->run_application("systemctl", {"start", "hostapd"}); + auto start_hostapd_output = run_application("systemctl", {"start", "hostapd"}); if (start_hostapd_output.exit_code != 0) { EVLOG_error << "Could not start hostapd"; } - auto start_dnsmasq_output = this->run_application("systemctl", {"start", "dnsmasq"}); + auto start_dnsmasq_output = run_application("systemctl", {"start", "dnsmasq"}); if (start_dnsmasq_output.exit_code != 0) { EVLOG_error << "Could not start dnsmasq"; } auto add_static_ip_output = - this->run_application("ip", {"addr", "add", this->config.ap_ipv4, "dev", this->config.ap_interface}); + run_application("ip", {"addr", "add", this->config.ap_ipv4, "dev", this->config.ap_interface}); if (add_static_ip_output.exit_code != 0) { EVLOG_error << "Could not add static ip to interface " << this->config.ap_interface; } @@ -831,21 +626,21 @@ void Setup::enable_ap() { void Setup::disable_ap() { auto del_static_ip_output = - this->run_application("ip", {"addr", "del", this->config.ap_ipv4, "dev", this->config.ap_interface}); + run_application("ip", {"addr", "del", this->config.ap_ipv4, "dev", this->config.ap_interface}); if (del_static_ip_output.exit_code != 0) { EVLOG_error << "Could not del static ip " << this->config.ap_ipv4 << " from interface " << this->config.ap_interface; } - auto stop_dnsmasq_output = this->run_application("systemctl", {"stop", "dnsmasq"}); + auto stop_dnsmasq_output = run_application("systemctl", {"stop", "dnsmasq"}); if (stop_dnsmasq_output.exit_code != 0) { EVLOG_error << "Could not stop dnsmasq"; } - auto stop_hostapd_output = this->run_application("systemctl", {"stop", "hostapd"}); + auto stop_hostapd_output = run_application("systemctl", {"stop", "hostapd"}); if (stop_hostapd_output.exit_code != 0) { EVLOG_error << "Could not stop hostapd"; } - auto wpa_cli_output = this->run_application("wpa_cli", {"-i", this->config.ap_interface, "reconnect"}); + auto wpa_cli_output = run_application("wpa_cli", {"-i", this->config.ap_interface, "reconnect"}); if (wpa_cli_output.exit_code != 0) { EVLOG_error << "Could not reconnect to wireless LAN"; } @@ -862,7 +657,7 @@ static void add_addr_infos_to_device(const json& addr_infos, NetworkDeviceInfo& } void Setup::populate_ip_addresses(std::vector& device_info) { - auto ip_output = this->run_application("ip", {"--json", "address", "show"}); + auto ip_output = run_application("ip", {"--json", "address", "show"}); if (ip_output.exit_code != 0) { return; } @@ -881,90 +676,28 @@ void Setup::populate_ip_addresses(std::vector& device_info) { } } -std::vector Setup::scan_wifi(const std::vector& device_info) { - std::vector wifi_info; +WifiConfigureClass::WifiScanList Setup::scan_wifi(const std::vector& device_info) { + WifiConfigureClass::WifiScanList wifi_info; + WifiConfigureClass wifi; for (auto device : device_info) { if (!device.wireless) { continue; } - // use wpa_cli to query wifi info - auto wpa_cli_scan_output = this->run_application("wpa_cli", {"-i", device.interface, "scan"}); - if (wpa_cli_scan_output.exit_code != 0) { - continue; - } - // FIXME: is there a proper signal to check if the scan is ready? Maybe in the socket based interface - std::this_thread::sleep_for(std::chrono::seconds(3)); - auto wpa_cli_scan_results_output = this->run_application("wpa_cli", {"-i", device.interface, "scan_results"}); - if (wpa_cli_scan_results_output.exit_code != 0) { - continue; - } - - auto scan_results = wpa_cli_scan_results_output.split_output; - if (scan_results.size() >= 2) { - // skip header - for (auto scan_results_it = std::next(scan_results.begin()); scan_results_it != scan_results.end(); - ++scan_results_it) { - std::vector scan_results_columns; - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) - boost::split(scan_results_columns, *scan_results_it, boost::is_any_of("\t")); - - WifiInfo info; - info.bssid = scan_results_columns.at(0); - info.ssid = scan_results_columns.at(4); - info.frequency = std::stoi(scan_results_columns.at(1)); - info.signal_level = std::stoi(scan_results_columns.at(2)); - info.flags = this->parse_wpa_cli_flags(scan_results_columns.at(3)); - - wifi_info.push_back(info); - } - } + auto dev_list = wifi.scan_wifi(device.interface); + wifi_info.insert(wifi_info.end(), dev_list.begin(), dev_list.end()); } return wifi_info; } std::string Setup::get_hostname() { - auto hostname_output = this->run_application("hostname", {}); + auto hostname_output = run_application("hostname", {}); if (hostname_output.exit_code == 0 && hostname_output.split_output.size() > 0) { return hostname_output.split_output.at(0); } return ""; } -CmdOutput Setup::run_application(const std::string& name, std::vector args) { - const auto path = boost::process::search_path(name); - - if (path.empty()) { - EVLOG_debug << fmt::format("The application '{}' could not be found", name); - return CmdOutput{"", {}, 1}; - } - - boost::process::ipstream stream; - boost::process::child cmd(path, boost::process::args(args), boost::process::std_out > stream); - std::string output; - std::vector split_output; - std::string temp; - while (std::getline(stream, temp)) { - output += temp + "\n"; - split_output.push_back(temp); - } - cmd.wait(); - return CmdOutput{output, split_output, cmd.exit_code()}; -} - -std::vector Setup::parse_wpa_cli_flags(std::string flags) { - const std::regex wpa_cli_flags_regex("\\[(.*?)\\]"); - - std::vector parsed_flags; - - for (auto regex_it = std::sregex_iterator(flags.begin(), flags.end(), wpa_cli_flags_regex); - regex_it != std::sregex_iterator(); ++regex_it) { - parsed_flags.push_back((*regex_it).str(1)); - } - - return parsed_flags; -} - } // namespace module diff --git a/modules/Setup/Setup.hpp b/modules/Setup/Setup.hpp index 80a140919..2b31cf1f9 100644 --- a/modules/Setup/Setup.hpp +++ b/modules/Setup/Setup.hpp @@ -18,29 +18,16 @@ // ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 // insert your custom include headers here +#include "WiFiSetup.hpp" #include namespace module { -struct WifiInfo { - std::string bssid; - std::string ssid; - int frequency; - int signal_level; - std::vector flags; - - operator std::string() { - json wifi_info = *this; - - return wifi_info.dump(); - } -}; -void to_json(json& j, const WifiInfo& k); - struct WifiCredentials { std::string interface; std::string ssid; std::string psk; + bool hidden; operator std::string() { @@ -52,21 +39,6 @@ struct WifiCredentials { void to_json(json& j, const WifiCredentials& k); void from_json(const json& j, WifiCredentials& k); -struct WifiList { - std::string interface; - int network_id; - std::string ssid; - bool connected; - int signal_level; - - operator std::string() { - json wifi_list = *this; - - return wifi_list.dump(); - } -}; -void to_json(json& j, const WifiList& k); - struct InterfaceAndNetworkId { std::string interface; int network_id; @@ -125,12 +97,6 @@ struct ApplicationInfo { } }; void to_json(json& j, const ApplicationInfo& k); - -struct CmdOutput { - std::string output; - std::vector split_output; - int exit_code; -}; } // namespace module // ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 @@ -204,31 +170,18 @@ class Setup : public Everest::ModuleBase { bool rfkill_unblock(std::string rfkill_id); bool rfkill_block(std::string rfkill_id); - bool is_wifi_interface(std::string interface); - std::vector list_configured_networks(std::string interface); void publish_configured_networks(); - int add_network(std::string interface); - bool add_and_enable_network(WifiCredentials wifi_credentials); - bool add_and_enable_network(std::string interface, std::string ssid, std::string psk); - bool set_network(std::string interface, int network_id, std::string ssid, std::string psk); - bool enable_network(std::string interface, int network_id); - bool disable_network(std::string interface, int network_id); - bool select_network(std::string interface, int network_id); - bool remove_network(std::string interface, int network_id); - bool remove_networks(std::string interface); + bool add_and_enable_network(const std::string& interface, const std::string& ssid, const std::string& psk, + bool hidden = false); bool remove_all_networks(); - bool save_config(std::string interface); bool reboot(); bool is_online(); void check_online_status(); void enable_ap(); void disable_ap(); void populate_ip_addresses(std::vector& device_info); - std::vector scan_wifi(const std::vector& device_info); + WpaCliSetup::WifiScanList scan_wifi(const std::vector& device_info); std::string get_hostname(); - - CmdOutput run_application(const std::string& name, std::vector args); - std::vector parse_wpa_cli_flags(std::string flags); // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 }; diff --git a/modules/Setup/WiFiSetup.cpp b/modules/Setup/WiFiSetup.cpp new file mode 100644 index 000000000..7a1212ffc --- /dev/null +++ b/modules/Setup/WiFiSetup.cpp @@ -0,0 +1,320 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include "WiFiSetup.hpp" +#include "RunApplication.hpp" + +#include +#include +#include +#include +#include + +/** + * @file + * @brief wpa_cli command failure detection + * + * `wpa_cli` sets an exit code of 0 unless the command is malformed. + * Failures are presented via text to stdout. + * Hence checking for failure to remove a network would mean checking + * the output for OK or FAIL. + * + * This is common across all calls to `wpa_cli`. + */ + +namespace module { + +constexpr const char* wpa_cli = "/usr/sbin/wpa_cli"; +constexpr const int not_connected_rssi = -100; // -100 dBm is the minimum for wifi + +bool WpaCliSetup::do_scan(const std::string& interface) { + if (!is_wifi_interface(interface)) { + return false; + } + + auto output = run_application(wpa_cli, {"-i", interface, "scan"}); + return output.exit_code == 0; +} + +WpaCliSetup::WifiScanList WpaCliSetup::do_scan_results(const std::string& interface) { + WifiScanList result = {}; + auto output = run_application(wpa_cli, {"-i", interface, "scan_results"}); + if (output.exit_code == 0) { + auto scan_results = output.split_output; + if (scan_results.size() >= 2) { + // skip header + for (auto scan_results_it = std::next(scan_results.begin()); scan_results_it != scan_results.end(); + ++scan_results_it) { + + std::vector columns; + std::istringstream ss(*scan_results_it); + std::string value; + while (std::getline(ss, value, '\t')) { + columns.push_back(std::move(value)); + } + + if (columns.size() >= 5) { + WifiScan info; + info.bssid = columns[0]; + info.ssid = columns[4]; + info.frequency = std::stoi(columns[1]); + info.signal_level = std::stoi(columns[2]); + info.flags = std::move(parse_flags(columns[3])); + result.push_back(std::move(info)); + } + } + } + } + return result; +} + +WpaCliSetup::Status WpaCliSetup::do_status(const std::string& interface) { + Status result = {}; + if (is_wifi_interface(interface)) { + auto output = run_application(wpa_cli, {"-i", interface, "status"}); + if (output.exit_code == 0) { + auto scan_results = output.split_output; + for (auto scan_results_it = scan_results.begin(); scan_results_it != scan_results.end(); + ++scan_results_it) { + + std::vector columns; + std::istringstream ss(*scan_results_it); + std::string value; + while (std::getline(ss, value, '=')) { + columns.push_back(std::move(value)); + } + + if (columns.size() == 2) { + result[columns[0]] = columns[1]; + } + } + } + } + return result; +} + +WpaCliSetup::Poll WpaCliSetup::do_signal_poll(const std::string& interface) { + Poll result = {}; + if (is_wifi_interface(interface)) { + auto output = run_application(wpa_cli, {"-i", interface, "signal_poll"}); + if (output.exit_code == 0) { + auto scan_results = output.split_output; + for (auto scan_results_it = scan_results.begin(); scan_results_it != scan_results.end(); + ++scan_results_it) { + + std::vector columns; + std::istringstream ss(*scan_results_it); + std::string value; + while (std::getline(ss, value, '=')) { + columns.push_back(std::move(value)); + } + + if (columns.size() == 2) { + result[columns[0]] = columns[1]; + } + } + } + } + return result; +} + +WpaCliSetup::flags_t WpaCliSetup::parse_flags(const std::string& flags) { + const std::regex flags_regex("\\[(.*?)\\]"); + + flags_t parsed_flags; + + for (auto it = std::sregex_iterator(flags.begin(), flags.end(), flags_regex); it != std::sregex_iterator(); ++it) { + parsed_flags.push_back((*it).str(1)); + } + + return parsed_flags; +} + +int WpaCliSetup::add_network(const std::string& interface) { + if (!is_wifi_interface(interface)) { + return -1; + } + + auto output = run_application(wpa_cli, {"-i", interface, "add_network"}); + + if ((output.exit_code != 0) || (output.split_output.size() != 1)) { + return -1; + } + + return std::stoi(output.split_output.at(0)); +} + +bool WpaCliSetup::set_network(const std::string& interface, int network_id, const std::string& ssid, + const std::string& psk, bool hidden) { + /* + * configuring a network needs: + * - ssid "" + * - psk "" or ABCDEF0123456789... (for WPA2) + * - key_mgmt NONE (for open networks) + * - scan_ssid 1 (for hidden networks) + */ + + if (!is_wifi_interface(interface)) { + return false; + } + + auto network_id_string = std::to_string(network_id); + auto ssid_parameter = "\"" + ssid + "\""; + + auto output = run_application(wpa_cli, {"-i", interface, "set_network", network_id_string, "ssid", ssid_parameter}); + + if (output.exit_code == 0) { + if (psk.empty()) { + output = run_application(wpa_cli, {"-i", interface, "set_network", network_id_string, "key_mgmt", "NONE"}); + } else { + output = run_application(wpa_cli, {"-i", interface, "set_network", network_id_string, "psk", psk}); + } + } + + if (hidden && (output.exit_code == 0)) { + output = run_application(wpa_cli, {"-i", interface, "set_network", network_id_string, "scan_ssid", "1"}); + } + + return output.exit_code == 0; +} + +bool WpaCliSetup::enable_network(const std::string& interface, int network_id) { + if (!is_wifi_interface(interface)) { + return false; + } + + auto network_id_string = std::to_string(network_id); + auto output = run_application(wpa_cli, {"-i", interface, "enable_network", network_id_string}); + return output.exit_code == 0; +} + +bool WpaCliSetup::disable_network(const std::string& interface, int network_id) { + if (!is_wifi_interface(interface)) { + return false; + } + + auto network_id_string = std::to_string(network_id); + auto output = run_application(wpa_cli, {"-i", interface, "disable_network", network_id_string}); + return output.exit_code == 0; +} + +bool WpaCliSetup::select_network(const std::string& interface, int network_id) { + if (!is_wifi_interface(interface)) { + return false; + } + + auto network_id_string = std::to_string(network_id); + auto output = run_application(wpa_cli, {"-i", interface, "select_network", network_id_string}); + return output.exit_code == 0; +} + +bool WpaCliSetup::remove_network(const std::string& interface, int network_id) { + if (!is_wifi_interface(interface)) { + return false; + } + + auto network_id_string = std::to_string(network_id); + auto output = run_application(wpa_cli, {"-i", interface, "remove_network", network_id_string}); + return output.exit_code == 0; +} + +bool WpaCliSetup::save_config(const std::string& interface) { + if (!is_wifi_interface(interface)) { + return false; + } + + auto output = run_application(wpa_cli, {"-i", interface, "save_config"}); + return output.exit_code == 0; +} + +WpaCliSetup::WifiScanList WpaCliSetup::scan_wifi(const std::string& interface) { + WifiScanList result = {}; + + if (do_scan(interface)) { + // FIXME: is there a proper signal to check if the scan is ready? Maybe in the socket based interface + std::this_thread::sleep_for(std::chrono::seconds(3)); + result = std::move(do_scan_results(interface)); + } + + return result; +} + +WpaCliSetup::WifiNetworkList WpaCliSetup::list_networks(const std::string& interface) { + WifiNetworkList result = {}; + if (is_wifi_interface(interface)) { + auto output = run_application(wpa_cli, {"-i", interface, "list_networks"}); + if (output.exit_code == 0) { + auto scan_results = output.split_output; + if (scan_results.size() >= 2) { + // skip header + for (auto scan_results_it = std::next(scan_results.begin()); scan_results_it != scan_results.end(); + ++scan_results_it) { + + std::vector columns; + std::istringstream ss(*scan_results_it); + std::string value; + while (std::getline(ss, value, '\t')) { + columns.push_back(std::move(value)); + } + + if (columns.size() >= 2) { + WifiNetwork info; + info.network_id = std::stoi(columns[0]); + info.ssid = columns[1]; + result.push_back(std::move(info)); + } + } + } + } + } + return result; +} + +WpaCliSetup::WifiNetworkStatusList WpaCliSetup::list_networks_status(const std::string& interface) { + WifiNetworkStatusList result = {}; + if (is_wifi_interface(interface)) { + auto network_list = list_networks(interface); + auto status_map = do_status(interface); + int connected_rssi = not_connected_rssi; + + // signal_poll raises errors when not connected + if (status_map["wpa_state"] == "COMPLETED") { + auto signal_map = do_signal_poll(interface); + if (auto it = signal_map.find("RSSI"); it != signal_map.end()) { + connected_rssi = std::stoi(it->second); + } + } + + for (auto& i : network_list) { + WifiNetworkStatus net; + net.interface = interface; + net.network_id = i.network_id; + net.ssid = i.ssid; + net.connected = false; + net.signal_level = not_connected_rssi; + + auto id_it = status_map.find("id"); + auto ssid_it = status_map.find("ssid"); + + if ((id_it != status_map.end()) && (ssid_it != status_map.end()) && + (std::stoi(id_it->second) == i.network_id) && (ssid_it->second == i.ssid)) { + net.connected = true; + net.signal_level = connected_rssi; + } + result.push_back(net); + } + } + return result; +} + +bool WpaCliSetup::is_wifi_interface(const std::string& interface) { + // check if /sys/class/net//wireless exists + + auto path = std::filesystem::path("/sys/class/net"); + path /= interface; + path /= "wireless"; + + return std::filesystem::exists(path); +} + +} // namespace module diff --git a/modules/Setup/WiFiSetup.hpp b/modules/Setup/WiFiSetup.hpp new file mode 100644 index 000000000..cf7c16180 --- /dev/null +++ b/modules/Setup/WiFiSetup.hpp @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef WIFISETUP_HPP +#define WIFISETUP_HPP + +#include +#include +#include + +namespace module { + +class WpaCliSetup { +public: + typedef std::vector flags_t; + + struct WifiScan { + std::string bssid; + std::string ssid; + int frequency; + int signal_level; + flags_t flags; + }; + typedef std::vector WifiScanList; + + struct WifiNetworkStatus { + std::string interface; + int network_id; + std::string ssid; + bool connected; + int signal_level; + }; + typedef std::vector WifiNetworkStatusList; + + struct WifiNetwork { + int network_id; + std::string ssid; + }; + typedef std::vector WifiNetworkList; + + typedef std::map Status; + typedef std::map Poll; + +protected: + virtual bool do_scan(const std::string& interface); + virtual WifiScanList do_scan_results(const std::string& interface); + virtual Status do_status(const std::string& interface); + virtual Poll do_signal_poll(const std::string& interface); + virtual flags_t parse_flags(const std::string& flags); + +public: + virtual ~WpaCliSetup() { + } + virtual int add_network(const std::string& interface); + virtual bool set_network(const std::string& interface, int network_id, const std::string& ssid, + const std::string& psk, bool hidden = false); + virtual bool enable_network(const std::string& interface, int network_id); + virtual bool disable_network(const std::string& interface, int network_id); + virtual bool select_network(const std::string& interface, int network_id); + virtual bool remove_network(const std::string& interface, int network_id); + virtual bool save_config(const std::string& interface); + virtual WifiScanList scan_wifi(const std::string& interface); + virtual WifiNetworkList list_networks(const std::string& interface); + virtual WifiNetworkStatusList list_networks_status(const std::string& interface); + virtual bool is_wifi_interface(const std::string& interface); +}; + +} // namespace module + +#endif // WIFISETUP_HPP diff --git a/modules/Setup/test/RunApplicationStub.cpp b/modules/Setup/test/RunApplicationStub.cpp new file mode 100644 index 000000000..7ba5164d1 --- /dev/null +++ b/modules/Setup/test/RunApplicationStub.cpp @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include +#include + +#include + +namespace stub { + +RunApplication* RunApplication::active_p = nullptr; + +RunApplication::RunApplication() : + results({ + {"add_network", {{}, {{"0"}}, 0}}, + {"set_network", {{}, {{"OK"}}, 0}}, + {"enable_network", {{}, {{"OK"}}, 0}}, + {"disable_network", {{}, {{"OK"}}, 0}}, + {"select_network", {{}, {{"OK"}}, 0}}, + {"remove_network", {{}, {{"OK"}}, 0}}, + {"save_config", {{}, {{"OK"}}, 0}}, + // scan_wifi uses scan and scan_results + {"scan", {{}, {{"OK"}}, 0}}, + {"scan_results", + {{}, + { + {"bssid / frequency / signal level / flags / ssid"}, + }, + 0}}, + {"list_networks", + {{}, + { + {"network id / ssid / bssid / flags"}, + }, + 0}}, + // list_networks_status uses list_networks status signal_poll + {"status", + {{}, + { + {"wpa_state=INACTIVE"}, + {"p2p_device_address=c2:ee:40:b0:57:b8"}, + {"address=c0:ee:40:b0:57:b8"}, + {"uuid=7dd9abf8-53f0-532b-a763-2f43537e4234"}, + }, + 0}}, + {"signal_poll", {{}, {{"FAIL"}}, 0}}, + }), + signal_poll_called(false), + psk_called(false), + key_mgmt_called(false), + scan_ssid_called(false) { + active_p = this; +} + +RunApplication::~RunApplication() { + active_p = nullptr; +} + +module::CmdOutput RunApplication::run_application(const std::string& name, std::vector args) { + module::CmdOutput result = {{}, {}, -1}; + EXPECT_EQ(name, "/usr/sbin/wpa_cli"); + EXPECT_EQ(args[0], "-i"); + if (args[2] == "signal_poll") { + signal_poll_called = true; + } else if (args[2] == "set_network") { + if (args[4] == "psk") { + psk_called = true; + } else if (args[4] == "key_mgmt") { + key_mgmt_called = true; + } else if (args[4] == "scan_ssid") { + scan_ssid_called = true; + } + } + auto it = results.find(args[2]); + if (it != results.end()) { + result = it->second; + if (!result.split_output.empty() && result.output.empty()) { + for (auto& line : result.output) { + result.output += line + "\n"; + } + } + } + return result; +} + +} // namespace stub + +namespace module { +CmdOutput run_application(const std::string& name, std::vector args) { + CmdOutput result = {{}, {}, -1}; + if (stub::RunApplication::active_p != nullptr) { + result = std::move(stub::RunApplication::active_p->run_application(name, args)); + } + return result; +} +} // namespace module diff --git a/modules/Setup/test/RunApplicationStub.hpp b/modules/Setup/test/RunApplicationStub.hpp new file mode 100644 index 000000000..bb00a981f --- /dev/null +++ b/modules/Setup/test/RunApplicationStub.hpp @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef RUNAPPLICATIONSTUB_HPP +#define RUNAPPLICATIONSTUB_HPP + +#include + +#include +#include + +namespace stub { + +class RunApplication { +public: + static RunApplication* active_p; + + std::map results; + bool signal_poll_called; + bool psk_called; + bool key_mgmt_called; + bool scan_ssid_called; + + RunApplication(); + virtual ~RunApplication(); + + virtual module::CmdOutput run_application(const std::string& name, std::vector args); +}; + +} // namespace stub + +#endif // RUNAPPLICATIONSTUB_HPP diff --git a/modules/Setup/test/WiFiSetupTest.cpp b/modules/Setup/test/WiFiSetupTest.cpp new file mode 100644 index 000000000..f299b196d --- /dev/null +++ b/modules/Setup/test/WiFiSetupTest.cpp @@ -0,0 +1,426 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#include +#include +#include + +namespace { + +class WpaCliSetupTest : public module::WpaCliSetup { +public: + // override to support testing + virtual bool is_wifi_interface(const std::string& interface) override { + if (interface == "ap0") { + return false; + } else if (interface == "eth0") { + return false; + } + return true; + }; +}; + +//----------------------------------------------------------------------------- +// add_network() +TEST(add_network, wired) { + stub::RunApplication ra; + WpaCliSetupTest obj; + ASSERT_EQ(obj.add_network("eth0"), -1); +} + +TEST(add_network, wireless) { + stub::RunApplication ra; + WpaCliSetupTest obj; + ASSERT_NE(obj.add_network("wlan0"), -1); +} + +TEST(add_network, access_point) { + stub::RunApplication ra; + WpaCliSetupTest obj; + ASSERT_EQ(obj.add_network("ap0"), -1); +} + +//----------------------------------------------------------------------------- +// set_network() +TEST(set_network, wpa2) { + stub::RunApplication ra; + WpaCliSetupTest obj; + ASSERT_TRUE(obj.set_network("wlan0", 0, "PlusnetWireless", "LetMeIn2")); + ASSERT_TRUE(ra.psk_called); + ASSERT_FALSE(ra.key_mgmt_called); + ASSERT_FALSE(ra.scan_ssid_called); +} + +TEST(set_network, open) { + stub::RunApplication ra; + WpaCliSetupTest obj; + ASSERT_TRUE(obj.set_network("wlan0", 0, "OpenNet", "")); + ASSERT_FALSE(ra.psk_called); + ASSERT_TRUE(ra.key_mgmt_called); + ASSERT_FALSE(ra.scan_ssid_called); +} + +TEST(set_network, hidden_wpa2) { + stub::RunApplication ra; + WpaCliSetupTest obj; + ASSERT_TRUE(obj.set_network("wlan0", 0, "Hidden", "LetMeIn3", true)); + ASSERT_TRUE(ra.psk_called); + ASSERT_FALSE(ra.key_mgmt_called); + ASSERT_TRUE(ra.scan_ssid_called); +} + +TEST(set_network, hidden_open) { + stub::RunApplication ra; + WpaCliSetupTest obj; + ASSERT_TRUE(obj.set_network("wlan0", 0, "Hidden", "", true)); + ASSERT_FALSE(ra.psk_called); + ASSERT_TRUE(ra.key_mgmt_called); + ASSERT_TRUE(ra.scan_ssid_called); +} + +//----------------------------------------------------------------------------- +// enable_network() +TEST(enable_network, exists) { + stub::RunApplication ra; + WpaCliSetupTest obj; + ASSERT_TRUE(obj.enable_network("wlan0", 0)); +} + +TEST(enable_network, doesnt_exist) { + stub::RunApplication ra; + ra.results["enable_network"] = {{}, {{"FAIL"}}, 0}; + WpaCliSetupTest obj; + // still returns an exit code of 0 + ASSERT_TRUE(obj.enable_network("wlan0", 1)); +} + +//----------------------------------------------------------------------------- +// disable_network() +TEST(disable_network, exists) { + stub::RunApplication ra; + WpaCliSetupTest obj; + ASSERT_TRUE(obj.disable_network("wlan0", 0)); +} + +TEST(disable_network, doesnt_exist) { + stub::RunApplication ra; + ra.results["disable_network"] = {{}, {{"FAIL"}}, 0}; + WpaCliSetupTest obj; + // still returns an exit code of 0 + ASSERT_TRUE(obj.disable_network("wlan0", 1)); +} + +//----------------------------------------------------------------------------- +// select_network() +TEST(select_network, exists) { + stub::RunApplication ra; + WpaCliSetupTest obj; + ASSERT_TRUE(obj.select_network("wlan0", 0)); +} + +TEST(select_network, doesnt_exist) { + stub::RunApplication ra; + ra.results["select_network"] = {{}, {{"FAIL"}}, 0}; + WpaCliSetupTest obj; + // still returns an exit code of 0 + ASSERT_TRUE(obj.select_network("wlan0", 1)); +} + +//----------------------------------------------------------------------------- +// remove_network() +TEST(remove_network, exists) { + stub::RunApplication ra; + WpaCliSetupTest obj; + ASSERT_TRUE(obj.remove_network("wlan0", 0)); +} + +TEST(remove_network, doesnt_exist) { + stub::RunApplication ra; + ra.results["remove_network"] = {{}, {{"FAIL"}}, 0}; + WpaCliSetupTest obj; + // still returns an exit code of 0 + ASSERT_TRUE(obj.remove_network("wlan0", 1)); +} + +TEST(remove_network, fail) { + stub::RunApplication ra; + ra.results["remove_network"] = {{}, {{"Invalid REMOVE_NETWORK command - at least 1 argument is required."}}, 255}; + WpaCliSetupTest obj; + ASSERT_FALSE(obj.remove_network("wlan0", -99)); +} + +//----------------------------------------------------------------------------- +// save_config() +TEST(save_config, success) { + stub::RunApplication ra; + WpaCliSetupTest obj; + ASSERT_TRUE(obj.save_config("wlan0")); +} + +TEST(save_config, fail) { + stub::RunApplication ra; + WpaCliSetupTest obj; + ASSERT_FALSE(obj.save_config("ap0")); +} + +//----------------------------------------------------------------------------- +// scan_wifi() +TEST(scan_wifi, none) { + stub::RunApplication ra; + WpaCliSetupTest obj; + auto res = obj.scan_wifi("wlan0"); + ASSERT_TRUE(res.empty()); +} + +TEST(scan_wifi, some) { + stub::RunApplication ra; + ra.results["scan_results"] = { + {}, + { + {"bssid / frequency / signal level / flags / ssid"}, + {"14:49:bc:06:81:19\t2412\t-72\t[WPA2-PSK-CCMP][ESS]\tPlusnetWireless"}, + {"6a:82:8c:38:b2:a1\t2412\t-93\t[WPA2-PSK-CCMP][ESS]\t\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00"}, + }, + 0}; + + WpaCliSetupTest obj; + auto res = obj.scan_wifi("wlan0"); + ASSERT_FALSE(res.empty()); + ASSERT_EQ(res.size(), 2); + + module::WpaCliSetup::flags_t expected = {"WPA2-PSK-CCMP", "ESS"}; + + EXPECT_EQ(res[0].bssid, "14:49:bc:06:81:19"); + EXPECT_EQ(res[0].ssid, "PlusnetWireless"); + EXPECT_EQ(res[0].frequency, 2412); + EXPECT_EQ(res[0].signal_level, -72); + EXPECT_EQ(res[0].flags, expected); + + EXPECT_EQ(res[1].bssid, "6a:82:8c:38:b2:a1"); + EXPECT_EQ(res[1].ssid, "\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00"); + EXPECT_EQ(res[1].frequency, 2412); + EXPECT_EQ(res[1].signal_level, -93); + EXPECT_EQ(res[1].flags, expected); +} + +TEST(scan_wifi, more) { + stub::RunApplication ra; + ra.results["scan_results"] = { + {}, + { + {"bssid / frequency / signal level / flags / ssid"}, + {"14:49:bc:06:81:19\t2412\t-71\t[WPA2-PSK-CCMP][ESS]\tPlusnetWireless"}, + {"14:49:bc:06:81:1b\t2412\t-71\t[WPA2-PSK-CCMP][ESS]\t\\x00\\x00\\x00\\x00\\x00\\x00\\x00"}, + {"00:1e:42:33:62:07\t2462\t-89\t[WPA2-PSK-CCMP+TKIP][ESS][UTF-8]\tRUT950_6207"}, + {"b4:ba:9d:16:e2:ba\t2437\t-92\t[WPA2-PSK-CCMP][WPS][ESS]\tSKYLZMEY"}, + {"14:49:bc:06:81:1c\t2412\t-72\t[ESS]\tTesting123"}, + {"36:49:5b:f8:e1:07\t2412\t-92\t[ESS]\tEE WiFi"}, + {"14:49:bc:06:81:18\t2412\t-73\t[WPA2-PSK-CCMP][ESS]\t\\x00\\x00\\x00\\x00\\x00\\x00\\x00"}, + {"6a:82:8c:38:b2:a1\t2412\t-88\t[WPA2-PSK-CCMP][ESS]\t\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00"}, + {"18:82:8c:38:b2:a5\t2412\t-92\t[WPA2-PSK-CCMP][WPS][ESS]\tBT-3GAG3M"}, + {"6a:82:8c:38:b2:a6\t2412\t-92\t[ESS]\tEE WiFi"}, + }, + 0}; + + WpaCliSetupTest obj; + auto res = obj.scan_wifi("wlan0"); + ASSERT_FALSE(res.empty()); + ASSERT_EQ(res.size(), 10); + + module::WpaCliSetup::flags_t expected1 = {"WPA2-PSK-CCMP", "ESS"}; + module::WpaCliSetup::flags_t expected2 = {"WPA2-PSK-CCMP+TKIP", "ESS", "UTF-8"}; + module::WpaCliSetup::flags_t expected3 = {"WPA2-PSK-CCMP", "WPS", "ESS"}; + module::WpaCliSetup::flags_t expected4 = {"ESS"}; + + EXPECT_EQ(res[0].bssid, "14:49:bc:06:81:19"); + EXPECT_EQ(res[0].ssid, "PlusnetWireless"); + EXPECT_EQ(res[0].frequency, 2412); + EXPECT_EQ(res[0].signal_level, -71); + EXPECT_EQ(res[0].flags, expected1); + + EXPECT_EQ(res[1].bssid, "14:49:bc:06:81:1b"); + EXPECT_EQ(res[1].ssid, "\\x00\\x00\\x00\\x00\\x00\\x00\\x00"); + EXPECT_EQ(res[1].frequency, 2412); + EXPECT_EQ(res[1].signal_level, -71); + EXPECT_EQ(res[1].flags, expected1); + + EXPECT_EQ(res[2].bssid, "00:1e:42:33:62:07"); + EXPECT_EQ(res[2].ssid, "RUT950_6207"); + EXPECT_EQ(res[2].frequency, 2462); + EXPECT_EQ(res[2].signal_level, -89); + EXPECT_EQ(res[2].flags, expected2); + + EXPECT_EQ(res[3].bssid, "b4:ba:9d:16:e2:ba"); + EXPECT_EQ(res[3].ssid, "SKYLZMEY"); + EXPECT_EQ(res[3].frequency, 2437); + EXPECT_EQ(res[3].signal_level, -92); + EXPECT_EQ(res[3].flags, expected3); + + EXPECT_EQ(res[4].bssid, "14:49:bc:06:81:1c"); + EXPECT_EQ(res[4].ssid, "Testing123"); + EXPECT_EQ(res[4].frequency, 2412); + EXPECT_EQ(res[4].signal_level, -72); + EXPECT_EQ(res[4].flags, expected4); + + EXPECT_EQ(res[5].bssid, "36:49:5b:f8:e1:07"); + EXPECT_EQ(res[5].ssid, "EE WiFi"); + EXPECT_EQ(res[5].frequency, 2412); + EXPECT_EQ(res[5].signal_level, -92); + EXPECT_EQ(res[5].flags, expected4); + + EXPECT_EQ(res[6].bssid, "14:49:bc:06:81:18"); + EXPECT_EQ(res[6].ssid, "\\x00\\x00\\x00\\x00\\x00\\x00\\x00"); + EXPECT_EQ(res[6].frequency, 2412); + EXPECT_EQ(res[6].signal_level, -73); + EXPECT_EQ(res[6].flags, expected1); + + EXPECT_EQ(res[7].bssid, "6a:82:8c:38:b2:a1"); + EXPECT_EQ(res[7].ssid, "\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00"); + EXPECT_EQ(res[7].frequency, 2412); + EXPECT_EQ(res[7].signal_level, -88); + EXPECT_EQ(res[7].flags, expected1); + + EXPECT_EQ(res[8].bssid, "18:82:8c:38:b2:a5"); + EXPECT_EQ(res[8].ssid, "BT-3GAG3M"); + EXPECT_EQ(res[8].frequency, 2412); + EXPECT_EQ(res[8].signal_level, -92); + EXPECT_EQ(res[8].flags, expected3); + + EXPECT_EQ(res[9].bssid, "6a:82:8c:38:b2:a6"); + EXPECT_EQ(res[9].ssid, "EE WiFi"); + EXPECT_EQ(res[9].frequency, 2412); + EXPECT_EQ(res[9].signal_level, -92); + EXPECT_EQ(res[9].flags, expected4); +} + +//----------------------------------------------------------------------------- +// list_networks() +TEST(list_networks, none) { + stub::RunApplication ra; + WpaCliSetupTest obj; + auto res = obj.list_networks("wlan0"); + ASSERT_TRUE(res.empty()); +} + +TEST(list_networks, one) { + stub::RunApplication ra; + ra.results["list_networks"] = {{}, + { + {"network id / ssid / bssid / flags"}, + {"0\t\tany\t[DISABLED]"}, + }, + 0}; + + WpaCliSetupTest obj; + auto res = obj.list_networks("wlan0"); + ASSERT_FALSE(res.empty()); + ASSERT_EQ(res.size(), 1); + + module::WpaCliSetup::flags_t expected = {"WPA2-PSK-CCMP", "ESS"}; + + EXPECT_EQ(res[0].network_id, 0); + EXPECT_EQ(res[0].ssid, ""); +} + +TEST(list_networks, two) { + stub::RunApplication ra; + ra.results["list_networks"] = {{}, + { + {"network id / ssid / bssid / flags"}, + {"0\t\tany\t[DISABLED]"}, + {"1\tPlusnetWireless\tany\t[CURRENT]"}, + }, + 0}; + + WpaCliSetupTest obj; + auto res = obj.list_networks("wlan0"); + ASSERT_FALSE(res.empty()); + ASSERT_EQ(res.size(), 2); + + module::WpaCliSetup::flags_t expected = {"WPA2-PSK-CCMP", "ESS"}; + + EXPECT_EQ(res[0].network_id, 0); + EXPECT_EQ(res[0].ssid, ""); + + EXPECT_EQ(res[1].network_id, 1); + EXPECT_EQ(res[1].ssid, "PlusnetWireless"); +} + +//----------------------------------------------------------------------------- +// list_networks_status() +TEST(list_networks, not_connected) { + stub::RunApplication ra; + WpaCliSetupTest obj; + auto res = obj.list_networks_status("wlan0"); + ASSERT_TRUE(res.empty()); + ASSERT_FALSE(ra.signal_poll_called); +} + +TEST(list_networks, connected) { + stub::RunApplication ra; + ra.results["list_networks"] = {{}, + { + {"network id / ssid / bssid / flags"}, + {"0\t\tany\t[DISABLED]"}, + {"1\tPlusnetWireless\tany\t[CURRENT]"}, + {"2\tHiddenNet\tany\t[DISABLED]"}, + }, + 0}; + ra.results["status"] = {{}, + { + {"bssid=14:49:bc:06:81:19"}, + {"freq=2412"}, + {"ssid=PlusnetWireless"}, + {"id=1"}, + {"mode=station"}, + {"wifi_generation=4"}, + {"pairwise_cipher=CCMP"}, + {"group_cipher=CCMP"}, + {"key_mgmt=WPA2-PSK"}, + {"wpa_state=COMPLETED"}, + {"ip_address=172.25.1.11"}, + {"p2p_device_address=c2:ee:40:b0:57:b8"}, + {"address=c0:ee:40:b0:57:b8"}, + {"uuid=7dd9abf8-53f0-532b-a763-2f43537e4234"}, + }, + 0}; + ra.results["signal_poll"] = {{}, + { + {"RSSI=-73"}, + {"LINKSPEED=54"}, + {"NOISE=9999"}, + {"FREQUENCY=2412"}, + {"WIDTH=20 MHz"}, + {"CENTER_FRQ1=2412"}, + }, + 0}; + WpaCliSetupTest obj; + auto res = obj.list_networks_status("wlan0"); + ASSERT_FALSE(res.empty()); + ASSERT_TRUE(ra.signal_poll_called); + ASSERT_EQ(res.size(), 3); + + EXPECT_EQ(res[0].interface, "wlan0"); + EXPECT_EQ(res[0].network_id, 0); + EXPECT_EQ(res[0].ssid, ""); + EXPECT_FALSE(res[0].connected); + EXPECT_EQ(res[0].signal_level, -100); + + EXPECT_EQ(res[1].interface, "wlan0"); + EXPECT_EQ(res[1].network_id, 1); + EXPECT_EQ(res[1].ssid, "PlusnetWireless"); + EXPECT_TRUE(res[1].connected); + EXPECT_EQ(res[1].signal_level, -73); + + EXPECT_EQ(res[2].interface, "wlan0"); + EXPECT_EQ(res[2].network_id, 2); + EXPECT_EQ(res[2].ssid, "HiddenNet"); + EXPECT_FALSE(res[2].connected); + EXPECT_EQ(res[2].signal_level, -100); +} + +//----------------------------------------------------------------------------- +// is_wifi_interface() +// not tested as it is checking for files in /proc so depends on the +// machine the tests are running on + +} // namespace diff --git a/modules/simulation/CMakeLists.txt b/modules/simulation/CMakeLists.txt index 6d8798987..2c71cd31e 100644 --- a/modules/simulation/CMakeLists.txt +++ b/modules/simulation/CMakeLists.txt @@ -1,5 +1,6 @@ +ev_add_module(DCSupplySimulator) +ev_add_module(IMDSimulator) ev_add_module(JsCarSimulator) ev_add_module(JsDCSupplySimulator) -ev_add_module(JsIMDSimulator) ev_add_module(JsSlacSimulator) ev_add_module(JsYetiSimulator) diff --git a/modules/simulation/DCSupplySimulator/CMakeLists.txt b/modules/simulation/DCSupplySimulator/CMakeLists.txt new file mode 100644 index 000000000..bac307954 --- /dev/null +++ b/modules/simulation/DCSupplySimulator/CMakeLists.txt @@ -0,0 +1,23 @@ +# +# 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 +# needed for std::scoped_lock +target_compile_features(${MODULE_NAME} PUBLIC cxx_std_17) +# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 + +target_sources(${MODULE_NAME} + PRIVATE + "main/power_supply_DCImpl.cpp" +) + +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 +# insert other things like install cmds etc here +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 diff --git a/modules/simulation/DCSupplySimulator/DCSupplySimulator.cpp b/modules/simulation/DCSupplySimulator/DCSupplySimulator.cpp new file mode 100644 index 000000000..33a76effb --- /dev/null +++ b/modules/simulation/DCSupplySimulator/DCSupplySimulator.cpp @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2023 chargebyte GmbH +// Copyright (C) 2023 Contributors to EVerest + +#include "DCSupplySimulator.hpp" + +namespace module { + +void DCSupplySimulator::init() { + invoke_init(*p_main); +} + +void DCSupplySimulator::ready() { + invoke_ready(*p_main); +} + +} // namespace module diff --git a/modules/simulation/DCSupplySimulator/DCSupplySimulator.hpp b/modules/simulation/DCSupplySimulator/DCSupplySimulator.hpp new file mode 100644 index 000000000..daf5a827d --- /dev/null +++ b/modules/simulation/DCSupplySimulator/DCSupplySimulator.hpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright chargebyte GmbH and Contributors to EVerest +#ifndef DCSUPPLY_SIMULATOR_HPP +#define DCSUPPLY_SIMULATOR_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 2 +// + +#include "ld-ev.hpp" + +// headers for provided interface implementations +#include + +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 +// insert your custom include headers here +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 + +namespace module { + +struct Conf {}; + +class DCSupplySimulator : public Everest::ModuleBase { +public: + DCSupplySimulator() = delete; + DCSupplySimulator(const ModuleInfo& info, std::unique_ptr p_main, Conf& config) : + ModuleBase(info), p_main(std::move(p_main)), config(config){}; + + const std::unique_ptr p_main; + const Conf& config; + + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + // insert your public definitions here + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + +protected: + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + // insert your protected definitions here + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + +private: + friend class LdEverest; + void init(); + void ready(); + + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 + // insert your private definitions here + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 +}; + +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 +// insert other definitions here +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 + +} // namespace module + +#endif // DCSUPPLY_SIMULATOR_HPP diff --git a/modules/simulation/DCSupplySimulator/main/power_supply_DCImpl.cpp b/modules/simulation/DCSupplySimulator/main/power_supply_DCImpl.cpp new file mode 100644 index 000000000..d98831881 --- /dev/null +++ b/modules/simulation/DCSupplySimulator/main/power_supply_DCImpl.cpp @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2023 chargebyte GmbH +// Copyright (C) 2023 Contributors to EVerest + +#include +#include + +#include "power_supply_DCImpl.hpp" + +namespace module { +namespace main { + +void power_supply_DCImpl::init() { + this->connector_voltage = 0.0; + this->connector_current = 0.0; + + this->power_supply_thread_handle = std::thread(&power_supply_DCImpl::power_supply_worker, this); +} + +void power_supply_DCImpl::ready() { +} + +types::power_supply_DC::Capabilities power_supply_DCImpl::handle_getCapabilities() { + types::power_supply_DC::Capabilities Capabilities = { + .bidirectional = this->config.bidirectional, + .current_regulation_tolerance_A = 2.0, + .peak_current_ripple_A = 2.0, + .max_export_voltage_V = static_cast(this->config.max_voltage), + .min_export_voltage_V = static_cast(this->config.min_voltage), + .max_export_current_A = static_cast(this->config.max_current), + .min_export_current_A = static_cast(this->config.min_current), + .max_export_power_W = static_cast(this->config.max_power), + .max_import_voltage_V = static_cast(this->config.max_voltage), + .min_import_voltage_V = static_cast(this->config.min_voltage), + .max_import_current_A = static_cast(this->config.max_current), + .min_import_current_A = static_cast(this->config.min_current), + .max_import_power_W = static_cast(this->config.max_power), + .conversion_efficiency_import = 0.85, + .conversion_efficiency_export = 0.9, + }; + return Capabilities; +} + +void power_supply_DCImpl::handle_setMode(types::power_supply_DC::Mode& value) { + this->mode = value; + + std::scoped_lock access_lock(this->power_supply_values_mutex); + if ((value == types::power_supply_DC::Mode::Off) || (value == types::power_supply_DC::Mode::Fault)) { + this->connector_voltage = 0.0; + this->connector_current = 0.0; + } else if (value == types::power_supply_DC::Mode::Export) { + this->connector_voltage = this->settings_connector_export_voltage; + this->connector_current = this->settings_connector_max_export_current; + } else if (value == types::power_supply_DC::Mode::Import) { + this->connector_voltage = this->settings_connector_import_voltage; + this->connector_current = this->settings_connector_max_import_current; + } + + mod->p_main->publish_mode(value); +} + +void power_supply_DCImpl::clampVoltageCurrent(double& voltage, double& current) { + voltage = voltage < this->config.min_voltage ? this->config.min_voltage + : voltage > this->config.max_voltage ? this->config.max_voltage + : voltage; + + current = current < this->config.min_current ? this->config.min_current + : current > this->config.max_current ? this->config.max_current + : current; +} + +void power_supply_DCImpl::handle_setExportVoltageCurrent(double& voltage, double& current) { + double temp_voltage = voltage; + double temp_current = current; + + clampVoltageCurrent(temp_voltage, temp_current); + + std::scoped_lock access_lock(this->power_supply_values_mutex); + this->settings_connector_export_voltage = temp_voltage; + this->settings_connector_max_export_current = temp_current; + + if (this->mode == types::power_supply_DC::Mode::Export) { + this->connector_voltage = this->settings_connector_export_voltage; + this->connector_current = this->settings_connector_max_export_current; + } +} + +void power_supply_DCImpl::handle_setImportVoltageCurrent(double& voltage, double& current) { + double temp_voltage = voltage; + double temp_current = current; + + clampVoltageCurrent(temp_voltage, temp_current); + + std::scoped_lock access_lock(this->power_supply_values_mutex); + this->settings_connector_import_voltage = temp_voltage; + this->settings_connector_max_import_current = temp_current; + + if (this->mode == types::power_supply_DC::Mode::Import) { + this->connector_voltage = this->settings_connector_import_voltage; + this->connector_current = -this->settings_connector_max_import_current; + } +} + +void power_supply_DCImpl::power_supply_worker(void) { + types::power_supply_DC::VoltageCurrent voltage_current; + + while (true) { + if (this->power_supply_thread_handle.shouldExit()) { + break; + } + + // set interval for publishing + std::this_thread::sleep_for(std::chrono::milliseconds(LOOP_SLEEP_MS)); + + std::scoped_lock access_lock(this->power_supply_values_mutex); + voltage_current.voltage_V = static_cast(this->connector_voltage); + voltage_current.current_A = static_cast(this->connector_current); + + this->mod->p_main->publish_voltage_current(voltage_current); + } +} +} // namespace main +} // namespace module diff --git a/modules/simulation/DCSupplySimulator/main/power_supply_DCImpl.hpp b/modules/simulation/DCSupplySimulator/main/power_supply_DCImpl.hpp new file mode 100644 index 000000000..2a3dfc30f --- /dev/null +++ b/modules/simulation/DCSupplySimulator/main/power_supply_DCImpl.hpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright chargebyte GmbH and Contributors to EVerest +#ifndef MAIN_POWER_SUPPLY_DC_IMPL_HPP +#define MAIN_POWER_SUPPLY_DC_IMPL_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 3 +// + +#include + +#include "../DCSupplySimulator.hpp" + +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 +// insert your custom include headers here +#include +#include +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 + +namespace module { +namespace main { + +struct Conf { + bool bidirectional; + double max_power; + double min_voltage; + double max_voltage; + double min_current; + double max_current; +}; + +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, "main"), mod(mod), config(config){}; + + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + // insert your public definitions here + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + +protected: + // command handler functions (virtual) + virtual types::power_supply_DC::Capabilities handle_getCapabilities() override; + virtual void handle_setMode(types::power_supply_DC::Mode& value) 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 + double settings_connector_export_voltage; + double settings_connector_import_voltage; + double settings_connector_max_export_current; + double settings_connector_max_import_current; + types::power_supply_DC::Mode mode; + double connector_voltage; + double connector_current; + std::mutex power_supply_values_mutex; + Everest::Thread power_supply_thread_handle; + void power_supply_worker(void); + + static constexpr int LOOP_SLEEP_MS{500}; + void clampVoltageCurrent(double& voltage, double& current); + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 +}; + +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 +// insert other definitions here +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 + +} // namespace main +} // namespace module + +#endif // MAIN_POWER_SUPPLY_DC_IMPL_HPP diff --git a/modules/simulation/DCSupplySimulator/manifest.yaml b/modules/simulation/DCSupplySimulator/manifest.yaml new file mode 100644 index 000000000..7a65540cf --- /dev/null +++ b/modules/simulation/DCSupplySimulator/manifest.yaml @@ -0,0 +1,36 @@ +description: Implementation of a programmable power supply for DC charging +provides: + main: + interface: power_supply_DC + description: Main interface for the power supply + config: + bidirectional: + description: Set to true to for bidirectional supply + type: boolean + default: true + max_power: + description: Max supported power in watt + type: number + default: 150000 + min_voltage: + description: Min supported voltage + type: number + default: 200.0 + max_voltage: + description: Max supported voltage + type: number + default: 900.0 + min_current: + description: Min supported current + type: number + default: 1.0 + max_current: + description: Max supported current + type: number + default: 200.0 +metadata: + license: https://opensource.org/licenses/Apache-2.0 + authors: + - Cornelius Claussen (Pionix GmbH) + - Fabian Hartung (chargebyte GmbH) + - Mohannad Oraby (chargebyte GmbH) diff --git a/modules/simulation/IMDSimulator/CMakeLists.txt b/modules/simulation/IMDSimulator/CMakeLists.txt new file mode 100644 index 000000000..7b7f3e9f7 --- /dev/null +++ b/modules/simulation/IMDSimulator/CMakeLists.txt @@ -0,0 +1,21 @@ +# +# 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 +# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 + +target_sources(${MODULE_NAME} + PRIVATE + "main/isolation_monitorImpl.cpp" +) + +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 +# insert other things like install cmds etc here +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 diff --git a/modules/simulation/IMDSimulator/IMDSimulator.cpp b/modules/simulation/IMDSimulator/IMDSimulator.cpp new file mode 100644 index 000000000..df235c2b5 --- /dev/null +++ b/modules/simulation/IMDSimulator/IMDSimulator.cpp @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#include "IMDSimulator.hpp" + +namespace module { + +void IMDSimulator::init() { + invoke_init(*p_main); +} + +void IMDSimulator::ready() { + invoke_ready(*p_main); +} + +} // namespace module diff --git a/modules/simulation/IMDSimulator/IMDSimulator.hpp b/modules/simulation/IMDSimulator/IMDSimulator.hpp new file mode 100644 index 000000000..a4f5f124c --- /dev/null +++ b/modules/simulation/IMDSimulator/IMDSimulator.hpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef IMDSIMULATOR_HPP +#define IMDSIMULATOR_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 2 +// + +#include "ld-ev.hpp" + +// headers for provided interface implementations +#include + +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 +// insert your custom include headers here +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 + +namespace module { + +struct Conf {}; + +class IMDSimulator : public Everest::ModuleBase { +public: + IMDSimulator() = delete; + IMDSimulator(const ModuleInfo& info, std::unique_ptr p_main, Conf& config) : + ModuleBase(info), p_main(std::move(p_main)), config(config){}; + + const std::unique_ptr p_main; + const Conf& config; + + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + // insert your public definitions here + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + +protected: + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + // insert your protected definitions here + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + +private: + friend class LdEverest; + void init(); + void ready(); + + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 + // insert your private definitions here + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 +}; + +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 +// insert other definitions here +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 + +} // namespace module + +#endif // IMDSIMULATOR_HPP diff --git a/modules/simulation/IMDSimulator/main/isolation_monitorImpl.cpp b/modules/simulation/IMDSimulator/main/isolation_monitorImpl.cpp new file mode 100644 index 000000000..862f94b02 --- /dev/null +++ b/modules/simulation/IMDSimulator/main/isolation_monitorImpl.cpp @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2023 chargebyte GmbH +// Copyright (C) 2023 Contributors to EVerest +#include "isolation_monitorImpl.hpp" +#include +#include +namespace module { +namespace main { + +void isolation_monitorImpl::init() { + this->isolation_monitoring_active = false; + this->isolation_measurement.resistance_F_Ohm = this->config.resistance_F_Ohm; + this->config_interval = this->config.interval; + + this->isolation_measurement_thread_handle = std::thread(&isolation_monitorImpl::isolation_measurement_worker, this); +} + +void isolation_monitorImpl::ready() { +} + +isolation_monitorImpl::~isolation_monitorImpl() { +} + +void isolation_monitorImpl::handle_start() { + if (this->isolation_monitoring_active == false) { + this->isolation_monitoring_active = true; + EVLOG_info << "Started simulated isolation monitoring with " << this->config_interval << " ms interval"; + } +}; + +void isolation_monitorImpl::isolation_measurement_worker() { + while (true) { + if (this->isolation_measurement_thread_handle.shouldExit()) { + break; + } + + if (this->isolation_monitoring_active == true) { + this->mod->p_main->publish_IsolationMeasurement(this->isolation_measurement); + EVLOG_debug << "Simulated isolation measurement finished"; + std::this_thread::sleep_for(std::chrono::milliseconds(this->config_interval - this->LOOP_SLEEP_MS)); + } + + std::this_thread::sleep_for(std::chrono::milliseconds(this->LOOP_SLEEP_MS)); + } +} + +void isolation_monitorImpl::handle_stop() { + if (this->isolation_monitoring_active == true) { + EVLOG_info << "Stopped simulated isolation monitoring"; + this->isolation_monitoring_active = false; + } +}; + +} // namespace main +} // namespace module diff --git a/modules/simulation/IMDSimulator/main/isolation_monitorImpl.hpp b/modules/simulation/IMDSimulator/main/isolation_monitorImpl.hpp new file mode 100644 index 000000000..106caa497 --- /dev/null +++ b/modules/simulation/IMDSimulator/main/isolation_monitorImpl.hpp @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2023 chargebyte GmbH +// Copyright (C) 2023 Contributors to EVerest +#ifndef MAIN_ISOLATION_MONITOR_IMPL_HPP +#define MAIN_ISOLATION_MONITOR_IMPL_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 3 +// + +#include + +#include "../IMDSimulator.hpp" + +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 +// insert your custom include headers here +#include +#include +#include +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 + +namespace module { +namespace main { + +struct Conf { + double resistance_F_Ohm; + int interval; +}; + +class isolation_monitorImpl : public isolation_monitorImplBase { +public: + isolation_monitorImpl() = delete; + isolation_monitorImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : + isolation_monitorImplBase(ev, "main"), mod(mod), config(config){}; + ~isolation_monitorImpl(); + + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + // insert your public definitions here + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + +protected: + // command handler functions (virtual) + virtual void handle_start() override; + virtual void handle_stop() 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 + types::isolation_monitor::IsolationMeasurement isolation_measurement; + std::atomic isolation_monitoring_active; + int config_interval; + static constexpr int LOOP_SLEEP_MS{20}; + + Everest::Thread isolation_measurement_thread_handle; + void isolation_measurement_worker(void); + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 +}; + +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 +// insert other definitions here +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 + +} // namespace main +} // namespace module + +#endif // MAIN_ISOLATION_MONITOR_IMPL_HPP diff --git a/modules/simulation/JsIMDSimulator/manifest.yaml b/modules/simulation/IMDSimulator/manifest.yaml similarity index 93% rename from modules/simulation/JsIMDSimulator/manifest.yaml rename to modules/simulation/IMDSimulator/manifest.yaml index 387f64674..df1bf9c28 100644 --- a/modules/simulation/JsIMDSimulator/manifest.yaml +++ b/modules/simulation/IMDSimulator/manifest.yaml @@ -12,8 +12,8 @@ provides: description: Measurement update interval in milliseconds type: integer default: 1000 -enable_external_mqtt: true metadata: license: https://opensource.org/licenses/Apache-2.0 authors: + - Fabian Hartung (chargebyte GmbH) - Cornelius Claussen (Pionix GmbH) diff --git a/modules/simulation/JsIMDSimulator/CMakeLists.txt b/modules/simulation/JsIMDSimulator/CMakeLists.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/modules/simulation/JsIMDSimulator/index.js b/modules/simulation/JsIMDSimulator/index.js deleted file mode 100644 index c81987277..000000000 --- a/modules/simulation/JsIMDSimulator/index.js +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2022 Pionix GmbH and Contributors to EVerest -const { evlog, boot_module } = require('everestjs'); -const { setInterval } = require('timers'); - -let config_resistance_F_Ohm; -let config_interval; -let intervalID; -let interval_running = false; - -boot_module(async ({ - setup, config, -}) => { - config_resistance_F_Ohm = config.impl.main.resistance_F_Ohm; - config_interval = config.impl.main.interval; - - // register commands - setup.provides.main.register.start((mod) => { - if (interval_running) return; - evlog.debug(`Started simulated isolation monitoring with ${config_interval}ms interval`); - - intervalID = setInterval(() => { - evlog.debug('Simulated isolation test finished'); - mod.provides.main.publish.IsolationMeasurement({ - resistance_F_Ohm: config_resistance_F_Ohm, - }); - }, config_interval, mod); - interval_running = true; - }); - - // register commands - setup.provides.main.register.stop(() => { - if (interval_running) { - evlog.debug('Stopped simulated isolation monitoring.'); - clearInterval(intervalID); - interval_running = false; - } - }); -}).then(() => { }); diff --git a/types/evse_manager.yaml b/types/evse_manager.yaml index 382ba58fb..9e083f739 100644 --- a/types/evse_manager.yaml +++ b/types/evse_manager.yaml @@ -128,8 +128,10 @@ types: - TransactionFinished - SessionFinished - Error + - ErrorCleared - AllErrorsCleared - PermanentFault + - PermanentFaultCleared - ReservationStart - ReservationEnd - ReplugStarted @@ -269,11 +271,23 @@ types: additionalProperties: false required: - error_code + - error_description + - error_severity properties: error_code: description: The error enum type: string $ref: /evse_manager#/ErrorEnum + error_description: + description: Description of the error (human readable) + type: string + error_severity: + description: Severity of the error + type: string + enum: + - High + - Medium + - Low vendor_error: description: The error code of the vendor type: string diff --git a/types/evse_security.yaml b/types/evse_security.yaml index e05c2dc70..1ab1c6ad6 100644 --- a/types/evse_security.yaml +++ b/types/evse_security.yaml @@ -176,12 +176,16 @@ types: required: - key - certificate + - certificate_single properties: key: description: The path of the PEM or DER encoded private key type: string certificate: - description: The path of the PEM or DER encoded certificate + description: The path of the PEM or DER encoded certificate chain + type: string + certificate_single: + description: The path of the PEM or DER encoded single certificate type: string password: description: Specifies the password for the private key if encrypted