Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

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

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

View File

@@ -0,0 +1,112 @@
set(INCLUDE_DIR "${PROJECT_SOURCE_DIR}/include")
# EVerest framework
# by default build as static
set(DEFAULT_LINKAGE SHARED)
# FIXME (aw): the following does not play well with CPM, because it get set by other dependencies ...
#if (DEFINED BUILD_SHARED_LIBS AND NOT BUILD_SHARED_LIBS)
# message(FATAL_ERROR "everest-framework library does not support static build yet")
#endif()
add_library(framework ${DEFAULT_LINKAGE})
add_library(everest::framework ALIAS framework)
set_target_properties(framework PROPERTIES SOVERSION ${PROJECT_VERSION})
target_sources(framework
PRIVATE
config.cpp
config/mqtt_settings.cpp
config/settings.cpp
config/sqlite_storage.cpp
config/storage_types.cpp
config/userconfig_storage.cpp
config/types.cpp
config_cache.cpp
config_service.cpp
conversions.cpp
error/error.cpp
error/error_database_map.cpp
error/error_filter.cpp
error/error_manager_impl.cpp
error/error_manager_req.cpp
error/error_manager_req_global.cpp
error/error_type_map.cpp
error/error_state_monitor.cpp
error/error_factory.cpp
everest.cpp
formatter.cpp
filesystem.cpp
module_adapter.cpp
module_config.cpp
mqtt_abstraction_impl.cpp
thread.cpp
types.cpp
serial.cpp
status_fifo.cpp
date.cpp
runtime.cpp
message_handler.cpp
)
# FIXME (aw): c++17 doesn't need necessarily to be public, but our
# headers are messed up
target_compile_features(framework PUBLIC cxx_std_17)
target_compile_options(framework PRIVATE ${COMPILER_WARNING_OPTIONS})
target_include_directories(framework
PUBLIC
$<BUILD_INTERFACE:${EVEREST_FRAMEWORK_GENERATED_INC_DIR}>
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/everest>
)
if(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY STREQUAL "custom" AND
EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_INCLUDE_DIR)
target_include_directories(framework
PUBLIC
$<BUILD_INTERFACE:${EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_INCLUDE_DIR}>
)
endif()
if(Boost_VERSION_STRING VERSION_LESS "1.69.0")
set(EVEREST_FRAMEWORK_BOOST_SYSTEM_LINK_LIBRARY Boost::system)
endif()
target_link_libraries(framework
PUBLIC
date::date-tz
nlohmann_json::nlohmann_json
nlohmann_json_schema_validator
fmt::fmt
everest::log
everest::sqlite
everest::yaml
$<BUILD_INTERFACE:everest_helpers>
$<INSTALL_INTERFACE:everest::helpers>
$<BUILD_INTERFACE:everest_util>
$<INSTALL_INTERFACE:everest::util>
${STD_FILESYSTEM_COMPAT_LIB}
PRIVATE
${EVEREST_FRAMEWORK_BOOST_SYSTEM_LINK_LIBRARY}
Boost::thread
Boost::program_options
everest::io
${ATOMIC_LIBS}
)
collect_migration_files(
LOCATION ${PROJECT_SOURCE_DIR}/schemas/migrations
INSTALL_DESTINATION "${CMAKE_INSTALL_DATADIR}/everest/migrations"
)
target_compile_definitions(framework
PRIVATE
TARGET_MIGRATION_FILE_VERSION=${TARGET_MIGRATION_FILE_VERSION}
)
set_target_properties(framework
PROPERTIES
POSITION_INDEPENDENT_CODE ON
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <utils/config/mqtt_settings.hpp>
namespace Everest {
bool MQTTSettings::uses_socket() const {
if (not broker_socket_path.empty()) {
return true;
}
return false;
}
MQTTSettings create_mqtt_settings(const std::string& mqtt_broker_socket_path, const std::string& mqtt_everest_prefix,
const std::string& mqtt_external_prefix) {
MQTTSettings mqtt_settings;
populate_mqtt_settings(mqtt_settings, mqtt_broker_socket_path, mqtt_everest_prefix, mqtt_external_prefix);
return mqtt_settings;
}
MQTTSettings create_mqtt_settings(const std::string& mqtt_broker_host, std::uint16_t mqtt_broker_port,
const std::string& mqtt_everest_prefix, const std::string& mqtt_external_prefix) {
MQTTSettings mqtt_settings;
populate_mqtt_settings(mqtt_settings, mqtt_broker_host, mqtt_broker_port, mqtt_everest_prefix,
mqtt_external_prefix);
return mqtt_settings;
}
void populate_mqtt_settings(MQTTSettings& mqtt_settings, const std::string& mqtt_broker_socket_path,
const std::string& mqtt_everest_prefix, const std::string& mqtt_external_prefix) {
mqtt_settings.broker_socket_path = mqtt_broker_socket_path;
mqtt_settings.everest_prefix = mqtt_everest_prefix;
mqtt_settings.external_prefix = mqtt_external_prefix;
}
void populate_mqtt_settings(MQTTSettings& mqtt_settings, const std::string& mqtt_broker_host,
std::uint16_t mqtt_broker_port, const std::string& mqtt_everest_prefix,
const std::string& mqtt_external_prefix) {
mqtt_settings.broker_host = mqtt_broker_host;
mqtt_settings.broker_port = mqtt_broker_port;
mqtt_settings.everest_prefix = mqtt_everest_prefix;
mqtt_settings.external_prefix = mqtt_external_prefix;
}
} // namespace Everest

View File

@@ -0,0 +1,64 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <utils/config/settings.hpp>
namespace Everest {
RuntimeSettings create_runtime_settings(const fs::path& prefix, const fs::path& etc_dir, const fs::path& data_dir,
const fs::path& modules_dir, const fs::path& logging_config_file,
const std::string& telemetry_prefix, bool telemetry_enabled,
bool validate_schema, bool forward_exceptions) {
RuntimeSettings runtime_settings;
runtime_settings.prefix = prefix;
runtime_settings.etc_dir = etc_dir;
runtime_settings.data_dir = data_dir;
runtime_settings.modules_dir = modules_dir;
runtime_settings.logging_config_file = logging_config_file;
runtime_settings.telemetry_prefix = telemetry_prefix;
runtime_settings.telemetry_enabled = telemetry_enabled;
runtime_settings.validate_schema = validate_schema;
runtime_settings.forward_exceptions = forward_exceptions;
return runtime_settings;
}
void populate_runtime_settings(RuntimeSettings& runtime_settings, const fs::path& prefix, const fs::path& etc_dir,
const fs::path& data_dir, const fs::path& modules_dir,
const fs::path& logging_config_file, const std::string& telemetry_prefix,
bool telemetry_enabled, bool validate_schema, bool forward_exceptions) {
runtime_settings.prefix = prefix;
runtime_settings.etc_dir = etc_dir;
runtime_settings.data_dir = data_dir;
runtime_settings.modules_dir = modules_dir;
runtime_settings.logging_config_file = logging_config_file;
runtime_settings.telemetry_prefix = telemetry_prefix;
runtime_settings.telemetry_enabled = telemetry_enabled;
runtime_settings.validate_schema = validate_schema;
runtime_settings.forward_exceptions = forward_exceptions;
}
} // namespace Everest
NLOHMANN_JSON_NAMESPACE_BEGIN
void adl_serializer<Everest::RuntimeSettings>::to_json(nlohmann::json& j, const Everest::RuntimeSettings& r) {
j = {{"prefix", r.prefix},
{"etc_dir", r.etc_dir},
{"data_dir", r.data_dir},
{"modules_dir", r.modules_dir},
{"telemetry_prefix", r.telemetry_prefix},
{"telemetry_enabled", r.telemetry_enabled},
{"validate_schema", r.validate_schema},
{"forward_exceptions", r.forward_exceptions}};
}
void adl_serializer<Everest::RuntimeSettings>::from_json(const nlohmann::json& j, Everest::RuntimeSettings& r) {
r.prefix = j.at("prefix").get<std::string>();
r.etc_dir = j.at("etc_dir").get<std::string>();
r.data_dir = j.at("data_dir").get<std::string>();
r.modules_dir = j.at("modules_dir").get<std::string>();
r.telemetry_prefix = j.at("telemetry_prefix").get<std::string>();
r.telemetry_enabled = j.at("telemetry_enabled").get<bool>();
r.validate_schema = j.at("validate_schema").get<bool>();
r.forward_exceptions = j.at("forward_exceptions").get<bool>();
}
NLOHMANN_JSON_NAMESPACE_END

View File

@@ -0,0 +1,898 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "utils/config/storage.hpp"
#include "utils/config/types.hpp"
#include <everest/compile_time_settings.hpp>
#include <everest/database/exceptions.hpp>
#include <everest/database/sqlite/schema_updater.hpp>
#include <everest/logging.hpp>
#include <utils/config/settings.hpp>
#include <utils/config/storage_sqlite.hpp>
#include <utils/date.hpp>
using json = nlohmann::json;
using namespace everest::db;
using namespace everest::db::sqlite;
namespace everest::config {
namespace {
const std::string& default_module_implementation_id() {
static const std::string DEFAULT_MODULE_IMPLEMENTATION_ID = "!module";
return DEFAULT_MODULE_IMPLEMENTATION_ID;
}
} // namespace
/// \brief Helper for accessing the column indices of the SETTING table
enum class SettingColumnIndex : int {
COL_ID = 0,
COL_PREFIX,
COL_CONFIG_FILE,
COL_CONFIGS_DIR,
COL_SCHEMAS_DIR,
COL_MODULES_DIR,
COL_INTERFACES_DIR,
COL_TYPES_DIR,
COL_ERRORS_DIR,
COL_WWW_DIR,
COL_LOGGING_CONFIG_FILE,
COL_CONTROLLER_PORT,
COL_CONTROLLER_RPC_TIMEOUT_MS,
COL_MQTT_BROKER_SOCKET_PATH,
COL_MQTT_BROKER_HOST,
COL_MQTT_BROKER_PORT,
COL_MQTT_EVEREST_PREFIX,
COL_MQTT_EXTERNAL_PREFIX,
COL_TELEMETRY_PREFIX,
COL_TELEMETRY_ENABLED,
COL_VALIDATE_SCHEMA,
COL_RUN_AS_USER,
COL_FORWARD_EXCEPTIONS,
};
/// \brief Helper for accessing the column indices of the CONFIGURATION table
enum class ConfigurationColumnIndex {
COL_MODULE_ID = 1,
COL_PARAMETER_NAME,
COL_VALUE,
COL_MUTABILITY_ID,
COL_DATATYPE_ID,
COL_UNIT,
COL_MODULE_IMPLEMENTATION_ID,
};
/// \brief Helper for accessing the column indices of the CONFIGURATION table of a specific MODULE_ID
enum class ConfigurationColumnModuleIdIndex {
COL_PARAMETER_NAME = 0,
COL_VALUE,
COL_MODULE_IMPLEMENTATION_ID,
COL_MUTABILITY_ID,
COL_DATATYPE_ID,
COL_UNIT,
};
namespace {
int to_int(SettingColumnIndex setting_column_index) {
return static_cast<int>(setting_column_index);
}
int to_int(ConfigurationColumnIndex configuration_column_index) {
return static_cast<int>(configuration_column_index);
}
int to_int(ConfigurationColumnModuleIdIndex configuration_column_module_id_index) {
return static_cast<int>(configuration_column_module_id_index);
}
} // namespace
SqliteStorage::SqliteStorage(const fs::path& db_path, const std::filesystem::path& migration_files_path) {
db = std::make_unique<Connection>(db_path);
SchemaUpdater updater{db.get()};
if (!updater.apply_migration_files(migration_files_path, TARGET_MIGRATION_FILE_VERSION)) {
if (db_path.parent_path().empty()) {
EVLOG_error
<< "Could not apply migrations for database at provided path: \"" << db_path.string()
<< "\" likely because the database path is just a filename. You MUST provide a full path to the "
"database.";
}
throw MigrationException("SQL migration failed");
}
if (!db->open_connection()) {
throw std::runtime_error("Could not open database at provided path: " + db_path.string());
} else {
EVLOG_info << "Established connection to database successfully: " << db_path;
}
}
GenericResponseStatus SqliteStorage::write_module_configs(const ModuleConfigurations& module_configs) {
try {
auto transaction = this->db->begin_transaction();
for (const auto& [module_id, module] : module_configs) {
ModuleData module_data;
module_data.module_id = module_id;
module_data.module_name = module.module_name;
module_data.standalone = module.standalone;
module_data.capabilities = module.capabilities;
if (this->write_module_data(module_data) != GenericResponseStatus::OK) {
EVLOG_error << "Failed to write module info for module: " << module_id;
return GenericResponseStatus::Failed;
}
for (const auto& [requirement_id, connections] : module.connections) {
for (const auto& connection : connections) {
Fulfillment fulfillment;
fulfillment.module_id = connection.module_id;
fulfillment.implementation_id = connection.implementation_id;
fulfillment.requirement = {requirement_id};
if (this->write_module_fulfillment(module_id, fulfillment) != GenericResponseStatus::OK) {
EVLOG_error << "Failed to write module fulfillment for module: " << module_id
<< " and requirement: " << requirement_id;
return GenericResponseStatus::Failed;
}
}
}
if (module.mapping.module.has_value()) {
const auto& map = module.mapping.module.value();
if (this->write_module_tier_mapping(module_id, default_module_implementation_id(), map.evse,
map.connector) != GenericResponseStatus::OK) {
EVLOG_error << "Failed to write module tier mapping for module: " << module_id;
return GenericResponseStatus::Failed;
}
}
for (const auto& [impl_id, mapping] : module.mapping.implementations) {
if (mapping.has_value()) {
const auto& map = mapping.value();
if (this->write_module_tier_mapping(module_id, impl_id, map.evse, map.connector) !=
GenericResponseStatus::OK) {
EVLOG_error << "Failed to write module tier mapping for module: " << module_id
<< " and implementation id: " << impl_id;
}
}
}
for (const auto& [impl_id, params] : module.configuration_parameters) {
for (const auto& param : params) {
ConfigurationParameterIdentifier identifier;
identifier.module_id = module_id;
identifier.module_implementation_id = impl_id;
identifier.configuration_parameter_name = param.name;
std::string value;
if (std::holds_alternative<std::string>(param.value)) {
value = std::get<std::string>(param.value);
} else {
const nlohmann::json temp = param.value;
value = temp.dump();
}
if (this->write_configuration_parameter(identifier, param.characteristics, value) !=
GetSetResponseStatus::OK) {
EVLOG_error << "Failed to write configuration parameter for module: " << module_id
<< ", param: " << identifier.configuration_parameter_name;
}
}
}
if (this->write_access(module_id, module.access) != GenericResponseStatus::OK) {
EVLOG_error << "Failed to write module access for module: " << module_id;
}
}
transaction->commit();
return GenericResponseStatus::OK;
} catch (const std::exception& e) {
EVLOG_error << "Failed writing config to database: " << e.what();
return GenericResponseStatus::Failed;
}
}
GenericResponseStatus SqliteStorage::wipe() {
const std::string sql = "PRAGMA FOREIGN_KEYS = ON; DELETE FROM MODULE; PRAGMA FOREIGN_KEYS = OFF;";
try {
if (this->db->execute_statement(sql)) {
return GenericResponseStatus::OK;
}
return GenericResponseStatus::Failed;
} catch (const std::exception& e) {
EVLOG_error << "Failed to wipe database: " << e.what();
return GenericResponseStatus::Failed;
}
}
GetModuleConfigsResponse SqliteStorage::get_module_configs() {
GetModuleConfigsResponse response;
try {
const std::string sql = "SELECT ID FROM MODULE";
auto stmt = this->db->new_statement(sql);
while (stmt->step() == SQLITE_ROW) {
auto module_config_response = this->get_module_config(stmt->column_text(0));
if (module_config_response.status == GenericResponseStatus::OK and
module_config_response.config.has_value()) {
response.module_configs[module_config_response.config.value().module_id] =
module_config_response.config.value();
} else {
response.status = GenericResponseStatus::Failed;
return response;
}
}
response.status = GenericResponseStatus::OK;
return response;
} catch (const std::exception& e) {
EVLOG_error << "Failed to get EVerest config: " << e.what();
response.status = GenericResponseStatus::Failed;
return response;
}
}
GetSettingsResponse SqliteStorage::get_settings() {
const std::string sql = "SELECT * FROM SETTING WHERE ID = 0";
auto stmt = this->db->new_statement(sql);
if (stmt->step() != SQLITE_ROW) {
return GetSettingsResponse{GenericResponseStatus::Failed, std::nullopt};
}
Settings settings;
[[maybe_unused]] const auto id =
stmt->column_int(to_int(SettingColumnIndex::COL_ID)); // ID is required and always present
// text
settings.prefix = stmt->column_text(to_int(SettingColumnIndex::COL_PREFIX));
settings.config_file = stmt->column_text(to_int(SettingColumnIndex::COL_CONFIG_FILE));
settings.configs_dir = stmt->column_text(to_int(SettingColumnIndex::COL_CONFIGS_DIR));
settings.schemas_dir = stmt->column_text(to_int(SettingColumnIndex::COL_SCHEMAS_DIR));
settings.modules_dir = stmt->column_text(to_int(SettingColumnIndex::COL_MODULES_DIR));
settings.interfaces_dir = stmt->column_text(to_int(SettingColumnIndex::COL_INTERFACES_DIR));
settings.types_dir = stmt->column_text(to_int(SettingColumnIndex::COL_TYPES_DIR));
settings.errors_dir = stmt->column_text(to_int(SettingColumnIndex::COL_ERRORS_DIR));
settings.www_dir = stmt->column_text(to_int(SettingColumnIndex::COL_WWW_DIR));
settings.logging_config_file = stmt->column_text(to_int(SettingColumnIndex::COL_LOGGING_CONFIG_FILE));
settings.mqtt_broker_socket_path = stmt->column_text(to_int(SettingColumnIndex::COL_MQTT_BROKER_SOCKET_PATH));
settings.mqtt_broker_host = stmt->column_text(to_int(SettingColumnIndex::COL_MQTT_BROKER_HOST));
settings.mqtt_everest_prefix = stmt->column_text(to_int(SettingColumnIndex::COL_MQTT_EVEREST_PREFIX));
settings.mqtt_external_prefix = stmt->column_text(to_int(SettingColumnIndex::COL_MQTT_EXTERNAL_PREFIX));
settings.telemetry_prefix = stmt->column_text(to_int(SettingColumnIndex::COL_TELEMETRY_PREFIX));
settings.run_as_user = stmt->column_text(to_int(SettingColumnIndex::COL_RUN_AS_USER));
// integer
settings.controller_port = stmt->column_int(to_int(SettingColumnIndex::COL_CONTROLLER_PORT));
settings.controller_rpc_timeout_ms = stmt->column_int(to_int(SettingColumnIndex::COL_CONTROLLER_RPC_TIMEOUT_MS));
settings.mqtt_broker_port = stmt->column_int(to_int(SettingColumnIndex::COL_MQTT_BROKER_PORT));
// boolean
settings.telemetry_enabled = stmt->column_int(to_int(SettingColumnIndex::COL_TELEMETRY_ENABLED)) != 0;
settings.validate_schema = stmt->column_int(to_int(SettingColumnIndex::COL_VALIDATE_SCHEMA)) != 0;
settings.forward_exceptions = stmt->column_int(to_int(SettingColumnIndex::COL_FORWARD_EXCEPTIONS)) != 0;
return GetSettingsResponse{GenericResponseStatus::OK, settings};
}
GetModuleConfigurationResponse SqliteStorage::get_module_config(const std::string& module_id) {
GetModuleConfigurationResponse response;
const auto module_data_response = this->get_module_data(module_id);
if (module_data_response.status == GenericResponseStatus::Failed or !module_data_response.module_data.has_value()) {
response.status = GenericResponseStatus::Failed;
return response;
}
const auto module_data = module_data_response.module_data.value();
const auto module_fulfillments_response = this->get_module_fulfillments(module_id);
if (module_fulfillments_response.status == GenericResponseStatus::Failed) {
response.status = GenericResponseStatus::Failed;
return response;
}
const auto module_fulfillments = module_fulfillments_response.module_fulfillments;
const auto module_tier_mappings_response = this->get_module_tier_mappings(module_id);
if (module_tier_mappings_response.status == GenericResponseStatus::Failed) {
response.status = GenericResponseStatus::Failed;
return response;
}
const auto module_tier_mappings = module_tier_mappings_response.module_tier_mappings;
const auto config_access_response = this->get_config_access(module_id);
if (config_access_response.status == GenericResponseStatus::Failed) {
response.status = GenericResponseStatus::Failed;
return response;
}
const auto config_access = config_access_response.config_access;
try {
const std::string sql =
"SELECT PARAMETER_NAME, VALUE, MODULE_IMPLEMENTATION_ID, MUTABILITY_ID, DATATYPE_ID, UNIT "
"FROM CONFIGURATION WHERE MODULE_ID = "
"@module_id";
auto stmt = this->db->new_statement(sql);
stmt->bind_text(1, module_id);
ModuleConfig module_config;
module_config.capabilities = module_data.capabilities;
module_config.module_id = module_data.module_id;
module_config.module_name = module_data.module_name;
module_config.standalone = module_data.standalone;
for (const auto& fulfillment : module_fulfillments) {
module_config.connections[fulfillment.requirement.id].push_back(fulfillment);
}
module_config.mapping = module_tier_mappings;
module_config.access.config = config_access;
while (stmt->step() == SQLITE_ROW) {
ConfigurationParameter configuration_parameter;
configuration_parameter.name =
stmt->column_text(to_int(ConfigurationColumnModuleIdIndex::COL_PARAMETER_NAME));
const auto value_str = stmt->column_text(to_int(ConfigurationColumnModuleIdIndex::COL_VALUE));
auto implementation_id =
stmt->column_text(to_int(ConfigurationColumnModuleIdIndex::COL_MODULE_IMPLEMENTATION_ID));
ConfigurationParameterCharacteristics characteristics;
characteristics.mutability =
static_cast<Mutability>(stmt->column_int(to_int(ConfigurationColumnModuleIdIndex::COL_MUTABILITY_ID)));
characteristics.datatype =
static_cast<Datatype>(stmt->column_int(to_int(ConfigurationColumnModuleIdIndex::COL_DATATYPE_ID)));
characteristics.unit = stmt->column_text_nullable(to_int(ConfigurationColumnModuleIdIndex::COL_UNIT));
configuration_parameter.characteristics = characteristics;
configuration_parameter.value = parse_config_value(characteristics.datatype, value_str);
module_config.configuration_parameters[implementation_id].push_back(configuration_parameter);
}
response.status = GenericResponseStatus::OK;
response.config = module_config;
return response;
} catch (const std::exception& e) {
EVLOG_error << "Failed to get module config with module_id: " << module_id;
response.status = GenericResponseStatus::Failed;
return response;
}
}
GetConfigurationParameterResponse
SqliteStorage::get_configuration_parameter(const ConfigurationParameterIdentifier& identifier) {
GetConfigurationParameterResponse response;
try {
const std::string sql = "SELECT VALUE, MUTABILITY_ID, DATATYPE_ID, UNIT FROM "
"CONFIGURATION WHERE MODULE_ID = "
"@module_id AND PARAMETER_NAME = @config_param_name AND MODULE_IMPLEMENTATION_ID = "
"@module_implementation_id";
auto stmt = this->db->new_statement(sql);
stmt->bind_text("@module_id", identifier.module_id);
stmt->bind_text("@config_param_name", identifier.configuration_parameter_name);
stmt->bind_text("@module_implementation_id", identifier.module_implementation_id.has_value()
? identifier.module_implementation_id.value()
: default_module_implementation_id());
const auto status = stmt->step();
if (status == SQLITE_DONE) {
response.status = GetSetResponseStatus::NotFound;
return response;
}
if (status == SQLITE_ROW) {
response.status = GetSetResponseStatus::OK;
ConfigurationParameter configuration_parameter;
configuration_parameter.name = identifier.configuration_parameter_name;
const auto value_str = stmt->column_text(0);
ConfigurationParameterCharacteristics characteristics;
characteristics.mutability = static_cast<Mutability>(stmt->column_int(1));
characteristics.datatype = static_cast<Datatype>(stmt->column_int(2));
characteristics.unit = stmt->column_text_nullable(3);
configuration_parameter.characteristics = characteristics;
configuration_parameter.value = parse_config_value(characteristics.datatype, value_str);
response.configuration_parameter = configuration_parameter;
return response;
}
} catch (const std::exception& e) {
EVLOG_error << "Failed to get config value with module_id: " << identifier.module_id
<< " and config_parameter_name: " << identifier.configuration_parameter_name;
response.status = GetSetResponseStatus::Failed;
return response;
}
return response;
}
GetSetResponseStatus
SqliteStorage::write_configuration_parameter(const ConfigurationParameterIdentifier& identifier,
const ConfigurationParameterCharacteristics characteristics,
const std::string& value) {
try {
const std::string insert_query = "INSERT OR REPLACE INTO CONFIGURATION (MODULE_ID, PARAMETER_NAME, "
"VALUE, "
"MUTABILITY_ID, DATATYPE_ID, UNIT, MODULE_IMPLEMENTATION_ID) VALUES "
"(?, ?, ?, ?, ?, ?, ?);";
auto stmt = this->db->new_statement(insert_query);
stmt->bind_text(to_int(ConfigurationColumnIndex::COL_MODULE_ID), identifier.module_id);
stmt->bind_text(to_int(ConfigurationColumnIndex::COL_PARAMETER_NAME), identifier.configuration_parameter_name);
stmt->bind_text(to_int(ConfigurationColumnIndex::COL_VALUE), value);
stmt->bind_int(to_int(ConfigurationColumnIndex::COL_MUTABILITY_ID),
static_cast<int>(characteristics.mutability));
stmt->bind_int(to_int(ConfigurationColumnIndex::COL_DATATYPE_ID), static_cast<int>(characteristics.datatype));
if (characteristics.unit.has_value()) {
stmt->bind_text(to_int(ConfigurationColumnIndex::COL_UNIT), characteristics.unit.value());
} else {
stmt->bind_null(to_int(ConfigurationColumnIndex::COL_UNIT));
}
if (identifier.module_implementation_id.has_value()) {
stmt->bind_text(to_int(ConfigurationColumnIndex::COL_MODULE_IMPLEMENTATION_ID),
identifier.module_implementation_id.value());
} else {
stmt->bind_null(to_int(ConfigurationColumnIndex::COL_MODULE_IMPLEMENTATION_ID));
}
if (stmt->step() != SQLITE_DONE) {
return GetSetResponseStatus::NotFound;
}
return GetSetResponseStatus::OK;
} catch (const std::exception& e) {
EVLOG_error << "Failed to set config value with module_id: " << identifier.module_id
<< " and config_parameter_name: " << identifier.configuration_parameter_name;
return GetSetResponseStatus::Failed;
}
}
bool SqliteStorage::contains_valid_config() {
const std::string sql = "SELECT VALID FROM CONFIG_META WHERE ID = 0";
auto stmt = this->db->new_statement(sql);
if (stmt->step() != SQLITE_ROW) {
return false;
}
const auto is_valid = stmt->column_int(0);
return is_valid == 1;
}
void SqliteStorage::mark_valid(const bool is_valid, const std::string& config_dump,
const std::optional<fs::path>& config_file_path) {
const std::string sql =
"INSERT OR REPLACE INTO CONFIG_META (ID, LAST_UPDATED, VALID, CONFIG_DUMP, CONFIG_FILE_PATH) VALUES (0, "
"@last_updated, @is_valid, @config_dump, @config_file_path);";
auto stmt = this->db->new_statement(sql);
const auto last_updated = Everest::Date::to_rfc3339(date::utc_clock::now());
stmt->bind_text("@last_updated", last_updated);
stmt->bind_int("@is_valid", is_valid ? 1 : 0);
stmt->bind_text("@config_dump", config_dump, SQLiteString::Transient);
if (config_file_path.has_value()) {
stmt->bind_text("@config_file_path", config_file_path.value().string(), SQLiteString::Transient);
} else {
stmt->bind_null("@config_file_path");
}
if (stmt->step() != SQLITE_DONE) {
EVLOG_error << "Failed to mark config as valid";
} else {
EVLOG_debug << "Marked config as valid";
}
}
GetSetResponseStatus SqliteStorage::update_configuration_parameter(const ConfigurationParameterIdentifier& identifier,
const std::string& value) {
try {
const std::string update_query = "UPDATE CONFIGURATION SET VALUE = @value "
"WHERE MODULE_ID = @module_id AND "
"PARAMETER_NAME = @parameter_name AND "
"MODULE_IMPLEMENTATION_ID = @module_implementation_id;";
auto stmt = this->db->new_statement(update_query);
stmt->bind_text("@value", value);
stmt->bind_text("@module_id", identifier.module_id);
stmt->bind_text("@parameter_name", identifier.configuration_parameter_name);
const std::string impl_id = identifier.module_implementation_id.value_or(default_module_implementation_id());
stmt->bind_text("@module_implementation_id", impl_id);
if (stmt->step() != SQLITE_DONE) {
return GetSetResponseStatus::Failed;
}
if (stmt->changes() == 0) {
return GetSetResponseStatus::NotFound;
}
return GetSetResponseStatus::OK;
} catch (const std::exception& e) {
EVLOG_error << "Failed to set config value with module_id: " << identifier.module_id
<< " and config_parameter_name: " << identifier.configuration_parameter_name;
return GetSetResponseStatus::Failed;
}
}
GenericResponseStatus SqliteStorage::write_module_data(const ModuleData& module_data) {
const std::string insert_query =
"INSERT OR REPLACE INTO MODULE (ID, NAME, STANDALONE, CAPABILITIES) VALUES (?, ?, ?, ?);";
auto stmt = this->db->new_statement(insert_query);
stmt->bind_text(1, module_data.module_id);
stmt->bind_text(2, module_data.module_name);
stmt->bind_int(3, module_data.standalone);
if (module_data.capabilities.has_value()) {
stmt->bind_text(4, json(module_data.capabilities.value()).dump(), SQLiteString::Transient);
} else {
stmt->bind_null(4);
}
if (stmt->step() != SQLITE_DONE) {
return GenericResponseStatus::Failed;
}
return GenericResponseStatus::OK;
}
GenericResponseStatus SqliteStorage::write_module_fulfillment(const std::string& module_id,
const Fulfillment& fulfillment) {
const std::string sql =
"INSERT OR REPLACE INTO MODULE_FULFILLMENT (MODULE_ID, REQUIREMENT_NAME, IMPLEMENTATION_ID, "
"IMPLEMENTATION_MODULE_ID) VALUES (?,?,?,?)";
auto stmt = this->db->new_statement(sql);
stmt->bind_text(1, module_id);
stmt->bind_text(2, fulfillment.requirement.id);
stmt->bind_text(3, fulfillment.implementation_id);
stmt->bind_text(4, fulfillment.module_id);
if (stmt->step() != SQLITE_DONE) {
return GenericResponseStatus::Failed;
}
return GenericResponseStatus::OK;
}
GenericResponseStatus SqliteStorage::write_module_tier_mapping(const std::string& module_id,
const std::string& implementation_id,
const int32_t evse_id,
const std::optional<int32_t> connector_id) {
const std::string sql = "INSERT OR REPLACE INTO MODULE_TIER_MAPPING (MODULE_ID, IMPLEMENTATION_ID, "
"EVSE_ID, CONNECTOR_ID) VALUES (?,?,?,?)";
auto stmt = this->db->new_statement(sql);
stmt->bind_text(1, module_id);
stmt->bind_text(2, implementation_id);
stmt->bind_int(3, evse_id);
if (connector_id.has_value()) {
stmt->bind_int(4, connector_id.value());
} else {
stmt->bind_null(4);
}
if (stmt->step() != SQLITE_DONE) {
return GenericResponseStatus::Failed;
}
return GenericResponseStatus::OK;
}
GenericResponseStatus SqliteStorage::write_access(const std::string& module_id, const Access& access) {
if (access.config.has_value()) {
return write_config_access(module_id, access.config.value());
}
return GenericResponseStatus::OK;
}
GenericResponseStatus SqliteStorage::write_config_access(const std::string& module_id,
const ConfigAccess& config_access) {
// write global config access to db
const std::string sql = "INSERT OR REPLACE INTO CONFIG_ACCESS (MODULE_ID, ALLOW_GLOBAL_READ, ALLOW_GLOBAL_WRITE, "
"ALLOW_SET_READ_ONLY) VALUES (?,?,?,?)";
auto stmt = this->db->new_statement(sql);
stmt->bind_text(1, module_id);
stmt->bind_int(2, config_access.allow_global_read ? 1 : 0);
stmt->bind_int(3, config_access.allow_global_write ? 1 : 0);
stmt->bind_int(4, config_access.allow_set_read_only ? 1 : 0);
if (stmt->step() != SQLITE_DONE) {
return GenericResponseStatus::Failed;
}
// write individual module config access to db
for (const auto& [other_module_id, module_config_access] : config_access.modules) {
const auto result = write_module_config_access(module_id, other_module_id, module_config_access);
if (result != GenericResponseStatus::OK) {
return GenericResponseStatus::Failed;
}
}
return GenericResponseStatus::OK;
}
GenericResponseStatus SqliteStorage::write_module_config_access(const std::string& module_id,
const std::string& other_module_id,
const ModuleConfigAccess& module_config_access) {
const std::string sql = "INSERT OR REPLACE INTO MODULE_CONFIG_ACCESS (MODULE_ID, OTHER_MODULE_ID, "
"ALLOW_READ, ALLOW_WRITE, ALLOW_SET_READ_ONLY) VALUES (?,?,?,?,?)";
auto stmt = this->db->new_statement(sql);
stmt->bind_text(1, module_id);
stmt->bind_text(2, other_module_id);
stmt->bind_int(3, module_config_access.allow_read ? 1 : 0);
stmt->bind_int(4, module_config_access.allow_write ? 1 : 0);
stmt->bind_int(5, module_config_access.allow_set_read_only ? 1 : 0);
if (stmt->step() != SQLITE_DONE) {
return GenericResponseStatus::Failed;
}
return GenericResponseStatus::OK;
}
GenericResponseStatus SqliteStorage::write_settings(const Everest::ManagerSettings& manager_settings) {
auto transaction = this->db->begin_transaction();
std::vector<std::string> keys = {"ID",
"PREFIX",
"CONFIG_FILE",
"CONFIGS_DIR",
"SCHEMAS_DIR",
"MODULES_DIR",
"INTERFACES_DIR",
"TYPES_DIR",
"ERRORS_DIR",
"WWW_DIR",
"LOGGING_CONFIG_FILE",
"CONTROLLER_PORT",
"CONTROLLER_RPC_TIMEOUT_MS",
"MQTT_BROKER_SOCKET_PATH",
"MQTT_BROKER_HOST",
"MQTT_BROKER_PORT",
"MQTT_EVEREST_PREFIX",
"MQTT_EXTERNAL_PREFIX",
"TELEMETRY_PREFIX",
"TELEMETRY_ENABLED",
"VALIDATE_SCHEMA",
"RUN_AS_USER",
"FORWARD_EXCEPTIONS"};
std::string sql = "INSERT INTO SETTING (";
for (size_t i = 0; i < keys.size(); ++i) {
sql += keys.at(i);
if (i < keys.size() - 1) {
sql += ", ";
}
}
sql += ") VALUES (";
for (size_t i = 0; i < keys.size(); ++i) {
sql += (i == 0 ? "?" : ", ?");
}
sql += ") ON CONFLICT(ID) DO UPDATE SET ";
for (size_t i = 1; i < keys.size(); ++i) {
sql += keys.at(i) + " = excluded." + keys.at(i);
if (i < keys.size() - 1) {
sql += ", ";
}
}
sql += ";";
auto stmt = this->db->new_statement(sql);
// ID is always 0
stmt->bind_int(to_int(SettingColumnIndex::COL_ID) + 1, 0);
auto bind_text_opt = [&](SettingColumnIndex index, int offset, const std::optional<std::string>& opt) {
if (opt.has_value()) {
stmt->bind_text(to_int(index) + offset, opt.value(), SQLiteString::Transient);
} else {
stmt->bind_null(to_int(index) + offset);
}
};
auto bind_path_opt = [&](SettingColumnIndex index, int offset, const std::optional<fs::path>& opt) {
if (opt.has_value()) {
stmt->bind_text(to_int(index) + offset, opt.value().string(), SQLiteString::Transient);
} else {
stmt->bind_null(to_int(index) + offset);
}
};
auto bind_int_opt = [&](SettingColumnIndex index, int offset, const std::optional<int>& opt) {
if (opt.has_value()) {
stmt->bind_int(to_int(index) + offset, opt.value());
} else {
stmt->bind_null(to_int(index) + offset);
}
};
auto bind_bool_opt = [&](SettingColumnIndex index, int offset, const std::optional<bool>& opt) {
if (opt) {
stmt->bind_int(to_int(index) + offset, opt.value() ? 1 : 0);
} else {
stmt->bind_null(to_int(index) + offset);
}
};
bind_path_opt(SettingColumnIndex::COL_PREFIX, 1, manager_settings.runtime_settings.prefix);
bind_path_opt(SettingColumnIndex::COL_CONFIG_FILE, 1, manager_settings.config_file);
bind_path_opt(SettingColumnIndex::COL_CONFIGS_DIR, 1, manager_settings.configs_dir);
bind_path_opt(SettingColumnIndex::COL_SCHEMAS_DIR, 1, manager_settings.schemas_dir);
bind_path_opt(SettingColumnIndex::COL_MODULES_DIR, 1, manager_settings.runtime_settings.modules_dir);
bind_path_opt(SettingColumnIndex::COL_INTERFACES_DIR, 1, manager_settings.interfaces_dir);
bind_path_opt(SettingColumnIndex::COL_TYPES_DIR, 1, manager_settings.types_dir);
bind_path_opt(SettingColumnIndex::COL_ERRORS_DIR, 1, manager_settings.errors_dir);
bind_path_opt(SettingColumnIndex::COL_WWW_DIR, 1, manager_settings.www_dir);
bind_path_opt(SettingColumnIndex::COL_LOGGING_CONFIG_FILE, 1,
manager_settings.runtime_settings.logging_config_file);
bind_int_opt(SettingColumnIndex::COL_CONTROLLER_PORT, 1, manager_settings.controller_port);
bind_int_opt(SettingColumnIndex::COL_CONTROLLER_RPC_TIMEOUT_MS, 1, manager_settings.controller_rpc_timeout_ms);
bind_text_opt(SettingColumnIndex::COL_MQTT_BROKER_SOCKET_PATH, 1,
manager_settings.mqtt_settings.broker_socket_path);
bind_text_opt(SettingColumnIndex::COL_MQTT_BROKER_HOST, 1, manager_settings.mqtt_settings.broker_host);
bind_int_opt(SettingColumnIndex::COL_MQTT_BROKER_PORT, 1, manager_settings.mqtt_settings.broker_port);
bind_text_opt(SettingColumnIndex::COL_MQTT_EVEREST_PREFIX, 1, manager_settings.mqtt_settings.everest_prefix);
bind_text_opt(SettingColumnIndex::COL_MQTT_EXTERNAL_PREFIX, 1, manager_settings.mqtt_settings.external_prefix);
bind_text_opt(SettingColumnIndex::COL_TELEMETRY_PREFIX, 1, manager_settings.runtime_settings.telemetry_prefix);
bind_bool_opt(SettingColumnIndex::COL_TELEMETRY_ENABLED, 1, manager_settings.runtime_settings.telemetry_enabled);
bind_bool_opt(SettingColumnIndex::COL_VALIDATE_SCHEMA, 1, manager_settings.runtime_settings.validate_schema);
bind_text_opt(SettingColumnIndex::COL_RUN_AS_USER, 1, manager_settings.run_as_user);
bind_bool_opt(SettingColumnIndex::COL_FORWARD_EXCEPTIONS, 1, manager_settings.runtime_settings.forward_exceptions);
if (stmt->step() != SQLITE_DONE) {
return GenericResponseStatus::Failed;
}
transaction->commit();
return GenericResponseStatus::OK;
}
GetModuleDataResponse SqliteStorage::get_module_data(const std::string& module_id) {
GetModuleDataResponse response;
const std::string sql = "SELECT NAME, STANDALONE, CAPABILITIES FROM MODULE WHERE ID = @module_id";
auto stmt = this->db->new_statement(sql);
stmt->bind_text("@module_id", module_id);
const auto status = stmt->step();
if (status == SQLITE_DONE) {
response.status = GenericResponseStatus::Failed;
return response;
}
if (status == SQLITE_ROW) {
ModuleData module_data;
module_data.module_id = module_id;
module_data.module_name = stmt->column_text(0);
module_data.standalone = stmt->column_int(1);
const auto capabilities_str = stmt->column_text_nullable(2);
if (capabilities_str.has_value()) {
module_data.capabilities = json::parse(capabilities_str.value()).get<std::vector<std::string>>();
}
response.module_data = module_data;
response.status = GenericResponseStatus::OK;
}
return response;
}
GetModuleFulfillmentsResponse SqliteStorage::get_module_fulfillments(const std::string& module_id) {
GetModuleFulfillmentsResponse response;
const std::string sql =
"SELECT REQUIREMENT_NAME, IMPLEMENTATION_ID, IMPLEMENTATION_MODULE_ID FROM MODULE_FULFILLMENT "
"WHERE MODULE_ID = @module_id";
auto stmt = this->db->new_statement(sql);
stmt->bind_text("@module_id", module_id);
size_t index = 0;
while (stmt->step() == SQLITE_ROW) {
Fulfillment fulfillment;
fulfillment.requirement = {stmt->column_text(0), index};
fulfillment.implementation_id = stmt->column_text(1);
fulfillment.module_id = stmt->column_text(2);
response.module_fulfillments.push_back(fulfillment);
index++;
}
response.status = GenericResponseStatus::OK; // FIXME: when to return failed?
return response;
}
GetModuleTierMappingsResponse SqliteStorage::get_module_tier_mappings(const std::string& module_id) {
GetModuleTierMappingsResponse response;
const std::string sql = "SELECT IMPLEMENTATION_ID, EVSE_ID, CONNECTOR_ID FROM MODULE_TIER_MAPPING "
"WHERE MODULE_ID = @module_id";
auto stmt = this->db->new_statement(sql);
stmt->bind_text("@module_id", module_id);
ModuleTierMappings module_tier_mappings;
while (stmt->step() == SQLITE_ROW) {
auto implementation_id = stmt->column_text(0);
Mapping mapping = {stmt->column_int(1)};
if (stmt->column_type(2) != SQLITE_NULL) {
mapping.connector = stmt->column_int(2);
}
if (implementation_id == default_module_implementation_id()) {
module_tier_mappings.module = mapping;
} else {
module_tier_mappings.implementations[implementation_id] = mapping;
}
}
response.module_tier_mappings = module_tier_mappings;
response.status = GenericResponseStatus::OK;
return response;
}
GetModuleConfigAccessResponse SqliteStorage::get_module_config_access(const std::string& module_id) {
GetModuleConfigAccessResponse response;
const std::string sql = "SELECT OTHER_MODULE_ID, ALLOW_SET_READ_ONLY FROM MODULE_CONFIG_ACCESS "
"WHERE MODULE_ID = @module_id";
auto stmt = this->db->new_statement(sql);
stmt->bind_text("@module_id", module_id);
std::map<std::string, everest::config::ModuleConfigAccess> module_config_access;
while (stmt->step() == SQLITE_ROW) {
const auto other_module_id = stmt->column_text(0);
const auto allow_set_read_only = stmt->column_int(1) != 0;
module_config_access[other_module_id].allow_set_read_only = allow_set_read_only;
}
response.module_config_access = module_config_access;
response.status = GenericResponseStatus::OK;
return response;
}
GetConfigAccessResponse SqliteStorage::get_config_access(const std::string& module_id) {
GetConfigAccessResponse response;
const std::string sql =
"SELECT ALLOW_GLOBAL_READ, ALLOW_SET_READ_ONLY FROM CONFIG_ACCESS WHERE MODULE_ID = @module_id";
auto stmt = this->db->new_statement(sql);
stmt->bind_text("@module_id", module_id);
const auto status = stmt->step();
if (status == SQLITE_DONE) {
response.status = GenericResponseStatus::OK;
return response;
}
if (status == SQLITE_ROW) {
ConfigAccess config_access;
config_access.allow_global_read = stmt->column_int(0) != 0;
config_access.allow_set_read_only = stmt->column_int(1) != 0;
const auto module_config_access = get_module_config_access(module_id);
if (module_config_access.status == GenericResponseStatus::OK) {
config_access.modules = module_config_access.module_config_access;
}
response.config_access = config_access;
response.status = GenericResponseStatus::OK;
}
return response;
}
} // namespace everest::config

View File

@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <utils/config/storage_types.hpp>
namespace everest::config {
bool ConfigurationParameterIdentifier::operator<(const ConfigurationParameterIdentifier& rhs) const {
return (
this->module_id < rhs.module_id ||
(this->module_id == rhs.module_id && this->configuration_parameter_name < rhs.configuration_parameter_name) ||
(this->module_id == rhs.module_id && this->configuration_parameter_name == rhs.configuration_parameter_name &&
this->module_implementation_id < rhs.module_implementation_id));
}
} // namespace everest::config

View File

@@ -0,0 +1,483 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include <nlohmann/json.hpp>
#include <utils/config/types.hpp>
#include <utils/types.hpp>
using json = nlohmann::json;
bool operator<(const Requirement& lhs, const Requirement& rhs) {
if (lhs.id < rhs.id) {
return true;
} else if (lhs.id == rhs.id) {
return (lhs.index < rhs.index);
} else {
return false;
}
}
namespace everest::config {
std::string config_entry_to_string(const everest::config::ConfigEntry& entry) {
return std::visit(VisitConfigEntry{}, entry);
}
bool ConfigurationParameter::validate_type() const {
return std::visit(
[&](auto&& arg) -> bool {
using T = std::decay_t<decltype(arg)>;
switch (characteristics.datatype) {
case Datatype::String:
return std::is_same_v<T, std::string>;
case Datatype::Boolean:
return std::is_same_v<T, bool>;
case Datatype::Integer:
return std::is_same_v<T, int>;
case Datatype::Decimal:
return std::is_same_v<T, double> || std::is_same_v<T, int>;
// allow integers where decimal is expected
default:
return false;
}
},
value);
}
Settings parse_settings(const json& settings_json) {
Settings settings;
if (auto it = settings_json.find("prefix"); it != settings_json.end()) {
settings.prefix = it->get<std::string>();
}
if (auto it = settings_json.find("config_file"); it != settings_json.end()) {
settings.config_file = it->get<std::string>();
}
if (auto it = settings_json.find("configs_dir"); it != settings_json.end()) {
settings.configs_dir = it->get<std::string>();
}
if (auto it = settings_json.find("schemas_dir"); it != settings_json.end()) {
settings.schemas_dir = it->get<std::string>();
}
if (auto it = settings_json.find("modules_dir"); it != settings_json.end()) {
settings.modules_dir = it->get<std::string>();
}
if (auto it = settings_json.find("interfaces_dir"); it != settings_json.end()) {
settings.interfaces_dir = it->get<std::string>();
}
if (auto it = settings_json.find("types_dir"); it != settings_json.end()) {
settings.types_dir = it->get<std::string>();
}
if (auto it = settings_json.find("errors_dir"); it != settings_json.end()) {
settings.errors_dir = it->get<std::string>();
}
if (auto it = settings_json.find("www_dir"); it != settings_json.end()) {
settings.www_dir = it->get<std::string>();
}
if (auto it = settings_json.find("logging_config_file"); it != settings_json.end()) {
settings.logging_config_file = it->get<std::string>();
}
if (auto it = settings_json.find("controller_port"); it != settings_json.end()) {
settings.controller_port = it->get<int>();
}
if (auto it = settings_json.find("controller_rpc_timeout_ms"); it != settings_json.end()) {
settings.controller_rpc_timeout_ms = it->get<int>();
}
if (auto it = settings_json.find("mqtt_broker_socket_path"); it != settings_json.end()) {
settings.mqtt_broker_socket_path = it->get<std::string>();
}
if (auto it = settings_json.find("mqtt_broker_host"); it != settings_json.end()) {
settings.mqtt_broker_host = it->get<std::string>();
}
if (auto it = settings_json.find("mqtt_broker_port"); it != settings_json.end()) {
settings.mqtt_broker_port = it->get<int>();
}
if (auto it = settings_json.find("mqtt_everest_prefix"); it != settings_json.end()) {
settings.mqtt_everest_prefix = it->get<std::string>();
}
if (auto it = settings_json.find("mqtt_external_prefix"); it != settings_json.end()) {
settings.mqtt_external_prefix = it->get<std::string>();
}
if (auto it = settings_json.find("telemetry_prefix"); it != settings_json.end()) {
settings.telemetry_prefix = it->get<std::string>();
}
if (auto it = settings_json.find("telemetry_enabled"); it != settings_json.end()) {
settings.telemetry_enabled = it->get<bool>();
}
if (auto it = settings_json.find("validate_schema"); it != settings_json.end()) {
settings.validate_schema = it->get<bool>();
}
if (auto it = settings_json.find("run_as_user"); it != settings_json.end()) {
settings.run_as_user = it->get<std::string>();
}
if (auto it = settings_json.find("forward_exceptions"); it != settings_json.end()) {
settings.forward_exceptions = it->get<bool>();
}
return settings;
}
namespace {
ModuleConfigurationParameters parse_config_parameters(const json& config_json) {
ModuleConfigurationParameters config_maps;
auto parse_entry = [](const std::string& name, const json& jval) -> ConfigurationParameter {
ConfigurationParameter param;
param.name = name;
param.characteristics.mutability = Mutability::ReadOnly;
if (jval.is_string()) {
param.value = jval.get<std::string>();
param.characteristics.datatype = Datatype::String;
} else if (jval.is_boolean()) {
param.value = jval.get<bool>();
param.characteristics.datatype = Datatype::Boolean;
} else if (jval.is_number_integer()) {
param.value = jval.get<int>();
param.characteristics.datatype = Datatype::Integer;
} else if (jval.is_number_float()) {
param.value = jval.get<double>();
param.characteristics.datatype = Datatype::Decimal;
} else {
throw std::runtime_error("Unsupported JSON type for config parameter: " + name);
}
return param;
};
if (config_json.contains("config_module")) {
const auto& config_module = config_json.at("config_module");
for (auto config_entry = config_module.begin(); config_entry != config_module.end(); ++config_entry) {
config_maps["!module"].push_back(parse_entry(config_entry.key(), config_entry.value()));
}
}
if (config_json.contains("config_implementation")) {
const auto& config_implementations = config_json.at("config_implementation");
for (auto impl = config_implementations.begin(); impl != config_implementations.end(); ++impl) {
for (auto config_entry = impl.value().begin(); config_entry != impl.value().end(); ++config_entry) {
config_maps[impl.key()].push_back(parse_entry(config_entry.key(), config_entry.value()));
}
}
}
return config_maps;
}
ModuleConnections parse_connections(const json& connections_json) {
ModuleConnections connections;
for (auto requirement = connections_json.begin(); requirement != connections_json.end(); ++requirement) {
for (const auto& connection : requirement.value()) {
if (!connection.contains("module_id")) {
throw ConfigParseException(ConfigParseException::MISSING_ENTRY, "module_id",
"Missing 'module_id' in connection");
}
if (!connection.contains("implementation_id")) {
throw ConfigParseException(ConfigParseException::MISSING_ENTRY, "implementation_id",
"Missing 'implementation_id' in connection");
}
Fulfillment fulfillment;
fulfillment.module_id = connection.at("module_id");
fulfillment.implementation_id = connection.at("implementation_id");
fulfillment.requirement = {requirement.key()};
connections[requirement.key()].push_back(fulfillment);
}
}
return connections;
}
ModuleConfig parse_module_config(const std::string& module_id, const json& module_json) {
if (!module_json.contains("module")) {
throw ConfigParseException(ConfigParseException::MISSING_ENTRY, "module", "Missing 'module' in config");
}
ModuleConfig module_config;
module_config.module_id = module_id;
module_config.module_name = module_json.at("module").get<std::string>();
module_config.standalone = module_json.value("standalone", false);
if (module_json.contains("capabilities")) {
module_config.capabilities = module_json["capabilities"].get<std::vector<std::string>>();
}
if (module_json.contains("connections")) {
module_config.connections = parse_connections(module_json.at("connections"));
}
if (module_json.contains("mapping")) {
module_config.mapping = parse_mapping(module_json.at("mapping"));
}
if (module_json.contains("telemetry")) {
module_config.telemetry_config = module_json.at("telemetry").get<TelemetryConfig>();
}
if (module_json.contains("access")) {
module_config.access = module_json.at("access").get<Access>();
}
module_config.configuration_parameters = parse_config_parameters(module_json);
return module_config;
}
} // namespace
ModuleTierMappings parse_mapping(const json& mapping_json) {
ModuleTierMappings mapping_config;
if (mapping_json.contains("module") && mapping_json["module"].contains("evse")) {
Mapping module_mapping(mapping_json["module"]["evse"].get<int32_t>());
if (mapping_json["module"].contains("connector")) {
module_mapping.connector = mapping_json["module"]["connector"].get<int32_t>();
}
mapping_config.module = module_mapping;
}
if (mapping_json.contains("implementations")) {
for (auto impl = mapping_json["implementations"].begin(); impl != mapping_json["implementations"].end();
++impl) {
Mapping impl_mapping(impl.value().at("evse").get<int32_t>());
if (impl.value().contains("connector")) {
impl_mapping.connector = impl.value().at("connector").get<int32_t>();
}
mapping_config.implementations[impl.key()] = impl_mapping;
}
}
return mapping_config;
}
ConfigEntry parse_config_value(Datatype datatype, const std::string& value_str) {
try {
switch (datatype) {
case Datatype::String:
return value_str;
case Datatype::Decimal:
return std::stod(value_str);
case Datatype::Integer:
return std::stoi(value_str);
case Datatype::Boolean:
return value_str == "true" || value_str == "1";
default:
throw std::out_of_range("Unsupported datatype: " + datatype_to_string(datatype));
}
} catch (const std::exception& e) {
throw std::runtime_error("Failed to parse config value '" + value_str + "' as type " +
datatype_to_string(datatype) + ": " + e.what());
}
}
ModuleConfigurations parse_module_configs(const json& active_modules_json) {
ModuleConfigurations module_configs;
for (auto module = active_modules_json.begin(); module != active_modules_json.end(); ++module) {
module_configs.insert({module.key(), parse_module_config(module.key(), module.value())});
}
return module_configs;
}
Datatype string_to_datatype(const std::string& str) {
if (str == "string") {
return Datatype::String;
} else if (str == "number") {
return Datatype::Decimal;
} else if (str == "integer") {
return Datatype::Integer;
} else if (str == "boolean" or "bool") {
return Datatype::Boolean;
} else if (str == "unknown") {
return Datatype::Unknown;
}
throw std::out_of_range("Could not convert: " + str + " to Datatype");
}
std::string datatype_to_string(const Datatype datatype) {
switch (datatype) {
case Datatype::String:
return "string";
case Datatype::Decimal:
return "number";
case Datatype::Integer:
return "integer";
case Datatype::Boolean:
return "bool";
case Datatype::Unknown:
return "unknown";
}
throw std::out_of_range("Could not convert Datatype to string");
}
Mutability string_to_mutability(const std::string& str) {
if (str == "ReadOnly") {
return Mutability::ReadOnly;
} else if (str == "ReadWrite") {
return Mutability::ReadWrite;
} else if (str == "WriteOnly") {
return Mutability::WriteOnly;
}
throw std::out_of_range("Could not convert: " + str + " to Mutability");
}
std::string mutability_to_string(const Mutability mutability) {
switch (mutability) {
case Mutability::ReadOnly:
return "ReadOnly";
case Mutability::ReadWrite:
return "ReadWrite";
case Mutability::WriteOnly:
return "WriteOnly";
}
throw std::out_of_range("Could not convert Mutability to string");
}
} // namespace everest::config
NLOHMANN_JSON_NAMESPACE_BEGIN
void adl_serializer<everest::config::ModuleConfigAccess>::to_json(nlohmann::json& j,
const everest::config::ModuleConfigAccess& m) {
j["allow_read"] = m.allow_read;
j["allow_write"] = m.allow_write;
j["allow_set_read_only"] = m.allow_set_read_only;
}
void adl_serializer<everest::config::ModuleConfigAccess>::from_json(const nlohmann::json& j,
everest::config::ModuleConfigAccess& m) {
if (j.contains("allow_read")) {
m.allow_read = j.at("allow_read").get<bool>();
}
if (j.contains("allow_write")) {
m.allow_write = j.at("allow_write").get<bool>();
}
if (j.contains("allow_set_read_only")) {
m.allow_set_read_only = j.at("allow_set_read_only").get<bool>();
}
}
void adl_serializer<everest::config::ConfigAccess>::to_json(nlohmann::json& j, const everest::config::ConfigAccess& c) {
j["allow_global_read"] = c.allow_global_read;
j["allow_global_write"] = c.allow_global_write;
j["allow_set_read_only"] = c.allow_set_read_only;
j["modules"] = c.modules;
}
void adl_serializer<everest::config::ConfigAccess>::from_json(const nlohmann::json& j,
everest::config::ConfigAccess& c) {
if (j.contains("allow_global_read")) {
c.allow_global_read = j.at("allow_global_read").get<bool>();
}
if (j.contains("allow_global_write")) {
c.allow_global_write = j.at("allow_global_write").get<bool>();
}
if (j.contains("allow_set_read_only")) {
c.allow_set_read_only = j.at("allow_set_read_only").get<bool>();
}
if (j.contains("modules")) {
c.modules = j.at("modules").get<std::map<std::string, everest::config::ModuleConfigAccess>>();
}
}
void adl_serializer<everest::config::Access>::to_json(nlohmann::json& j, const everest::config::Access& a) {
if (a.config.has_value()) {
j["config"] = a.config.value();
}
}
void adl_serializer<everest::config::Access>::from_json(const nlohmann::json& j, everest::config::Access& a) {
if (j.contains("config")) {
a.config = j.at("config").get<everest::config::ConfigAccess>();
}
}
void adl_serializer<everest::config::ConfigurationParameterCharacteristics>::to_json(
nlohmann::json& j, const everest::config::ConfigurationParameterCharacteristics& c) {
j["datatype"] = datatype_to_string(c.datatype);
j["mutability"] = mutability_to_string(c.mutability);
if (c.unit.has_value()) {
j["unit"] = c.unit.value();
}
}
void adl_serializer<everest::config::ConfigurationParameterCharacteristics>::from_json(
const nlohmann::json& j, everest::config::ConfigurationParameterCharacteristics& c) {
c.datatype = everest::config::string_to_datatype(j.at("datatype").get<std::string>());
c.mutability = everest::config::string_to_mutability(j.at("mutability").get<std::string>());
if (j.contains("unit")) {
c.unit = j.at("unit").get<std::string>();
}
}
void adl_serializer<everest::config::ConfigEntry>::to_json(nlohmann::json& j,
const everest::config::ConfigEntry& entry) {
std::visit([&j](auto&& arg) { j = arg; }, entry);
}
void adl_serializer<everest::config::ConfigEntry>::from_json(const nlohmann::json& j,
everest::config::ConfigEntry& entry) {
if (j.is_boolean()) {
entry = j.get<bool>();
} else if (j.is_number_integer()) {
entry = j.get<int>();
} else if (j.is_number_float()) {
entry = j.get<double>();
} else if (j.is_string()) {
entry = j.get<std::string>();
} else {
throw std::runtime_error("Unsupported JSON type for ConfigEntry");
}
}
void adl_serializer<everest::config::ConfigurationParameter>::to_json(
nlohmann::json& j, const everest::config::ConfigurationParameter& p) {
j["name"] = p.name;
j["value"] = p.value;
j["characteristics"] = p.characteristics;
}
void adl_serializer<everest::config::ConfigurationParameter>::from_json(const nlohmann::json& j,
everest::config::ConfigurationParameter& p) {
p.name = j.at("name").get<std::string>();
p.value = j.at("value").get<everest::config::ConfigEntry>();
p.characteristics = j.at("characteristics").get<everest::config::ConfigurationParameterCharacteristics>();
}
void adl_serializer<everest::config::ModuleConfig>::to_json(nlohmann::json& j, const everest::config::ModuleConfig& m) {
j["standalone"] = m.standalone;
j["module_name"] = m.module_name;
j["module_id"] = m.module_id;
if (m.capabilities.has_value()) {
j["capabilities"] = m.capabilities.value();
}
if (m.telemetry_config.has_value()) {
j["telemetry_config"] = m.telemetry_config.value();
}
j["configuration_parameters"] = m.configuration_parameters;
j["telemetry_enabled"] = m.telemetry_enabled;
j["connections"] = m.connections;
j["mapping"] = m.mapping;
j["access"] = m.access;
}
void adl_serializer<everest::config::ModuleConfig>::from_json(const nlohmann::json& j,
everest::config::ModuleConfig& m) {
m.standalone = j.at("standalone").get<bool>();
m.module_name = j.at("module_name").get<std::string>();
m.module_id = j.at("module_id").get<std::string>();
if (j.contains("capabilities")) {
m.capabilities = j.at("capabilities").get<std::vector<std::string>>();
}
if (j.contains("telemetry_config")) {
m.telemetry_config = j.at("telemetry_config").get<TelemetryConfig>();
}
m.configuration_parameters = j.at("configuration_parameters").get<everest::config::ModuleConfigurationParameters>();
m.telemetry_enabled = j.at("telemetry_enabled").get<bool>();
m.connections = j.at("connections").get<everest::config::ModuleConnections>();
m.mapping = j.at("mapping").get<ModuleTierMappings>();
if (j.contains("access")) {
m.access = j.at("access").get<everest::config::Access>();
}
}
NLOHMANN_JSON_NAMESPACE_END

View File

@@ -0,0 +1,109 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <exception>
#include <everest/logging.hpp>
#include <utils/config/storage.hpp>
#include <utils/config/storage_userconfig.hpp>
#include <utils/conversions.hpp>
#include <utils/yaml_loader.hpp>
namespace everest::config {
UserConfigStorage::UserConfigStorage(const fs::path& user_config_path) : user_config_path(user_config_path) {
try {
if (fs::exists(user_config_path)) {
this->user_config = Everest::load_yaml(user_config_path);
}
} catch (const std::exception& e) {
EVLOG_error << "Could not load user-config at " << user_config_path.string() << ": " << e.what();
}
}
GenericResponseStatus UserConfigStorage::write_module_configs(const ModuleConfigurations& /*module_configs*/) {
return GenericResponseStatus::Failed;
}
GenericResponseStatus UserConfigStorage::write_settings(const Everest::ManagerSettings& /*manager_settings*/) {
return GenericResponseStatus::Failed;
}
GenericResponseStatus UserConfigStorage::wipe() {
try {
this->user_config = nlohmann::json::object();
Everest::save_yaml(this->user_config, this->user_config_path);
} catch (const std::exception& e) {
EVLOG_error << "Could not save user-config to " << this->user_config_path.string() << ": " << e.what();
return GenericResponseStatus::Failed;
}
return GenericResponseStatus::OK;
}
GetModuleConfigsResponse UserConfigStorage::get_module_configs() {
GetModuleConfigsResponse response;
response.status = GenericResponseStatus::Failed;
return response;
}
GetSettingsResponse UserConfigStorage::get_settings() {
GetSettingsResponse response;
response.status = GenericResponseStatus::Failed;
return response;
}
GetModuleConfigurationResponse UserConfigStorage::get_module_config(const std::string& /*module_id*/) {
GetModuleConfigurationResponse response;
response.status = GenericResponseStatus::Failed;
return response;
}
GetConfigurationParameterResponse
UserConfigStorage::get_configuration_parameter(const ConfigurationParameterIdentifier& /*identifier*/) {
GetConfigurationParameterResponse response;
response.status = GetSetResponseStatus::Failed;
return response;
}
GetSetResponseStatus
UserConfigStorage::update_configuration_parameter(const ConfigurationParameterIdentifier& /*identifier*/,
const std::string& /*value*/) {
return GetSetResponseStatus::Failed;
}
GetSetResponseStatus
UserConfigStorage::write_configuration_parameter(const ConfigurationParameterIdentifier& identifier,
const ConfigurationParameterCharacteristics characteristics,
const std::string& value) {
// TODO: expect module_id etc. to be valid already
if (not this->user_config.contains("active_modules")) {
this->user_config["active_modules"] = nlohmann::json::object();
}
auto& active_modules = this->user_config.at("active_modules");
if (not active_modules.contains(identifier.module_id)) {
active_modules[identifier.module_id] = json::object();
}
auto& module_cfg = active_modules[identifier.module_id];
const auto impl_id = identifier.module_implementation_id.value_or("!module");
if (impl_id == "!module") {
if (not module_cfg.contains("config_module")) {
module_cfg["config_module"] = json::object();
}
auto& config_module = module_cfg["config_module"];
config_module[identifier.configuration_parameter_name] =
everest::config::parse_config_value(characteristics.datatype, value);
} else {
if (not module_cfg.contains("config_implementation")) {
module_cfg["config_implementation"] = json::object();
}
auto& config_implementation = module_cfg["config_implementation"];
if (not config_implementation.contains(impl_id)) {
config_implementation[impl_id] = json::object();
}
auto& config_implementation_impl_id = config_implementation[impl_id];
config_implementation_impl_id[identifier.configuration_parameter_name] =
everest::config::parse_config_value(characteristics.datatype, value);
}
Everest::save_yaml(this->user_config, this->user_config_path);
return GetSetResponseStatus::OK;
}
const nlohmann::json& UserConfigStorage::get_user_config() const {
return this->user_config;
}
} // namespace everest::config

View File

@@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <utils/config_cache.hpp>
NLOHMANN_JSON_NAMESPACE_BEGIN
void adl_serializer<Everest::ConfigCache>::to_json(nlohmann::json& j, const Everest::ConfigCache& c) {
j = {{"provides_impl", c.provides_impl}, {"cmds", c.cmds}};
}
void adl_serializer<Everest::ConfigCache>::from_json(const nlohmann::json& j, Everest::ConfigCache& c) {
c.provides_impl = j.at("provides_impl");
c.cmds = j.at("cmds");
}
NLOHMANN_JSON_NAMESPACE_END

View File

@@ -0,0 +1,645 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <exception>
#include <everest/logging.hpp>
#include <utils/config/storage.hpp>
#include <utils/config/types.hpp>
#include <utils/config_service.hpp>
#include <utils/conversions.hpp>
namespace Everest {
namespace config {
bool ModuleIdType::operator<(const ModuleIdType& rhs) const {
return (this->module_id < rhs.module_id ||
(this->module_id == rhs.module_id && this->module_type < rhs.module_type));
}
enum class AccessMethod {
Read,
Write
};
namespace {
bool access_allowed(const everest::config::Access& access, const std::string& origin, const std::string& target,
AccessMethod method) {
if (origin == target) {
// a module can always read and write its own config
return true;
}
if (not access.config.has_value()) {
return false;
}
const auto& config_access = access.config.value();
const auto& config_access_modules_it = config_access.modules.find(target);
switch (method) {
case AccessMethod::Read:
if (config_access.allow_global_read) {
return true;
}
if (config_access_modules_it != config_access.modules.end()) {
if (config_access_modules_it->second.allow_read) {
return true;
}
}
break;
case AccessMethod::Write:
if (config_access.allow_global_write) {
return true;
}
if (config_access_modules_it != config_access.modules.end()) {
if (config_access_modules_it->second.allow_write) {
return true;
}
}
break;
}
return false;
}
nlohmann::json get_module_config(const std::string& module_id, const ManagerConfig& config) {
const auto& module_configurations = config.get_module_configurations();
return get_serialized_module_config(module_id, module_configurations);
}
everest::config::ModuleConfigurationParameters
update_mutability(const everest::config::ModuleConfigurationParameters& configuration_parameters,
bool allow_set_read_only = false) {
if (not allow_set_read_only) {
return configuration_parameters;
}
everest::config::ModuleConfigurationParameters updated_configuration_parameters = configuration_parameters;
for (auto& [impl_id, config_params] : updated_configuration_parameters) {
for (auto& config_param : config_params) {
auto& mutability = config_param.characteristics.mutability;
if (mutability == everest::config::Mutability::ReadOnly) {
config_param.characteristics.mutability = everest::config::Mutability::ReadWrite;
}
}
}
return updated_configuration_parameters;
}
GetResponse handle_get_module_config(const std::string& origin, const ManagerConfig& config) {
GetResponse get_response;
get_response.type = GetType::Module;
get_response.data = get_module_config(origin, config);
return get_response;
}
Response handle_get_config_value(const GetRequest& get_request, const std::string& origin, ManagerConfig& config) {
Response response;
response.type = Type::Get;
GetResponse get_response;
get_response.type = GetType::Value;
if (not get_request.identifier.has_value()) {
response.status = ResponseStatus::Error; // no identifier is always an error in this case
response.status_info = "No identifier provided";
} else {
const auto identifier = get_request.identifier.value();
const auto& module_configs = config.get_module_configurations();
const everest::config::Access access = module_configs.at(origin).access;
if (not access_allowed(access, origin, identifier.module_id, AccessMethod::Read)) {
response.status = ResponseStatus::AccessDenied;
response.status_info =
fmt::format("Access to config item denied: {} cannot access {}", origin, identifier.module_id);
} else {
const auto get_config_value_response = config.get_config_value(identifier);
if (get_config_value_response.status == everest::config::GetSetResponseStatus::OK) {
if (get_config_value_response.configuration_parameter.has_value()) {
get_response.data = get_config_value_response.configuration_parameter.value();
response.status = ResponseStatus::Ok;
}
}
}
}
response.response = get_response;
return response;
}
GetResponse handle_get_all_configs(const std::string& origin, const ManagerConfig& config) {
// FIXME: we might run into size limits when the config is really large, split this up in the
// future!
GetResponse get_response;
get_response.type = GetType::All;
json all_configs = json::object();
const auto& module_configs = config.get_module_configurations();
everest::config::Access access = module_configs.at(origin).access;
for (const auto& [module_id, module_name] : config.get_module_names()) {
if (not access_allowed(access, origin, module_id, AccessMethod::Read)) {
// request.origin has no access to module_id.config
continue;
}
auto allow_set_read_only = false;
if (access.config.has_value()) {
const auto config_access = access.config.value();
allow_set_read_only = config_access.allow_set_read_only;
if (not allow_set_read_only) {
// check if allow_set_read_only is set for specific modules
const auto module_config_access_it = config_access.modules.find(module_id);
if (module_config_access_it != config_access.modules.end()) {
allow_set_read_only = module_config_access_it->second.allow_set_read_only;
}
}
}
all_configs[module_id] =
update_mutability(module_configs.at(module_id).configuration_parameters, allow_set_read_only);
}
get_response.data = all_configs;
return get_response;
}
GetResponse handle_get_all_mappings(const std::string& origin, const ManagerConfig& config) {
GetResponse get_response;
get_response.type = GetType::AllMappings;
json all_mappings = json::object();
const auto& module_configs = config.get_module_configurations();
const everest::config::Access access = module_configs.at(origin).access;
for (const auto& [module_id, module_name] : config.get_module_names()) {
if (not access_allowed(access, origin, module_id, AccessMethod::Read)) {
// request.origin has no access to module_id.mappings
continue;
}
all_mappings[module_id] = module_configs.at(module_id).mapping;
}
get_response.data = all_mappings;
return get_response;
}
Response handle_set_request(const SetRequest& set_request, const std::string& origin, ManagerConfig& config) {
Response response;
response.type = Type::Set;
SetResponse set_response;
set_response.status = SetResponseStatus::Rejected;
const auto& module_configs = config.get_module_configurations();
const everest::config::Access access = module_configs.at(origin).access;
if (not access_allowed(access, origin, set_request.identifier.module_id, AccessMethod::Write)) {
set_response.status = SetResponseStatus::Rejected;
response.status = ResponseStatus::AccessDenied;
response.status_info =
fmt::format("Access to config item denied: {} cannot access {}", origin, set_request.identifier.module_id);
} else {
// TODO: explicit input validation
const auto& target_module_config = module_configs.at(set_request.identifier.module_id);
everest::config::SetConfigStatus status = everest::config::SetConfigStatus::Rejected;
const auto impl_id = set_request.identifier.module_implementation_id.value_or(MODULE_IMPLEMENTATION_ID);
for (const auto& config_entry : target_module_config.configuration_parameters.at(impl_id)) {
if (config_entry.name == set_request.identifier.configuration_parameter_name) {
try {
const auto value = parse_config_value(config_entry.characteristics.datatype, set_request.value);
status = config.set_config_value(set_request.identifier, value);
response.status = ResponseStatus::Ok;
} catch (const std::exception& e) {
response.status = ResponseStatus::Error;
response.status_info = fmt::format("Could not set config entry {} of module {}: {}",
set_request.identifier.configuration_parameter_name,
set_request.identifier.module_id, e.what());
}
break;
}
}
set_response.status = conversions::set_config_status_to_set_response_status(status);
}
response.response = set_response;
return response;
}
} // namespace
ConfigServiceClient::ConfigServiceClient(std::shared_ptr<MQTTAbstraction> mqtt_abstraction,
const std::string& module_id,
const std::map<std::string, std::string, std::less<>>& module_names) :
mqtt_abstraction(mqtt_abstraction), origin(module_id), module_names(module_names) {
}
std::map<ModuleIdType, everest::config::ModuleConfigurationParameters> ConfigServiceClient::get_module_configs() {
Request get_request;
get_request.type = Type::Get;
get_request.request = GetRequest{GetType::All};
get_request.origin = this->origin;
MQTTRequest mqtt_request;
mqtt_request.response_topic =
fmt::format("{}modules/{}/response", mqtt_abstraction->get_everest_prefix(), get_request.origin);
mqtt_request.request_topic = fmt::format("{}config/request", mqtt_abstraction->get_everest_prefix());
mqtt_request.request_data = json(get_request).dump();
try {
Response response = mqtt_abstraction->get(mqtt_request, mqtt_get_config_retries);
if (response.status != ResponseStatus::Ok) {
EVLOG_error << "Could not get module configs via MQTT";
return {};
}
GetResponse get_response = std::get<GetResponse>(response.response);
std::map<ModuleIdType, everest::config::ModuleConfigurationParameters> module_configs;
for (const auto& [module_id, config_maps] : get_response.data.items()) {
ModuleIdType module_id_type;
module_id_type.module_id = module_id;
module_id_type.module_type = module_names.at(module_id);
module_configs[module_id_type] = config_maps;
}
return module_configs;
} catch (const std::exception& e) {
EVLOG_error << "Could not get module configs via MQTT: " << e.what();
return {};
}
}
std::map<std::string, ModuleTierMappings> ConfigServiceClient::get_mappings() {
Request get_request;
get_request.type = Type::Get;
get_request.request = GetRequest{GetType::AllMappings};
get_request.origin = this->origin;
MQTTRequest mqtt_request;
mqtt_request.response_topic =
fmt::format("{}modules/{}/response", mqtt_abstraction->get_everest_prefix(), get_request.origin);
mqtt_request.request_topic = fmt::format("{}config/request", mqtt_abstraction->get_everest_prefix());
mqtt_request.request_data = json(get_request).dump();
try {
Response response = mqtt_abstraction->get(mqtt_request, mqtt_get_config_retries);
if (response.status != ResponseStatus::Ok) {
EVLOG_error << "Could not get mappings configs via MQTT";
return {};
}
GetResponse get_response = std::get<GetResponse>(response.response);
std::map<std::string, ModuleTierMappings> mappings;
for (const auto& [module_id, mapping] : get_response.data.items()) {
mappings[module_id] = everest::config::parse_mapping(mapping);
}
return mappings;
} catch (const std::exception& e) {
EVLOG_error << "Could not get mappings configs via MQTT: " << e.what();
return {};
}
}
SetConfigResult
ConfigServiceClient::set_config_value(const everest::config::ConfigurationParameterIdentifier& identifier,
const std::string& value) {
SetConfigResult result;
Request request;
request.type = Type::Set;
request.origin = this->origin;
SetRequest set_request;
set_request.identifier = identifier;
set_request.value = value;
request.request = set_request;
try {
MQTTRequest mqtt_request;
mqtt_request.response_topic =
fmt::format("{}modules/{}/response", mqtt_abstraction->get_everest_prefix(), request.origin);
mqtt_request.request_topic = fmt::format("{}config/request", mqtt_abstraction->get_everest_prefix());
mqtt_request.request_data = json(request).dump();
const Response response = mqtt_abstraction->get(mqtt_request, mqtt_get_config_retries);
result.status = response.status;
result.status_info = response.status_info;
if (response.status == ResponseStatus::Ok) {
if (response.type.has_value() and response.type.value() == Type::Set) {
const SetResponse set_response = std::get<SetResponse>(response.response);
result.set_status = conversions::set_response_status_to_set_config_status(set_response.status);
}
}
return result;
} catch (const std::exception& e) {
EVLOG_info << "Could not set config value: " << identifier.module_id << ": "
<< identifier.module_implementation_id.value_or(MODULE_IMPLEMENTATION_ID) << ": "
<< identifier.configuration_parameter_name << ": " << e.what();
result.status = ResponseStatus::Error;
}
return result;
}
GetConfigResult
ConfigServiceClient::get_config_value(const everest::config::ConfigurationParameterIdentifier& identifier) {
GetConfigResult result;
Request request;
request.type = Type::Get;
request.origin = this->origin;
GetRequest get_request;
get_request.type = GetType::Value;
get_request.identifier = identifier;
request.request = get_request;
try {
MQTTRequest mqtt_request;
mqtt_request.response_topic =
fmt::format("{}modules/{}/response", mqtt_abstraction->get_everest_prefix(), request.origin);
mqtt_request.request_topic = fmt::format("{}config/request", mqtt_abstraction->get_everest_prefix());
mqtt_request.request_data = json(request).dump();
const Response response = mqtt_abstraction->get(mqtt_request, mqtt_get_config_retries);
result.status = response.status;
result.status_info = response.status_info;
if (response.status == ResponseStatus::Ok) {
if (response.type.has_value() and response.type.value() == Type::Get) {
const GetResponse get_response = std::get<GetResponse>(response.response);
result.configuration_parameter = get_response.data;
}
}
} catch (const std::exception& e) {
EVLOG_info << "Could not get config value: " << identifier.module_id << ": "
<< identifier.module_implementation_id.value_or(MODULE_IMPLEMENTATION_ID) << ": "
<< identifier.configuration_parameter_name << ": " << e.what();
}
return result;
}
ConfigService::ConfigService(MQTTAbstraction& mqtt_abstraction, std::shared_ptr<ManagerConfig> config) :
mqtt_abstraction(mqtt_abstraction), config(config) {
// TODO: thread-safe?
const Handler global_config_request_handler = [&mqtt_abstraction, config](const std::string& /*topic*/,
const nlohmann::json& data) {
Response response;
response.status = ResponseStatus::Error;
try {
Request request = data;
response.type = request.type;
const auto response_topic =
fmt::format("{}modules/{}/response", mqtt_abstraction.get_everest_prefix(), request.origin);
if (request.type == Type::Get) {
const GetRequest get_request = std::get<GetRequest>(request.request);
if (get_request.type == GetType::Module) {
response.response = handle_get_module_config(request.origin, *config);
response.status = ResponseStatus::Ok;
} else if (get_request.type == GetType::Value) {
response = handle_get_config_value(get_request, request.origin, *config);
} else if (get_request.type == GetType::All) {
response.response = handle_get_all_configs(request.origin, *config);
response.status = ResponseStatus::Ok;
} else if (get_request.type == GetType::AllMappings) {
response.response = handle_get_all_mappings(request.origin, *config);
response.status = ResponseStatus::Ok;
}
} else if (request.type == Type::Set) {
response = handle_set_request(std::get<SetRequest>(request.request), request.origin, *config);
}
MqttMessagePayload payload{MqttMessageType::GetConfigResponse, response};
mqtt_abstraction.publish(response_topic, payload, QOS::QOS2);
} catch (const std::exception& e) {
EVLOG_error << "Exception during handling of request: " << e.what();
} catch (...) {
EVLOG_error << "Could not parse request: " << data.dump();
}
};
const std::string global_config_request_topic =
fmt::format("{}config/request", mqtt_abstraction.get_everest_prefix());
this->get_config_token = std::make_shared<TypedHandler>(HandlerType::GetConfig,
std::make_shared<Handler>(global_config_request_handler));
mqtt_abstraction.register_handler(global_config_request_topic, this->get_config_token, QOS::QOS2);
}
namespace conversions {
std::string type_to_string(Type type) {
switch (type) {
case Type::Get:
return "Get";
case Type::Set:
return "Set";
case Type::Unknown:
return "Unknown";
}
throw std::out_of_range("Could not convert Type to string");
}
Type string_to_type(const std::string& type) {
if (type == "Get") {
return Type::Get;
} else if (type == "Set") {
return Type::Set;
} else if (type == "Unknown") {
return Type::Unknown;
}
throw std::out_of_range("Could not convert " + type + " to Type");
}
std::string get_type_to_string(GetType type) {
switch (type) {
case GetType::All:
return "All";
case GetType::Module:
return "Module";
case GetType::Value:
return "Value";
case GetType::AllMappings:
return "AllMappings";
case GetType::Unknown:
return "Unknown";
}
throw std::out_of_range("Could not convert GetType to string");
}
GetType string_to_get_type(const std::string& type) {
if (type == "All") {
return GetType::All;
} else if (type == "Module") {
return GetType::Module;
} else if (type == "Value") {
return GetType::Value;
} else if (type == "AllMappings") {
return GetType::AllMappings;
} else if (type == "Unknown") {
return GetType::Unknown;
}
throw std::out_of_range("Could not convert " + type + " to GetType");
}
std::string response_status_to_string(ResponseStatus status) {
switch (status) {
case ResponseStatus::Ok:
return "Ok";
case ResponseStatus::Error:
return "Error";
case ResponseStatus::AccessDenied:
return "AccessDenied";
}
throw std::out_of_range("Could not convert ResponseStatus to string");
}
ResponseStatus string_to_response_status(const std::string& status) {
if (status == "Ok") {
return ResponseStatus::Ok;
} else if (status == "Error") {
return ResponseStatus::Error;
} else if (status == "AccessDenied") {
return ResponseStatus::AccessDenied;
}
throw std::out_of_range("Could not convert " + status + " to ResponseStatus");
}
std::string set_response_status_to_string(SetResponseStatus status) {
switch (status) {
case SetResponseStatus::Accepted:
return "Accepted";
case SetResponseStatus::Rejected:
return "Rejected";
case SetResponseStatus::RebootRequired:
return "RebootRequired";
}
throw std::out_of_range("Could not convert SetResponseStatus to string");
}
SetResponseStatus string_to_set_response_status(const std::string& status) {
if (status == "Accepted") {
return SetResponseStatus::Accepted;
} else if (status == "Rejected") {
return SetResponseStatus::Rejected;
} else if (status == "RebootRequired") {
return SetResponseStatus::RebootRequired;
}
throw std::out_of_range("Could not convert " + status + " to SetResponseStatus");
}
everest::config::SetConfigStatus set_response_status_to_set_config_status(SetResponseStatus status) {
switch (status) {
case SetResponseStatus::Accepted:
return everest::config::SetConfigStatus::Accepted;
case SetResponseStatus::Rejected:
return everest::config::SetConfigStatus::Rejected;
case SetResponseStatus::RebootRequired:
return everest::config::SetConfigStatus::RebootRequired;
}
throw std::out_of_range("Could not convert SetResponseStatus to SetConfigStatus");
}
SetResponseStatus set_config_status_to_set_response_status(everest::config::SetConfigStatus status) {
switch (status) {
case everest::config::SetConfigStatus::Accepted:
return SetResponseStatus::Accepted;
case everest::config::SetConfigStatus::Rejected:
return SetResponseStatus::Rejected;
case everest::config::SetConfigStatus::RebootRequired:
return SetResponseStatus::RebootRequired;
}
throw std::out_of_range("Could not convert SetConfigStatus to SetResponseStatus");
}
} // namespace conversions
std::ostream& operator<<(std::ostream& os, const GetType& t) {
os << conversions::get_type_to_string(t);
return os;
}
void to_json(nlohmann::json& j, const GetRequest& r) {
j = {{"type", conversions::get_type_to_string(r.type)}};
if (r.identifier.has_value()) {
j["identifier"] = r.identifier.value();
}
}
void from_json(const nlohmann::json& j, GetRequest& r) {
r.type = conversions::string_to_get_type(j.at("type"));
if (j.contains("identifier")) {
r.identifier = j.at("identifier");
}
}
void to_json(nlohmann::json& j, const SetRequest& r) {
j = {{"value", r.value}};
j["identifier"] = nlohmann::json(r.identifier);
}
void from_json(const nlohmann::json& j, SetRequest& r) {
r.identifier = j.at("identifier");
r.value = j.at("value");
}
void to_json(nlohmann::json& j, const GetResponse& r) {
j = {{"type", conversions::get_type_to_string(r.type)}, {"data", r.data}};
}
void from_json(const nlohmann::json& j, GetResponse& r) {
r.type = conversions::string_to_get_type(j.at("type"));
r.data = j.at("data");
}
void to_json(nlohmann::json& j, const SetResponse& r) {
j = {{"status", conversions::set_response_status_to_string(r.status)}};
}
void from_json(const nlohmann::json& j, SetResponse& r) {
r.status = conversions::string_to_set_response_status(j.at("status"));
}
void to_json(nlohmann::json& j, const Request& r) {
j = {{"type", conversions::type_to_string(r.type)}, {"origin", r.origin}};
j["request"] = Everest::variant_to_json(r.request);
}
void from_json(const nlohmann::json& j, Request& r) {
r.type = conversions::string_to_type(j.at("type"));
if (r.type == Type::Get) {
r.request = j.at("request").get<GetRequest>();
} else if (r.type == Type::Set) {
r.request = j.at("request").get<SetRequest>();
}
r.origin = j.at("origin");
}
void to_json(nlohmann::json& j, const Response& r) {
j = {{"status", conversions::response_status_to_string(r.status)}, {"status_info", r.status_info}};
if (r.type.has_value()) {
j["type"] = conversions::type_to_string(r.type.value());
}
j["response"] = Everest::variant_to_json(r.response);
}
void from_json(const nlohmann::json& j, Response& r) {
r.status = conversions::string_to_response_status(j.at("status"));
r.status_info = j.at("status_info");
if (j.contains("type")) {
Type type = conversions::string_to_type(j.at("type"));
r.type = type;
if (type == Type::Get) {
r.response = j.at("response").get<GetResponse>();
} else if (type == Type::Set) {
r.response = j.at("response").get<SetResponse>();
}
}
}
} // namespace config
} // namespace Everest
NLOHMANN_JSON_NAMESPACE_BEGIN
void adl_serializer<everest::config::ConfigurationParameterIdentifier>::to_json(
nlohmann::json& j, const everest::config::ConfigurationParameterIdentifier& c) {
j = {{"module_id", c.module_id}, {"configuration_parameter_name", c.configuration_parameter_name}};
if (c.module_implementation_id.has_value()) {
j["module_implementation_id"] = c.module_implementation_id.value();
}
}
void adl_serializer<everest::config::ConfigurationParameterIdentifier>::from_json(
const nlohmann::json& j, everest::config::ConfigurationParameterIdentifier& c) {
c.module_id = j.at("module_id");
c.configuration_parameter_name = j.at("configuration_parameter_name");
if (j.contains("module_implementation_id")) {
c.module_implementation_id = j.at("module_implementation_id");
}
}
NLOHMANN_JSON_NAMESPACE_END

View File

@@ -0,0 +1,61 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <utils/conversions.hpp>
namespace Everest {
namespace conversions {
constexpr auto CMD_ERROR_TYPE_MESSAGE_PARSING_ERROR = "MessageParsingError";
constexpr auto CMD_ERROR_TYPE_SCHEMA_VALIDATION_ERROR = "SchemaValidationError";
constexpr auto CMD_ERROR_TYPE_HANDLER_EXCEPTION = "HandlerException";
constexpr auto CMD_ERROR_TYPE_CMD_TIMEOUT = "CmdTimeout";
constexpr auto CMD_ERROR_TYPE_SHUTDOWN = "Shutdown";
constexpr auto CMD_ERROR_TYPE_NOT_READY = "NotReady";
std::string cmd_error_type_to_string(CmdErrorType cmd_error) {
switch (cmd_error) {
case CmdErrorType::MessageParsingError:
return CMD_ERROR_TYPE_MESSAGE_PARSING_ERROR;
case CmdErrorType::SchemaValidationError:
return CMD_ERROR_TYPE_SCHEMA_VALIDATION_ERROR;
case CmdErrorType::HandlerException:
return CMD_ERROR_TYPE_HANDLER_EXCEPTION;
case CmdErrorType::CmdTimeout:
return CMD_ERROR_TYPE_CMD_TIMEOUT;
case CmdErrorType::Shutdown:
return CMD_ERROR_TYPE_SHUTDOWN;
case CmdErrorType::NotReady:
return CMD_ERROR_TYPE_NOT_READY;
}
throw std::runtime_error("Unknown CmdError");
}
CmdErrorType string_to_cmd_error_type(const std::string& cmd_error_string) {
if (cmd_error_string == CMD_ERROR_TYPE_MESSAGE_PARSING_ERROR) {
return CmdErrorType::MessageParsingError;
} else if (cmd_error_string == CMD_ERROR_TYPE_SCHEMA_VALIDATION_ERROR) {
return CmdErrorType::SchemaValidationError;
} else if (cmd_error_string == CMD_ERROR_TYPE_HANDLER_EXCEPTION) {
return CmdErrorType::HandlerException;
} else if (cmd_error_string == CMD_ERROR_TYPE_CMD_TIMEOUT) {
return CmdErrorType::CmdTimeout;
} else if (cmd_error_string == CMD_ERROR_TYPE_SHUTDOWN) {
return CmdErrorType::Shutdown;
} else if (cmd_error_string == CMD_ERROR_TYPE_NOT_READY) {
return CmdErrorType::NotReady;
}
throw std::runtime_error("Unknown CmdError");
}
} // namespace conversions
void to_json(nlohmann::json& j, const CmdResultError& e) {
j = {{conversions::ERROR_TYPE, conversions::cmd_error_type_to_string(e.event)}, {conversions::ERROR_MSG, e.msg}};
}
void from_json(const nlohmann::json& j, CmdResultError& e) {
e.event = conversions::string_to_cmd_error_type(j.at(conversions::ERROR_TYPE));
e.msg = j.at(conversions::ERROR_MSG);
}
} // namespace Everest

View File

@@ -0,0 +1,101 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
#include <charconv>
#include <string_view>
#include <utils/date.hpp>
namespace Everest {
namespace Date {
std::string to_rfc3339(const std::chrono::time_point<date::utc_clock>& t) {
return date::format("%FT%TZ", std::chrono::time_point_cast<std::chrono::milliseconds>(t));
}
std::chrono::time_point<date::utc_clock> from_rfc3339_slow(const std::string& t) {
std::istringstream infile{t};
std::chrono::time_point<date::utc_clock> tp;
infile >> date::parse("%FT%T", tp);
return tp;
}
constexpr std::size_t typical_len = 24;
std::chrono::time_point<date::utc_clock> from_rfc3339(const std::string& t) {
std::string_view input(t);
// attempt a slow parse if input size doesn't match our expectations
if (input.size() < typical_len) {
return from_rfc3339_slow(t);
}
std::string_view y_str = input.substr(0, 4);
std::string_view m_str = input.substr(5, 2);
std::string_view d_str = input.substr(8, 2);
std::string_view h_str = input.substr(11, 2);
std::string_view M_str = input.substr(14, 2);
std::string_view s_str = input.substr(17, 2);
std::string_view ms_str = input.substr(20, 3);
int y;
{
auto [ptr, ec] = std::from_chars(y_str.data(), y_str.data() + y_str.size(), y);
if (ec != std::errc{}) {
return from_rfc3339_slow(t);
}
}
int m;
{
auto [ptr, ec] = std::from_chars(m_str.data(), m_str.data() + m_str.size(), m);
if (ec != std::errc{}) {
return from_rfc3339_slow(t);
}
}
int d;
{
auto [ptr, ec] = std::from_chars(d_str.data(), d_str.data() + d_str.size(), d);
if (ec != std::errc{}) {
return from_rfc3339_slow(t);
}
}
int h;
{
auto [ptr, ec] = std::from_chars(h_str.data(), h_str.data() + h_str.size(), h);
if (ec != std::errc{}) {
return from_rfc3339_slow(t);
}
}
int M;
{
auto [ptr, ec] = std::from_chars(M_str.data(), M_str.data() + M_str.size(), M);
if (ec != std::errc{}) {
return from_rfc3339_slow(t);
}
}
int s;
{
auto [ptr, ec] = std::from_chars(s_str.data(), s_str.data() + s_str.size(), s);
if (ec != std::errc{}) {
return from_rfc3339_slow(t);
}
}
int ms;
{
auto [ptr, ec] = std::from_chars(ms_str.data(), ms_str.data() + ms_str.size(), ms);
if (ec != std::errc{}) {
return from_rfc3339_slow(t);
}
}
using namespace date;
using namespace std::chrono;
return utc_clock::from_sys(sys_days{year{y} / m / d} + hours{h} + minutes{M} + seconds{s} + milliseconds{ms});
}
} // namespace Date
} // namespace Everest

View File

@@ -0,0 +1,119 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <utils/error.hpp>
#include <everest/helpers/helpers.hpp>
#include <everest/logging.hpp>
namespace Everest {
namespace error {
UUID::UUID() {
uuid = everest::helpers::get_uuid();
}
UUID::UUID(const std::string& uuid_) : uuid(uuid_) {
}
bool UUID::operator<(const UUID& other) const {
return uuid < other.uuid;
}
bool UUID::operator==(const UUID& other) const {
return this->uuid == other.uuid;
}
bool UUID::operator!=(const UUID& other) const {
return !(*this == other);
}
std::string UUID::to_string() const {
return uuid;
}
Error::Error(const ErrorType& type_, const ErrorSubType& sub_type_, const std::string& message_,
const std::string& description_, const ImplementationIdentifier& origin_, const std::string& vendor_id_,
const Severity& severity_, const time_point& timestamp_, const UUID& uuid_, const State& state_) :
type(type_),
sub_type(sub_type_),
message(message_),
description(description_),
origin(origin_),
vendor_id(vendor_id_),
severity(severity_),
timestamp(timestamp_),
uuid(uuid_),
state(state_) {
}
Error::Error(const ErrorType& type_, const ErrorSubType& sub_type_, const std::string& message_,
const std::string& description_, const ImplementationIdentifier& origin_, const Severity& severity_) :
Error(type_, sub_type_, message_, description_, origin_, UTILS_ERROR_DEFAULTS_VENDOR_ID, severity_,
UTILS_ERROR_DEFAULTS_TIMESTAMP, UTILS_ERROR_DEFAULTS_UUID) {
}
Error::Error(const ErrorType& type_, const ErrorSubType& sub_type_, const std::string& message_,
const std::string& description_, const std::string& origin_module_,
const std::string& origin_implementation_, const Severity& severity_) :
Error(type_, sub_type_, message_, description_,
ImplementationIdentifier(origin_module_, origin_implementation_, std::nullopt), severity_) {
}
Error::Error() :
Error(UTILS_ERROR_DEFAULTS_TYPE, UTILS_ERROR_DEFAULTS_SUB_TYPE, UTILS_ERROR_DEFAULTS_MESSAGE,
UTILS_ERROR_DEFAULTS_DESCRIPTION, UTILS_ERROR_DEFAULTS_ORIGIN, UTILS_ERROR_DEFAULTS_SEVERITY) {
}
std::string severity_to_string(const Severity& s) {
switch (s) {
case Severity::Low:
return "Low";
case Severity::Medium:
return "Medium";
case Severity::High:
return "High";
}
EVLOG_error << "No known string conversion for provided enum of type Severity. Defaulting to High.";
return "High";
}
Severity string_to_severity(const std::string& s) {
if (s == "Low") {
return Severity::Low;
} else if (s == "Medium") {
return Severity::Medium;
} else if (s == "High") {
return Severity::High;
}
EVLOG_error << "Provided string " << s << " could not be converted to enum of type Severity. Defaulting to High.";
return Severity::High;
}
std::string state_to_string(const State& s) {
switch (s) {
case State::Active:
return "Active";
case State::ClearedByModule:
return "ClearedByModule";
case State::ClearedByReboot:
return "ClearedByReboot";
}
EVLOG_error << "No known string conversion for provided enum of type State. Defaulting to Active.";
return "Active";
}
State string_to_state(const std::string& s) {
if (s == "Active") {
return State::Active;
} else if (s == "ClearedByModule") {
return State::ClearedByModule;
} else if (s == "ClearedByReboot") {
return State::ClearedByReboot;
}
EVLOG_error << "Provided string " << s << " could not be converted to enum of type State. Defaulting to Active.";
return State::Active;
}
} // namespace error
} // namespace Everest

View File

@@ -0,0 +1,110 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <utils/error/error_database_map.hpp>
#include <everest/logging.hpp>
#include <utils/error.hpp>
#include <utils/error/error_json.hpp>
#include <algorithm>
namespace Everest {
namespace error {
void ErrorDatabaseMap::add_error(ErrorPtr error) {
const std::lock_guard<std::mutex> lock(this->errors_mutex);
if (this->errors.find(error->uuid) != this->errors.end()) {
std::stringstream ss;
ss << "Error with handle " << error->uuid.to_string() << " already exists in ErrorDatabaseMap." << std::endl;
ss << "Error object: " << nlohmann::json(*error).dump(2);
EVLOG_error << ss.str();
return;
}
this->errors[error->uuid] = error;
}
std::list<ErrorPtr> ErrorDatabaseMap::get_errors(const std::list<ErrorFilter>& filters) const {
const std::lock_guard<std::mutex> lock(this->errors_mutex);
return this->get_errors_no_mutex(filters);
}
std::list<ErrorPtr> ErrorDatabaseMap::get_errors_no_mutex(const std::list<ErrorFilter>& filters) const {
BOOST_LOG_FUNCTION();
std::list<ErrorPtr> result;
std::transform(this->errors.begin(), this->errors.end(), std::back_inserter(result),
[](const std::pair<ErrorHandle, ErrorPtr>& entry) { return entry.second; });
for (const ErrorFilter& filter : filters) {
std::function<bool(const ErrorPtr&)> pred;
switch (filter.get_filter_type()) {
case FilterType::State: {
pred = []([[maybe_unused]] const ErrorPtr& error) { return false; };
EVLOG_error << "ErrorDatabaseMap does not support StateFilter. Ignoring.";
} break;
case FilterType::Origin: {
pred = [&filter](const ErrorPtr& error) { return error->origin != filter.get_origin_filter(); };
} break;
case FilterType::Type: {
pred = [&filter](const ErrorPtr& error) { return error->type != filter.get_type_filter().value; };
} break;
case FilterType::Severity: {
pred = [&filter](const ErrorPtr& error) {
switch (filter.get_severity_filter()) {
case SeverityFilter::LOW_GE: {
return error->severity < Severity::Low;
} break;
case SeverityFilter::MEDIUM_GE: {
return error->severity < Severity::Medium;
} break;
case SeverityFilter::HIGH_GE: {
return error->severity < Severity::High;
} break;
}
EVLOG_error << "No known condition for provided enum of type SeverityFilter.";
return false;
};
} break;
case FilterType::TimePeriod: {
pred = [&filter](const ErrorPtr& error) {
return error->timestamp < filter.get_time_period_filter().from ||
error->timestamp > filter.get_time_period_filter().to;
};
} break;
case FilterType::Handle: {
pred = [&filter](const ErrorPtr& error) { return error->uuid != filter.get_handle_filter(); };
} break;
case FilterType::SubType: {
pred = [&filter](const ErrorPtr& error) { return error->sub_type != filter.get_sub_type_filter().value; };
} break;
case FilterType::VendorId: {
pred = [&filter](const ErrorPtr& error) { return error->vendor_id != filter.get_vendor_id_filter().value; };
} break;
default:
EVLOG_error << "No known pred for provided enum of type FilterType. Ignoring.";
return result;
}
result.remove_if(pred);
}
return result;
}
std::list<ErrorPtr> ErrorDatabaseMap::edit_errors(const std::list<ErrorFilter>& filters, EditErrorFunc edit_func) {
const std::lock_guard<std::mutex> lock(this->errors_mutex);
std::list<ErrorPtr> result = this->get_errors_no_mutex(filters);
for (const ErrorPtr& error : result) {
edit_func(error);
}
return result;
}
std::list<ErrorPtr> ErrorDatabaseMap::remove_errors(const std::list<ErrorFilter>& filters) {
BOOST_LOG_FUNCTION();
const EditErrorFunc remove_func = [this](const ErrorPtr& error) { this->errors.erase(error->uuid); };
return this->edit_errors(filters, remove_func);
}
} // namespace error
} // namespace Everest

View File

@@ -0,0 +1,185 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <utility>
#include <utils/error/error_factory.hpp>
#include <utils/error/error_type_map.hpp>
namespace Everest {
namespace error {
ErrorFactory::ErrorFactory(std::shared_ptr<ErrorTypeMap> error_type_map_) :
ErrorFactory(error_type_map_, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt,
std::nullopt) {
}
ErrorFactory::ErrorFactory(std::shared_ptr<ErrorTypeMap> error_type_map_, ImplementationIdentifier default_origin_) :
ErrorFactory(error_type_map_, default_origin_, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt,
std::nullopt) {
}
ErrorFactory::ErrorFactory(std::shared_ptr<ErrorTypeMap> error_type_map_, ImplementationIdentifier default_origin_,
Severity default_severity_) :
ErrorFactory(error_type_map_, default_origin_, default_severity_, std::nullopt, std::nullopt, std::nullopt,
std::nullopt, std::nullopt) {
}
ErrorFactory::ErrorFactory(std::shared_ptr<ErrorTypeMap> error_type_map_,
std::optional<ImplementationIdentifier> default_origin_,
std::optional<Severity> default_severity_, std::optional<State> default_state_,
std::optional<ErrorType> default_type_, std::optional<ErrorSubType> default_sub_type_,
std::optional<std::string> default_message_, std::optional<std::string> default_vendor_id_) :
error_type_map(error_type_map_),
default_origin(std::move(default_origin_)),
default_severity(default_severity_),
default_state(default_state_),
default_type(std::move(default_type_)),
default_sub_type(std::move(default_sub_type_)),
default_message(std::move(default_message_)),
default_vendor_id(std::move(default_vendor_id_)) {
}
Error ErrorFactory::create_error() const {
Error error;
if (default_origin.has_value()) {
error.origin = default_origin.value();
}
if (default_severity.has_value()) {
error.severity = default_severity.value();
}
if (default_state.has_value()) {
error.state = default_state.value();
}
if (default_type.has_value()) {
error.type = default_type.value();
}
if (default_sub_type.has_value()) {
error.sub_type = default_sub_type.value();
}
if (default_message.has_value()) {
error.message = default_message.value();
}
if (default_vendor_id.has_value()) {
error.vendor_id = default_vendor_id.value();
}
set_description(error);
return error;
}
Error ErrorFactory::create_error(const ErrorType& type, const ErrorSubType& sub_type,
const std::string& message) const {
Error error;
error.type = type;
error.sub_type = sub_type;
error.message = message;
if (default_origin.has_value()) {
error.origin = default_origin.value();
}
if (default_severity.has_value()) {
error.severity = default_severity.value();
}
if (default_state.has_value()) {
error.state = default_state.value();
}
if (default_vendor_id.has_value()) {
error.vendor_id = default_vendor_id.value();
}
set_description(error);
return error;
}
Error ErrorFactory::create_error(const ErrorType& type, const ErrorSubType& sub_type, const std::string& message,
const Severity severity) const {
Error error;
error.type = type;
error.sub_type = sub_type;
error.message = message;
error.severity = severity;
if (default_origin.has_value()) {
error.origin = default_origin.value();
}
if (default_state.has_value()) {
error.state = default_state.value();
}
if (default_vendor_id.has_value()) {
error.vendor_id = default_vendor_id.value();
}
set_description(error);
return error;
}
Error ErrorFactory::create_error(const ErrorType& type, const ErrorSubType& sub_type, const std::string& message,
const State state) const {
Error error;
error.type = type;
error.sub_type = sub_type;
error.message = message;
error.state = state;
if (default_origin.has_value()) {
error.origin = default_origin.value();
}
if (default_severity.has_value()) {
error.severity = default_severity.value();
}
if (default_vendor_id.has_value()) {
error.vendor_id = default_vendor_id.value();
}
set_description(error);
return error;
}
Error ErrorFactory::create_error(const ErrorType& type, const ErrorSubType& sub_type, const std::string& message,
const Severity severity, const State state) const {
Error error;
error.type = type;
error.sub_type = sub_type;
error.message = message;
error.severity = severity;
error.state = state;
if (default_origin.has_value()) {
error.origin = default_origin.value();
}
if (default_vendor_id.has_value()) {
error.vendor_id = default_vendor_id.value();
}
set_description(error);
return error;
}
void ErrorFactory::set_default_origin(const ImplementationIdentifier& origin) {
default_origin = origin;
}
void ErrorFactory::set_default_severity(Severity severity) {
default_severity = severity;
}
void ErrorFactory::set_default_state(State state) {
default_state = state;
}
void ErrorFactory::set_default_type(const ErrorType& type) {
default_type = type;
}
void ErrorFactory::set_default_sub_type(const ErrorSubType& sub_type) {
default_sub_type = sub_type;
}
void ErrorFactory::set_default_message(const std::string& message) {
default_message = message;
}
void ErrorFactory::set_default_vendor_id(const std::string& vendor_id) {
default_vendor_id = vendor_id;
}
void ErrorFactory::set_description(Error& error) const {
if (error.type == "") {
return;
}
error.description = error_type_map->get_description(error.type);
}
} // namespace error
} // namespace Everest

View File

@@ -0,0 +1,181 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <utils/error/error_filter.hpp>
#include <everest/exceptions.hpp>
#include <everest/logging.hpp>
namespace Everest {
namespace error {
std::string state_filter_to_string(const StateFilter& f) {
return state_to_string(f);
}
StateFilter string_to_state_filter(const std::string& s) {
return string_to_state(s);
}
std::string severity_filter_to_string(const SeverityFilter& f) {
switch (f) {
case SeverityFilter::LOW_GE:
return "LOW_GE";
case SeverityFilter::MEDIUM_GE:
return "MEDIUM_GE";
case SeverityFilter::HIGH_GE:
return "HIGH_GE";
}
EVLOG_error << "No known string conversion for provided enum of type SeverityFilter. Defaulting to HIGH_GE";
return "HIGH_GE";
}
SeverityFilter string_to_severity_filter(const std::string& s) {
if (s == "LOW_GE") {
return SeverityFilter::LOW_GE;
} else if (s == "MEDIUM_GE") {
return SeverityFilter::MEDIUM_GE;
} else if (s == "HIGH_GE") {
return SeverityFilter::HIGH_GE;
}
EVLOG_error << "Provided string " << s
<< " could not be converted to enum of type SeverityFilter. Defaulting to HIGH_GE";
return SeverityFilter::HIGH_GE;
}
TypeFilter::TypeFilter(const ErrorType& value_) : value(value_) {
}
SubTypeFilter::SubTypeFilter(const ErrorSubType& value_) : value(value_) {
}
VendorIdFilter::VendorIdFilter(const std::string& value_) : value(value_) {
}
std::string filter_type_to_string(const FilterType& f) {
switch (f) {
case FilterType::State:
return "State";
case FilterType::Origin:
return "Origin";
case FilterType::Type:
return "Type";
case FilterType::Severity:
return "Severity";
case FilterType::TimePeriod:
return "TimePeriod";
case FilterType::Handle:
return "Handle";
case FilterType::SubType:
return "SubType";
case FilterType::VendorId:
return "VendorId";
}
EVLOG_error << "No known string conversion for provided enum of type FilterType. Defaulting to Type";
return "Type";
}
FilterType string_to_filter_type(const std::string& s) {
if (s == "State") {
return FilterType::State;
} else if (s == "Origin") {
return FilterType::Origin;
} else if (s == "Type") {
return FilterType::Type;
} else if (s == "Severity") {
return FilterType::Severity;
} else if (s == "TimePeriod") {
return FilterType::TimePeriod;
} else if (s == "Handle") {
return FilterType::Handle;
} else if (s == "SubType") {
return FilterType::SubType;
} else if (s == "VendorId") {
return FilterType::VendorId;
}
EVLOG_error << "Provided string " << s << " could not be converted to enum of type FilterType. Deafulting to Type.";
return FilterType::Type;
}
ErrorFilter::ErrorFilter() = default;
ErrorFilter::ErrorFilter(const FilterVariant& filter_) : filter(filter_) {
}
FilterType ErrorFilter::get_filter_type() const {
if (filter.index() == 0) {
EVLOG_error << "Filter type is not set. Defaulting to 'FilterType::State'.";
return FilterType::State;
}
return static_cast<FilterType>(filter.index());
}
StateFilter ErrorFilter::get_state_filter() const {
if (this->get_filter_type() != FilterType::State) {
EVLOG_error << "Filter type is not StateFilter. Defaulting to 'StateFilter::Active'.";
return StateFilter::Active;
}
return std::get<StateFilter>(filter);
}
OriginFilter ErrorFilter::get_origin_filter() const {
if (this->get_filter_type() != FilterType::Origin) {
EVLOG_error << "Filter type is not OriginFilter. Defaulting to "
"'OriginFilter::ImplementationIdentifier(\"no-module-id-provided\", "
"\"no-implementation-id-provided\")'.";
return OriginFilter(
ImplementationIdentifier("no-module-id-provided", "no-implementation-id-provided", std::nullopt));
}
return std::get<OriginFilter>(filter);
}
TypeFilter ErrorFilter::get_type_filter() const {
if (this->get_filter_type() != FilterType::Type) {
EVLOG_error << "Filter type is not TypeFilter. Defaulting to 'TypeFilter(\"no-type-provided\")'.";
return TypeFilter("no-type-provided");
}
return std::get<TypeFilter>(filter);
}
SeverityFilter ErrorFilter::get_severity_filter() const {
if (this->get_filter_type() != FilterType::Severity) {
EVLOG_error << "Filter type is not SeverityFilter. Defaulting to 'SeverityFilter::HIGH_GE'.";
return SeverityFilter::HIGH_GE;
}
return std::get<SeverityFilter>(filter);
}
TimePeriodFilter ErrorFilter::get_time_period_filter() const {
if (this->get_filter_type() != FilterType::TimePeriod) {
EVLOG_error << "Filter type is not TimePeriodFilter. Defaulting to 'TimePeriodFilter{Error::time_point(), "
"Error::time_point()}'.";
return TimePeriodFilter{Error::time_point(), Error::time_point()};
}
return std::get<TimePeriodFilter>(filter);
}
HandleFilter ErrorFilter::get_handle_filter() const {
if (this->get_filter_type() != FilterType::Handle) {
EVLOG_error << "Filter type is not HandleFilter. Defaulting to 'HandleFilter()'.";
return HandleFilter();
}
return std::get<HandleFilter>(filter);
}
SubTypeFilter ErrorFilter::get_sub_type_filter() const {
if (this->get_filter_type() != FilterType::SubType) {
EVLOG_error << "Filter type is not SubTypeFilter. Defaulting to 'SubTypeFilter(\"no-sub-type-provided\")'.";
return SubTypeFilter("no-sub-type-provided");
}
return std::get<SubTypeFilter>(filter);
}
VendorIdFilter ErrorFilter::get_vendor_id_filter() const {
if (this->get_filter_type() != FilterType::VendorId) {
throw EverestBaseLogicError("Filter type is not VendorIdFilter.");
}
return std::get<VendorIdFilter>(filter);
}
} // namespace error
} // namespace Everest

View File

@@ -0,0 +1,151 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <utility>
#include <utils/error/error_manager_impl.hpp>
#include <utils/error.hpp>
#include <utils/error/error_database.hpp>
#include <utils/error/error_json.hpp>
#include <utils/error/error_type_map.hpp>
#include <everest/logging.hpp>
#include <list>
#include <memory>
namespace Everest {
namespace error {
ErrorManagerImpl::ErrorManagerImpl(std::shared_ptr<ErrorTypeMap> error_type_map_,
std::shared_ptr<ErrorDatabase> error_database_,
std::list<ErrorType> allowed_error_types_,
ErrorManagerImpl::PublishErrorFunc publish_raised_error_,
ErrorManagerImpl::PublishErrorFunc publish_cleared_error_,
const bool validate_error_types_) :
error_type_map(error_type_map_),
database(error_database_),
allowed_error_types(std::move(allowed_error_types_)),
publish_raised_error(std::move(publish_raised_error_)),
publish_cleared_error(std::move(publish_cleared_error_)),
validate_error_types(validate_error_types_) {
if (validate_error_types) {
for (const ErrorType& type : allowed_error_types) {
if (!error_type_map->has(type)) {
EVLOG_error << "Error type '" << type << "' in allowed_error_types is not defined, ignored.";
}
}
}
}
void ErrorManagerImpl::raise_error(const Error& error) {
if (validate_error_types) {
if (std::find(allowed_error_types.begin(), allowed_error_types.end(), error.type) ==
allowed_error_types.end()) {
std::stringstream ss;
ss << "Error type " << error.type << " is not allowed to be raised. Ignoring.";
ss << std::endl << "Error object: " << nlohmann::json(error).dump(2);
EVLOG_error << ss.str();
return;
}
}
const std::lock_guard<std::mutex> lock(mutex);
if (!can_be_raised(error.type, error.sub_type)) {
std::stringstream ss;
ss << "Error can't be raised, because type " << error.type << ", sub_type " << error.sub_type
<< " is already active.";
ss << std::endl << "Error object: " << nlohmann::json(error).dump(2);
EVLOG_debug << ss.str();
return;
}
database->add_error(std::make_shared<Error>(error));
this->publish_raised_error(error);
EVLOG_error << "Error raised, type: " << error.type << ", sub_type: " << error.sub_type
<< ", message: " << error.message;
}
std::list<ErrorPtr> ErrorManagerImpl::clear_error(const ErrorType& type) {
const ErrorSubType sub_type("");
return clear_error(type, sub_type);
}
std::list<ErrorPtr> ErrorManagerImpl::clear_error(const ErrorType& type, const ErrorSubType& sub_type) {
const std::lock_guard<std::mutex> lock(mutex);
if (!can_be_cleared(type, sub_type)) {
EVLOG_debug << "Error can't be cleared, because type " << type << ", sub_type " << sub_type
<< " is not active.";
return {};
}
const std::list<ErrorFilter> filters = {ErrorFilter(TypeFilter(type)), ErrorFilter(SubTypeFilter(sub_type))};
std::list<ErrorPtr> res = database->remove_errors(filters);
if (res.size() > 1) {
std::stringstream ss;
ss << "There are more than one matching error, this is not valid:" << std::endl;
for (const ErrorPtr& error : res) {
ss << nlohmann::json(*error).dump(2) << std::endl;
}
EVLOG_error << ss.str();
return {};
}
const ErrorPtr error = res.front();
error->state = State::ClearedByModule;
this->publish_cleared_error(*error);
EVLOG_info << "Cleared error of type " << type << " with sub_type " << sub_type;
return res;
}
std::list<ErrorPtr> ErrorManagerImpl::clear_all_errors() {
const std::lock_guard<std::mutex> lock(mutex);
const std::list<ErrorFilter> filters = {};
std::list<ErrorPtr> res = database->remove_errors(filters);
if (res.empty()) {
EVLOG_debug << "No errors can be cleared, because no errors are active";
return res;
}
std::stringstream ss;
ss << "Cleared " << res.size() << " errors:" << std::endl;
for (const ErrorPtr& error : res) {
error->state = State::ClearedByModule;
this->publish_cleared_error(*error);
ss << " - type: " << error->type << ", sub_type: " << error->sub_type << std::endl;
}
EVLOG_info << ss.str();
return res;
}
std::list<ErrorPtr> ErrorManagerImpl::clear_all_errors(const ErrorType& error_type) {
const std::lock_guard<std::mutex> lock(mutex);
if (!can_be_cleared(error_type)) {
EVLOG_debug << "Errors can't be cleared, because type " << error_type << " is not active.";
return {};
}
const std::list<ErrorFilter> filters = {ErrorFilter(TypeFilter(error_type))};
std::list<ErrorPtr> res = database->remove_errors(filters);
std::stringstream ss;
ss << "Cleared " << res.size() << " errors of type " << error_type << " with sub_types:" << std::endl;
for (const ErrorPtr& error : res) {
error->state = State::ClearedByModule;
this->publish_cleared_error(*error);
ss << " - " << error->sub_type << std::endl;
}
EVLOG_info << ss.str();
return res;
}
bool ErrorManagerImpl::can_be_raised(const ErrorType& type, const ErrorSubType& sub_type) const {
const std::list<ErrorFilter> filters = {ErrorFilter(TypeFilter(type)), ErrorFilter(SubTypeFilter(sub_type))};
return database->get_errors(filters).empty();
}
bool ErrorManagerImpl::can_be_cleared(const ErrorType& type, const ErrorSubType& sub_type) const {
const std::list<ErrorFilter> filters = {ErrorFilter(TypeFilter(type)), ErrorFilter(SubTypeFilter(sub_type))};
return !database->get_errors(filters).empty();
}
bool ErrorManagerImpl::can_be_cleared(const ErrorType& type) const {
const std::list<ErrorFilter> filters = {ErrorFilter(TypeFilter(type))};
return !database->get_errors(filters).empty();
}
} // namespace error
} // namespace Everest

View File

@@ -0,0 +1,125 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <utility>
#include <utils/error/error_manager_req.hpp>
#include <utils/error.hpp>
#include <utils/error/error_database.hpp>
#include <utils/error/error_filter.hpp>
#include <utils/error/error_json.hpp>
#include <utils/error/error_type_map.hpp>
#include <everest/logging.hpp>
namespace Everest {
namespace error {
ErrorManagerReq::ErrorManagerReq(std::shared_ptr<ErrorTypeMap> error_type_map_,
std::shared_ptr<ErrorDatabase> error_database_,
std::list<ErrorType> allowed_error_types_, SubscribeErrorFunc subscribe_error_func_) :
error_type_map(error_type_map_),
database(error_database_),
allowed_error_types(std::move(allowed_error_types_)),
subscribe_error_func(std::move(subscribe_error_func_)) {
for (const ErrorType& type : allowed_error_types) {
if (!error_type_map->has(type)) {
EVLOG_error << "Error type '" << type << "' in allowed_error_types is not defined, ignored.";
}
}
const ErrorCallback on_raise = [this](const Error& error) { this->on_error_raised(error); };
const ErrorCallback on_clear = [this](const Error& error) { this->on_error_cleared(error); };
for (const ErrorType& type : allowed_error_types) {
subscribe_error_func(type, on_raise, on_clear);
error_subscriptions[type] = {};
}
}
ErrorManagerReq::Subscription::Subscription(const ErrorType& type_, const ErrorCallback& callback_,
const ErrorCallback& clear_callback_) :
type(type_), callback(callback_), clear_callback(clear_callback_) {
}
void ErrorManagerReq::subscribe_error(const ErrorType& type, const ErrorCallback& callback,
const ErrorCallback& clear_callback) {
if (error_subscriptions.count(type) != 1) {
EVLOG_error << "Tpye " << type << " is not known, ignore subscription";
return;
}
const Subscription sub(type, callback, clear_callback);
error_subscriptions.at(type).push_back(sub);
}
void ErrorManagerReq::subscribe_all_errors(const ErrorCallback& callback, const ErrorCallback& clear_callback) {
for (const ErrorType& type : allowed_error_types) {
const Subscription sub(type, callback, clear_callback);
error_subscriptions.at(type).push_back(sub);
}
}
void ErrorManagerReq::on_error_raised(const Error& error) {
if (std::find(allowed_error_types.begin(), allowed_error_types.end(), error.type) == allowed_error_types.end()) {
std::stringstream ss;
ss << "Error can't be raised by module_id " << error.origin.module_id << " and implemenetation_id "
<< error.origin.implementation_id << ", ignored.";
ss << std::endl << "Error object: " << nlohmann::json(error).dump(2);
EVLOG_error << ss.str();
return;
}
if (!error_type_map->has(error.type)) {
std::stringstream ss;
ss << "Error type '" << error.type << "' is not defined, ignored.";
ss << std::endl << "Error object: " << nlohmann::json(error).dump(2);
EVLOG_error << ss.str();
return;
}
std::list<ErrorPtr> errors =
database->get_errors({ErrorFilter(TypeFilter(error.type)), ErrorFilter(SubTypeFilter(error.sub_type)),
ErrorFilter(OriginFilter(error.origin))});
if (!errors.empty()) {
// Error is already raised, ignoring identical new error
// FIXME: can we prevent this from happening in the first place?
return;
}
database->add_error(std::make_shared<Error>(error));
on_error(error, true);
}
void ErrorManagerReq::on_error_cleared(const Error& error) {
const std::list<ErrorFilter> filters = {ErrorFilter(TypeFilter(error.type)),
ErrorFilter(SubTypeFilter(error.sub_type))};
const std::list<ErrorPtr> res = database->remove_errors(filters);
if (res.size() < 1) {
std::stringstream ss;
ss << "Error wasn't raised, type: " << error.type << ", sub_type: " << error.sub_type << ", ignored.";
ss << std::endl << "Error object: " << nlohmann::json(error).dump(2);
EVLOG_error << ss.str();
return;
}
if (res.size() > 1) {
std::stringstream ss;
ss << "More than one error is cleared, type: " << error.type << ", sub_type: " << error.sub_type
<< ", ignored.";
for (const ErrorPtr& error : res) {
ss << std::endl << "Error object: " << nlohmann::json(*error).dump(2);
}
EVLOG_error << ss.str();
return;
}
on_error(error, false);
}
void ErrorManagerReq::on_error(const Error& error, const bool raised) const {
for (const Subscription& sub : error_subscriptions.at(error.type)) {
if (raised) {
sub.callback(error);
} else {
sub.clear_callback(error);
}
}
}
} // namespace error
} // namespace Everest

View File

@@ -0,0 +1,112 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <utility>
#include <utils/error/error_manager_req_global.hpp>
#include <everest/logging.hpp>
#include <utils/error/error_database.hpp>
#include <utils/error/error_json.hpp>
#include <utils/error/error_type_map.hpp>
namespace Everest {
namespace error {
ErrorManagerReqGlobal::ErrorManagerReqGlobal(std::shared_ptr<ErrorTypeMap> error_type_map_,
std::shared_ptr<ErrorDatabase> error_database_,
SubscribeGlobalAllErrorsFunc subscribe_global_all_errors_func_) :
error_type_map(error_type_map_),
database(error_database_),
subscribe_global_all_errors_func(std::move(subscribe_global_all_errors_func_)),
subscriptions({}) {
this->subscribe_global_all_errors_func([this](const Error& error) { this->on_error_raised(error); },
[this](const Error& error) { this->on_error_cleared(error); });
}
void ErrorManagerReqGlobal::subscribe_global_all_errors(const ErrorCallback& callback,
const ErrorCallback& clear_callback) {
const Subscription sub(callback, clear_callback);
subscriptions.push_back(sub);
}
ErrorManagerReqGlobal::Subscription::Subscription(const ErrorCallback& callback_,
const ErrorCallback& clear_callback_) :
callback(callback_), clear_callback(clear_callback_) {
}
void ErrorManagerReqGlobal::on_error_raised(const Error& error) {
if (!error_type_map->has(error.type)) {
std::stringstream ss;
ss << "Error type '" << error.type << "' is not defined, ignoring error";
ss << std::endl << "Error object: " << nlohmann::json(error).dump(2);
EVLOG_error << ss.str();
return;
}
std::list<ErrorPtr> errors =
database->get_errors({ErrorFilter(TypeFilter(error.type)), ErrorFilter(SubTypeFilter(error.sub_type)),
ErrorFilter(OriginFilter(error.origin))});
if (!errors.empty()) {
std::stringstream ss;
ss << "Error of type '" << error.type << "' and sub type '" << error.sub_type
<< "' is already raised, ignoring new error";
ss << std::endl << "Error object: " << nlohmann::json(error).dump(2);
EVLOG_error << ss.str();
return;
}
database->add_error(std::make_shared<Error>(error));
errors = database->get_errors({ErrorFilter(TypeFilter(error.type)), ErrorFilter(SubTypeFilter(error.sub_type)),
ErrorFilter(OriginFilter(error.origin))});
if (errors.size() != 1) {
EVLOG_error << "Error wasn't added, type: " << error.type << ", sub type: " << error.sub_type;
return;
}
for (const Subscription& sub : subscriptions) {
sub.callback(error);
}
}
void ErrorManagerReqGlobal::on_error_cleared(const Error& error) {
if (!error_type_map->has(error.type)) {
std::stringstream ss;
ss << "Error type '" << error.type << "' is not defined, ignoring error";
ss << std::endl << "Error object: " << nlohmann::json(error).dump(2);
EVLOG_error << ss.str();
return;
}
const std::list<ErrorPtr> errors =
database->get_errors({ErrorFilter(TypeFilter(error.type)), ErrorFilter(SubTypeFilter(error.sub_type)),
ErrorFilter(OriginFilter(error.origin))});
if (errors.empty()) {
std::stringstream ss;
ss << "Error of type '" << error.type << "' and sub type '" << error.sub_type
<< "' is not raised, ignoring clear error";
ss << std::endl << "Error object: " << nlohmann::json(error).dump(2);
EVLOG_error << ss.str();
return;
}
const std::list<ErrorPtr> res =
database->remove_errors({ErrorFilter(TypeFilter(error.type)), ErrorFilter(SubTypeFilter(error.sub_type)),
ErrorFilter(OriginFilter(error.origin))});
if (res.size() > 1) {
std::stringstream ss;
ss << "More than one error is cleared, type: " << error.type << ", sub type: " << error.sub_type;
for (const ErrorPtr& error : res) {
ss << std::endl << "Error object: " << nlohmann::json(*error).dump(2);
}
EVLOG_error << ss.str();
return;
}
if (res.empty()) {
std::stringstream ss;
ss << "Error wasn't removed, type: " << error.type << ", sub type: " << error.sub_type;
ss << std::endl << "Error object: " << nlohmann::json(error).dump(2);
EVLOG_error << ss.str();
return;
}
for (const Subscription& sub : subscriptions) {
sub.clear_callback(error);
}
}
} // namespace error
} // namespace Everest

View File

@@ -0,0 +1,46 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <utility>
#include <utils/error/error_state_monitor.hpp>
#include <utils/error/error_database.hpp>
#include <utils/error/error_filter.hpp>
#include <everest/logging.hpp>
namespace Everest {
namespace error {
ErrorStateMonitor::StateCondition::StateCondition(ErrorType type_, ErrorSubType sub_type_, bool active_) :
type(std::move(type_)), sub_type(std::move(sub_type_)), active(active_) {
}
ErrorStateMonitor::ErrorStateMonitor(std::shared_ptr<ErrorDatabase> error_database_) : database(error_database_) {
}
bool ErrorStateMonitor::is_error_active(const ErrorType& type, const ErrorSubType& sub_type) const {
const std::list<ErrorFilter> filters = {ErrorFilter(TypeFilter(type)), ErrorFilter(SubTypeFilter(sub_type))};
return database->get_errors(filters).size() > 0;
}
std::list<ErrorPtr> ErrorStateMonitor::get_active_errors() const {
return database->get_errors({});
}
bool ErrorStateMonitor::is_condition_satisfied(const StateCondition& condition) const {
const bool res = is_error_active(condition.type, condition.sub_type);
return res == condition.active;
}
bool ErrorStateMonitor::is_condition_satisfied(const std::list<StateCondition>& condition) const {
for (const auto& cond : condition) {
if (!is_condition_satisfied(cond)) {
return false;
}
}
return true;
}
} // namespace error
} // namespace Everest

View File

@@ -0,0 +1,94 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <utility>
#include <utils/error/error_type_map.hpp>
#include <utils/error.hpp>
#include <utils/yaml_loader.hpp>
#include <everest/logging.hpp>
namespace Everest {
namespace error {
ErrorTypeMap::ErrorTypeMap(const std::filesystem::path& error_types_dir) {
load_error_types(error_types_dir);
}
void ErrorTypeMap::load_error_types(const std::filesystem::path& error_types_dir) {
BOOST_LOG_FUNCTION();
if (!std::filesystem::is_directory(error_types_dir) || !std::filesystem::exists(error_types_dir)) {
EVLOG_error << "Error types directory '" << error_types_dir.string()
<< "' does not exist, error types not loaded.";
return;
}
for (const auto& entry : std::filesystem::directory_iterator(error_types_dir)) {
if (!entry.is_regular_file()) {
continue;
}
if (entry.path().extension() != ".yaml") {
continue;
}
const std::string prefix = entry.path().stem().string();
json error_type_file = Everest::load_yaml(entry.path());
if (!error_type_file.contains("errors")) {
EVLOG_warning << "Error type file '" << entry.path().string() << "' does not contain 'errors' key.";
continue;
}
if (!error_type_file.at("errors").is_array()) {
EVLOG_error << "Error type file '" << entry.path().string()
<< "' does not contain an array with key 'errors', skipped.";
continue;
}
for (const auto& error : error_type_file["errors"]) {
if (!error.contains("name")) {
EVLOG_error << "Error type file '" << entry.path().string()
<< "' contains an error without a 'name' key, skipped.";
continue;
}
std::string description;
if (!error.contains("description")) {
EVLOG_error << "Error type file '" << entry.path().string()
<< "' contains an error without a 'description' key, using default description";
description = "No description found";
} else {
description = error.at("description").get<std::string>();
}
ErrorType complete_name = prefix + "/" + error.at("name").get<std::string>();
if (this->has(complete_name)) {
EVLOG_error << "Error type file '" << entry.path().string() << "' contains an error with the name '"
<< complete_name << "' which is already defined, skipped.";
continue;
}
error_types[complete_name] = description;
}
}
}
void ErrorTypeMap::load_error_types_map(std::map<ErrorType, std::string> error_types_map) {
this->error_types = std::move(error_types_map);
}
std::string ErrorTypeMap::get_description(const ErrorType& error_type) const {
std::string description;
try {
description = error_types.at(error_type);
} catch (...) {
EVLOG_error << "Error type '" << error_type << "' is not defined, returning default description.";
description = "No description found";
}
return description;
}
bool ErrorTypeMap::has(const ErrorType& error_type) const {
return error_types.find(error_type) != error_types.end();
}
const ErrorTypes& ErrorTypeMap::get_error_types() const {
return error_types;
}
} // namespace error
} // namespace Everest

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,71 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <fmt/core.h>
#include <everest/exceptions.hpp>
#include <everest/logging.hpp>
#include <utils/exceptions.hpp>
#include <utils/filesystem.hpp>
namespace Everest {
/// \brief Check if the provided \p path is a directory
/// \returns The canonical version of the provided path
/// \throws BootException if the path doesn't exist or isn't a directory
fs::path assert_dir(const std::string& path, const std::string& path_alias) {
auto fs_path = fs::path(path);
if (!fs::exists(fs_path)) {
throw BootException(fmt::format("{} path '{}' does not exist", path_alias, path));
}
fs_path = fs::canonical(fs_path);
if (!fs::is_directory(fs_path)) {
throw BootException(fmt::format("{} path '{}' is not a directory", path_alias, path));
}
return fs_path;
}
/// \brief Check if the provided \p path is a file
/// \returns The canonical version of the provided path
/// \throws BootException if the path doesn't exist or isn't a regular file
fs::path assert_file(const std::string& path, const std::string& file_alias) {
auto fs_file = fs::path(path);
if (!fs::exists(fs_file)) {
throw BootException(fmt::format("{} file '{}' does not exist", file_alias, path));
}
fs_file = fs::canonical(fs_file);
if (!fs::is_regular_file(fs_file)) {
throw BootException(fmt::format("{} file '{}' is not a regular file", file_alias, path));
}
return fs_file;
}
/// \returns true if the file at the provided \p path has an extensions \p ext
bool has_extension(const std::string& path, const std::string& ext) {
auto path_ext = fs::path(path).extension().string();
// lowercase the string
std::transform(path_ext.begin(), path_ext.end(), path_ext.begin(), [](unsigned char c) { return std::tolower(c); });
return path_ext == ext;
}
/// \returns a path that has been prefixed by \p prefix from the provided json \p value
std::string get_prefixed_path_from_json(const nlohmann::json& value, const fs::path& prefix) {
auto settings_configs_dir = value.get<std::string>();
if (fs::path(settings_configs_dir).is_relative()) {
settings_configs_dir = (prefix / settings_configs_dir).string();
}
return settings_configs_dir;
}
} // namespace Everest

View File

@@ -0,0 +1,11 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest
#include <utils/formatter.hpp>
#include <fmt/format.h>
namespace everest::formatting {
void throw_format_error(const char* message) {
throw fmt::format_error(message);
}
} // namespace everest::formatting

View File

@@ -0,0 +1,500 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "everest/util/misc/bind.hpp"
#include "everest/util/misc/container.hpp"
#include <utils/message_handler.hpp>
#include <everest/logging.hpp>
#include <fmt/format.h>
#include <optional>
namespace Everest {
namespace {
bool check_topic_matches(std::string_view full_topic, std::string_view wildcard_topic) {
// Verbatim match
if (full_topic == wildcard_topic) {
return true;
}
std::size_t full_topic_pos = 0;
std::size_t wildcard_topic_pos = 0;
while (wildcard_topic_pos < wildcard_topic.size()) {
// Always match on the first multi-level wildcard found
if (wildcard_topic[wildcard_topic_pos] == '#') {
return true;
}
std::size_t wildcard_topic_next = wildcard_topic.find('/', wildcard_topic_pos);
std::size_t wildcard_topic_substr_len = (wildcard_topic_next == std::string_view::npos)
? std::string_view::npos
: wildcard_topic_next - wildcard_topic_pos;
std::string_view wildcard_topic_substr = wildcard_topic.substr(wildcard_topic_pos, wildcard_topic_substr_len);
std::size_t full_topic_next = full_topic.find('/', full_topic_pos);
std::size_t full_topic_substr_len =
(full_topic_next == std::string_view::npos) ? std::string_view::npos : full_topic_next - full_topic_pos;
std::string_view full_topic_substr = full_topic.substr(full_topic_pos, full_topic_substr_len);
if (wildcard_topic_substr != "+" && wildcard_topic_substr != full_topic_substr) {
return false;
}
if (wildcard_topic_next == std::string_view::npos) {
return full_topic_next == std::string_view::npos;
}
if (full_topic_next == std::string_view::npos) {
return wildcard_topic.substr(wildcard_topic_next + 1) == "#";
}
wildcard_topic_pos = wildcard_topic_next + 1;
full_topic_pos = full_topic_next + 1;
}
return full_topic_pos >= full_topic.size();
}
// Pure function: collects all handlers whose registered topic (with MQTT wildcard support)
// matches the incoming topic. Kept outside MessageHandler to signal it has no dependency
// on class state; callers must hold the handler map lock before passing data.
std::vector<MessageHandler::SharedTypedHandler>
copy_shared_handler_wildcard(std::map<std::string, std::vector<MessageHandler::SharedTypedHandler>> const& data,
std::string const& topic) {
std::vector<MessageHandler::SharedTypedHandler> handler_copy;
for (const auto& [wildcard_topic, handlers_vec] : data) {
if (check_topic_matches(topic, wildcard_topic)) {
handler_copy.insert(handler_copy.end(), handlers_vec.begin(), handlers_vec.end());
}
}
return handler_copy;
}
std::vector<MessageHandler::SharedTypedHandler> copy_shared_handler(MessageHandler::MultiHandlerMap const& data,
std::string const& topic) {
std::vector<MessageHandler::SharedTypedHandler> handler_copy;
auto const* ptr = everest::lib::util::find_ptr(data, topic);
if (ptr != nullptr) {
handler_copy = ptr->second;
}
return handler_copy;
}
MessageHandler::SharedTypedHandler copy_shared_handler(MessageHandler::SingleHandlerMap const& data,
std::string const& topic) {
MessageHandler::SharedTypedHandler handler_copy;
auto const* ptr = everest::lib::util::find_ptr(data, topic);
if (ptr != nullptr) {
handler_copy = ptr->second;
}
return handler_copy;
}
void warn_on_high_queue_size(everest::lib::util::simple_queue<ParsedMessage> const& queue, std::string const& topic) {
if (queue.size() >= MAX_PENDING_MESSAGES_PER_TOPIC) {
EVLOG_warning << "Pending message queue for topic '" << topic << "' has reached the limit ("
<< MAX_PENDING_MESSAGES_PER_TOPIC << "). Handler may be stuck or too slow.";
}
}
} // namespace
using everest::lib::util::bind_obj;
MessageHandler::MessageHandler() {
operation_thread_pool = std::make_unique<ThreadPool>(
THREAD_POOL_SCALING_MIN_THREAD_COUNT, std::thread::hardware_concurrency(), THREAD_POOL_SCALING_IDLE_TIMEOUT);
operation_dispatcher_thread = std::thread([this] { run_operation_dispatcher(); });
result_worker_thread = std::thread([this] { run_result_message_worker(); });
external_mqtt_worker_thread = std::thread([this] { run_external_mqtt_worker(); });
}
MessageHandler::~MessageHandler() {
stop();
}
void MessageHandler::add(const ParsedMessage& message) {
EVLOG_verbose << "Adding message to queue: " << message.topic << " with data: " << message.data;
MqttMessageType msg_type = MqttMessageType::ExternalMQTT; // Default to ExternalMQTT if msg_type is not present
if (message.data.is_object()) {
auto msg_type_it = message.data.find("msg_type");
if (msg_type_it != message.data.end() && msg_type_it->is_string()) {
msg_type = string_to_mqtt_message_type(msg_type_it->get<std::string>());
}
}
if (msg_type == MqttMessageType::CmdResult || msg_type == MqttMessageType::GetConfigResponse) {
EVLOG_verbose << "Pushing cmd_result message to queue: " << message.data;
result_message_queue.push(message);
} else if (msg_type == MqttMessageType::GlobalReady) {
const auto topic_copy = message.topic;
const auto data_copy = message.data.at("data");
// Steal the previous ready thread under the monitor lock, then join it outside.
// Using steal-then-join avoids holding the lock during join(), which could block
// other add() calls for an arbitrary duration while the handler runs.
std::thread old_ready;
{
auto handle = ready.handle();
old_ready = std::move(*handle);
*handle = std::thread([this, topic_copy, data_copy] {
SharedTypedHandler action;
{
auto handle = handlers.handle();
action = handle->global_ready;
}
if (action) {
(*action->handler)(topic_copy, data_copy);
}
});
} // release ready monitor lock before joining
if (old_ready.joinable()) {
old_ready.join();
}
} else if (msg_type == MqttMessageType::ExternalMQTT) {
external_mqtt_message_queue.push(message);
} else {
operation_message_queue.push(message);
}
}
void MessageHandler::stop() {
if (!running.exchange(false)) {
return; // Already stopped
}
operation_message_queue.stop();
result_message_queue.stop();
external_mqtt_message_queue.stop();
// Join the dispatcher first: it must not be able to call schedule_operation_message()
// (which dereferences operation_thread_pool) after the pool is destroyed.
if (operation_dispatcher_thread.joinable()) {
operation_dispatcher_thread.join();
}
// The thread_pool destructor handles stopping and joining its workers.
operation_thread_pool.reset();
if (result_worker_thread.joinable()) {
result_worker_thread.join();
}
if (external_mqtt_worker_thread.joinable()) {
external_mqtt_worker_thread.join();
}
std::thread ready_to_join;
{
auto handle = ready.handle();
ready_to_join = std::move(*handle);
}
if (ready_to_join.joinable()) {
ready_to_join.join();
}
}
void MessageHandler::run_operation_dispatcher() {
while (auto message = operation_message_queue.wait_and_pop()) {
dispatch_operation_message(std::move(message.value()));
}
EVLOG_debug << "Operation dispatcher thread stopped";
}
void MessageHandler::dispatch_operation_message(ParsedMessage&& message) {
{
auto handle = operations.handle();
if (everest::lib::util::exists(handle->in_flight, message.topic)) {
auto& pending_queue = handle->pending_messages[message.topic];
warn_on_high_queue_size(pending_queue, message.topic);
pending_queue.push(std::move(message));
return;
}
handle->in_flight.insert(message.topic);
}
schedule_operation_message(std::move(message));
}
void MessageHandler::schedule_operation_message(ParsedMessage&& message) {
auto handle_operation_message_ftor = bind_obj(&MessageHandler::handle_operation_message, this);
auto on_operation_message_done_ftor = bind_obj(&MessageHandler::on_operation_message_done, this);
auto operation = [handle = std::move(handle_operation_message_ftor),
done = std::move(on_operation_message_done_ftor), message = std::move(message)]() {
try {
handle(message.topic, message.data);
} catch (...) {
done(message.topic);
throw;
}
done(message.topic);
};
if (operation_thread_pool) {
operation_thread_pool->run(std::move(operation));
}
}
void MessageHandler::on_operation_message_done(const std::string& topic) {
std::optional<ParsedMessage> next_message;
{
auto handle = operations.handle();
if (!running) {
// Shutting down: stop scheduling and release the in-flight slot.
handle->in_flight.erase(topic);
return;
}
auto opt_pending_it = everest::lib::util::find_optional(handle->pending_messages, topic);
if (not opt_pending_it.has_value()) {
handle->in_flight.erase(topic);
return;
}
auto& pending_it = opt_pending_it.value();
auto& pending_messages = pending_it->second;
next_message = pending_messages.pop();
if (pending_messages.empty()) {
handle->pending_messages.erase(pending_it);
}
}
if (!next_message.has_value()) {
EVLOG_error << "Internal error: pending_messages queue for topic '" << topic
<< "' was empty when expected to contain a message";
auto handle = operations.handle();
handle->in_flight.erase(topic);
return;
}
schedule_operation_message(std::move(*next_message));
}
void MessageHandler::run_result_message_worker() {
while (auto message = result_message_queue.wait_and_pop()) {
handle_result_message(message->topic, message->data);
}
EVLOG_debug << "Cmd result worker thread stopped";
}
void MessageHandler::run_external_mqtt_worker() {
while (auto message = external_mqtt_message_queue.wait_and_pop()) {
handle_external_mqtt_message(message->topic, message->data);
}
EVLOG_debug << "External MQTT worker thread stopped";
}
void MessageHandler::handle_operation_message(const std::string& topic, const json& payload) {
MqttMessageType msg_type = MqttMessageType::ExternalMQTT;
// Determine message type
auto msg_type_it = payload.find("msg_type");
if (msg_type_it != payload.end() && msg_type_it->is_string()) {
msg_type = string_to_mqtt_message_type(msg_type_it->get_ref<const std::string&>());
}
auto data_it = payload.find("data");
const json& data = (data_it != payload.end()) ? *data_it : payload;
switch (msg_type) {
case MqttMessageType::Var:
handle_var_message(topic, data);
break;
case MqttMessageType::Cmd:
handle_cmd_message(topic, data);
break;
case MqttMessageType::ExternalMQTT:
handle_external_mqtt_message(topic, data);
break;
case MqttMessageType::RaiseError:
case MqttMessageType::ClearError:
handle_error_message(topic, data);
break;
case MqttMessageType::GetConfig:
handle_get_config_message(topic, data);
break;
case MqttMessageType::ModuleReady:
handle_module_ready_message(topic, data);
break;
default:
break;
}
}
void MessageHandler::handle_result_message(const std::string& topic, const json& payload) {
auto msg_type_it = payload.find("msg_type");
if (msg_type_it == payload.end()) {
EVLOG_warning << "Received cmd_result message without msg_type: " << payload;
return;
}
const auto msg_type = string_to_mqtt_message_type(msg_type_it->get<std::string>());
if (msg_type == MqttMessageType::CmdResult) {
handle_cmd_result(topic, payload);
} else if (msg_type == MqttMessageType::GetConfigResponse) {
handle_get_config_response(topic, payload);
} else {
EVLOG_warning << "Received invalid cmd_result message: " << payload;
}
}
void MessageHandler::register_handler(const std::string& topic, std::shared_ptr<TypedHandler> handler) {
switch (handler->type) {
case HandlerType::Call: {
auto lock = handlers.handle();
lock->cmd[topic] = handler;
break;
}
case HandlerType::Result: {
auto lock = responses.handle();
lock->cmd[handler->id] = handler;
break;
}
case HandlerType::SubscribeVar: {
auto lock = handlers.handle();
lock->var[topic].push_back(handler);
break;
}
case HandlerType::SubscribeError: {
auto lock = handlers.handle();
lock->error[topic].push_back(handler);
break;
}
case HandlerType::ExternalMQTT: {
auto lock = handlers.handle();
lock->external_var[topic].push_back(handler);
break;
}
case HandlerType::GetConfig: {
auto lock = handlers.handle();
lock->get_module_config[topic] = handler;
break;
}
case HandlerType::GetConfigResponse: {
auto lock = responses.handle();
lock->config = handler;
break;
}
case HandlerType::ModuleReady: {
auto lock = handlers.handle();
lock->module_ready[topic] = handler;
break;
}
case HandlerType::GlobalReady: {
auto lock = handlers.handle();
lock->global_ready = handler;
break;
}
default:
EVLOG_warning << "Unknown handler type for topic: " << topic;
break;
}
}
// Private message handler methods
void MessageHandler::handle_var_message(const std::string& topic, const json& data) {
std::vector<SharedTypedHandler> handler_copy;
{
auto handle = handlers.handle();
handler_copy = copy_shared_handler(handle->var, topic);
}
const auto& json_data = data.at("data");
for (const auto& handler : handler_copy) {
(*handler->handler)(topic, json_data);
}
}
void MessageHandler::handle_cmd_message(const std::string& topic, const json& data) {
SharedTypedHandler handler_copy;
{
auto handle = handlers.handle();
handler_copy = copy_shared_handler(handle->cmd, topic);
}
if (handler_copy) {
(*handler_copy->handler)(topic, data);
}
}
void MessageHandler::handle_external_mqtt_message(const std::string& topic, const json& data) {
std::vector<SharedTypedHandler> handler_copy;
{
auto handle = handlers.handle();
handler_copy = copy_shared_handler_wildcard(handle->external_var, topic);
}
for (const auto& handler : handler_copy) {
(*handler->handler)(topic, data);
}
}
void MessageHandler::handle_error_message(const std::string& topic, const json& data) {
std::vector<SharedTypedHandler> handler_copy;
{
auto handle = handlers.handle();
handler_copy = copy_shared_handler_wildcard(handle->error, topic);
}
for (const auto& handler : handler_copy) {
(*handler->handler)(topic, data);
}
}
void MessageHandler::handle_get_config_message(const std::string& topic, const json& data) {
SharedTypedHandler handler_copy;
{
auto handle = handlers.handle();
handler_copy = copy_shared_handler(handle->get_module_config, topic);
}
if (handler_copy) {
(*handler_copy->handler)(topic, data);
}
}
void MessageHandler::handle_module_ready_message(const std::string& topic, const json& data) {
SharedTypedHandler handler_copy;
{
auto handle = handlers.handle();
handler_copy = copy_shared_handler(handle->module_ready, topic);
}
if (handler_copy) {
(*handler_copy->handler)(topic, data);
}
}
void MessageHandler::handle_cmd_result(const std::string& topic, const json& payload) {
const auto& data = payload.at("data").at("data");
const auto& id = data.at("id").get<std::string>();
std::shared_ptr<TypedHandler> handler_copy;
{
auto handle = responses.handle();
auto it = handle->cmd.find(id);
if (it != handle->cmd.end()) {
handler_copy = it->second;
handle->cmd.erase(it);
}
}
if (handler_copy) {
(*handler_copy->handler)(topic, data);
}
}
void MessageHandler::handle_get_config_response(const std::string& topic, const json& payload) {
std::shared_ptr<TypedHandler> handler_copy;
{
auto handle = responses.handle();
if (handle->config) {
handler_copy = handle->config;
}
}
if (handler_copy) {
(*handler_copy->handler)(topic, payload.at("data"));
}
}
} // namespace Everest

View File

@@ -0,0 +1,78 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <framework/ModuleAdapter.hpp>
namespace Everest {
constexpr auto mqtt_provider_default_precision = 5;
ModuleBase::ModuleBase(const ModuleInfo& info) : info(info){};
void ModuleBase::invoke_init(ImplementationBase& impl) {
impl.init();
}
void ModuleBase::invoke_ready(ImplementationBase& impl) {
impl.ready();
}
void ModuleAdapter::check_complete() {
// FIXME (aw): I should throw if some of my handlers are not set
return;
}
void ModuleAdapter::gather_cmds(ImplementationBase& impl) {
impl._gather_cmds(registered_commands);
}
MqttProvider::MqttProvider(ModuleAdapter& ev) : ev(ev){};
void MqttProvider::publish(const std::string& topic, const std::string& data, bool retain) {
ev.ext_mqtt_publish(topic, data, retain);
}
void MqttProvider::publish(const std::string& topic, const char* data, bool retain) {
ev.ext_mqtt_publish(topic, std::string(data), retain);
}
void MqttProvider::publish(const std::string& topic, bool data, bool retain) {
if (data) {
ev.ext_mqtt_publish(topic, "true", retain);
} else {
ev.ext_mqtt_publish(topic, "false", retain);
}
}
void MqttProvider::publish(const std::string& topic, int data, bool retain) {
ev.ext_mqtt_publish(topic, std::to_string(data), retain);
}
void MqttProvider::publish(const std::string& topic, double data, int precision, bool retain) {
std::stringstream stream;
stream << std::fixed << std::setprecision(precision) << data;
ev.ext_mqtt_publish(topic, stream.str(), retain);
}
void MqttProvider::publish(const std::string& topic, double data, bool retain) {
this->publish(topic, data, mqtt_provider_default_precision, retain);
}
UnsubscribeToken MqttProvider::subscribe(const std::string& topic, StringHandler handler) const {
return ev.ext_mqtt_subscribe(topic, std::move(handler));
}
UnsubscribeToken MqttProvider::subscribe(const std::string& topic, StringPairHandler handler) const {
return ev.ext_mqtt_subscribe_pair(topic, std::move(handler));
}
TelemetryProvider::TelemetryProvider(ModuleAdapter& ev) : ev(ev){};
void TelemetryProvider::publish(const std::string& category, const std::string& subcategory, const std::string& type,
const TelemetryMap& telemetry) {
ev.telemetry_publish(category, subcategory, type, telemetry);
}
void TelemetryProvider::publish(const std::string& category, const std::string& subcategory,
const TelemetryMap& telemetry) {
publish(category, subcategory, subcategory, telemetry);
}
} // namespace Everest

View File

@@ -0,0 +1,105 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <future>
#include <fmt/core.h>
#include <everest/exceptions.hpp>
#include <everest/logging.hpp>
#include <utils/config_service.hpp>
#include <utils/module_config.hpp>
#include <utils/types.hpp>
namespace Everest {
using json = nlohmann::json;
namespace {
// TODO: make this use the ConfigServiceClient as well?
// TODO: needs changes in the bindings to support a new get_module_config call
json get_definitions(std::shared_ptr<MQTTAbstraction> mqtt) {
const auto& everest_prefix = mqtt->get_everest_prefix();
json result;
const auto interface_names_topic = fmt::format("{}interfaces", everest_prefix);
const auto interface_names = mqtt->get(interface_names_topic, QOS::QOS2, config::mqtt_get_config_retries);
auto interface_definitions = json::object();
for (const auto& interface : interface_names) {
auto interface_topic = fmt::format("{}interface_definitions/{}", everest_prefix, interface.get<std::string>());
auto interface_definition = mqtt->get(interface_topic, QOS::QOS2, config::mqtt_get_config_retries);
interface_definitions[interface] = interface_definition;
}
result["interface_definitions"] = interface_definitions;
const auto type_names_topic = fmt::format("{}types", everest_prefix);
const auto type_names = mqtt->get(type_names_topic, QOS::QOS2, config::mqtt_get_config_retries);
auto type_definitions = json::object();
for (const auto& type_name : type_names) {
// type_definition keys already start with a / so omit it in the topic name
auto type_topic = fmt::format("{}type_definitions{}", everest_prefix, type_name.get<std::string>());
auto type_definition = mqtt->get(type_topic, QOS::QOS2, config::mqtt_get_config_retries);
type_definitions[type_name] = type_definition;
}
result["types"] = type_definitions;
const auto settings_topic = fmt::format("{}settings", everest_prefix);
const auto settings = mqtt->get(settings_topic, QOS::QOS2, config::mqtt_get_config_retries);
result["settings"] = settings;
const auto validate_schema = settings.value("validate_schema", json(false)).get<bool>();
if (validate_schema) {
const auto schemas_topic = fmt::format("{}schemas", everest_prefix);
const auto schemas = mqtt->get(schemas_topic, QOS::QOS2, config::mqtt_get_config_retries);
result["schemas"] = schemas;
}
const auto module_names_topic = fmt::format("{}module_names", everest_prefix);
const auto module_names = mqtt->get(module_names_topic, QOS::QOS2, config::mqtt_get_config_retries);
result["module_names"] = module_names;
auto manifests = json::object();
for (const auto& module_name : module_names) {
auto manifest_topic = fmt::format("{}manifests/{}", everest_prefix, module_name.get<std::string>());
auto manifest = mqtt->get(manifest_topic, QOS::QOS2, config::mqtt_get_config_retries);
manifests[module_name] = manifest;
}
result["manifests"] = manifests;
return result;
}
} // namespace
json get_module_config(std::shared_ptr<MQTTAbstraction> mqtt, const std::string& module_id) {
const auto& everest_prefix = mqtt->get_everest_prefix();
config::GetRequest get_request;
get_request.type = config::GetType::Module;
config::Request request;
request.request = get_request;
request.origin = module_id;
request.type = config::Type::Get;
MQTTRequest mqtt_request;
mqtt_request.response_topic = fmt::format("{}modules/{}/response", everest_prefix, module_id);
mqtt_request.request_topic = fmt::format("{}config/request", everest_prefix);
mqtt_request.request_data = json(request).dump();
json result;
config::Response response = mqtt->get(mqtt_request, config::mqtt_get_config_retries);
EVLOG_verbose << fmt::format("Incoming config for {}", module_id);
if (response.status == config::ResponseStatus::Ok and response.type.has_value() and
response.type.value() == config::Type::Get) {
auto get_response = std::get<config::GetResponse>(response.response);
result = get_response.data;
}
result.update(get_definitions(mqtt));
return result;
}
} // namespace Everest

View File

@@ -0,0 +1,445 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <algorithm>
#include <chrono>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <limits>
#include <memory>
#include <thread>
#include <fcntl.h>
#include <netdb.h>
#include <netinet/tcp.h>
#include <poll.h>
#include <sys/eventfd.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/split.hpp>
#include <fmt/format.h>
#include <everest/exceptions.hpp>
#include <everest/io/mqtt/mosquitto_cpp.hpp>
#include <everest/io/mqtt/mqtt_client.hpp>
#include <everest/logging.hpp>
#include <utils/mqtt_abstraction_impl.hpp>
namespace Everest {
constexpr auto mqtt_keep_alive = 20;
constexpr auto mqtt_get_timeout_ms = 5000; ///< Timeout for MQTT get in milliseconds
constexpr auto mqtt_reconnect_timeout_ms = 2000; ///< MQTT reconnect timeout
namespace {
everest::lib::io::mqtt::mqtt_client::QoS to_io_qos(Everest::QOS qos,
everest::lib::io::mqtt::mqtt_client::QoS default_qos) {
switch (qos) {
case QOS::QOS0:
return everest::lib::io::mqtt::mqtt_client::QoS::at_most_once;
break;
case QOS::QOS1:
return everest::lib::io::mqtt::mqtt_client::QoS::at_least_once;
break;
case QOS::QOS2:
return everest::lib::io::mqtt::mqtt_client::QoS::exactly_once;
break;
default:
break;
}
return default_qos;
}
} // namespace
MessageWithQOS::MessageWithQOS(const std::string& topic, const std::string& payload, QOS qos, bool retain) :
Message{topic, payload}, qos(qos), retain(retain) {
}
MQTTAbstractionImpl::MQTTAbstractionImpl(const MQTTSettings& mqtt_settings) :
mqtt_everest_prefix(mqtt_settings.everest_prefix),
mqtt_external_prefix(mqtt_settings.external_prefix),
running(true) {
BOOST_LOG_FUNCTION();
EVLOG_debug << "Initializing MQTT abstraction layer...";
if (mqtt_settings.uses_socket()) {
this->mqtt_server_socket_path = mqtt_settings.broker_socket_path;
} else {
this->mqtt_server_address = mqtt_settings.broker_host;
this->mqtt_server_port = mqtt_settings.broker_port;
}
this->mqtt_is_connected = false;
this->mqtt_client = std::make_unique<everest::lib::io::mqtt::mqtt_client>(mqtt_reconnect_timeout_ms);
this->mqtt_client->set_error_handler([](int error, std::string const& msg) {
if (error) {
EVLOG_error << fmt::format("MQTT error: {}", msg);
}
});
this->mqtt_client->set_callback_connect([this](auto& mqtt, auto, auto, auto const&) { this->on_mqtt_connect(); });
this->mqtt_client->set_callback_disconnect([this](auto& mqtt, auto, auto const&) { this->on_mqtt_disconnect(); });
}
MQTTAbstractionImpl::~MQTTAbstractionImpl() {
if (this->running.load()) {
this->disconnect();
}
// this->mqtt_mainloop_thread.join();
}
bool MQTTAbstractionImpl::connect() {
BOOST_LOG_FUNCTION();
if (this->mqtt_is_connected) {
return true;
}
if (!this->mqtt_server_socket_path.empty()) {
EVLOG_debug << fmt::format("Connecting to MQTT broker: {}", this->mqtt_server_socket_path);
const auto result = this->mqtt_client->connect(this->mqtt_server_socket_path, mqtt_keep_alive);
return (result == everest::lib::io::mqtt::ErrorCode::Success);
} else {
EVLOG_debug << fmt::format("Connecting to MQTT broker: {}:{}", this->mqtt_server_address,
this->mqtt_server_port);
try {
const auto result =
this->mqtt_client->connect("", this->mqtt_server_address, this->mqtt_server_port, mqtt_keep_alive);
return (result == everest::lib::io::mqtt::ErrorCode::Success);
} catch (std::exception& e) {
EVLOG_critical << fmt::format("Could not connect to MQTT broker: {}", e.what());
}
return false;
}
}
void MQTTAbstractionImpl::disconnect() {
BOOST_LOG_FUNCTION();
this->disconnect_event.notify();
// FIXME(kai): always set connected to false for the moment
this->mqtt_is_connected = false;
}
void MQTTAbstractionImpl::publish(const std::string& topic, const json& json) {
BOOST_LOG_FUNCTION();
publish(topic, json, QOS::QOS2);
}
void MQTTAbstractionImpl::publish(const std::string& topic, const json& json, QOS qos, bool retain) {
BOOST_LOG_FUNCTION();
publish(topic, json.dump(), qos, retain);
}
void MQTTAbstractionImpl::publish(const std::string& topic, const std::string& data) {
BOOST_LOG_FUNCTION();
publish(topic, data, QOS::QOS0);
}
void MQTTAbstractionImpl::publish(const std::string& topic, const std::string& data, QOS qos, bool retain) {
BOOST_LOG_FUNCTION();
if (topic.empty()) {
return;
}
auto mqtt_qos = to_io_qos(qos, everest::lib::io::mqtt::mqtt_client::QoS::at_most_once);
if (retain) {
if (not(data.empty() and qos == QOS::QOS0)) {
// topic should be retained, so save the topic in retained_topics
// do not save the topic when the payload is empty and QOS is set to 0 which means a retained topic is to be
// cleared
auto handle = this->managed_topics.handle();
handle->retained_topics.push_back(topic);
}
}
{
auto handle = messages_before_connected.handle();
if (!this->mqtt_is_connected) {
handle->push_back(std::make_shared<MessageWithQOS>(topic, data, qos, retain));
return;
}
}
const auto error = this->mqtt_client->publish(topic, data, mqtt_qos, retain, {});
if (error != everest::lib::io::mqtt::ErrorCode::Success) {
EVLOG_error << "MQTT error during publishing";
}
EVLOG_verbose << fmt::format("publishing to topic: {} with payload: {} and qos: {} and retain: {}", topic, data,
static_cast<int>(qos), static_cast<int>(retain));
}
void MQTTAbstractionImpl::subscribe(const std::string& topic) {
BOOST_LOG_FUNCTION();
subscribe(topic, QOS::QOS2);
}
void MQTTAbstractionImpl::subscribe(const std::string& topic, QOS qos) {
BOOST_LOG_FUNCTION();
auto max_qos_level = to_io_qos(qos, everest::lib::io::mqtt::mqtt_client::QoS::at_most_once);
auto handle = this->managed_topics.handle();
handle->subscribed_topics.insert(topic);
this->ev_handler.add_action([this, topic, max_qos_level]() {
const auto result = this->mqtt_client->subscribe(
topic,
[this, topic]([[maybe_unused]] everest::lib::io::mqtt::mosquitto_cpp& client,
everest::lib::io::mqtt::mosquitto_cpp::message const& message) {
this->message_queue.emplace(topic, message.payload);
this->new_message_event.notify();
},
max_qos_level);
});
}
void MQTTAbstractionImpl::unsubscribe(const std::string& topic) {
BOOST_LOG_FUNCTION();
auto handle = this->managed_topics.handle();
if (handle->subscribed_topics.find(topic) == handle->subscribed_topics.end()) {
EVLOG_warning << fmt::format("Tried to unsubscribe from topic {} but it was not subscribed", topic);
return;
}
EVLOG_verbose << fmt::format("Unsubscribing from topic: {}", topic);
handle->subscribed_topics.erase(topic);
this->ev_handler.add_action([this, topic]() { this->mqtt_client->unsubscribe(topic, {}); });
}
void MQTTAbstractionImpl::clear_retained_topics() {
BOOST_LOG_FUNCTION();
auto handle = this->managed_topics.handle();
for (const auto& retained_topic : handle->retained_topics) {
this->publish(retained_topic, std::string(), QOS::QOS0, true);
EVLOG_verbose << "Cleared retained topic: " << retained_topic;
}
handle->retained_topics.clear();
}
json MQTTAbstractionImpl::get(const MQTTRequest& request, std::size_t retries) {
BOOST_LOG_FUNCTION();
std::size_t attempt = 0;
while (attempt <= retries) {
try {
return this->get_internal(request);
} catch (const EverestTimeoutError& error) {
if (attempt < retries) {
attempt += 1;
} else {
std::rethrow_exception(std::current_exception());
}
}
}
EVLOG_AND_THROW(
EverestInternalError(fmt::format("Unknown error while waiting for result of get({})", request.response_topic)));
}
std::unique_ptr<MQTTAbstraction> make_mqtt_abstraction(const MQTTSettings& mqtt_settings) {
return std::make_unique<MQTTAbstractionImpl>(mqtt_settings);
}
json MQTTAbstractionImpl::get(const std::string& topic, QOS qos, std::size_t retries) {
BOOST_LOG_FUNCTION();
const MQTTRequest request = {topic, qos};
return this->get(request, retries);
}
const std::string& MQTTAbstractionImpl::get_everest_prefix() const {
BOOST_LOG_FUNCTION();
return mqtt_everest_prefix;
}
const std::string& MQTTAbstractionImpl::get_external_prefix() const {
BOOST_LOG_FUNCTION();
return mqtt_external_prefix;
}
nlohmann::json MQTTAbstractionImpl::get_internal(const MQTTRequest& request) {
BOOST_LOG_FUNCTION();
std::promise<json> res_promise;
std::future<json> res_future = res_promise.get_future();
// Why repsonse by value, wouldn't a reference do?
const auto res_handler = [&res_promise](const std::string& /*topic*/, json response) {
res_promise.set_value(std::move(response));
};
// FIXME: use configurable HandlerType?
const auto res_token =
std::make_shared<TypedHandler>(HandlerType::GetConfigResponse, std::make_shared<Handler>(res_handler));
this->register_handler(request.response_topic, res_token, request.qos);
if (request.request_topic.has_value()) {
if (request.request_data.has_value()) {
MqttMessagePayload payload{MqttMessageType::GetConfig, json::parse(request.request_data.value())};
this->publish(request.request_topic.value(), payload, request.qos);
} else {
MqttMessagePayload payload{MqttMessageType::GetConfig, json{}};
this->publish(request.request_topic.value(), payload, request.qos);
}
}
// wait for result future
auto res_future_status = res_future.wait_for(request.timeout);
if (res_future_status == std::future_status::timeout) {
this->unregister_handler(request.response_topic, res_token);
EVLOG_AND_THROW(
EverestTimeoutError(fmt::format("Timeout while waiting for result of get({})", request.response_topic)));
}
if (res_future_status == std::future_status::ready) {
this->unregister_handler(request.response_topic, res_token);
return res_future.get();
}
return json{};
}
std::shared_future<void> MQTTAbstractionImpl::spawn_main_loop_thread() {
BOOST_LOG_FUNCTION();
std::packaged_task<void(void)> task([this]() {
try {
this->ev_handler.register_event_handler(this->mqtt_client.get());
this->ev_handler.register_event_handler(&this->disconnect_event,
[this](const auto&) { this->running = false; });
this->ev_handler.register_event_handler(&this->new_message_event,
[this](const auto&) { on_mqtt_message(); });
this->ev_handler.run(this->running);
} catch (boost::exception& e) {
EVLOG_critical << fmt::format("Caught MQTT mainloop boost::exception:\n{}",
boost::diagnostic_information(e, true));
exit(1);
} catch (std::exception& e) {
EVLOG_critical << fmt::format("Caught MQTT mainloop std::exception:\n{}",
boost::diagnostic_information(e, true));
exit(1);
}
});
this->main_loop_future = task.get_future();
this->mqtt_mainloop_thread = std::thread(std::move(task));
return this->main_loop_future;
}
std::shared_future<void> MQTTAbstractionImpl::get_main_loop_future() {
BOOST_LOG_FUNCTION();
return this->main_loop_future;
}
void MQTTAbstractionImpl::on_mqtt_message() {
while (true) {
auto msg = message_queue.pop();
if (not msg.has_value()) {
break;
}
handle_mqtt_message(msg.value());
}
}
void MQTTAbstractionImpl::handle_mqtt_message(const Message& message) {
BOOST_LOG_FUNCTION();
EVLOG_verbose << "Incoming MQTT message. topic: " << message.topic << " payload: " << message.payload;
const auto& topic = message.topic;
const auto& payload = message.payload;
try {
std::string_view topic_view = topic;
std::string_view mqtt_everest_prefix_view = mqtt_everest_prefix;
if (topic_view.size() >= mqtt_everest_prefix_view.size() &&
topic_view.compare(0, mqtt_everest_prefix_view.size(), mqtt_everest_prefix_view) == 0) {
EVLOG_verbose << fmt::format("topic {} starts with {}", topic, mqtt_everest_prefix);
try {
this->message_handler.add(ParsedMessage{std::move(topic), json::parse(payload.begin(), payload.end())});
} catch (nlohmann::detail::parse_error& e) {
EVLOG_warning << fmt::format("Could not decode json for incoming topic '{}': {}", topic, payload);
return;
}
} else {
EVLOG_debug << fmt::format("Message parsing for topic '{}' not implemented. Wrapping in json object.",
topic);
this->message_handler.add(ParsedMessage{std::move(topic), std::move(payload)});
}
} catch (const boost::exception& e) {
EVLOG_critical << fmt::format("Caught MQTT on_message boost::exception:\n{}",
boost::diagnostic_information(e, true));
exit(1);
} catch (const std::exception& e) {
EVLOG_critical << fmt::format("Caught MQTT on_message std::exception:\n{}",
boost::diagnostic_information(e, true));
exit(1);
}
}
void MQTTAbstractionImpl::on_mqtt_connect() {
BOOST_LOG_FUNCTION();
EVLOG_debug << "Connected to MQTT broker";
// Drain the messages_before_connected under the lock, then publish outside it.
// publish() itself acquires the same monitor lock, so calling it
// while holding the handle would deadlock
shared_messages to_publish;
{
auto handle = messages_before_connected.handle();
this->mqtt_is_connected = true;
to_publish = std::move(*handle);
}
for (auto& message : to_publish) {
this->publish(message->topic, message->payload, message->qos, message->retain);
}
}
void MQTTAbstractionImpl::on_mqtt_disconnect() {
BOOST_LOG_FUNCTION();
EVLOG_AND_THROW(EverestInternalError("Lost connection to MQTT broker"));
}
void MQTTAbstractionImpl::register_handler(const std::string& topic, std::shared_ptr<TypedHandler> handler, QOS qos) {
BOOST_LOG_FUNCTION();
auto subscription_required = [this](const std::string& topic) {
auto handle = this->managed_topics.handle();
return std::find(handle->subscribed_topics.begin(), handle->subscribed_topics.end(), topic) ==
handle->subscribed_topics.end();
};
this->message_handler.register_handler(topic, handler);
if (subscription_required(topic)) {
EVLOG_verbose << fmt::format("Subscribing to {}", topic);
this->subscribe(topic, qos);
}
}
void MQTTAbstractionImpl::unregister_handler(const std::string& topic, const Token& token) {
BOOST_LOG_FUNCTION();
EVLOG_verbose << fmt::format("Unregistering handler {} for {}", fmt::ptr(&token), topic);
if (this->mqtt_is_connected) {
this->unsubscribe(topic);
}
}
} // namespace Everest

View File

@@ -0,0 +1,796 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <framework/runtime.hpp>
#include <utils/config/storage_sqlite.hpp>
#include <utils/error.hpp>
#include <utils/error/error_factory.hpp>
#include <utils/error/error_json.hpp>
#include <utils/error/error_manager_impl.hpp>
#include <utils/error/error_manager_req.hpp>
#include <utils/error/error_state_monitor.hpp>
#include <utils/filesystem.hpp>
#include <algorithm>
#include <cstdlib>
#include <fstream>
#include <boost/program_options.hpp>
namespace Everest {
namespace po = boost::program_options;
std::string parse_string_option(const po::variables_map& vm, const char* option) {
if (vm.count(option) == 0) {
return "";
}
return vm[option].as<std::string>();
}
void populate_module_info_path_from_runtime_settings(ModuleInfo& mi, const RuntimeSettings& rs) {
mi.paths.etc = rs.etc_dir;
mi.paths.libexec = rs.modules_dir / mi.name;
mi.paths.share = rs.data_dir / defaults::MODULES_DIR / mi.name;
}
ManagerSettings::ManagerSettings(const std::string& prefix_, const std::string& config_) :
boot_mode(ConfigBootMode::YamlFile) { // NOLINT(cppcoreguidelines-use-default-member-init): already default
// initialized, but repeated for clarity
init_prefix_and_data_dir(prefix_);
init_config_file(config_);
const auto settings = everest::config::parse_settings(config.value("settings", json::object()));
if (settings.prefix.has_value()) {
EVLOG_warning << "Setting the prefix in the config file is deprecated. Please use the --prefix command line "
"option instead.";
}
init_settings(settings);
}
ManagerSettings::ManagerSettings(const std::string& prefix_, const std::string& db_, DatabaseTag) :
boot_mode(ConfigBootMode::Database) {
init_prefix_and_data_dir(prefix_);
db_dir = assert_file(db_, "User provided database");
const auto migrations_dir = this->runtime_settings.data_dir / "migrations";
this->storage = std::make_unique<everest::config::SqliteStorage>(db_dir, migrations_dir);
if (!this->storage->contains_valid_config()) {
throw BootException("Database not initialized or valid");
}
EVLOG_info << "Booting and parsing configuration from database: " << db_dir;
const auto settings_response = this->storage->get_settings();
if (settings_response.status != everest::config::GenericResponseStatus::OK or
!settings_response.settings.has_value()) {
throw BootException("Failed to load settings from database");
}
const auto settings = settings_response.settings.value();
init_settings(settings);
this->storage->write_settings(*this);
}
ManagerSettings::ManagerSettings(const std::string& prefix_, const std::string& config_, const std::string& db_) :
boot_mode(ConfigBootMode::DatabaseInit) {
init_prefix_and_data_dir(prefix_);
init_config_file(config_);
const auto migrations_dir = this->runtime_settings.data_dir / "migrations";
this->storage = std::make_unique<everest::config::SqliteStorage>(fs::path(db_), migrations_dir);
everest::config::Settings settings;
if (this->storage->contains_valid_config()) {
EVLOG_info << "Booting and parsing configuration from database: " << db_;
const auto settings_response = this->storage->get_settings();
if (settings_response.status != everest::config::GenericResponseStatus::OK or
!settings_response.settings.has_value()) {
throw BootException("Failed to load settings from database");
}
settings = settings_response.settings.value();
} else {
EVLOG_info << "Database not initialized or valid, falling back to YAML config file: " << config_;
this->storage->wipe();
settings = everest::config::parse_settings(config.value("settings", json::object()));
}
init_settings(settings);
this->storage->write_settings(*this);
}
void ManagerSettings::init_settings(const everest::config::Settings& settings) {
if (this->runtime_settings.prefix.empty()) {
throw std::runtime_error(
"Prefix must be set before initializing the settings. Please call init_prefix_and_data_dir() first.");
}
if (this->runtime_settings.data_dir.empty()) {
throw std::runtime_error("Data directory must be set before initializing the settings. Please call "
"init_prefix_and_data_dir() first.");
}
const auto prefix = this->runtime_settings.prefix;
const auto data_dir = this->runtime_settings.data_dir;
fs::path etc_dir;
{
// etc directory
const auto default_etc_dir = (fs::path(defaults::SYSCONF_DIR) / defaults::NAMESPACE).relative_path();
if (prefix.string() == "/usr") {
etc_dir = fs::path("/") / default_etc_dir;
} else if (prefix.filename() == "usr") {
etc_dir = prefix.parent_path() / default_etc_dir;
} else {
etc_dir = prefix / default_etc_dir;
}
etc_dir = assert_dir(etc_dir.string(), "Default etc directory");
}
if (settings.configs_dir.has_value()) {
auto settings_configs_dir = get_prefixed_path_from_json(settings.configs_dir.value().string(), prefix);
configs_dir = assert_dir(settings_configs_dir, "Config provided configs directory");
} else {
configs_dir = assert_dir(etc_dir.string(), "Default configs directory");
}
if (settings.schemas_dir.has_value()) {
const auto settings_schemas_dir = get_prefixed_path_from_json(settings.schemas_dir.value().string(), prefix);
schemas_dir = assert_dir(settings_schemas_dir, "Config provided schema directory");
} else {
const auto default_schemas_dir = data_dir / defaults::SCHEMAS_DIR;
schemas_dir = assert_dir(default_schemas_dir.string(), "Default schema directory");
}
if (settings.interfaces_dir.has_value()) {
const auto settings_interfaces_dir =
get_prefixed_path_from_json(settings.interfaces_dir.value().string(), prefix);
interfaces_dir = assert_dir(settings_interfaces_dir, "Config provided interface directory");
} else {
const auto default_interfaces_dir = data_dir / defaults::INTERFACES_DIR;
interfaces_dir = assert_dir(default_interfaces_dir, "Default interface directory");
}
fs::path modules_dir;
if (settings.modules_dir.has_value()) {
const auto settings_modules_dir = get_prefixed_path_from_json(settings.modules_dir.value().string(), prefix);
modules_dir = assert_dir(settings_modules_dir, "Config provided module directory");
} else {
const auto default_modules_dir = prefix / defaults::LIBEXEC_DIR / defaults::NAMESPACE / defaults::MODULES_DIR;
modules_dir = assert_dir(default_modules_dir, "Default module directory");
}
if (settings.types_dir.has_value()) {
const auto settings_types_dir = get_prefixed_path_from_json(settings.types_dir.value().string(), prefix);
types_dir = assert_dir(settings_types_dir, "Config provided type directory");
} else {
const auto default_types_dir = data_dir / defaults::TYPES_DIR;
types_dir = assert_dir(default_types_dir, "Default type directory");
}
if (settings.errors_dir.has_value()) {
const auto settings_errors_dir = get_prefixed_path_from_json(settings.errors_dir.value().string(), prefix);
errors_dir = assert_dir(settings_errors_dir, "Config provided error directory");
} else {
const auto default_errors_dir = data_dir / defaults::ERRORS_DIR;
errors_dir = assert_dir(default_errors_dir, "Default error directory");
}
if (settings.www_dir.has_value()) {
const auto settings_www_dir = get_prefixed_path_from_json(settings.www_dir.value().string(), prefix);
www_dir = assert_dir(settings_www_dir, "Config provided www directory");
} else {
const auto default_www_dir = data_dir / defaults::WWW_DIR;
www_dir = assert_dir(default_www_dir, "Default www directory");
}
fs::path logging_config_file;
if (settings.logging_config_file.has_value()) {
const auto settings_logging_config_file =
get_prefixed_path_from_json(settings.logging_config_file.value().string(), prefix);
logging_config_file = assert_file(settings_logging_config_file, "Config provided logging config");
} else {
auto default_logging_config_file =
fs::path(defaults::SYSCONF_DIR) / defaults::NAMESPACE / defaults::LOGGING_CONFIG_NAME;
if (prefix.string() != "/usr") {
default_logging_config_file = prefix / default_logging_config_file;
} else {
default_logging_config_file = fs::path("/") / default_logging_config_file;
}
logging_config_file = assert_file(default_logging_config_file, "Default logging config");
}
if (settings.controller_port.has_value()) {
controller_port = settings.controller_port.value();
} else {
controller_port = defaults::CONTROLLER_PORT;
}
if (settings.controller_rpc_timeout_ms.has_value()) {
controller_rpc_timeout_ms = settings.controller_rpc_timeout_ms.value();
} else {
controller_rpc_timeout_ms = defaults::CONTROLLER_RPC_TIMEOUT_MS;
}
std::string mqtt_broker_socket_path;
std::string mqtt_broker_host;
std::uint16_t mqtt_broker_port = 0;
std::string mqtt_everest_prefix;
std::string mqtt_external_prefix;
// Unix Domain Socket configuration MUST be set in the configuration,
// doesn't have a default value if not provided thus it takes precedence
// over default values - this is to have backward compatiblity in term of configuration
// in case both UDS (Unix Domain Sockets) and IDS (Internet Domain Sockets) are set in config, raise exception
if (settings.mqtt_broker_socket_path.has_value()) {
mqtt_broker_socket_path = settings.mqtt_broker_socket_path.value();
}
if (settings.mqtt_broker_host.has_value()) {
mqtt_broker_host = settings.mqtt_broker_host.value();
if (!mqtt_broker_socket_path.empty()) {
// invalid configuration, can't have both UDS and IDS
throw BootException(
fmt::format("Setting both the Unix Domain Socket {} and Internet Domain Socket {} in config is invalid",
mqtt_broker_socket_path, mqtt_broker_host));
}
} else {
mqtt_broker_host = defaults::MQTT_BROKER_HOST;
}
// overwrite mqtt broker host with environment variable
// NOLINTNEXTLINE(concurrency-mt-unsafe): not problematic that this function is not threadsafe here
const char* mqtt_server_address = std::getenv("MQTT_SERVER_ADDRESS");
if (mqtt_server_address != nullptr) {
mqtt_broker_host = mqtt_server_address;
if (!mqtt_broker_socket_path.empty()) {
// invalid configuration, can't have both UDS and IDS
throw BootException(
fmt::format("Setting both the Unix Domain Socket {} and Internet Domain Socket {} in "
"config and as environment variable respectivelly (as MQTT_SERVER_ADDRESS) is not allowed",
mqtt_broker_socket_path, mqtt_broker_host));
}
}
if (settings.mqtt_broker_port.has_value()) {
mqtt_broker_port = settings.mqtt_broker_port.value();
} else {
mqtt_broker_port = defaults::MQTT_BROKER_PORT;
}
// overwrite mqtt broker port with environment variable
// NOLINTNEXTLINE(concurrency-mt-unsafe): not problematic that this function is not threadsafe here
const char* mqtt_server_port = std::getenv("MQTT_SERVER_PORT");
if (mqtt_server_port != nullptr) {
try {
mqtt_broker_port = std::stoul(mqtt_server_port);
} catch (...) {
EVLOG_warning << "Environment variable MQTT_SERVER_PORT set, but not set to an integer. Ignoring.";
}
}
if (settings.mqtt_everest_prefix.has_value()) {
mqtt_everest_prefix = settings.mqtt_everest_prefix.value();
} else {
mqtt_everest_prefix = defaults::MQTT_EVEREST_PREFIX;
}
// always make sure the everest mqtt prefix ends with '/'
if (mqtt_everest_prefix.length() > 0 && mqtt_everest_prefix.back() != '/') {
mqtt_everest_prefix = mqtt_everest_prefix += "/";
}
if (settings.mqtt_external_prefix.has_value()) {
mqtt_external_prefix = settings.mqtt_external_prefix.value();
} else {
mqtt_external_prefix = defaults::MQTT_EXTERNAL_PREFIX;
}
if (mqtt_everest_prefix == mqtt_external_prefix) {
throw BootException(fmt::format("mqtt_everest_prefix '{}' cannot be equal to mqtt_external_prefix '{}'!",
mqtt_everest_prefix, mqtt_external_prefix));
}
if (not mqtt_broker_socket_path.empty()) {
populate_mqtt_settings(this->mqtt_settings, mqtt_broker_socket_path, mqtt_everest_prefix, mqtt_external_prefix);
} else {
populate_mqtt_settings(this->mqtt_settings, mqtt_broker_host, mqtt_broker_port, mqtt_everest_prefix,
mqtt_external_prefix);
}
run_as_user = settings.run_as_user.value_or("");
auto version_information_path = data_dir / VERSION_INFORMATION_FILE;
if (fs::exists(version_information_path)) {
std::ifstream ifs(version_information_path.string());
version_information = std::string(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
} else {
version_information = "unknown";
}
std::string telemetry_prefix;
if (settings.telemetry_prefix.has_value()) {
telemetry_prefix = settings.telemetry_prefix.value();
} else {
telemetry_prefix = defaults::TELEMETRY_PREFIX;
}
// always make sure the telemetry mqtt prefix ends with '/'
if (telemetry_prefix.length() > 0 && telemetry_prefix.back() != '/') {
telemetry_prefix = telemetry_prefix += "/";
}
bool telemetry_enabled = defaults::TELEMETRY_ENABLED;
if (settings.telemetry_enabled.has_value()) {
telemetry_enabled = settings.telemetry_enabled.value();
} else {
telemetry_enabled = defaults::TELEMETRY_ENABLED;
}
bool validate_schema = defaults::VALIDATE_SCHEMA;
if (settings.validate_schema.has_value()) {
validate_schema = settings.validate_schema.value();
} else {
validate_schema = defaults::VALIDATE_SCHEMA;
}
bool forward_exceptions = defaults::FORWARD_EXCEPTIONS;
if (settings.forward_exceptions.has_value()) {
forward_exceptions = settings.forward_exceptions.value();
} else {
forward_exceptions = defaults::FORWARD_EXCEPTIONS;
}
populate_runtime_settings(this->runtime_settings, prefix, etc_dir, data_dir, modules_dir, logging_config_file,
telemetry_prefix, telemetry_enabled, validate_schema, forward_exceptions);
}
void ManagerSettings::init_prefix_and_data_dir(const std::string& prefix_) {
fs::path prefix;
if (prefix_.length() != 0) {
// user provided
prefix = assert_dir(prefix_, "User provided prefix");
}
if (prefix.empty()) {
prefix = assert_dir(defaults::PREFIX, "Default prefix");
}
runtime_settings.data_dir =
assert_dir((prefix / defaults::DATAROOT_DIR / defaults::NAMESPACE).string(), "Default share directory");
runtime_settings.prefix = prefix;
}
void ManagerSettings::init_config_file(const std::string& config_) {
if (this->runtime_settings.prefix.empty()) {
throw std::runtime_error(
"Prefix must be set before initializing the config file. Please call init_prefix_and_data_dir() first.");
}
if (config_.length() != 0) {
try {
config_file = assert_file(config_, "User profided config");
} catch (const BootException& e) {
if (has_extension(config_file, ".yaml")) {
throw;
}
// otherwise, we propbably got a simple config file name
}
}
if (config_file.empty()) {
auto config_file_prefix = this->runtime_settings.prefix;
if (config_file_prefix.empty()) {
config_file_prefix = assert_dir(defaults::PREFIX, "Default prefix");
}
if (config_file_prefix.string() == "/usr") {
// we're going to look in /etc, which isn't prefixed by /usr
config_file_prefix = "/";
}
if (config_.length() != 0) {
// user provided short form
const auto user_config_file =
config_file_prefix / defaults::SYSCONF_DIR / defaults::NAMESPACE / fmt::format("{}.yaml", config_);
const auto short_form_alias = fmt::format("User provided (by using short form: '{}')", config_);
config_file = assert_file(user_config_file, short_form_alias);
} else {
// default
config_file =
assert_file(config_file_prefix / defaults::SYSCONF_DIR / defaults::NAMESPACE / defaults::CONFIG_NAME,
"Default config");
}
}
// now the config file should have been found
if (config_file.empty()) {
throw std::runtime_error("Assertion for found config file failed");
}
config = load_yaml(config_file);
if (config == nullptr) {
EVLOG_info << "Config file is null, treating it as empty";
config = json::object();
} else if (!config.is_object()) {
throw BootException(fmt::format("Config file '{}' is not an object", config_file.string()));
}
}
ModuleCallbacks::ModuleCallbacks(
const std::function<void(ModuleAdapter module_adapter)>& register_module_adapter,
const std::function<std::vector<cmd>(const RequirementInitialization& requirement_init)>& everest_register,
const std::function<void(ModuleConfigs module_configs, const ModuleInfo& info)>& init,
const std::function<void()>& ready) :
register_module_adapter(register_module_adapter), everest_register(everest_register), init(init), ready(ready) {
}
// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays): pass-through of argc and argv from main()
ModuleLoader::ModuleLoader(int argc, char* argv[], ModuleCallbacks callbacks, VersionInformation version_information) :
runtime_settings(nullptr), callbacks(std::move(callbacks)), version_information(std::move(version_information)) {
try {
if (!this->parse_command_line(argc, argv)) {
this->should_exit = true;
}
} catch (std::exception& e) {
std::cout << "Error during command line parsing: " << e.what() << "\n";
this->should_exit = true;
}
}
int ModuleLoader::initialize() {
if (this->should_exit) {
return EXIT_FAILURE;
}
Logging::init(this->logging_config_file.string(), this->module_id);
const auto start_time = std::chrono::steady_clock::now();
this->mqtt = std::shared_ptr<MQTTAbstraction>(make_mqtt_abstraction(this->mqtt_settings));
this->mqtt->connect();
this->mqtt->spawn_main_loop_thread();
const auto result = get_module_config(this->mqtt, this->module_id);
const auto get_config_time = std::chrono::steady_clock::now();
EVLOG_debug << "Module " << fmt::format(TERMINAL_STYLE_OK, "{}", module_id) << " get_config() ["
<< std::chrono::duration_cast<std::chrono::milliseconds>(get_config_time - start_time).count() << "ms]";
RuntimeSettings result_settings = result.at("settings");
this->runtime_settings = std::make_unique<RuntimeSettings>(std::move(result_settings));
if (!this->runtime_settings) {
return 0;
}
const auto& rs = this->runtime_settings;
const auto shutdown_mqtt = [this]() {
if (this->mqtt) {
try {
this->mqtt->disconnect();
} catch (const std::exception& e) {
EVLOG_critical << fmt::format("MQTT disconnect in exception path failed: {}", e.what());
}
}
};
try {
const auto config = Config(this->mqtt_settings, result);
const auto config_instantiation_time = std::chrono::steady_clock::now();
EVLOG_debug
<< "Module " << fmt::format(TERMINAL_STYLE_OK, "{}", module_id) << " after Config() instantiation ["
<< std::chrono::duration_cast<std::chrono::milliseconds>(config_instantiation_time - start_time).count()
<< "ms]";
if (!config.contains(this->module_id)) {
EVLOG_error << fmt::format("Module id '{}' not found in config!", this->module_id);
return 2;
}
const std::string module_identifier = config.printable_identifier(this->module_id);
const auto module_name = config.get_module_name(this->module_id);
if ((this->application_name != module_name) and (this->application_name != module_identifier)) {
EVLOG_error << fmt::format(
"Module id '{}': Expected a '{}' module, but it looks like you started a '{}' module.", this->module_id,
module_name, this->application_name);
}
EVLOG_debug << fmt::format("Initializing framework for module {}...", module_identifier);
EVLOG_verbose << fmt::format("Setting process name to: '{}'...", module_identifier);
const int prctl_return = prctl(PR_SET_NAME, module_identifier.c_str());
if (prctl_return == 1) {
EVLOG_warning << fmt::format("Could not set process name to '{}', it remains '{}'", module_identifier,
this->original_process_name);
}
Logging::update_process_name(module_identifier);
auto everest = Everest(this->module_id, config, rs->validate_schema, this->mqtt, rs->telemetry_prefix,
rs->telemetry_enabled, rs->forward_exceptions);
// module import
EVLOG_debug << fmt::format("Initializing module {}...", module_identifier);
if (!everest.connect()) {
if (this->mqtt_settings.broker_socket_path.empty()) {
EVLOG_error << fmt::format("Cannot connect to MQTT broker at {}:{}", this->mqtt_settings.broker_host,
this->mqtt_settings.broker_port);
} else {
EVLOG_error << fmt::format("Cannot connect to MQTT broker socket at {}",
this->mqtt_settings.broker_socket_path);
}
return 1;
}
ModuleAdapter module_adapter;
module_adapter.call = [&everest](const Requirement& req, const std::string& cmd_name, const Parameters& args) {
return everest.call_cmd(req, cmd_name, args);
};
module_adapter.publish = [&everest](const std::string& req, const std::string& var_name, const Value& value) {
return everest.publish_var(req, var_name, value);
};
module_adapter.subscribe = [&everest](const Requirement& req, const std::string& var_name,
const ValueCallback& callback) {
return everest.subscribe_var(req, var_name, callback);
};
module_adapter.get_error_manager_impl = [&everest](const std::string& impl_id) {
return everest.get_error_manager_impl(impl_id);
};
module_adapter.get_error_state_monitor_impl = [&everest](const std::string& impl_id) {
return everest.get_error_state_monitor_impl(impl_id);
};
module_adapter.get_error_factory = [&everest](const std::string& impl_id) {
return everest.get_error_factory(impl_id);
};
module_adapter.get_error_manager_req = [&everest](const Requirement& req) {
return everest.get_error_manager_req(req);
};
module_adapter.get_error_state_monitor_req = [&everest](const Requirement& req) {
return everest.get_error_state_monitor_req(req);
};
module_adapter.get_global_error_manager = [&everest]() { return everest.get_global_error_manager(); };
module_adapter.get_global_error_state_monitor = [&everest]() {
return everest.get_global_error_state_monitor();
};
module_adapter.get_config_service_client = [&everest]() { return everest.get_config_service_client(); };
// NOLINTNEXTLINE(modernize-avoid-bind): prefer bind here for readability
module_adapter.ext_mqtt_publish =
std::bind(&Everest::Everest::external_mqtt_publish, &everest, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3);
module_adapter.ext_mqtt_subscribe = [&everest](const std::string& topic, const StringHandler& handler) {
return everest.provide_external_mqtt_handler(topic, handler);
};
module_adapter.ext_mqtt_subscribe_pair = [&everest](const std::string& topic,
const StringPairHandler& handler) {
return everest.provide_external_mqtt_handler(topic, handler);
};
module_adapter.telemetry_publish = [&everest](const std::string& category, const std::string& subcategory,
const std::string& type, const TelemetryMap& telemetry) {
return everest.telemetry_publish(category, subcategory, type, telemetry);
};
module_adapter.get_mapping = [&everest]() { return everest.get_3_tier_model_mapping(); };
this->callbacks.register_module_adapter(module_adapter);
// FIXME (aw): would be nice to move this config related thing toward the module_init function
const std::vector<cmd> cmds =
this->callbacks.everest_register(config.get_requirement_initialization(this->module_id));
for (const auto& command : cmds) {
everest.provide_cmd(command);
}
const auto module_configs = config.get_module_configs(this->module_id);
auto module_info = config.get_module_info(this->module_id);
populate_module_info_path_from_runtime_settings(module_info, *rs);
module_info.telemetry_enabled = everest.is_telemetry_enabled();
const auto module_mappings = everest.get_3_tier_model_mapping();
if (module_mappings.has_value()) {
module_info.mapping = module_mappings.value().module;
}
this->callbacks.init(module_configs, module_info);
everest.spawn_main_loop_thread();
// register the modules ready handler with the framework
// this handler gets called when the global ready signal is received
everest.register_on_ready_handler(this->callbacks.ready);
// the module should now be ready
everest.signal_ready();
const auto end_time = std::chrono::steady_clock::now();
EVLOG_info << "Module " << fmt::format(TERMINAL_STYLE_BLUE, "{}", module_id) << " initialized ["
<< std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count() << "ms]";
everest.wait_for_main_loop_end();
EVLOG_info << "Exiting...";
} catch (boost::exception& e) {
EVLOG_critical << fmt::format("Caught top level boost::exception:\n{}", boost::diagnostic_information(e, true));
shutdown_mqtt();
return EXIT_FAILURE;
} catch (std::exception& e) {
EVLOG_critical << fmt::format("Caught top level std::exception:\n{}", boost::diagnostic_information(e, true));
shutdown_mqtt();
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
// NOLINTNEXTLINE(cppcoreguidelines-avoid-c-arrays): pass-through of argc and argv from main()
bool ModuleLoader::parse_command_line(int argc, char* argv[]) {
po::options_description desc("EVerest");
desc.add_options()("version", "Print version and exit");
desc.add_options()("help,h", "produce help message");
desc.add_options()("prefix", po::value<std::string>(), "Set main EVerest directory");
desc.add_options()("module,m", po::value<std::string>(),
"Which module should be executed (module id from config file)");
desc.add_options()("dontvalidateschema", "Don't validate json schema on every message");
desc.add_options()("config", po::value<std::string>(), "Just kept for compatibility, not used anymore.");
desc.add_options()("log_config", po::value<std::string>(), "The path to a custom logging config");
desc.add_options()("mqtt_broker_socket_path", po::value<std::string>(), "The MQTT broker socket path");
desc.add_options()("mqtt_broker_host", po::value<std::string>(), "The MQTT broker hostname");
desc.add_options()("mqtt_broker_port", po::value<int>(), "The MQTT broker port");
desc.add_options()("mqtt_everest_prefix", po::value<std::string>(), "The MQTT everest prefix");
desc.add_options()("mqtt_external_prefix", po::value<std::string>(), "The external MQTT prefix");
po::variables_map vm;
po::store(po::parse_command_line(argc, argv, desc), vm);
po::notify(vm);
std::string argv0;
if (argc > 0) {
argv0 = *argv;
if (not argv0.empty()) {
this->application_name = fs::path(argv0).stem().string();
}
}
if (vm.count("help") != 0) {
std::cout << desc << "\n";
return false;
}
if (vm.count("version") != 0) {
std::cout << argv0 << " (" << this->version_information.project_name << " "
<< this->version_information.project_version << " " << this->version_information.git_version << ")"
<< std::endl;
return false;
}
if (vm.count("config") != 0) {
std::cout
<< "--config is not used anymore, modules request their config automatically via MQTT"
<< "\n"
<< "If you want to influence this config loading behavior you can specify the appropriate --mqtt_* flags"
<< "\n";
}
std::string mqtt_broker_socket_path;
if (vm.count("mqtt_broker_socket_path") != 0) {
mqtt_broker_socket_path = vm["mqtt_broker_socket_path"].as<std::string>();
}
std::string mqtt_broker_host;
if (vm.count("mqtt_broker_host") != 0) {
mqtt_broker_host = vm["mqtt_broker_host"].as<std::string>();
if (!mqtt_broker_socket_path.empty()) {
// invalid configuration, can't have both UDS and IDS
throw BootException(
fmt::format("Setting both the Unix Domain Socket {} and Internet Domain Socket {} in config is invalid",
mqtt_broker_socket_path, mqtt_broker_host));
}
} else {
mqtt_broker_host = defaults::MQTT_BROKER_HOST;
}
// overwrite mqtt broker host with environment variable
// NOLINTNEXTLINE(concurrency-mt-unsafe): not problematic that this function is not threadsafe here
const char* mqtt_server_address = std::getenv("MQTT_SERVER_ADDRESS");
if (mqtt_server_address != nullptr) {
mqtt_broker_host = mqtt_server_address;
if (!mqtt_broker_socket_path.empty()) {
// invalid configuration, can't have both UDS and IDS
throw BootException(
fmt::format("Setting both the Unix Domain Socket {} and Internet Domain Socket {} in "
"config and as environment variable respectivelly (as MQTT_SERVER_ADDRESS) is not allowed",
mqtt_broker_socket_path, mqtt_broker_host));
}
}
std::uint16_t mqtt_broker_port = defaults::MQTT_BROKER_PORT;
if (vm.count("mqtt_broker_port") != 0) {
const auto mqtt_broker_port_int = vm["mqtt_broker_port"].as<int>();
if (mqtt_broker_port_int > std::numeric_limits<std::uint16_t>::max()) {
throw BootException(fmt::format("MQTT broker port {} is out of range.", mqtt_broker_port_int));
}
mqtt_broker_port = static_cast<std::uint16_t>(mqtt_broker_port_int);
}
// overwrite mqtt broker port with environment variable
// NOLINTNEXTLINE(concurrency-mt-unsafe): not problematic that this function is not threadsafe here
const char* mqtt_server_port = std::getenv("MQTT_SERVER_PORT");
if (mqtt_server_port != nullptr) {
try {
mqtt_broker_port = std::stoul(mqtt_server_port);
} catch (...) {
EVLOG_warning << "Environment variable MQTT_SERVER_PORT set, but not set to an integer. Ignoring.";
}
}
std::string mqtt_everest_prefix;
if (vm.count("mqtt_everest_prefix") != 0) {
mqtt_everest_prefix = vm["mqtt_everest_prefix"].as<std::string>();
} else {
mqtt_everest_prefix = defaults::MQTT_EVEREST_PREFIX;
}
// always make sure the everest mqtt prefix ends with '/'
if (mqtt_everest_prefix.length() > 0 && mqtt_everest_prefix.back() != '/') {
mqtt_everest_prefix = mqtt_everest_prefix += "/";
}
std::string mqtt_external_prefix;
if (vm.count("mqtt_external_prefix") != 0) {
mqtt_external_prefix = vm["mqtt_external_prefix"].as<std::string>();
} else {
mqtt_external_prefix = "";
}
if (not mqtt_broker_socket_path.empty()) {
populate_mqtt_settings(this->mqtt_settings, mqtt_broker_socket_path, mqtt_everest_prefix, mqtt_external_prefix);
} else {
populate_mqtt_settings(this->mqtt_settings, mqtt_broker_host, mqtt_broker_port, mqtt_everest_prefix,
mqtt_external_prefix);
}
if (vm.count("log_config") != 0) {
auto command_line_logging_config_file = vm["log_config"].as<std::string>();
this->logging_config_file =
assert_file(command_line_logging_config_file, "Command line provided logging config");
} else {
auto default_logging_config_file =
fs::path(defaults::SYSCONF_DIR) / defaults::NAMESPACE / defaults::LOGGING_CONFIG_NAME;
if (std::strcmp(defaults::PREFIX, "/usr") != 0) {
default_logging_config_file = defaults::PREFIX / default_logging_config_file;
} else {
default_logging_config_file = fs::path("/") / default_logging_config_file;
}
this->logging_config_file = assert_file(default_logging_config_file, "Default logging config");
}
this->original_process_name = argv0;
if (vm.count("module") != 0) {
this->module_id = vm["module"].as<std::string>();
} else {
EVTHROW(EVEXCEPTION(EverestApiError, "--module parameter is required"));
}
return true;
}
} // namespace Everest

View File

@@ -0,0 +1,179 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <utils/helpers.hpp>
#include <utils/serial.hpp>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
namespace Everest {
Serial::Serial() {
cobsDecodeReset();
}
Serial::~Serial() {
if (fd != -1) {
close(fd);
}
}
bool Serial::openDevice(const char* device, int _baud) {
fd = open(device, O_RDWR | O_NOCTTY | O_SYNC);
if (fd < 0) {
printf("Serial: error %d opening %s: %s\n", errno, device, strerror(errno));
return false;
} // else printf ("Serial: opened %s as %i\n", device, fd);
cobsDecodeReset();
switch (_baud) {
case 9600:
baud = B9600;
break;
case 19200:
baud = B19200;
break;
case 38400:
baud = B38400;
break;
case 57600:
baud = B57600;
break;
case 115200:
baud = B115200;
break;
case 230400:
baud = B230400;
break;
default:
baud = 0;
return false;
}
return setSerialAttributes();
}
bool Serial::setSerialAttributes() {
struct termios tty;
if (tcgetattr(fd, &tty) != 0) {
printf("Serial: error %d from tcgetattr\n", errno);
return false;
}
cfsetospeed(&tty, baud);
cfsetispeed(&tty, baud);
tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8; // 8-bit chars
// disable IGNBRK for mismatched speed tests; otherwise receive break
// as \000 chars
tty.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON | IXOFF | IXANY);
tty.c_lflag = 0; // no signaling chars, no echo,
// no canonical processing
tty.c_oflag = 0; // no remapping, no delays
tty.c_cc[VMIN] = 0; // read blocks
tty.c_cc[VTIME] = 5; // 0.5 seconds read timeout
tty.c_cflag |= (CLOCAL | CREAD); // ignore modem controls,
// enable reading
tty.c_cflag &= ~(PARENB | PARODD); // shut off parity
tty.c_cflag &= ~CSTOPB;
tty.c_cflag &= ~CRTSCTS;
if (tcsetattr(fd, TCSANOW, &tty) != 0) {
printf("Serial: error %d from tcsetattr\n", errno);
return false;
}
// printf ("Success setting tcsetattr\n");
return true;
}
void Serial::cobsDecodeReset() {
code = 0xff;
block = 0;
decode = msg;
}
uint32_t Serial::crc32(uint8_t* buf, int len) {
int i, j;
uint32_t b, crc, msk;
i = 0;
crc = 0xFFFFFFFF;
while (i < len) {
b = buf[i];
crc = crc ^ b;
for (j = 7; j >= 0; j--) {
msk = -(crc & 1);
crc = (crc >> 1) ^ (0xEDB88320 & msk);
}
i = i + 1;
}
// printf("%X",crc);
return crc;
}
void Serial::handlePacket(uint8_t* buf, int len) {
// FIXME (aw): why is there no implementation here?
}
void Serial::cobsDecode(uint8_t* buf, int len) {
for (int i = 0; i < len; i++)
cobsDecodeByte(buf[i]);
}
void Serial::cobsDecodeByte(uint8_t byte) {
// check max length
if ((decode - msg == 2048 - 1) && byte != 0x00) {
printf("cobsDecode: Buffer overflow\n");
cobsDecodeReset();
}
if (block) {
// we're currently decoding and should not get a 0
if (byte == 0x00) {
// probably found some garbage -> reset
printf("cobsDecode: Garbage detected\n");
cobsDecodeReset();
return;
}
*decode++ = byte;
} else {
if (code != 0xff) {
*decode++ = 0;
}
block = code = byte;
if (code == 0x00) {
// we're finished, reset everything and commit
if (decode == msg) {
// we received nothing, just a 0x00
printf("cobsDecode: Received nothing\n");
} else {
// set back decode with one, as it gets post-incremented
handlePacket(msg, helpers::clamp_to<int>(decode - 1 - msg));
}
cobsDecodeReset();
return; // need to return here, because of block--
}
}
block--;
}
std::string Serial::hexdump(const std::uint8_t* const msg, int msg_len) const {
std::stringstream ss;
for (int i = 0; i < msg_len; i++) {
if (i > 0) {
ss << " ";
}
ss << "0x" << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(msg[i]);
}
return ss.str();
}
std::string Serial::hexdump(const std::vector<std::uint8_t>& msg) const {
return hexdump(msg.data(), msg.size());
}
} // namespace Everest

View File

@@ -0,0 +1,57 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
#include <utils/status_fifo.hpp>
#include <stdexcept>
#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
namespace Everest {
StatusFifo StatusFifo::create_from_path(const std::string& fifo_path) {
if (fifo_path.length() == 0) {
return StatusFifo();
}
// try to open the file
auto fd = open(fifo_path.c_str(), O_WRONLY | O_NONBLOCK);
// note: for fifo files, opening for write only in non-blocking mode will fail with ENXIO if the other end hasn't
// opened the file yet
if (fd == -1) {
if (errno == ENXIO) {
auto msg = std::string("Failed to open status fifo at ") + fifo_path + " (fifo not opened for read?)";
throw std::runtime_error(msg);
} else {
auto msg =
std::string("Failed to open status fifo at ") + fifo_path + " (fifo file not created with mkfifo?)";
throw std::runtime_error(msg);
}
}
return StatusFifo(fd);
}
void StatusFifo::update(const std::string& message) {
if (disabled) {
return;
}
const auto ret = write(fd, message.c_str(), message.length());
if (ret == -1) {
// NOTE (aw): if we fail to write, we might assume, that the reader of the fifo is not interested in us anymore
// so we won't send any further messages
disabled = true;
}
}
StatusFifo::~StatusFifo() {
if (opened) {
close(fd);
}
}
} // namespace Everest

View File

@@ -0,0 +1,29 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest
#include <utils/thread.hpp>
namespace Everest {
Thread::~Thread() {
stop();
}
void Thread::stop() {
if (handle.joinable()) {
exit_signal = true;
handle.join();
}
exit_signal = false;
}
bool Thread::shouldExit() {
return exit_signal;
}
Thread& Thread::operator=(std::thread&& t) {
stop();
handle = std::move(t);
return *this;
}
} // namespace Everest

View File

@@ -0,0 +1,186 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <cstddef>
#include <utils/types.hpp>
TypedHandler::TypedHandler(const std::string& name_, const std::string& id_, HandlerType type_,
std::shared_ptr<Handler> handler_) :
name(name_), id(id_), type(type_), handler(std::move(handler_)) {
}
TypedHandler::TypedHandler(const std::string& name_, HandlerType type_, std::shared_ptr<Handler> handler_) :
TypedHandler(name_, "", type_, std::move(handler_)) {
}
TypedHandler::TypedHandler(HandlerType type_, std::shared_ptr<Handler> handler_) :
TypedHandler("", "", type_, std::move(handler_)) {
}
ImplementationIdentifier::ImplementationIdentifier(const std::string& module_id_, const std::string& implementation_id_,
std::optional<Mapping> mapping_) :
module_id(module_id_), implementation_id(implementation_id_), mapping(mapping_) {
}
std::string ImplementationIdentifier::to_string() const {
return this->module_id + "->" + this->implementation_id;
}
bool operator==(const ImplementationIdentifier& lhs, const ImplementationIdentifier& rhs) {
return lhs.module_id == rhs.module_id && lhs.implementation_id == rhs.implementation_id;
}
bool operator!=(const ImplementationIdentifier& lhs, const ImplementationIdentifier& rhs) {
return !(lhs == rhs);
}
std::string mqtt_message_type_to_string(MqttMessageType type) {
switch (type) {
case MqttMessageType::Var:
return "Var";
case MqttMessageType::Cmd:
return "Cmd";
case MqttMessageType::CmdResult:
return "CmdResult";
case MqttMessageType::ExternalMQTT:
return "ExternalMQTT";
case MqttMessageType::RaiseError:
return "RaiseError";
case MqttMessageType::ClearError:
return "ClearError";
case MqttMessageType::GetConfig:
return "GetConfig";
case MqttMessageType::GetConfigResponse:
return "GetConfigResponse";
case MqttMessageType::Heartbeat:
return "Heartbeat";
case MqttMessageType::ModuleReady:
return "ModuleReady";
case MqttMessageType::GlobalReady:
return "GlobalReady";
default:
throw std::runtime_error("Unknown MQTT message type");
}
}
MqttMessageType string_to_mqtt_message_type(std::string_view str) {
if (str == "Var") {
return MqttMessageType::Var;
} else if (str == "Cmd") {
return MqttMessageType::Cmd;
} else if (str == "CmdResult") {
return MqttMessageType::CmdResult;
} else if (str == "ExternalMQTT") {
return MqttMessageType::ExternalMQTT;
} else if (str == "RaiseError") {
return MqttMessageType::RaiseError;
} else if (str == "ClearError") {
return MqttMessageType::ClearError;
} else if (str == "GetConfig") {
return MqttMessageType::GetConfig;
} else if (str == "GetConfigResponse") {
return MqttMessageType::GetConfigResponse;
} else if (str == "Heartbeat") {
return MqttMessageType::Heartbeat;
} else if (str == "ModuleReady") {
return MqttMessageType::ModuleReady;
} else if (str == "GlobalReady") {
return MqttMessageType::GlobalReady;
}
throw std::runtime_error(fmt::format("Unknown MQTT message type string: {}", str));
}
NLOHMANN_JSON_NAMESPACE_BEGIN
void adl_serializer<Mapping>::to_json(json& j, const Mapping& m) {
j = {{"evse", m.evse}};
if (m.connector.has_value()) {
j["connector"] = m.connector.value();
}
}
Mapping adl_serializer<Mapping>::from_json(const json& j) {
auto m = Mapping(j.at("evse").get<int>());
if (j.contains("connector")) {
m.connector = j.at("connector").get<int>();
}
return m;
}
void adl_serializer<TelemetryConfig>::to_json(json& j, const TelemetryConfig& t) {
j = {{"id", t.id}};
}
TelemetryConfig adl_serializer<TelemetryConfig>::from_json(const json& j) {
auto t = TelemetryConfig(j.at("id").get<int>());
return t;
}
void adl_serializer<ModuleTierMappings>::to_json(json& j, const ModuleTierMappings& m) {
if (m.module.has_value()) {
j = {{"module", m.module.value()}};
}
if (m.implementations.size() > 0) {
j["implementations"] = json::object();
for (auto& impl_mapping : m.implementations) {
if (impl_mapping.second.has_value()) {
j["implementations"][impl_mapping.first] = impl_mapping.second.value();
}
}
}
}
ModuleTierMappings adl_serializer<ModuleTierMappings>::from_json(const json& j) {
ModuleTierMappings m;
if (!j.is_null()) {
if (j.contains("module")) {
m.module = j.at("module");
}
if (j.contains("implementations")) {
for (auto& impl : j.at("implementations").items()) {
m.implementations[impl.key()] = impl.value();
}
}
}
return m;
}
void adl_serializer<Requirement>::to_json(json& j, const Requirement& r) {
j = {{"id", r.id}};
if (r.index != 0) {
j["index"] = r.index;
}
}
Requirement adl_serializer<Requirement>::from_json(const json& j) {
Requirement r;
r.id = j.at("id").get<std::string>();
if (j.contains("index")) {
r.index = j.at("index").get<size_t>();
}
return r;
}
void adl_serializer<Fulfillment>::to_json(json& j, const Fulfillment& f) {
j = {{"module_id", f.module_id}, {"implementation_id", f.implementation_id}, {"requirement", f.requirement}};
}
Fulfillment adl_serializer<Fulfillment>::from_json(const json& j) {
Fulfillment f;
f.module_id = j.at("module_id").get<std::string>();
f.implementation_id = j.at("implementation_id").get<std::string>();
f.requirement = j.at("requirement").get<Requirement>();
return f;
}
void adl_serializer<MqttMessagePayload>::to_json(json& j, const MqttMessagePayload& m) {
j = {{"msg_type", mqtt_message_type_to_string(m.type)}, {"data", m.data}};
}
MqttMessagePayload adl_serializer<MqttMessagePayload>::from_json(const json& j) {
return MqttMessagePayload{string_to_mqtt_message_type(j.at("msg_type").get<std::string>()),
j.at("data").get<json>()};
}
NLOHMANN_JSON_NAMESPACE_END