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,11 @@
ev_add_module(ChargerInfo)
ev_add_module(ErrorHistory)
ev_add_module(Linux_Systemd_Rauc)
ev_add_module(LocalAllowlistTokenValidator)
ev_add_module(PacketSniffer)
ev_add_module(PersistentStore)
ev_add_module(SerialCommHub)
ev_add_module(Setup)
ev_add_module(Store)
ev_add_module(System)
ev_add_module(YamlStore)

View File

@@ -0,0 +1,21 @@
#
# AUTO GENERATED - MARKED REGIONS WILL BE KEPT
# template version 3
#
# module setup:
# - ${MODULE_NAME}: module name
ev_setup_cpp_module()
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
# insert your custom targets and additional config variables here
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
target_sources(${MODULE_NAME}
PRIVATE
"main/charger_informationImpl.cpp"
)
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
# insert other things like install cmds etc here
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1

View File

@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "ChargerInfo.hpp"
namespace module {
void ChargerInfo::init() {
invoke_init(*p_main);
}
void ChargerInfo::ready() {
invoke_ready(*p_main);
}
} // namespace module

View File

@@ -0,0 +1,72 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef CHARGER_INFO_HPP
#define CHARGER_INFO_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 2
//
#include "ld-ev.hpp"
// headers for provided interface implementations
#include <generated/interfaces/charger_information/Implementation.hpp>
// headers for required interface implementations
#include <generated/interfaces/charger_information/Interface.hpp>
#include <generated/interfaces/kvs/Interface.hpp>
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
// insert your custom include headers here
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
namespace module {
struct Conf {
std::string firmware_version_file;
};
class ChargerInfo : public Everest::ModuleBase {
public:
ChargerInfo() = delete;
ChargerInfo(const ModuleInfo& info, std::unique_ptr<charger_informationImplBase> p_main,
std::vector<std::unique_ptr<kvsIntf>> r_kvs,
std::vector<std::unique_ptr<charger_informationIntf>> r_charger_information, Conf& config) :
ModuleBase(info),
p_main(std::move(p_main)),
r_kvs(std::move(r_kvs)),
r_charger_information(std::move(r_charger_information)),
config(config){};
const std::unique_ptr<charger_informationImplBase> p_main;
const std::vector<std::unique_ptr<kvsIntf>> r_kvs;
const std::vector<std::unique_ptr<charger_informationIntf>> r_charger_information;
const Conf& config;
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
// insert your public definitions here
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
protected:
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
// insert your protected definitions here
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
private:
friend class LdEverest;
void init();
void ready();
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
// insert your private definitions here
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
};
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
// insert other definitions here
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
} // namespace module
#endif // CHARGER_INFO_HPP

View File

@@ -0,0 +1,6 @@
manufacturer: "Pionix GmbH"
manufacturer_url: "https://www.pionix.com/"
vendor: "Pionix"
model: "BelayBox"
chargepoint_serial: "0123"
firmware_version: "0.1.2"

View File

@@ -0,0 +1,80 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "charger_informationImpl.hpp"
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
namespace module {
namespace main {
void charger_informationImpl::init() {
}
void charger_informationImpl::ready() {
json info(handle_get_charger_information());
EVLOG_debug << "ChargerInformation: " << info.dump();
}
std::string charger_informationImpl::load_fw_version_from_file(const std::string& fn) {
std::ifstream fw_version_file(fn);
std::string first_line;
if (!fw_version_file) {
return "";
}
if (!std::getline(fw_version_file, first_line)) {
return "";
}
return first_line;
}
types::charger_information::ChargerInformation charger_informationImpl::handle_get_charger_information() {
std::vector<std::string> keys = {"vendor", "model", "chargepoint_serial", "chargebox_serial",
"friendly_name", "manufacturer", "manufacturer_url", "model_url",
"model_number", "model_revision", "board_revision", "firmware_version"};
json info = {};
// in case we are chained, retrieve data from previous module
if (not this->mod->r_charger_information.empty()) {
info = this->mod->r_charger_information[0]->call_get_charger_information();
}
// iterate over all linked key-value stores and merge all items,
// when a key exists in more than one kvs then the last one wins
for (const auto& kvs : mod->r_kvs) {
for (const auto k : keys) {
if (kvs->call_exists(k)) {
const auto v = kvs->call_load(k);
info[k] = std::get<std::string>(v);
}
}
}
// finally check whether we should load the firmware version from a simple plain text file
if (!mod->config.firmware_version_file.empty()) {
info["firmware_version"] = load_fw_version_from_file(mod->config.firmware_version_file);
}
// generate fallback friendly_name: we use the chargepoint's serial,
// because we assume that this one is printed on a device label and not the (internal)
// chargebox' serial number (aka controller serial number) which is usually only important for
// the manufacturer itself
if (info.contains("vendor") and info.contains("model") and info.contains("chargepoint_serial") and
not info.contains("friendly_name")) {
info["friendly_name"] = info["vendor"].get<std::string>() + " " + info["model"].get<std::string>() + " [" +
info["chargepoint_serial"].get<std::string>() + "]";
}
EVLOG_debug << "ChargerInformation: " << info.dump();
return info;
}
} // namespace main
} // namespace module

View File

@@ -0,0 +1,63 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef MAIN_CHARGER_INFORMATION_IMPL_HPP
#define MAIN_CHARGER_INFORMATION_IMPL_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 3
//
#include <generated/interfaces/charger_information/Implementation.hpp>
#include "../ChargerInfo.hpp"
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
// insert your custom include headers here
#include <string>
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
namespace module {
namespace main {
struct Conf {};
class charger_informationImpl : public charger_informationImplBase {
public:
charger_informationImpl() = delete;
charger_informationImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<ChargerInfo>& mod, Conf& config) :
charger_informationImplBase(ev, "main"), mod(mod), config(config){};
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
// insert your public definitions here
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
protected:
// command handler functions (virtual)
virtual types::charger_information::ChargerInformation handle_get_charger_information() override;
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
// insert your protected definitions here
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
private:
const Everest::PtrContainer<ChargerInfo>& mod;
const Conf& config;
virtual void init() override;
virtual void ready() override;
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
// insert your private definitions here
std::string load_fw_version_from_file(const std::string& fn);
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
};
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
// insert other definitions here
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
} // namespace main
} // namespace module
#endif // MAIN_CHARGER_INFORMATION_IMPL_HPP

View File

@@ -0,0 +1,30 @@
description: >-
Provides a charger information interface, backed by simple KVS interface.
With the optional dependency to another charger information node, it is possible to chain
multiple sources of information.
config:
firmware_version_file:
description: >-
Sometimes the firmware version cannot be retrieved from the key-value interface.
Then it is possible to provide the firmware version string from a simple text file
in the filesystem. The first line of this file is used as-is, the remaining file content
is ignored.
Give the full path to the file which contains the firmware version string.
type: string
provides:
main:
interface: charger_information
description: Provides the charger information interface
requires:
kvs:
interface: kvs
min_connections: 1
max_connections: 10
charger_information:
interface: charger_information
min_connections: 0
max_connections: 1
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Michael Heimpold

View File

@@ -0,0 +1,43 @@
load("@rules_cc//cc:defs.bzl", "cc_test")
load("//modules:module.bzl", "cc_everest_module")
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_TEST_INCOMPATIBLE")
cc_everest_module(
name = "ErrorHistory",
srcs = glob([
"*.cpp",
"*.hpp",
]),
impls = ["error_history"],
deps = [
"//lib/everest/sqlite:everest-sqlite",
],
)
cc_test(
name = "ErrorHistory_test",
target_compatible_with = CROSS_TEST_INCOMPATIBLE,
srcs = glob(
[
"tests/*.cpp",
"tests/*.hpp",
"ErrorDatabaseSqlite.*",
],
exclude = [
"main/car_simulatorImpl.*",
"main/car_simulation.*",
],
),
local_defines = [
"BUILD_TZ_LIB=ON",
"USE_SYSTEM_TZ_DB=ON",
"USE_OS_TZDB=1",
"USE_AUTOLOAD=0",
"HAS_REMOTE_API=0",
],
deps = [
"//lib/everest/framework",
"//lib/everest/log:liblog",
"@catch2//:catch2_main",
],
)

View File

@@ -0,0 +1,36 @@
#
# AUTO GENERATED - MARKED REGIONS WILL BE KEPT
# template version 3
#
# module setup:
# - ${MODULE_NAME}: module name
ev_setup_cpp_module()
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
option(BUILD_TESTING "Run unit tests" OFF)
set(CMAKE_PREFIX_PATH "/usr/lib/x86_64-linux-gnu" ${CMAKE_PREFIX_PATH})
find_package(SQLite3 REQUIRED)
target_link_libraries(${MODULE_NAME}
PRIVATE
SQLite::SQLite3
everest::sqlite
)
target_sources(${MODULE_NAME}
PRIVATE
"ErrorDatabaseSqlite.cpp"
)
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
target_sources(${MODULE_NAME}
PRIVATE
"error_history/error_historyImpl.cpp"
)
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
if(EVEREST_CORE_BUILD_TESTING)
add_subdirectory(tests)
endif()
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1

View File

@@ -0,0 +1,320 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "ErrorDatabaseSqlite.hpp"
#include <everest/database/exceptions.hpp>
#include <everest/database/sqlite/connection.hpp>
#include <everest/database/sqlite/statement.hpp>
#include <everest/exceptions.hpp>
#include <everest/logging.hpp>
#include <utils/date.hpp>
#include <set>
namespace module {
ErrorDatabaseSqlite::ErrorDatabaseSqlite(const fs::path& db_path_, const bool reset_) :
db_path(fs::absolute(db_path_)) {
BOOST_LOG_FUNCTION();
std::lock_guard<std::mutex> lock(this->db_mutex);
bool reset = reset_ || !fs::exists(this->db_path);
if (reset) {
EVLOG_info << "Resetting database";
this->reset_database();
} else {
EVLOG_info << "Using database at " << this->db_path;
try {
this->check_database();
} catch (const std::exception& e) {
EVLOG_error << "Error checking database: " << e.what();
EVLOG_info << "Resetting database";
this->reset_database();
}
}
}
void ErrorDatabaseSqlite::check_database() {
BOOST_LOG_FUNCTION();
EVLOG_info << "Checking database";
try {
auto db = std::make_shared<everest::db::sqlite::Connection>(this->db_path);
if (!db->open_connection()) {
EVLOG_error << "Error opening database";
throw everest::db::ConnectionException(db->get_error_message());
}
std::string sql = "SELECT name";
sql += " FROM sqlite_schema";
sql += " WHERE type = 'table' AND name NOT LIKE 'sqlite_%';";
auto stmt = db->new_statement(sql);
bool has_errors_table = false;
int status;
while ((status = stmt->step()) == SQLITE_ROW) {
std::string table_name = stmt->column_text(0);
if (table_name == "errors") {
if (has_errors_table) {
throw Everest::EverestConfigError("Database contains multiple errors tables");
}
has_errors_table = true;
EVLOG_debug << "Found errors table";
} else {
EVLOG_warning << "Found unknown table: " << table_name;
}
}
if (status != SQLITE_DONE) {
throw Everest::EverestConfigError(db->get_error_message());
}
if (!has_errors_table) {
throw Everest::EverestConfigError("Database does not contain errors table");
}
sql = "PRAGMA table_info(errors);";
auto stmt2 = db->new_statement(sql);
std::set<std::string> columns;
while ((status = stmt2->step()) == SQLITE_ROW) {
auto variant = stmt2->column_variant("name");
columns.insert(std::get<std::string>(variant));
}
std::set<std::string> required_columns = {
"uuid", "type", "description", "message", "origin_module", "origin_implementation",
"timestamp", "severity", "state", "sub_type", "vendor_id"};
if (status != SQLITE_DONE) {
throw Everest::EverestConfigError(db->get_error_message());
}
if (columns != required_columns) {
throw Everest::EverestConfigError("Errors table does not contain all required columns");
}
} catch (const std::exception& e) {
EVLOG_error << "Error checking database: " << e.what();
}
}
void ErrorDatabaseSqlite::reset_database() {
BOOST_LOG_FUNCTION();
fs::path database_directory = this->db_path.parent_path();
if (!fs::exists(database_directory)) {
fs::create_directories(database_directory);
}
if (fs::exists(this->db_path)) {
fs::remove(this->db_path);
}
try {
everest::db::sqlite::Connection db(this->db_path);
if (!db.open_connection()) {
EVLOG_error << "Error opening database during reset";
throw everest::db::ConnectionException(db.get_error_message());
}
std::string sql = "CREATE TABLE errors("
"uuid TEXT PRIMARY KEY NOT NULL,"
"type TEXT NOT NULL,"
"description TEXT NOT NULL,"
"message TEXT NOT NULL,"
"origin_module TEXT NOT NULL,"
"origin_implementation TEXT NOT NULL,"
"timestamp TEXT NOT NULL,"
"severity TEXT NOT NULL,"
"state TEXT NOT NULL,"
"sub_type TEXT NOT NULL,"
"vendor_id TEXT NOT NULL);";
if (!db.execute_statement(sql)) {
EVLOG_error << "Error creating database during reset";
throw everest::db::QueryExecutionException(db.get_error_message());
}
} catch (const std::exception& e) {
EVLOG_error << "Error resetting the database: " << e.what();
}
}
void ErrorDatabaseSqlite::add_error(Everest::error::ErrorPtr error) {
std::lock_guard<std::mutex> lock(this->db_mutex);
this->add_error_without_mutex(error);
}
void ErrorDatabaseSqlite::add_error_without_mutex(Everest::error::ErrorPtr error) {
BOOST_LOG_FUNCTION();
try {
everest::db::sqlite::Connection db(this->db_path);
if (!db.open_connection()) {
EVLOG_error << "Error opening database";
throw everest::db::ConnectionException(db.get_error_message());
}
std::string sql = "INSERT INTO errors(uuid, type, description, message, origin_module, origin_implementation, "
"timestamp, severity, state, sub_type, vendor_id) VALUES(";
sql += "?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11);";
auto stmt = db.new_statement(sql);
stmt->bind_text(1, error->uuid.to_string(), everest::db::sqlite::SQLiteString::Transient);
stmt->bind_text(2, error->type);
stmt->bind_text(3, error->description);
stmt->bind_text(4, error->message);
stmt->bind_text(5, error->origin.module_id);
stmt->bind_text(6, error->origin.implementation_id);
stmt->bind_text(7, Everest::Date::to_rfc3339(error->timestamp), everest::db::sqlite::SQLiteString::Transient);
stmt->bind_text(8, Everest::error::severity_to_string(error->severity),
everest::db::sqlite::SQLiteString::Transient);
stmt->bind_text(9, Everest::error::state_to_string(error->state), everest::db::sqlite::SQLiteString::Transient);
stmt->bind_text(10, error->sub_type);
stmt->bind_text(11, error->vendor_id);
if (stmt->step() != SQLITE_DONE) {
throw everest::db::QueryExecutionException(db.get_error_message());
}
} catch (const std::exception& e) {
EVLOG_error << "Error adding error to database: " << e.what();
}
}
std::string ErrorDatabaseSqlite::filter_to_sql_condition(const Everest::error::ErrorFilter& filter) {
std::string condition{};
switch (filter.get_filter_type()) {
case Everest::error::FilterType::State: {
condition = "(state = '" + Everest::error::state_to_string(filter.get_state_filter()) + "')";
} break;
case Everest::error::FilterType::Origin: {
condition = "(origin_module = '" + filter.get_origin_filter().module_id + "' AND " +
"origin_implementation = '" + filter.get_origin_filter().implementation_id + "')";
} break;
case Everest::error::FilterType::Type: {
condition = "(type = '" + filter.get_type_filter().value + "')";
} break;
case Everest::error::FilterType::Severity: {
switch (filter.get_severity_filter()) {
case Everest::error::SeverityFilter::LOW_GE: {
condition = "(severity = '" + Everest::error::severity_to_string(Everest::error::Severity::Low) +
"' OR severity = '" + Everest::error::severity_to_string(Everest::error::Severity::Medium) +
"' OR severity = '" + Everest::error::severity_to_string(Everest::error::Severity::High) + "')";
} break;
case Everest::error::SeverityFilter::MEDIUM_GE: {
condition = "(severity = '" + Everest::error::severity_to_string(Everest::error::Severity::Medium) +
"' OR severity = '" + Everest::error::severity_to_string(Everest::error::Severity::High) + "')";
} break;
case Everest::error::SeverityFilter::HIGH_GE: {
condition = "(severity = '" + Everest::error::severity_to_string(Everest::error::Severity::High) + "')";
} break;
}
} break;
case Everest::error::FilterType::TimePeriod: {
condition = "(timestamp BETWEEN '" + Everest::Date::to_rfc3339(filter.get_time_period_filter().from) +
"' AND '" + Everest::Date::to_rfc3339(filter.get_time_period_filter().to) + "')";
} break;
case Everest::error::FilterType::Handle: {
condition = "(uuid = '" + filter.get_handle_filter().to_string() + "')";
} break;
case Everest::error::FilterType::SubType: {
condition = "(sub_type = '" + filter.get_sub_type_filter().value + "')";
} break;
case Everest::error::FilterType::VendorId: {
condition = "(vendor_id = '" + filter.get_vendor_id_filter().value + "')";
} break;
}
return condition;
}
std::optional<std::string>
ErrorDatabaseSqlite::filters_to_sql_condition(const std::list<Everest::error::ErrorFilter>& filters) {
std::optional<std::string> condition = std::nullopt;
if (!filters.empty()) {
auto it = filters.begin();
condition = filter_to_sql_condition(*it);
it++;
while (it != filters.end()) {
condition = condition.value() + " AND " + ErrorDatabaseSqlite::filter_to_sql_condition(*it);
it++;
}
}
return condition;
}
std::list<Everest::error::ErrorPtr>
ErrorDatabaseSqlite::get_errors(const std::list<Everest::error::ErrorFilter>& filters) const {
std::lock_guard<std::mutex> lock(this->db_mutex);
return this->get_errors(ErrorDatabaseSqlite::filters_to_sql_condition(filters));
}
std::list<Everest::error::ErrorPtr> ErrorDatabaseSqlite::get_errors(const std::optional<std::string>& condition) const {
BOOST_LOG_FUNCTION();
std::list<Everest::error::ErrorPtr> result;
try {
everest::db::sqlite::Connection db(this->db_path);
if (!db.open_connection()) {
EVLOG_error << "Error opening database";
throw everest::db::ConnectionException(db.get_error_message());
}
std::string sql = "SELECT * FROM errors";
if (condition.has_value()) {
sql += " WHERE " + condition.value();
}
EVLOG_debug << "Executing SQL statement: " << sql;
auto stmt = db.new_statement(sql);
int status;
while ((status = stmt->step()) == SQLITE_ROW) {
const Everest::error::ErrorType err_type(std::get<std::string>(stmt->column_variant("type")));
const std::string err_description = std::get<std::string>(stmt->column_variant("description"));
const std::string err_msg = std::get<std::string>(stmt->column_variant("message"));
const std::string err_origin_module_id = std::get<std::string>(stmt->column_variant("origin_module"));
const std::string err_origin_impl_id = std::get<std::string>(stmt->column_variant("origin_implementation"));
const ImplementationIdentifier err_origin(err_origin_module_id, err_origin_impl_id);
const Everest::error::Error::time_point err_timestamp =
Everest::Date::from_rfc3339(std::get<std::string>(stmt->column_variant("timestamp")));
const Everest::error::Severity err_severity =
Everest::error::string_to_severity(std::get<std::string>(stmt->column_variant("severity")));
const Everest::error::State err_state =
Everest::error::string_to_state(std::get<std::string>(stmt->column_variant("state")));
const Everest::error::ErrorHandle err_handle(
Everest::error::ErrorHandle(std::get<std::string>(stmt->column_variant("uuid"))));
const Everest::error::ErrorSubType err_sub_type(std::get<std::string>(stmt->column_variant("sub_type")));
const std::string err_vendor_id = std::get<std::string>(stmt->column_variant("vendor_id"));
Everest::error::ErrorPtr error = std::make_shared<Everest::error::Error>(
err_type, err_sub_type, err_msg, err_description, err_origin, err_vendor_id, err_severity,
err_timestamp, err_handle, err_state);
result.push_back(error);
}
if (status != SQLITE_DONE) {
throw everest::db::QueryExecutionException(db.get_error_message());
}
} catch (const std::exception& e) {
EVLOG_error << "Error getting errors from database: " << e.what();
}
return result;
}
std::list<Everest::error::ErrorPtr>
ErrorDatabaseSqlite::edit_errors(const std::list<Everest::error::ErrorFilter>& filters, EditErrorFunc edit_func) {
std::lock_guard<std::mutex> lock(this->db_mutex);
std::list<Everest::error::ErrorPtr> result = this->remove_errors_without_mutex(filters);
for (Everest::error::ErrorPtr& error : result) {
edit_func(error);
this->add_error_without_mutex(error);
}
return result;
}
std::list<Everest::error::ErrorPtr>
ErrorDatabaseSqlite::remove_errors(const std::list<Everest::error::ErrorFilter>& filters) {
std::lock_guard<std::mutex> lock(this->db_mutex);
return this->remove_errors_without_mutex(filters);
}
std::list<Everest::error::ErrorPtr>
ErrorDatabaseSqlite::remove_errors_without_mutex(const std::list<Everest::error::ErrorFilter>& filters) {
BOOST_LOG_FUNCTION();
std::optional<std::string> condition = ErrorDatabaseSqlite::filters_to_sql_condition(filters);
std::list<Everest::error::ErrorPtr> result = this->get_errors(condition);
try {
everest::db::sqlite::Connection db(this->db_path);
if (!db.open_connection()) {
EVLOG_error << "Error opening database";
throw everest::db::ConnectionException(db.get_error_message());
}
std::string sql = "DELETE FROM errors";
if (condition.has_value()) {
sql += " WHERE " + condition.value();
}
db.execute_statement(sql);
} catch (const std::exception& e) {
EVLOG_error << "Error removing errors from database: " << e.what();
}
return result;
}
} // namespace module

View File

@@ -0,0 +1,43 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef ERROR_HISTORY_ERROR_DATABASE_SQLITE_HPP
#define ERROR_HISTORY_ERROR_DATABASE_SQLITE_HPP
#include <utils/error/error_database.hpp>
#include <filesystem>
namespace fs = std::filesystem;
namespace module {
class ErrorDatabaseSqlite : public Everest::error::ErrorDatabase {
public:
explicit ErrorDatabaseSqlite(const fs::path& db_path_, const bool reset_ = false);
std::list<Everest::error::ErrorPtr>
get_errors(const std::list<Everest::error::ErrorFilter>& filters) const override;
void add_error(Everest::error::ErrorPtr error) override;
std::list<Everest::error::ErrorPtr> edit_errors(const std::list<Everest::error::ErrorFilter>& filters,
EditErrorFunc edit_func) override;
std::list<Everest::error::ErrorPtr> remove_errors(const std::list<Everest::error::ErrorFilter>& filters) override;
private:
void add_error_without_mutex(Everest::error::ErrorPtr error);
std::list<Everest::error::ErrorPtr>
remove_errors_without_mutex(const std::list<Everest::error::ErrorFilter>& filters);
std::list<Everest::error::ErrorPtr> get_errors(const std::optional<std::string>& condition) const;
static std::string filter_to_sql_condition(const Everest::error::ErrorFilter& filter);
static std::optional<std::string> filters_to_sql_condition(const std::list<Everest::error::ErrorFilter>& filters);
void reset_database();
void check_database();
const fs::path db_path;
mutable std::mutex db_mutex;
};
} // namespace module
#endif // ERROR_HISTORY_ERROR_DATABASE_SQLITE_HPP

View File

@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "ErrorHistory.hpp"
namespace module {
void ErrorHistory::init() {
invoke_init(*p_error_history);
}
void ErrorHistory::ready() {
invoke_ready(*p_error_history);
}
} // namespace module

View File

@@ -0,0 +1,58 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef ERROR_HISTORY_HPP
#define ERROR_HISTORY_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 2
//
#include "ld-ev.hpp"
// headers for provided interface implementations
#include <generated/interfaces/error_history/Implementation.hpp>
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
// insert your custom include headers here
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
namespace module {
struct Conf {};
class ErrorHistory : public Everest::ModuleBase {
public:
ErrorHistory() = delete;
ErrorHistory(const ModuleInfo& info, std::unique_ptr<error_historyImplBase> p_error_history, Conf& config) :
ModuleBase(info), p_error_history(std::move(p_error_history)), config(config){};
const std::unique_ptr<error_historyImplBase> p_error_history;
const Conf& config;
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
// insert your public definitions here
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
protected:
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
// insert your protected definitions here
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
private:
friend class LdEverest;
void init();
void ready();
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
// insert your private definitions here
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
};
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
// insert other definitions here
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
} // namespace module
#endif // ERROR_HISTORY_HPP

View File

@@ -0,0 +1,127 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "error_historyImpl.hpp"
#include "../ErrorDatabaseSqlite.hpp"
#include <filesystem>
namespace fs = std::filesystem;
namespace module {
namespace error_history {
void error_historyImpl::init() {
this->db = std::make_shared<ErrorDatabaseSqlite>(this->config.database_path);
Everest::error::StateFilter state_filter(Everest::error::State::Active);
Everest::error::ErrorFilter error_filter(state_filter);
this->db->edit_errors(
{error_filter}, [](Everest::error::ErrorPtr error) { error->state = Everest::error::State::ClearedByReboot; });
subscribe_global_all_errors(
[this](const Everest::error::Error& error) { this->handle_global_all_errors(error); },
[this](const Everest::error::Error& error) { this->handle_global_all_errors_cleared(error); });
}
void error_historyImpl::ready() {
}
Everest::error::StateFilter convert_state_filter(types::error_history::State filter) {
switch (filter) {
case types::error_history::State::Active:
return Everest::error::StateFilter::Active;
case types::error_history::State::ClearedByModule:
return Everest::error::StateFilter::ClearedByModule;
case types::error_history::State::ClearedByReboot:
return Everest::error::StateFilter::ClearedByReboot;
}
throw std::out_of_range("No known enum conversion from enum type types::error_history::State to enum type "
"Everest::error::StateFilter");
}
Everest::error::SeverityFilter convert_severity_filter(types::error_history::SeverityFilter filter) {
switch (filter) {
case types::error_history::SeverityFilter::LOW_GE:
return Everest::error::SeverityFilter::LOW_GE;
case types::error_history::SeverityFilter::MEDIUM_GE:
return Everest::error::SeverityFilter::MEDIUM_GE;
case types::error_history::SeverityFilter::HIGH_GE:
return Everest::error::SeverityFilter::HIGH_GE;
}
throw std::out_of_range("No known enum conversion from enum type types::error_history::SeverityFilter to enum type "
"Everest::error::SeverityFilter");
}
std::vector<types::error_history::ErrorObject>
error_historyImpl::handle_get_errors(types::error_history::FilterArguments& filters) {
std::list<Everest::error::ErrorFilter> error_filters;
if (filters.state_filter.has_value()) {
Everest::error::StateFilter state_filter = convert_state_filter(filters.state_filter.value());
error_filters.push_back(Everest::error::ErrorFilter(state_filter));
}
if (filters.origin_filter.has_value()) {
Everest::error::OriginFilter origin_filter(filters.origin_filter.value().module_id,
filters.origin_filter.value().implementation_id);
error_filters.push_back(Everest::error::ErrorFilter(origin_filter));
}
if (filters.type_filter.has_value()) {
Everest::error::TypeFilter type_filter(filters.type_filter.value());
error_filters.push_back(Everest::error::ErrorFilter(type_filter));
}
if (filters.severity_filter.has_value()) {
Everest::error::SeverityFilter severity_filter = convert_severity_filter(filters.severity_filter.value());
error_filters.push_back(Everest::error::ErrorFilter(severity_filter));
}
if (filters.timeperiod_filter.has_value()) {
Everest::error::TimePeriodFilter timeperiod_filter;
timeperiod_filter.from = Everest::Date::from_rfc3339(filters.timeperiod_filter.value().timestamp_from);
timeperiod_filter.to = Everest::Date::from_rfc3339(filters.timeperiod_filter.value().timestamp_to);
error_filters.push_back(Everest::error::ErrorFilter(timeperiod_filter));
}
if (filters.handle_filter.has_value()) {
Everest::error::HandleFilter handle_filter(filters.handle_filter.value());
error_filters.push_back(Everest::error::ErrorFilter(handle_filter));
}
std::list<Everest::error::ErrorPtr> errors = this->db->get_errors(error_filters);
std::vector<types::error_history::ErrorObject> result;
std::transform(errors.begin(), errors.end(), std::back_inserter(result), [](Everest::error::ErrorPtr error) {
types::error_history::ErrorObject error_object;
error_object.uuid = error->uuid.to_string();
error_object.timestamp = Everest::Date::to_rfc3339(error->timestamp);
std::string string_state = Everest::error::state_to_string(error->state);
error_object.state = types::error_history::string_to_state(string_state);
std::string string_severity = Everest::error::severity_to_string(error->severity);
error_object.severity = types::error_history::string_to_severity(string_severity);
error_object.type = error->type;
error_object.sub_type = error->sub_type;
error_object.origin.module_id = error->origin.module_id;
error_object.origin.implementation_id = error->origin.implementation_id;
error_object.message = error->message;
error_object.description = error->description;
return error_object;
});
return result;
}
void error_historyImpl::handle_global_all_errors(const Everest::error::Error& error) {
Everest::error::ErrorPtr error_ptr = std::make_shared<Everest::error::Error>(error);
this->db->add_error(error_ptr);
}
void error_historyImpl::handle_global_all_errors_cleared(const Everest::error::Error& error) {
Everest::error::HandleFilter handle_filter(error.uuid);
Everest::error::ErrorFilter error_filter(handle_filter);
int edited_errors =
this->db
->edit_errors({error_filter},
[](Everest::error::ErrorPtr error) { error->state = Everest::error::State::ClearedByModule; })
.size();
if (edited_errors == 0) {
EVLOG_error << "ErrorHistory: Error with uuid " << error.uuid.to_string() << " not found in database.";
} else if (edited_errors > 1) {
EVLOG_error << "ErrorHistory: Multiple errors with uuid " << error.uuid.to_string() << " found in database.";
}
}
} // namespace error_history
} // namespace module

View File

@@ -0,0 +1,68 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef ERROR_HISTORY_ERROR_HISTORY_IMPL_HPP
#define ERROR_HISTORY_ERROR_HISTORY_IMPL_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 3
//
#include <generated/interfaces/error_history/Implementation.hpp>
#include "../ErrorHistory.hpp"
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
#include "../ErrorDatabaseSqlite.hpp"
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
namespace module {
namespace error_history {
struct Conf {
std::string database_path;
};
class error_historyImpl : public error_historyImplBase {
public:
error_historyImpl() = delete;
error_historyImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<ErrorHistory>& mod, Conf& config) :
error_historyImplBase(ev, "error_history"), mod(mod), config(config){};
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
friend class ErrorDatabaseSqlite; // for write access to db
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
protected:
// command handler functions (virtual)
virtual std::vector<types::error_history::ErrorObject>
handle_get_errors(types::error_history::FilterArguments& filters) override;
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
// insert your protected definitions here
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
private:
const Everest::PtrContainer<ErrorHistory>& mod;
const Conf& config;
virtual void init() override;
virtual void ready() override;
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
// insert your private definitions here
void handle_global_all_errors(const Everest::error::Error& error);
void handle_global_all_errors_cleared(const Everest::error::Error& error);
std::shared_ptr<ErrorDatabaseSqlite> db;
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
};
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
// insert other definitions here
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
} // namespace error_history
} // namespace module
#endif // ERROR_HISTORY_ERROR_HISTORY_IMPL_HPP

View File

@@ -0,0 +1,14 @@
description: This module provides a persistent error history
provides:
error_history:
description: Error history
interface: error_history
config:
database_path:
type: string
description: Absolute path to the database file
enable_global_errors: true
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Andreas Heinrich

View File

@@ -0,0 +1,30 @@
set(TARGET_NAME ${PROJECT_NAME}_module_error_history_tests)
add_executable(${TARGET_NAME})
target_sources(${TARGET_NAME}
PRIVATE
error_database_sqlite_tests.cpp
../ErrorDatabaseSqlite.cpp
helpers.cpp
)
# target_include_directories(module_error_history_tests
# PRIVATE
# )
target_link_libraries(${TARGET_NAME}
PRIVATE
everest::framework
everest::log
everest::sqlite
SQLite::SQLite3
Catch2::Catch2WithMain
)
if(NOT DISABLE_EDM)
list(APPEND CMAKE_MODULE_PATH ${CPM_PACKAGE_catch2_SOURCE_DIR}/extras)
include(Catch)
catch_discover_tests(${TARGET_NAME})
endif()
add_test(${TARGET_NAME} ${TARGET_NAME})
ev_register_test_target(${TARGET_NAME})

View File

@@ -0,0 +1,288 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <catch2/catch_all.hpp>
#include "../ErrorDatabaseSqlite.hpp"
#include "helpers.hpp"
SCENARIO("Check ErrorDatabaseSqlite class", "[!throws]") {
GIVEN("An ErrorDatabaseSqlite object") {
const std::string bin_dir = get_bin_dir().string() + "/";
const std::string db_name = get_unique_db_name();
TestDatabase db(bin_dir + "/databases/" + db_name, true);
WHEN("Getting all errors") {
THEN("The database should be empty") {
auto errors = db.get_errors(std::list<Everest::error::ErrorFilter>());
REQUIRE(errors.empty());
}
}
WHEN("Adding an error") {
std::vector<Everest::error::ErrorPtr> test_errors = {std::make_shared<Everest::error::Error>(
"test_type", "test_sub_type", "test_message", "test_description",
ImplementationIdentifier("test_origin_module", "test_origin_implementation"), "everest-test",
Everest::error::Severity::Low, date::utc_clock::now(), Everest::error::UUID(),
Everest::error::State::Active)};
db.add_error(test_errors.at(0));
THEN("The error should be in the database") {
check_expected_errors_in_list(test_errors, db.get_errors(std::list<Everest::error::ErrorFilter>()));
}
}
WHEN("Adding multiple errors") {
std::vector<Everest::error::ErrorPtr> test_errors = {
std::make_shared<Everest::error::Error>(
"test_type_a", "test_sub_type_a", "test_message_a", "test_description_a",
ImplementationIdentifier("test_origin_module_a", "test_origin_implementation_a"), "everest-test",
Everest::error::Severity::High, date::utc_clock::now(), Everest::error::UUID(),
Everest::error::State::ClearedByModule),
std::make_shared<Everest::error::Error>(
"test_type_b", "test_sub_type_b", "test_message_b", "test_description_b",
ImplementationIdentifier("test_origin_module_b", "test_origin_implementation_b"), "everest-test",
Everest::error::Severity::Medium, date::utc_clock::now(), Everest::error::UUID(),
Everest::error::State::ClearedByReboot)};
for (Everest::error::ErrorPtr error : test_errors) {
db.add_error(error);
}
THEN("The errors should be in the database") {
auto errors = db.get_errors(std::list<Everest::error::ErrorFilter>());
check_expected_errors_in_list(test_errors, errors);
}
}
}
GIVEN("12 Errors in a connected ErrorDatabaseSqlite object") {
const std::string bin_dir = get_bin_dir().string() + "/";
const std::string db_name = get_unique_db_name();
TestDatabase db(bin_dir + "/databases/" + db_name, true);
std::vector<Everest::error::ErrorPtr> test_errors = get_test_errors();
for (Everest::error::ErrorPtr error : test_errors) {
db.add_error(error);
}
WHEN("Getting all errors") {
auto errors = db.get_errors(std::list<Everest::error::ErrorFilter>());
THEN("The result should contain all 12 errors") {
check_expected_errors_in_list(test_errors, errors);
}
}
WHEN("Getting all errors with StateFilter") {
auto errors = db.get_errors({Everest::error::ErrorFilter(Everest::error::StateFilter::Active)});
THEN("The result should contain specific errors") {
std::vector<Everest::error::ErrorPtr> expected_errors({test_errors[3], test_errors[4], test_errors[5]});
check_expected_errors_in_list(expected_errors, errors);
}
}
WHEN("Getting all errors with OriginFilter") {
auto errors = db.get_errors({Everest::error::ErrorFilter(
Everest::error::OriginFilter("test_origin_module_a", "test_origin_implementation_a"))});
THEN("The result should contain specific errors") {
std::vector<Everest::error::ErrorPtr> expected_errors(
{test_errors[0], test_errors[2], test_errors[4], test_errors[6], test_errors[8], test_errors[10]});
check_expected_errors_in_list(expected_errors, errors);
}
}
WHEN("Getting all errors with TypeFilter") {
auto errors = db.get_errors({Everest::error::ErrorFilter(Everest::error::TypeFilter("test_type_c"))});
THEN("The result should contain specific errors") {
std::vector<Everest::error::ErrorPtr> expected_errors(
{test_errors[2], test_errors[3], test_errors[4], test_errors[5], test_errors[9], test_errors[10]});
check_expected_errors_in_list(expected_errors, errors);
}
}
WHEN("Getting all errors with SeverityFilter") {
auto errors = db.get_errors({Everest::error::ErrorFilter(Everest::error::SeverityFilter::MEDIUM_GE)});
THEN("The result should contain specific errors") {
std::vector<Everest::error::ErrorPtr> expected_errors({test_errors[4], test_errors[5], test_errors[6],
test_errors[7], test_errors[8], test_errors[9],
test_errors[10], test_errors[11]});
check_expected_errors_in_list(expected_errors, errors);
}
}
WHEN("Getting all errors with TimePeriodFilter") {
auto errors = db.get_errors({Everest::error::ErrorFilter(
Everest::error::TimePeriodFilter{date::utc_clock::now() + std::chrono::minutes(150),
date::utc_clock::now() + std::chrono::minutes(270)})});
THEN("The result should contain specific errors") {
std::vector<Everest::error::ErrorPtr> expected_errors({test_errors[3], test_errors[4]});
check_expected_errors_in_list(expected_errors, errors);
}
}
WHEN("Getting all errors with HandleFilter") {
auto errors =
db.get_errors({Everest::error::ErrorFilter(Everest::error::HandleFilter(test_errors[4]->uuid))});
THEN("The result should contain specific errors") {
std::vector<Everest::error::ErrorPtr> expected_errors({test_errors[4]});
check_expected_errors_in_list(expected_errors, errors);
}
}
WHEN("Getting all errors with multiple filters") {
auto errors = db.get_errors({Everest::error::ErrorFilter(Everest::error::StateFilter::Active),
Everest::error::ErrorFilter(Everest::error::OriginFilter(
"test_origin_module_a", "test_origin_implementation_a"))});
THEN("The result should contain specific errors") {
std::vector<Everest::error::ErrorPtr> expected_errors({test_errors[4]});
check_expected_errors_in_list(expected_errors, errors);
}
}
WHEN("Filtering all errors out") {
auto errors = db.get_errors({
Everest::error::ErrorFilter(Everest::error::StateFilter::ClearedByModule),
Everest::error::ErrorFilter(
Everest::error::OriginFilter("test_origin_module_a", "test_origin_implementation_a")),
Everest::error::ErrorFilter(Everest::error::TypeFilter("test_type_c")),
Everest::error::ErrorFilter(Everest::error::SeverityFilter::HIGH_GE),
});
THEN("The result should contain no errors") {
REQUIRE(errors.empty());
}
}
WHEN("Edit error type") {
std::list<Everest::error::ErrorFilter> filters = {
Everest::error::ErrorFilter(Everest::error::HandleFilter(test_errors[4]->uuid))};
Everest::error::ErrorDatabase::EditErrorFunc edit_func = [](Everest::error::ErrorPtr error) {
error->type = "new_type";
};
REQUIRE(db.get_errors(filters).size() > 0);
db.edit_errors(filters, edit_func);
THEN("The error should be edited") {
auto errors = db.get_errors(filters);
REQUIRE(errors.size() == 1);
REQUIRE(errors.front()->type == "new_type");
}
}
WHEN("Edit error state") {
std::list<Everest::error::ErrorFilter> filters = {
Everest::error::ErrorFilter(Everest::error::HandleFilter(test_errors[4]->uuid))};
Everest::error::ErrorDatabase::EditErrorFunc edit_func = [](Everest::error::ErrorPtr error) {
error->state = Everest::error::State::ClearedByModule;
};
REQUIRE(db.get_errors(filters).size() > 0);
db.edit_errors(filters, edit_func);
THEN("The error should be edited") {
auto errors = db.get_errors(filters);
REQUIRE(errors.size() == 1);
REQUIRE(errors.front()->state == Everest::error::State::ClearedByModule);
}
}
WHEN("Edit error severity") {
std::list<Everest::error::ErrorFilter> filters = {
Everest::error::ErrorFilter(Everest::error::HandleFilter(test_errors[4]->uuid))};
Everest::error::ErrorDatabase::EditErrorFunc edit_func = [](Everest::error::ErrorPtr error) {
error->severity = Everest::error::Severity::High;
};
REQUIRE(db.get_errors(filters).size() > 0);
db.edit_errors(filters, edit_func);
THEN("The error should be edited") {
auto errors = db.get_errors(filters);
REQUIRE(errors.size() == 1);
REQUIRE(errors.front()->severity == Everest::error::Severity::High);
}
}
WHEN("Edit error message") {
std::list<Everest::error::ErrorFilter> filters = {
Everest::error::ErrorFilter(Everest::error::HandleFilter(test_errors[4]->uuid))};
Everest::error::ErrorDatabase::EditErrorFunc edit_func = [](Everest::error::ErrorPtr error) {
error->message = "new_message";
};
REQUIRE(db.get_errors(filters).size() > 0);
db.edit_errors(filters, edit_func);
THEN("The error should be edited") {
auto errors = db.get_errors(filters);
REQUIRE(errors.size() == 1);
REQUIRE(errors.front()->message == "new_message");
}
}
WHEN("Edit error description") {
std::list<Everest::error::ErrorFilter> filters = {
Everest::error::ErrorFilter(Everest::error::HandleFilter(test_errors[4]->uuid))};
Everest::error::ErrorDatabase::EditErrorFunc edit_func = [](Everest::error::ErrorPtr error) {
error->description = "new_description";
};
REQUIRE(db.get_errors(filters).size() > 0);
db.edit_errors(filters, edit_func);
THEN("The error should be edited") {
auto errors = db.get_errors(filters);
REQUIRE(errors.size() == 1);
REQUIRE(errors.front()->description == "new_description");
}
}
WHEN("Edit error origin") {
std::list<Everest::error::ErrorFilter> filters = {
Everest::error::ErrorFilter(Everest::error::HandleFilter(test_errors[4]->uuid))};
Everest::error::ErrorDatabase::EditErrorFunc edit_func = [](Everest::error::ErrorPtr error) {
error->origin = ImplementationIdentifier("new_origin_module", "new_origin_implementation");
};
REQUIRE(db.get_errors(filters).size() > 0);
db.edit_errors(filters, edit_func);
THEN("The error should be edited") {
auto errors = db.get_errors(filters);
REQUIRE(errors.size() == 1);
REQUIRE(errors.front()->origin ==
ImplementationIdentifier("new_origin_module", "new_origin_implementation"));
}
}
WHEN("Edit error timestamp") {
std::list<Everest::error::ErrorFilter> filters = {
Everest::error::ErrorFilter(Everest::error::HandleFilter(test_errors[4]->uuid))};
auto new_timestamp = date::utc_clock::now() + std::chrono::hours(10);
Everest::error::ErrorDatabase::EditErrorFunc edit_func = [&new_timestamp](Everest::error::ErrorPtr error) {
error->timestamp = new_timestamp;
};
REQUIRE(db.get_errors(filters).size() > 0);
db.edit_errors(filters, edit_func);
THEN("The error should be edited") {
auto errors = db.get_errors(filters);
REQUIRE(errors.size() == 1);
REQUIRE(Everest::Date::to_rfc3339(errors.front()->timestamp) ==
Everest::Date::to_rfc3339(new_timestamp));
}
}
WHEN("Edit error uuid") {
Everest::error::UUID new_uuid;
std::list<Everest::error::ErrorFilter> filters = {
Everest::error::ErrorFilter(Everest::error::HandleFilter(test_errors[4]->uuid))};
Everest::error::ErrorDatabase::EditErrorFunc edit_func = [&new_uuid](Everest::error::ErrorPtr error) {
error->uuid = new_uuid;
};
REQUIRE(db.get_errors(filters).size() > 0);
db.edit_errors(filters, edit_func);
THEN("The error should be edited") {
auto errors = db.get_errors(filters);
REQUIRE(errors.size() == 0);
errors = db.get_errors({Everest::error::ErrorFilter(Everest::error::HandleFilter(new_uuid))});
REQUIRE(errors.size() == 1);
}
}
WHEN("Remove error") {
std::list<Everest::error::ErrorFilter> filters = {
Everest::error::ErrorFilter(Everest::error::HandleFilter(test_errors[4]->uuid))};
REQUIRE(db.get_errors(filters).size() > 0);
db.remove_errors(filters);
THEN("The error should be removed") {
auto errors = db.get_errors(filters);
REQUIRE(errors.size() == 0);
}
}
WHEN("Remove multiple errors") {
std::list<Everest::error::ErrorFilter> filters = {
Everest::error::ErrorFilter(Everest::error::StateFilter::Active),
Everest::error::ErrorFilter(
Everest::error::OriginFilter("test_origin_module_c", "test_origin_implementation_c"))};
REQUIRE(db.get_errors(filters).size() > 0);
db.remove_errors(filters);
THEN("The errors should be removed") {
auto errors = db.get_errors(filters);
REQUIRE(errors.size() == 0);
}
}
WHEN("Remove all errors") {
std::list<Everest::error::ErrorFilter> filters = {};
REQUIRE(db.get_errors(filters).size() > 0);
db.remove_errors(filters);
THEN("The errors should be removed") {
auto errors = db.get_errors(filters);
REQUIRE(errors.size() == 0);
}
}
}
}

View File

@@ -0,0 +1,134 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "helpers.hpp"
#include <catch2/catch_all.hpp>
#include <utils/error.hpp>
fs::path get_bin_dir() {
return fs::canonical("/proc/self/exe").parent_path();
}
std::string get_unique_db_name() {
return "error_database_sqlite_" + Everest::error::UUID().to_string() + ".db";
}
std::vector<Everest::error::ErrorPtr> get_test_errors() {
return {// index 0
std::make_shared<Everest::error::Error>(
"test_type_a", "test_sub_type_a", "test_message_a", "test_description_a",
ImplementationIdentifier("test_origin_module_a", "test_origin_implementation_a"), "everest-test",
Everest::error::Severity::Low, date::utc_clock::now(), Everest::error::UUID(),
Everest::error::State::ClearedByModule),
// index 1
std::make_shared<Everest::error::Error>(
"test_type_b", "test_sub_type_b", "test_message_b", "test_description_b",
ImplementationIdentifier("test_origin_module_b", "test_origin_implementation_b"), "everest-test",
Everest::error::Severity::Low, date::utc_clock::now() + std::chrono::hours(1), Everest::error::UUID(),
Everest::error::State::ClearedByModule),
// index 2
std::make_shared<Everest::error::Error>(
"test_type_c", "test_sub_type_c", "test_message_c", "test_description_c",
ImplementationIdentifier("test_origin_module_a", "test_origin_implementation_a"), "everest-test",
Everest::error::Severity::Low, date::utc_clock::now() + std::chrono::hours(2), Everest::error::UUID(),
Everest::error::State::ClearedByModule),
// index 3
std::make_shared<Everest::error::Error>(
"test_type_c", "test_sub_type_c", "test_message_c", "test_description_c",
ImplementationIdentifier("test_origin_module_c", "test_origin_implementation_c"), "everest-test",
Everest::error::Severity::Low, date::utc_clock::now() + std::chrono::hours(3), Everest::error::UUID(),
Everest::error::State::Active),
// index 4
std::make_shared<Everest::error::Error>(
"test_type_c", "test_sub_type_a", "test_message_c", "test_description_c",
ImplementationIdentifier("test_origin_module_a", "test_origin_implementation_a"), "everest-test",
Everest::error::Severity::Medium, date::utc_clock::now() + std::chrono::hours(4),
Everest::error::UUID(), Everest::error::State::Active),
// index 5
std::make_shared<Everest::error::Error>(
"test_type_c", "test_sub_type_a", "test_message_c", "test_description_c",
ImplementationIdentifier("test_origin_module_c", "test_origin_implementation_c"), "everest-test",
Everest::error::Severity::Medium, date::utc_clock::now() + std::chrono::hours(5),
Everest::error::UUID(), Everest::error::State::Active),
// index 6
std::make_shared<Everest::error::Error>(
"test_type_a", "test_sub_type_a", "test_message_a", "test_description_a",
ImplementationIdentifier("test_origin_module_a", "test_origin_implementation_a"), "everest-test",
Everest::error::Severity::Medium, date::utc_clock::now() + std::chrono::hours(6),
Everest::error::UUID(), Everest::error::State::ClearedByReboot),
// index 7
std::make_shared<Everest::error::Error>(
"test_type_a", "test_sub_type_a", "test_message_a", "test_description_a",
ImplementationIdentifier("test_origin_module_c", "test_origin_implementation_c"), "everest-test",
Everest::error::Severity::Medium, date::utc_clock::now() + std::chrono::hours(7),
Everest::error::UUID(), Everest::error::State::ClearedByReboot),
// index 8
std::make_shared<Everest::error::Error>(
"test_type_a", "test_sub_type_a", "test_message_a", "test_description_a",
ImplementationIdentifier("test_origin_module_a", "test_origin_implementation_a"), "everest-test",
Everest::error::Severity::High, date::utc_clock::now() + std::chrono::hours(8), Everest::error::UUID(),
Everest::error::State::ClearedByReboot),
// index 9
std::make_shared<Everest::error::Error>(
"test_type_c", "test_sub_type_c", "test_message_c", "test_description_c",
ImplementationIdentifier("test_origin_module_c", "test_origin_implementation_c"), "everest-test",
Everest::error::Severity::High, date::utc_clock::now() + std::chrono::hours(9), Everest::error::UUID(),
Everest::error::State::ClearedByReboot),
// index 10
std::make_shared<Everest::error::Error>(
"test_type_c", "test_sub_type_c", "test_message_c", "test_description_c",
ImplementationIdentifier("test_origin_module_a", "test_origin_implementation_a"), "everest-test",
Everest::error::Severity::High, date::utc_clock::now() + std::chrono::hours(10), Everest::error::UUID(),
Everest::error::State::ClearedByReboot),
// index 11
std::make_shared<Everest::error::Error>(
"test_type_b", "test_sub_type_b", "test_message_b", "test_description_b",
ImplementationIdentifier("test_origin_module_c", "test_origin_implementation_c"), "everest-test",
Everest::error::Severity::High, date::utc_clock::now() + std::chrono::hours(11), Everest::error::UUID(),
Everest::error::State::ClearedByReboot)};
}
void check_expected_errors_in_list(const std::vector<Everest::error::ErrorPtr>& expected_errors,
const std::list<Everest::error::ErrorPtr>& errors) {
REQUIRE(expected_errors.size() == errors.size());
for (Everest::error::ErrorPtr exp_err : expected_errors) {
auto result = std::find_if(errors.begin(), errors.end(), [&exp_err](const Everest::error::ErrorPtr& err) {
return exp_err->uuid == err->uuid;
});
REQUIRE(result != errors.end());
REQUIRE((*result)->type == exp_err->type);
REQUIRE((*result)->message == exp_err->message);
REQUIRE((*result)->description == exp_err->description);
REQUIRE((*result)->origin == exp_err->origin);
REQUIRE((*result)->severity == exp_err->severity);
REQUIRE(Everest::Date::to_rfc3339((*result)->timestamp) == Everest::Date::to_rfc3339(exp_err->timestamp));
REQUIRE((*result)->state == exp_err->state);
}
}
TestDatabase::TestDatabase(const fs::path& db_path_, const bool reset_) :
db_path(db_path_), db(std::make_unique<module::ErrorDatabaseSqlite>(db_path_, reset_)) {
}
TestDatabase::~TestDatabase() {
fs::remove(db_path);
}
void TestDatabase::add_error(Everest::error::ErrorPtr error) {
db->add_error(error);
}
std::list<Everest::error::ErrorPtr>
TestDatabase::get_errors(const std::list<Everest::error::ErrorFilter>& filters) const {
return db->get_errors(filters);
}
std::list<Everest::error::ErrorPtr> TestDatabase::edit_errors(const std::list<Everest::error::ErrorFilter>& filters,
Everest::error::ErrorDatabase::EditErrorFunc edit_func) {
return db->edit_errors(filters, edit_func);
}
std::list<Everest::error::ErrorPtr> TestDatabase::remove_errors(const std::list<Everest::error::ErrorFilter>& filters) {
return db->remove_errors(filters);
}

View File

@@ -0,0 +1,60 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef ERROR_HISTORY_TESTS_HELPERS_HPP
#define ERROR_HISTORY_TESTS_HELPERS_HPP
#include "../ErrorDatabaseSqlite.hpp"
#include <filesystem>
#include <list>
#include <utils/error.hpp>
namespace fs = std::filesystem;
///
/// \brief get the path to the binary directory
/// \return the path to the binary directory
///
fs::path get_bin_dir();
///
/// \brief get a unique database name
/// \return a unique database name
///
std::string get_unique_db_name();
///
/// \brief get a vector of test errors
/// \return a vector of test errors
///
std::vector<Everest::error::ErrorPtr> get_test_errors();
///
/// \brief check if the given errors are equal
/// \param expected_errors the expected errors
/// \param errors the errors to check
///
void check_expected_errors_in_list(const std::vector<Everest::error::ErrorPtr>& expected_errors,
const std::list<Everest::error::ErrorPtr>& errors);
///
/// \brief wrapper class for the ErrorDatabaseSqlite class
/// This class is used to test the ErrorDatabaseSqlite class
/// It proxies the ErrorDatabaseSqlite class, but
/// the destructor deletes the database file
///
class TestDatabase {
public:
explicit TestDatabase(const fs::path& db_path_, const bool reset_ = false);
~TestDatabase();
void add_error(Everest::error::ErrorPtr error);
std::list<Everest::error::ErrorPtr> get_errors(const std::list<Everest::error::ErrorFilter>& filters) const;
std::list<Everest::error::ErrorPtr> edit_errors(const std::list<Everest::error::ErrorFilter>& filters,
Everest::error::ErrorDatabase::EditErrorFunc edit_func);
std::list<Everest::error::ErrorPtr> remove_errors(const std::list<Everest::error::ErrorFilter>& filters);
private:
std::unique_ptr<module::ErrorDatabaseSqlite> db;
const fs::path db_path;
};
#endif // ERROR_HISTORY_TESTS_HELPERS_HPP

View File

@@ -0,0 +1,40 @@
#
# AUTO GENERATED - MARKED REGIONS WILL BE KEPT
# template version 3
#
# module setup:
# - ${MODULE_NAME}: module name
ev_setup_cpp_module()
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
# insert your custom targets and additional config variables here
target_link_libraries(${MODULE_NAME}
PRIVATE
Pal::Sigslot
everest::run_application
everest::system
)
target_sources(${MODULE_NAME}
PRIVATE
"diagnostics_handler.cpp"
"rauc_dbus.cpp"
)
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
target_sources(${MODULE_NAME}
PRIVATE
"main/systemImpl.cpp"
)
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
# insert other things like install cmds etc here
install(
PROGRAMS
constants.env
diagnostics_uploader.sh
diagnostics_collector.sh
DESTINATION "${EVEREST_MODULE_INSTALL_PREFIX}/${MODULE_NAME}"
)
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1

View File

@@ -0,0 +1,126 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "Linux_Systemd_Rauc.hpp"
#include <exception>
#include <stdexcept>
#include <string_view>
#include <type_traits>
#include <everest/system/safe_system.hpp>
namespace {
template <typename T> void safe_extract(T& dst, const Object& src, const std::string_view& item) {
try {
std::string s{item};
dst = src.at(s);
} catch (const std::exception& ex) {
EVLOG_error << "Store[" << item << "] error: " << ex.what();
if constexpr (std::is_integral_v<T>) {
dst = module::Rauc::request_id_default;
} else {
dst.clear();
}
}
}
} // namespace
namespace module {
void Linux_Systemd_Rauc::init() {
invoke_init(*p_main);
rauc.configure(this->config.VerifyUpdateScriptPath);
store_path = info.id + "_update_transaction";
// This is a transaction we should store permanently
rauc.signal_store_update_transaction.connect([this](Rauc::UpdateTransaction t) {
EVLOG_info << "Store update transaction: " << t.boot_slot << ' ' << t.primary_slot << " id: " << t.request_id;
Object tx = {
{"current_primary_slot", t.primary_slot}, {"current_boot_slot", t.boot_slot}, {"request_id", t.request_id}};
r_store->call_store(store_path, tx);
});
rauc.signal_remove_update_transaction.connect([this]() {
EVLOG_info << "Update transaction removed.";
r_store->call_delete(store_path);
});
rauc.signal_firmware_update_status.connect(
[this](const types::system::FirmwareUpdateStatusEnum& status, int32_t request_id) {
EVLOG_info << "Report status to OCPP: " << types::system::firmware_update_status_enum_to_string(status)
<< " Request id: " << request_id;
p_main->publish_firmware_update_status({status, request_id});
if (status == types::system::FirmwareUpdateStatusEnum::InstallRebooting) {
std::lock_guard<std::recursive_mutex> lock(this->firmware_update_progress_mx);
if (this->firmware_update_waiting_for_ocpp_unblocking) {
EVLOG_info << "Reboot is blocked by OCPP (waiting for 'allow_firmware_installation' call)";
this->firmware_update_reboot_scheduled = true;
} else {
reboot_after_firmware_update();
}
}
if (status == types::system::FirmwareUpdateStatusEnum::InstallVerificationFailed) {
EVLOG_info << "Resetting firmware update state due to reported 'InstallVerificationFailed' status.";
std::unique_lock<std::recursive_mutex> lock(this->firmware_update_progress_mx);
this->firmware_update_waiting_for_ocpp_unblocking = false;
}
});
}
void Linux_Systemd_Rauc::reboot_after_firmware_update() {
{
std::lock_guard<std::recursive_mutex> lock(this->firmware_update_progress_mx);
this->firmware_update_reboot_scheduled = false;
}
EVLOG_error << "-------------- Reboot after installation of update in 10 seconds ---------------";
sleep(10);
try {
auto [cmd, args] = everest::lib::system::split_command_line(config.RebootCommand);
auto res = everest::lib::system::safe_system(cmd, &args);
if (res.status != everest::lib::system::CommandExecutionStatus::CMD_SUCCESS || res.code != 0) {
EVLOG_error << "Unable to trigger reboot: ("
<< everest::lib::system::cmd_execution_status_to_string(res.status) << ": "
<< std::to_string(res.code) << ")";
EVLOG_info << "Failed command: '" << everest::lib::system::command_string_repr(cmd, args) << "'";
}
} catch (const std::exception& ex) {
EVLOG_error << "Configured reboot command is invalid: " << ex.what();
}
}
void Linux_Systemd_Rauc::ready() {
invoke_ready(*p_main);
// Check if we are booting directly after an update install,
// in this case close the update process on the CSMS
if (r_store->call_exists(store_path)) {
Rauc::UpdateTransaction tx;
auto t = std::get<Object>(r_store->call_load(store_path));
safe_extract(tx.boot_slot, t, "current_boot_slot");
safe_extract(tx.primary_slot, t, "current_primary_slot");
safe_extract(tx.request_id, t, "request_id");
rauc.check_previous_transaction(tx);
}
}
void Linux_Systemd_Rauc::firmware_update_may_proceed_with_reboot_callback() {
std::lock_guard<std::recursive_mutex> lock(this->firmware_update_progress_mx);
this->firmware_update_waiting_for_ocpp_unblocking = false;
if (this->firmware_update_reboot_scheduled) {
this->reboot_after_firmware_update();
}
}
void Linux_Systemd_Rauc::install_firmware_bundle(const std::string& filename, int32_t request_id) {
{
std::lock_guard<std::recursive_mutex> lock(this->firmware_update_progress_mx);
this->firmware_update_waiting_for_ocpp_unblocking = true;
}
this->rauc.install_bundle(filename, request_id);
}
} // namespace module

View File

@@ -0,0 +1,83 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef LINUX_SYSTEMD_RAUC_HPP
#define LINUX_SYSTEMD_RAUC_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 2
//
#include "ld-ev.hpp"
// headers for provided interface implementations
#include <generated/interfaces/system/Implementation.hpp>
// headers for required interface implementations
#include <generated/interfaces/kvs/Interface.hpp>
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
// insert your custom include headers here
#include "rauc_dbus.hpp"
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
namespace module {
struct Conf {
double DefaultRetries;
double DefaultRetryInterval;
std::string OCPPLogPath;
std::string SessionLogPath;
std::string RebootCommand;
std::string VerifyUpdateScriptPath;
};
class Linux_Systemd_Rauc : public Everest::ModuleBase {
public:
Linux_Systemd_Rauc() = delete;
Linux_Systemd_Rauc(const ModuleInfo& info, std::unique_ptr<systemImplBase> p_main, std::unique_ptr<kvsIntf> r_store,
Conf& config) :
ModuleBase(info), p_main(std::move(p_main)), r_store(std::move(r_store)), config(config){};
const std::unique_ptr<systemImplBase> p_main;
const std::unique_ptr<kvsIntf> r_store;
const Conf& config;
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
// insert your public definitions here
Rauc rauc;
void firmware_update_may_proceed_with_reboot_callback();
void install_firmware_bundle(const std::string& filename, int32_t request_id);
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
protected:
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
// insert your protected definitions here
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
private:
friend class LdEverest;
void init();
void ready();
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
// insert your private definitions here
std::string store_path;
std::recursive_mutex firmware_update_progress_mx;
bool firmware_update_waiting_for_ocpp_unblocking =
false; // Set to true in case of OTA update via OCPP; set to false when OCPP has signaled that installation may
// proceed
bool firmware_update_reboot_scheduled = false; // Set to true once a firmware update is installed but a restart has
// been blocked by firmware_update_waiting_for_ocpp_unblocking
void reboot_after_firmware_update();
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
};
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
// insert other definitions here
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
} // namespace module
#endif // LINUX_SYSTEMD_RAUC_HPP

View File

@@ -0,0 +1,27 @@
CONNECTION_TIMEOUT=20
DOWNLOADED="Downloaded"
DOWNLOADING="Downloading"
DOWNLOAD_FAILED="DownloadFailed"
DOWNLOAD_SCHEDULED="DownloadScheduled"
DOWNLOAD_PAUSED="DownloadPaused"
IDLE="Idle"
INSTALLATION_FAILED="InstallationFailed"
INSTALLING="Installing"
INSTALLED="Installed"
INSTALL_REBOOTING="InstallRebooting"
INSTALL_SCHEDULED="InstallScheduled"
INSTALL_VERIFICATION_FAILED="InstallVerificationFailed"
PERMISSION_DENIED="PermissionDenied"
NOT_SUPPORTED_OPERATION="NotSupportedOperation"
BAD_MESSAGE="BadMessage"
INVALID_SIGNATURE="InvalidSignature"
SIGNATURE_VERIFIED="SignatureVerified"
UPLOADED="Uploaded"
UPLOAD_FAILURE="UploadFailure"
UPLOADING="Uploading"

View File

@@ -0,0 +1,186 @@
#!/bin/sh
#
#
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
#
#
# time windowed collection is supported as follows:
# - systemd journal supports "since" and "until" to specify the time window
# - OCPP logs are rotated daily and a log file is included when its modification
# time falls between "since" and "until"
# - Completed session logs are included when their modification time falls
# between "since" and "until" (i.e. the end of session time is used)
# - In-progress session logs are included when their modification time falls
# between "since" and now i.e. where "until" is specified then in-progress
# sessions are unlikely to be included
error() {
echo "$script error: $1" 1>&2
exit 1
}
usage() {
echo "Usage: $script <journal|ocpp|session>"
echo " [--dir /full/path/to/log/directory]"
echo " [--since \"yyyy-mm-dd hh:mm:ss\"]"
echo " [--until \"yyyy-mm-dd hh:mm:ss\"]"
exit 2
}
section_start() {
printf "### START $1\n"
}
section_end() {
printf "\n### END $1\n"
}
# remove T from date/time strings
# 2023-12-18T16:06:03.435Z -> 2023-12-18 16:06:03.435Z
reformat_date() {
pre=$(echo $1 | cut -c1-10)
post=$(echo $1 | cut -c12-19)
if [ -n "$pre" ] && [ -n "$post" ]; then
echo "$pre $post"
fi
}
# obtain the modification time of a file as a timestamp
modify_ts() {
if [ -f "$1" ]; then
stat -c %Y "$1"
else
echo 0
fi
}
# convert the supplied date/time to a timestamp
date_ts() {
if [ -n "$1" ]; then
date -u +%s -d "$1"
fi
}
# output file if modified between optional dates
# $1 file name
# $2 optional not before date
# $3 optional not after date
# $4 line to output before file
output_file() {
if [ -f "$1" ]; then
local modify_time=$(modify_ts "$1")
local not_before=$(date_ts "$2")
local not_after=$(date_ts "$3")
local output=1
[ -n "$not_before" ] && [ $modify_time -lt $not_before ] && output=0
[ -n "$not_after" ] && [ $modify_time -gt $not_after ] && output=0
if [ $output -ne 0 ]; then
[ -n "$4" ] && echo "$4"
sed -e '/^$/d' "$1"
fi
fi
}
# output systemd journal
# $1 optional not before date
# $2 optional not after date
#
# journalctl doesn't like empty arguments
do_journal() {
section_start JOURNAL
if [ -n "$1" ]; then
if [ -n "$2" ]; then
journalctl --output=json --no-pager "--since=$1" "--until=$2"
else
journalctl --output=json --no-pager "--since=$1"
fi
else
journalctl --output=json --no-pager
fi
local res=$?
section_end JOURNAL
return $res
}
# output Everest OCPP logs
# $1 Everest OCPP log directory
# $2 optional not before date
# $3 optional not after date
do_ocpp() {
[ -z "$1" ] && error "Missing OCPP log directory"
local res=0
section_start OCPP
if [ -d "$1" ]; then
find "$1" -name \*.log\* | sort -r | while read ocpp
do
output_file "$ocpp" "$2" "$3"
done
res=$?
fi
section_end OCPP
return $res
}
# output Everest charging session logs
# $1 Everest session log directory
# $2 optional not before date
# $3 optional not after date
do_session() {
[ -z "$1" ] && error "Missing session log directory"
local name
local res=0
section_start SESSION
if [ -d "$1" ]; then
find "$1" -type d | sort -n | while read session
do
name=$(basename "${session}")
if [ -f "${session}/eventlog.csv" ]; then
output_file "${session}/eventlog.csv" "$2" "$3" "--- ${name}"
res=$?
fi
if [ -f "${session}/incomplete-eventlog.csv" ]; then
output_file "${session}/incomplete-eventlog.csv" "$2" "$3" "--- ${name}"
res=$?
fi
done
fi
section_end SESSION
return $res
}
script=$(basename $0)
[ -z "$1" ] && error "Missing sub-command"
cmd=$(basename "${1}")
shift
TEMP=$(getopt -o "" --long 'dir:,since:,until:' -n "$script" -- "$@")
if [ $? -ne 0 ]; then
error "getopt parsing"
fi
# Note the quotes around "$TEMP": they are essential!
eval set -- "$TEMP"
unset TEMP
dir=
since=
until=
while true
do
case "$1" in
"--dir"|"--since"|"--until") eval "${1#--}=\"$2\""; shift 2; continue;;
"--") shift; break;;
esac
done
since=$(reformat_date "$since")
until=$(reformat_date "$until")
case $cmd in
journal) do_journal "$since" "$until";;
ocpp) do_ocpp "$dir" "$since" "$until";;
session) do_session "$dir" "$since" "$until";;
*) usage;;
esac

View File

@@ -0,0 +1,79 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "diagnostics_handler.hpp"
#include <everest/logging.hpp>
#include <everest/system/safe_system.hpp>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <vector>
namespace module {
DiagnosticsHandler::log_result_t DiagnosticsHandler::create_log(const std::string& filename,
const std::optional<std::string>& from,
const std::optional<std::string>& to) {
log_result_t result = log_result_t::success;
int fd;
if ((fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC, S_IWUSR | S_IXUSR)) == -1) {
EVLOG_error << "Unable to create file journal file: " << filename << " (" << errno << ")";
result = log_result_t::error_file;
} else {
result = create_journal_log(fd, from, to);
if (result == log_result_t::success) {
result = create_ocpp_log(fd, from, to);
}
if (result == log_result_t::success) {
result = create_session_log(fd, from, to);
}
(void)close(fd);
}
return result;
}
DiagnosticsHandler::log_result_t DiagnosticsHandler::collect_logs(int fd, const char* logType,
const std::optional<std::string>& logDir,
const std::optional<std::string>& from,
const std::optional<std::string>& to) {
log_result_t result = log_result_t::success;
std::string arg_cmd = "diagnostics_collector.sh";
std::vector<std::string> args;
std::string cmd = script_dir + "/" + arg_cmd;
args.push_back(arg_cmd);
args.emplace_back(logType);
if (logDir.has_value()) {
args.emplace_back("--dir");
args.push_back(logDir.value());
}
if (from.has_value()) {
args.emplace_back("--since");
args.push_back(from.value());
}
if (to.has_value()) {
args.emplace_back("--until");
args.push_back(to.value());
}
const auto res = everest::lib::system::safe_system(fd, cmd, &args);
if (res.status != everest::lib::system::CommandExecutionStatus::CMD_SUCCESS || res.code != 0) {
EVLOG_error << "Unable to extract journal logs from:" << from.value_or("<not specified>")
<< " to:" << to.value_or("<not specified>") << " ("
<< everest::lib::system::cmd_execution_status_to_string(res.status) << ": "
<< std::to_string(res.code) << ")";
EVLOG_info << "Failed command: '" << everest::lib::system::command_string_repr(cmd, args) << "'";
result = log_result_t::error_parameter;
}
return result;
}
} // namespace module

View File

@@ -0,0 +1,58 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef DIAGNOSTICS_HANDLER_HPP
#define DIAGNOSTICS_HANDLER_HPP
#include <optional>
#include <string>
namespace module {
class DiagnosticsHandler {
protected:
std::string script_dir;
std::string ocpp_dir;
std::string session_dir;
public:
enum class log_result_t {
success,
error,
error_file,
error_parameter,
};
DiagnosticsHandler() = delete;
DiagnosticsHandler(const std::string& scriptDir, const std::string& ocppDir, const std::string& sessionDir) :
script_dir(scriptDir), ocpp_dir(ocppDir), session_dir(sessionDir) {
}
log_result_t create_log(const std::string& filename, const std::optional<std::string>& from,
const std::optional<std::string>& to);
protected:
log_result_t create_journal_log(int fd, const std::optional<std::string>& from,
const std::optional<std::string>& to) {
return collect_logs(fd, "journal", std::nullopt, from, to);
}
log_result_t create_ocpp_log(int fd, const std::optional<std::string>& from, const std::optional<std::string>& to) {
return collect_logs(fd, "ocpp", ocpp_dir, from, to);
}
log_result_t create_session_log(int fd, const std::optional<std::string>& from,
const std::optional<std::string>& to) {
return collect_logs(fd, "session", session_dir, from, to);
}
/// @brief collect logs between two optional dates
/// @param fd - the file descriptor to write the logs to
/// @param logType - journal/ocpp/session
/// @param logDir - directory location for OCPP and session logs
/// @param from - format: "yyyy-mm-dd hh:mm"
/// @param to - format: "yyyy-mm-dd hh:mm"
/// @return result
log_result_t collect_logs(int fd, const char* logType, const std::optional<std::string>& logDir,
const std::optional<std::string>& from, const std::optional<std::string>& to);
};
} // namespace module
#endif // DIAGNOSTICS_HANDLER_HPP

View File

@@ -0,0 +1,27 @@
#!/bin/bash
#
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
#
. "${1}"
echo "$UPLOADING"
sleep 2
curl -L --progress-bar --connect-timeout "$CONNECTION_TIMEOUT" -T "${4}" "${2}"
curl_exit_code=$?
if [[ $curl_exit_code -eq 0 ]]; then
echo "$UPLOADED"
elif [[ $curl_exit_code -eq 67 ]] || [[ $curl_exit_code -eq 35 ]] || [[ $curl_exit_code -eq 69 ]] ||
[[ $curl_exit_code -eq 9 ]]; then
echo "$PERMISSION_DENIED"
elif [[ $curl_exit_code -eq 3 ]] || [[ $curl_exit_code -eq 6 ]] || [[ $curl_exit_code -eq 10 ]] ||
[[ $curl_exit_code -eq 87 ]]; then
echo "$BAD_MESSAGE"
elif [[ $curl_exit_code -eq 1 ]]; then
echo "$NOT_SUPPORTED_OPERATION"
else
echo "$UPLOAD_FAILURE"
fi

View File

@@ -0,0 +1,191 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "systemImpl.hpp"
#include "diagnostics_handler.hpp"
#include <fstream>
#include <everest/run_application/run_application.hpp>
using namespace everest::run_application;
namespace module {
namespace main {
const std::string CONSTANTS = "constants.env";
const std::string DIAGNOSTICS_UPLOADER = "diagnostics_uploader.sh";
namespace fs = std::filesystem;
// FIXME (aw): this function needs to be refactored into some kind of utility library
fs::path create_temp_file(const fs::path& dir, const std::string& prefix) {
const std::string fn_template = (dir / prefix).string() + "XXXXXX" + std::string(1, '\0');
std::vector<char> fn_template_buffer{fn_template.begin(), fn_template.end()};
// mkstemp needs to have at least 6 XXXXXX at the end and it will replace these
// with a valid file name
auto fd = mkstemp(fn_template_buffer.data());
if (fd == -1) {
EVLOG_AND_THROW(Everest::EverestBaseRuntimeError("Failed to create temporary file at: " + fn_template));
}
// close the file descriptor
close(fd);
return fn_template_buffer.data();
}
void systemImpl::init() {
this->scripts_path = mod->info.paths.libexec;
}
void systemImpl::ready() {
}
types::system::UpdateFirmwareResponse
systemImpl::handle_update_firmware(types::system::FirmwareUpdateRequest& firmware_update_request) {
// FIXME: implement planned updates at a specific time
// FIXME: we don't care about the certificate and signature provided as an argument for now.
// RAUC will not use them anyhow and updates will be equally secure whether they are launched by OCPP secure update
// mechanism or the old non-secure mechanism.
if (mod->rauc.is_idle()) {
EVLOG_info << "Installing bundle from URL: " << firmware_update_request.location;
this->mod->install_firmware_bundle(firmware_update_request.location, firmware_update_request.request_id);
return types::system::UpdateFirmwareResponse::Accepted;
} else {
return types::system::UpdateFirmwareResponse::Rejected;
}
}
types::system::UploadLogsResponse
systemImpl::handle_upload_logs(types::system::UploadLogsRequest& upload_logs_request) {
types::system::UploadLogsResponse response;
if (this->log_upload_running) {
response.upload_logs_status = types::system::UploadLogsStatus::AcceptedCanceled;
} else {
response.upload_logs_status = types::system::UploadLogsStatus::Accepted;
}
const auto date_time = Everest::Date::to_rfc3339(date::utc_clock::now());
const auto diagnostics_file_path = create_temp_file(fs::temp_directory_path(), "diagnostics-" + date_time);
const auto diagnostics_file_name = diagnostics_file_path.filename().string();
response.upload_logs_status = types::system::UploadLogsStatus::Accepted;
response.file_name = diagnostics_file_name;
// populate file with available logs within the specified time window
DiagnosticsHandler diag(mod->info.paths.libexec, mod->config.OCPPLogPath, mod->config.SessionLogPath);
const auto create_result = diag.create_log(diagnostics_file_path.c_str(), upload_logs_request.oldest_timestamp,
upload_logs_request.latest_timestamp);
this->upload_logs_thread =
std::thread([this, create_result, upload_logs_request, diagnostics_file_name, diagnostics_file_path]() {
if (this->log_upload_running) {
EVLOG_info << "Received Log upload request and log upload already running - cancelling current upload";
this->interrupt_log_upload.exchange(true);
EVLOG_info << "Waiting for other log upload to finish...";
std::unique_lock<std::mutex> lk(this->log_upload_mutex);
this->log_upload_cv.wait(lk, [this]() { return !this->log_upload_running; });
EVLOG_info << "Previous Log upload finished!";
}
std::lock_guard<std::mutex> lg(this->log_upload_mutex);
EVLOG_info << "Starting upload of log file";
this->interrupt_log_upload.exchange(false);
this->log_upload_running = true;
const auto diagnostics_uploader = this->scripts_path / DIAGNOSTICS_UPLOADER;
const auto constants = this->scripts_path / CONSTANTS;
std::vector<std::string> args = {constants.string(), upload_logs_request.location, diagnostics_file_name,
diagnostics_file_path.string()};
bool uploaded = false;
int32_t retries = 0;
const auto total_retries = upload_logs_request.retries.value_or(this->mod->config.DefaultRetries);
const auto retry_interval =
upload_logs_request.retry_interval_s.value_or(this->mod->config.DefaultRetryInterval);
types::system::LogStatus log_status;
if (create_result == DiagnosticsHandler::log_result_t::error_file) {
// problem creating the file - nothing to upload
log_status.log_status = types::system::LogStatusEnum::UploadFailure;
this->publish_log_status(log_status);
} else {
while (!uploaded && retries <= total_retries && !this->interrupt_log_upload) {
retries += 1;
log_status.request_id = upload_logs_request.request_id.value_or(-1);
run_application(
diagnostics_uploader.string(), args, [this, &log_status](const std::string& output_line) {
if (output_line == "Uploaded") {
log_status.log_status = types::system::string_to_log_status_enum(output_line);
} else if (output_line == "UploadFailure" || output_line == "PermissionDenied" ||
output_line == "BadMessage" || output_line == "NotSupportedOperation") {
log_status.log_status = types::system::LogStatusEnum::UploadFailure;
} else {
log_status.log_status = types::system::LogStatusEnum::Uploading;
}
this->publish_log_status(log_status);
if (this->interrupt_log_upload) {
return CmdControl::Terminate;
}
return CmdControl::Continue;
});
if (this->interrupt_log_upload) {
EVLOG_info << "Uploading Logs was interrupted, terminating upload script, requestId: "
<< log_status.request_id;
// N01.FR.20
log_status.log_status = types::system::LogStatusEnum::AcceptedCanceled;
this->publish_log_status(log_status);
} else if (log_status.log_status != types::system::LogStatusEnum::Uploaded &&
retries <= total_retries) {
std::this_thread::sleep_for(std::chrono::seconds(retry_interval));
} else {
uploaded = true;
}
}
}
this->log_upload_running = false;
this->log_upload_cv.notify_one();
EVLOG_info << "Log upload thread finished";
});
this->upload_logs_thread.detach();
return response;
}
bool systemImpl::handle_is_reset_allowed(types::system::ResetType& type) {
// Allow resets at any time for now
return true;
}
void systemImpl::handle_reset(types::system::ResetType& type, bool& scheduled) {
if (type == types::system::ResetType::Soft) {
EVLOG_info << "Performing soft reset";
// This will effectivly stop everest and make it restart via systemd
exit(255);
} else {
EVLOG_info << "Performing hard reset";
// this reboots the whole linux system
system("/sbin/reboot");
}
}
bool systemImpl::handle_set_system_time(std::string& timestamp) {
// currently not supported, system runs on network time
return true;
}
types::system::BootReason systemImpl::handle_get_boot_reason() {
return types::system::BootReason::Unknown;
}
void systemImpl::handle_allow_firmware_installation() {
EVLOG_info << "Received allow_firmware_installation command - allow firmware update to proceed with reboot.";
this->mod->firmware_update_may_proceed_with_reboot_callback();
}
} // namespace main
} // namespace module

View File

@@ -0,0 +1,76 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef MAIN_SYSTEM_IMPL_HPP
#define MAIN_SYSTEM_IMPL_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 3
//
#include <generated/interfaces/system/Implementation.hpp>
#include "../Linux_Systemd_Rauc.hpp"
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
// insert your custom include headers here
#include <filesystem>
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
namespace module {
namespace main {
struct Conf {};
class systemImpl : public systemImplBase {
public:
systemImpl() = delete;
systemImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<Linux_Systemd_Rauc>& mod, Conf& config) :
systemImplBase(ev, "main"), mod(mod), config(config){};
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
// insert your public definitions here
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
protected:
// command handler functions (virtual)
virtual types::system::UpdateFirmwareResponse
handle_update_firmware(types::system::FirmwareUpdateRequest& firmware_update_request) override;
virtual void handle_allow_firmware_installation() override;
virtual types::system::UploadLogsResponse
handle_upload_logs(types::system::UploadLogsRequest& upload_logs_request) override;
virtual bool handle_is_reset_allowed(types::system::ResetType& type) override;
virtual void handle_reset(types::system::ResetType& type, bool& scheduled) override;
virtual bool handle_set_system_time(std::string& timestamp) override;
virtual types::system::BootReason handle_get_boot_reason() override;
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
// insert your protected definitions here
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
private:
const Everest::PtrContainer<Linux_Systemd_Rauc>& mod;
const Conf& config;
virtual void init() override;
virtual void ready() override;
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
// insert your private definitions here
std::filesystem::path scripts_path;
bool log_upload_running{false};
std::atomic_bool interrupt_log_upload;
std::thread upload_logs_thread;
std::mutex log_upload_mutex;
std::condition_variable log_upload_cv;
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
};
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
// insert other definitions here
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
} // namespace main
} // namespace module
#endif // MAIN_SYSTEM_IMPL_HPP

View File

@@ -0,0 +1,45 @@
description: This module implements system wide operations for the base linux system
config:
DefaultRetries:
description: Specifies how many times Charge Point tries to upload or download files on previous failure.
type: number
default: 1
DefaultRetryInterval:
description: >-
Specifies in seconds after which time a retry of an upload or download on previous failure may be attempted.
type: number
default: 1
OCPPLogPath:
description: Path to folder where logs of all OCPP messages get written to (see ocpp MessageLogPath)
type: string
default: /var/log/everest/ocpp
SessionLogPath:
description: Output directory for session log files (see evse_manager session_logging_path)
type: string
default: /var/log/everest/session
RebootCommand:
description: Command to execute for rebooting the system
type: string
default: /sbin/reboot
VerifyUpdateScriptPath:
description: >-
Full path to shell script that checks if an OTA update has been successful. The script is executed after
an update and shall verify the success of the update. It shall return 0 in case of success and
non-zero in case of failure.
If empty no check is performed and the update is considered successful and it is marked as good.
type: string
default: ""
provides:
main:
description: Implements the system interface
interface: system
requires:
store:
interface: kvs
enable_external_mqtt: false
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Piet Gömpel
- Cornelius Claussen
- James Chapman

View File

@@ -0,0 +1,165 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "rauc_dbus.hpp"
#include <everest/logging.hpp>
namespace module {
using namespace everest::lib::system;
void Rauc::configure_handlers() {
namespace interface = rauc_dbus::interface;
namespace property = rauc_dbus::property;
namespace signal = rauc_dbus::signal;
// Subscribe to Complete signal (when install_bundle command finishes)
dbus::registerSignalHandler(proxy, interface::Installer, signal::Completed, [this](sdbus::Signal signal) {
// Complete signal has one int argument
int i;
signal >> i;
if (update_request_id != request_id_default) {
if (i == 0) {
EVLOG_info << "RAUC: Installation successful, needs reboot to activate";
// Signal to the module code to store the transaction in the database.
// We will use this on next boot to signal a Success/Failed Installation
signal_store_update_transaction(create_transaction(update_request_id, timeout_us));
// The module code should reboot now since we signal InstallRebooting.
signal_firmware_update_status(types::system::FirmwareUpdateStatusEnum::InstallRebooting,
update_request_id);
} else {
EVLOG_error << "RAUC: Installation failed with error code: " << i;
if (is_installing) {
signal_firmware_update_status(types::system::FirmwareUpdateStatusEnum::InstallVerificationFailed,
update_request_id);
is_installing = false;
}
}
} else {
EVLOG_debug << "RAUC: status from another source";
}
// this was the last message, so reset request_id
update_request_id = request_id_default;
});
// Subscribe to property changes std::function<void(Signal signal)>;
dbus::registerSignalHandler(
proxy, interface::DBus_Properties, signal::PropertiesChanged, [this](sdbus::Signal signal) {
// org.freedesktop.DBus.Properties.PropertiesChanged (STRING interface_name,
// ARRAY of DICT_ENTRY<STRING,VARIANT>
// changed_properties, ARRAY<STRING>
// invalidated_properties);
std::string interface;
signal >> interface;
// ignore updates when initiated by someone else
if (update_request_id != request_id_default) {
if (interface == interface::Installer) {
std::map<std::string, sdbus::Variant> changed_properties;
signal >> changed_properties;
for (const auto& [key, value] : changed_properties) {
if (key == property::Progress) {
Progress r = value.get<Progress>();
EVLOG_info << "Progress " << r.percent << "% " << r.description;
// Map progress to OCPP structs
if (r.description.find("Verifying signature done") != std::string::npos) {
signal_firmware_update_status(types::system::FirmwareUpdateStatusEnum::Downloaded,
update_request_id);
signal_firmware_update_status(
types::system::FirmwareUpdateStatusEnum::SignatureVerified, update_request_id);
signature_verified = true;
} else if (r.description.find("Verifying signature failed") != std::string::npos) {
signal_firmware_update_status(types::system::FirmwareUpdateStatusEnum::Downloaded,
update_request_id);
signal_firmware_update_status(types::system::FirmwareUpdateStatusEnum::InvalidSignature,
update_request_id);
signature_verified = true;
// If bundle checking failed but we never got to signature verification download must
// have failed
} else if (!signature_verified &&
r.description.find("Checking bundle failed") != std::string::npos) {
signal_firmware_update_status(types::system::FirmwareUpdateStatusEnum::DownloadFailed,
update_request_id);
} else if (r.description.find("Copying") != std::string::npos &&
r.description.find("done") == std::string::npos) {
is_installing = true;
signal_firmware_update_status(types::system::FirmwareUpdateStatusEnum::Installing,
update_request_id);
}
} else if (key == property::Operation) {
auto r = rauc_dbus::rauc_messages::string_to_operation(value.get<std::string>());
if (r == Operation::Idle) {
EVLOG_info << "RAUC operation: Idle";
} else if (r == Operation::Installing) {
EVLOG_info << "RAUC operation: Installing";
}
}
}
std::vector<std::string> invalidated_properties;
signal >> invalidated_properties;
}
}
});
}
bool Rauc::decide_if_good(const rauc_dbus::rauc_messages::UpdateTransaction& saved, const CurrentState& current) {
// The original approach uses the primary slot, however this might not be
// as reliable as hoped. A change in boot slot should be more reliable
// however prior to this change the boot slot wasn't saved
bool result{false};
if (RaucBase::decide_if_good(saved, current)) {
if (saved.boot_slot.empty()) {
// use the previous approach
EVLOG_warning << "OTA: fallback to using primary slot";
result = saved.primary_slot == current.primary_slot;
} else {
// use the new approach
result = saved.boot_slot != current.boot_slot;
}
}
return result;
}
// Call on boot and pass a previous transaction that was not closed yet
void Rauc::check_previous_transaction(UpdateTransaction t) {
signal_remove_update_transaction();
if (rauc_dbus::RaucBaseSync::check_previous_transaction(t, timeout_us)) {
signal_firmware_update_status(types::system::FirmwareUpdateStatusEnum::Installed, t.request_id);
} else {
signal_firmware_update_status(types::system::FirmwareUpdateStatusEnum::InstallationFailed, t.request_id);
}
}
// Returns immediately. Progress is signalled with signal_firmware_update_status
rauc_dbus::rauc_messages::CmdResult Rauc::install_bundle(const std::string& filename, int32_t request_id) {
signature_verified = false;
is_installing = false;
update_request_id = request_id;
const auto ret = rauc_dbus::RaucBaseSync::install_bundle(filename, timeout_us);
if (ret.success) {
signal_firmware_update_status(types::system::FirmwareUpdateStatusEnum::Downloading, update_request_id);
} else {
update_request_id = request_id_default;
}
return ret;
}
bool Rauc::is_idle() {
// Note it is important to query rauc here as well as it may be busy with a local install
return (get_operation() == rauc_dbus::rauc_messages::Operation::Idle) && !is_installing;
}
} // namespace module

View File

@@ -0,0 +1,61 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef RAUC_DBUS_HPP
#define RAUC_DBUS_HPP
#include <everest/system/rauc_dbus_base.hpp>
#include <sigslot/signal.hpp>
#include <generated/types/system.hpp>
#include <atomic>
#include <cstdint>
#include <unistd.h>
namespace module {
namespace rauc_dbus = everest::lib::system::rauc_dbus;
class Rauc : public rauc_dbus::RaucBaseSync {
public:
// Note OCPP defaults to -1 when not provided
static constexpr std::int32_t request_id_default = 0;
private:
constexpr static std::uint64_t timeout_us = 10 * 1000 * 1000; // 10 seconds
std::atomic<int32_t> update_request_id{request_id_default};
std::atomic_bool is_installing{false};
std::atomic_bool signature_verified{false};
void configure_handlers() override;
bool decide_if_good(const rauc_dbus::rauc_messages::UpdateTransaction& saved, const CurrentState& current) override;
public:
using CmdResult = rauc_dbus::rauc_messages::CmdResult;
using UpdateTransaction = rauc_dbus::rauc_messages::UpdateTransaction;
using Operation = rauc_dbus::rauc_messages::Operation;
using Progress = rauc_dbus::rauc_messages::Progress;
using rauc_dbus::RaucBaseSync::RaucBaseSync;
Rauc(sdbus::dont_run_event_loop_thread_t) = delete;
void check_previous_transaction(UpdateTransaction t);
bool is_idle();
CmdResult install_bundle(const std::string& filename, int32_t request_id);
void mark(const std::string& mark_s, const std::string& slot) {
rauc_dbus::RaucBaseSync::mark(mark_s, slot, timeout_us);
}
sigslot::signal<types::system::FirmwareUpdateStatusEnum, int32_t> signal_firmware_update_status;
// Emitted when installed update is ready for reboot, the transaction needs to be stored persistently. On next boot,
// call check_previous_transaction() with this as an argument
sigslot::signal<UpdateTransaction> signal_store_update_transaction;
sigslot::signal<> signal_remove_update_transaction;
};
} // namespace module
#endif

View File

@@ -0,0 +1,21 @@
#
# AUTO GENERATED - MARKED REGIONS WILL BE KEPT
# template version 3
#
# module setup:
# - ${MODULE_NAME}: module name
ev_setup_cpp_module()
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
# insert your custom targets and additional config variables here
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
target_sources(${MODULE_NAME}
PRIVATE
"token_validator/auth_token_validatorImpl.cpp"
)
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
# insert other things like install cmds etc here
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1

View File

@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "LocalAllowlistTokenValidator.hpp"
namespace module {
void LocalAllowlistTokenValidator::init() {
invoke_init(*p_token_validator);
}
void LocalAllowlistTokenValidator::ready() {
invoke_ready(*p_token_validator);
}
} // namespace module

View File

@@ -0,0 +1,61 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef LOCAL_ALLOWLIST_TOKEN_VALIDATOR_HPP
#define LOCAL_ALLOWLIST_TOKEN_VALIDATOR_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 2
//
#include "ld-ev.hpp"
// headers for provided interface implementations
#include <generated/interfaces/auth_token_validator/Implementation.hpp>
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
// insert your custom include headers here
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
namespace module {
struct Conf {
std::string allowlist_file;
};
class LocalAllowlistTokenValidator : public Everest::ModuleBase {
public:
LocalAllowlistTokenValidator() = delete;
LocalAllowlistTokenValidator(const ModuleInfo& info,
std::unique_ptr<auth_token_validatorImplBase> p_token_validator, Conf& config) :
ModuleBase(info), p_token_validator(std::move(p_token_validator)), config(config){};
const std::unique_ptr<auth_token_validatorImplBase> p_token_validator;
const Conf& config;
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
// insert your public definitions here
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
protected:
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
// insert your protected definitions here
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
private:
friend class LdEverest;
void init();
void ready();
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
// insert your private definitions here
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
};
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
// insert other definitions here
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
} // namespace module
#endif // LOCAL_ALLOWLIST_TOKEN_VALIDATOR_HPP

View File

@@ -0,0 +1,15 @@
description: Token Validator for local allow list of RFID tags
config:
allowlist_file:
description: path/filename of the file containing one RFID token per line
type: string
default: /mnt/user_data/etc/allowlist_rfid.txt
provides:
token_validator:
description: Validator interface for auth
interface: auth_token_validator
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Cornelius Claussen

View File

@@ -0,0 +1,45 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include <fstream>
#include "auth_token_validatorImpl.hpp"
namespace module {
namespace token_validator {
void auth_token_validatorImpl::init() {
}
void auth_token_validatorImpl::ready() {
}
types::authorization::ValidationResult
auth_token_validatorImpl::handle_validate_token(types::authorization::ProvidedIdToken& provided_token) {
types::authorization::ValidationResult result;
result.authorization_status = types::authorization::AuthorizationStatus::Invalid;
// load file each time we validate so that EVerest requires no restart when the file is changed
std::ifstream file;
try {
file.open(mod->config.allowlist_file);
while (!file.eof()) {
std::string token;
getline(file, token);
if (token == provided_token.id_token.value) {
result.authorization_status = types::authorization::AuthorizationStatus::Accepted;
break;
}
}
} catch (std::ifstream::failure e) {
EVLOG_error << "Error opening/reading file " + mod->config.allowlist_file;
}
file.close();
return result;
}
} // namespace token_validator
} // namespace module

View File

@@ -0,0 +1,63 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#ifndef TOKEN_VALIDATOR_AUTH_TOKEN_VALIDATOR_IMPL_HPP
#define TOKEN_VALIDATOR_AUTH_TOKEN_VALIDATOR_IMPL_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 3
//
#include <generated/interfaces/auth_token_validator/Implementation.hpp>
#include "../LocalAllowlistTokenValidator.hpp"
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
// insert your custom include headers here
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
namespace module {
namespace token_validator {
struct Conf {};
class auth_token_validatorImpl : public auth_token_validatorImplBase {
public:
auth_token_validatorImpl() = delete;
auth_token_validatorImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<LocalAllowlistTokenValidator>& mod,
Conf& config) :
auth_token_validatorImplBase(ev, "token_validator"), mod(mod), config(config){};
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
// insert your public definitions here
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
protected:
// command handler functions (virtual)
virtual types::authorization::ValidationResult
handle_validate_token(types::authorization::ProvidedIdToken& provided_token) override;
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
// insert your protected definitions here
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
private:
const Everest::PtrContainer<LocalAllowlistTokenValidator>& mod;
const Conf& config;
virtual void init() override;
virtual void ready() override;
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
// insert your private definitions here
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
};
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
// insert other definitions here
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
} // namespace token_validator
} // namespace module
#endif // TOKEN_VALIDATOR_AUTH_TOKEN_VALIDATOR_IMPL_HPP

View File

@@ -0,0 +1,23 @@
#
# AUTO GENERATED - MARKED REGIONS WILL BE KEPT
# template version 3
#
# module setup:
# - ${MODULE_NAME}: module name
ev_setup_cpp_module()
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
# insert your custom targets and additional config variables here
list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
find_package(PCAP REQUIRED)
target_link_libraries(${MODULE_NAME}
PRIVATE
${PCAP_LIBRARY}
)
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
# insert other things like install cmds etc here
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1

View File

@@ -0,0 +1,113 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "PacketSniffer.hpp"
#include <fmt/core.h>
#include <fmt/chrono.h>
namespace module {
const bool PROMISC_MODE = true;
const int PACKET_BUFFER_TIMEOUT_MS = 1000;
const int ALL_PACKETS_PROCESSED = -1;
const int WAIT_FOR_MS = 10;
const int BUFFERSIZE = 8192;
void PacketSniffer::init() {
p_handle = pcap_open_live(config.device.c_str(), BUFFERSIZE, PROMISC_MODE, PACKET_BUFFER_TIMEOUT_MS, errbuf);
std::string errb{errbuf};
if (p_handle == nullptr) {
EVLOG_error << fmt::format("Could not open device \"{}\", Sniffing disabled.{}", config.device,
errb.size() > 0 ? (std::string(" Error: ") + errb) : "");
return;
}
if (config.device != "any" && pcap_datalink(p_handle) != DLT_EN10MB) {
EVLOG_error << fmt::format("Device \"{}\" doesn't provide Ethernet headers - not supported. Sniffing disabled.",
config.device);
pcap_close(p_handle);
return;
}
EVLOG_info << fmt::format("Sniffing on device \"{}\"", config.device);
r_evse_manager->subscribe_session_event([this](types::evse_manager::SessionEvent session_event) {
if (session_event.event == types::evse_manager::SessionEventEnum::SessionStarted) {
if (!already_started) {
if (!session_event.session_started.has_value()) {
EVLOG_warning
<< "SessionStarted event type doesn't contain session_started data. Ignoring this event.";
return;
}
std::string logging_path;
if (!config.session_logging_path.empty()) {
logging_path = config.session_logging_path;
} else if (session_event.session_started->logging_path.has_value()) {
logging_path = session_event.session_started->logging_path.value();
} else {
EVLOG_warning << "No logging path configured and none provided in SessionStarted event. "
"Skipping capture.";
return;
}
capturing_stopped = false;
std::thread(&PacketSniffer::capture, this, logging_path, session_event.uuid).detach();
} else {
EVLOG_warning << fmt::format("Capturing already started. Ignoring this SessionStarted event");
}
} else if (session_event.event == types::evse_manager::SessionEventEnum::SessionFinished) {
capturing_stopped = true;
pcap_breakloop(p_handle);
}
});
}
void PacketSniffer::ready() {
}
void PacketSniffer::capture(const std::string& logpath, const std::string& session_id) {
already_started = true;
std::string fn = fmt::format("{}/ethernet-traffic.pcap", logpath);
if (not config.session_logging_path.empty()) {
const auto now = std::chrono::system_clock::now();
const auto time_t_now = std::chrono::system_clock::to_time_t(now);
std::tm local_tm{};
localtime_r(&time_t_now, &local_tm);
const auto timestamp = fmt::format("{:%Y-%m-%d_%H-%M-%S%z}", local_tm);
fn = fmt::format("{}/{}_{}.pcap", logpath, timestamp, session_id);
}
EVLOG_info << fmt::format("Starting capturing to {}", fn);
if ((pdumpfile = pcap_dump_open(p_handle, fn.c_str())) == nullptr) {
EVLOG_error << fmt::format("Error opening savefile {} for writing: {}", fn, pcap_geterr(p_handle));
return;
}
while (!capturing_stopped) {
const int ret =
pcap_dispatch(p_handle, ALL_PACKETS_PROCESSED, &pcap_dump, reinterpret_cast<u_char*>(pdumpfile));
if (ret <= PCAP_ERROR) {
const std::string base_msg = fmt::format("Error reading packets from interface \"{}\"", config.device);
if (ret == PCAP_ERROR) {
EVLOG_error << fmt::format("{}, error: {}", base_msg, pcap_geterr(p_handle));
} else if (ret == PCAP_ERROR_BREAK) {
EVLOG_warning << fmt::format("{}, interrupted but no packets received", base_msg);
} else {
EVLOG_error << fmt::format("{}, unexpected error: {}", base_msg, ret);
}
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(WAIT_FOR_MS));
}
pcap_dump_close(pdumpfile);
EVLOG_info << fmt::format("Stopped capturing to {}", fn);
already_started = false;
}
} // namespace module

View File

@@ -0,0 +1,68 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef PACKET_SNIFFER_HPP
#define PACKET_SNIFFER_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 2
//
#include "ld-ev.hpp"
// headers for required interface implementations
#include <generated/interfaces/evse_manager/Interface.hpp>
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
// insert your custom include headers here
#include <pcap.h>
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
namespace module {
struct Conf {
std::string device;
std::string session_logging_path;
};
class PacketSniffer : public Everest::ModuleBase {
public:
PacketSniffer() = delete;
PacketSniffer(const ModuleInfo& info, std::unique_ptr<evse_managerIntf> r_evse_manager, Conf& config) :
ModuleBase(info), r_evse_manager(std::move(r_evse_manager)), config(config){};
const std::unique_ptr<evse_managerIntf> r_evse_manager;
const Conf& config;
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
// insert your public definitions here
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
protected:
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
// insert your protected definitions here
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
private:
friend class LdEverest;
void init();
void ready();
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
// insert your private definitions here
void capture(const std::string& logpath, const std::string& session_id);
pcap_t* p_handle{nullptr};
pcap_dumper_t* pdumpfile{nullptr};
char errbuf[PCAP_ERRBUF_SIZE]{""};
std::atomic_bool capturing_stopped{false};
std::atomic_bool already_started{false};
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
};
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
// insert other definitions here
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
} // namespace module
#endif // PACKET_SNIFFER_HPP

View File

@@ -0,0 +1,118 @@
# FindPCAP.cmake
# ===========================================
# See https://github.com/zeek/cmake/FindPCAP.cmake for usage and update instructions.
#
# BSD License
# -----------
#[[
Copyright (c) 1995-2017, The Regents of the University of California
through the Lawrence Berkeley National Laboratory and the
International Computer Science Institute. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
(1) Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
(2) Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
(3) Neither the name of the University of California, Lawrence Berkeley
National Laboratory, U.S. Dept. of Energy, International Computer
Science Institute, nor the names of contributors may be used to endorse
or promote products derived from this software without specific prior
written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
Note that some files in the distribution may carry their own copyright
notices.
]]
# - Try to find libpcap include dirs and libraries
#
# Usage of this module as follows:
#
# find_package(PCAP)
#
# Variables used by this module, they can change the default behaviour and need
# to be set before calling find_package:
#
# PCAP_ROOT_DIR Set this variable to the root installation of
# libpcap if the module has problems finding the
# proper installation path.
#
# Variables defined by this module:
#
# PCAP_FOUND System has libpcap, include and library dirs found
# PCAP_INCLUDE_DIR The libpcap include directories.
# PCAP_LIBRARY The libpcap library (possibly includes a thread
# library e.g. required by pf_ring's libpcap)
# HAVE_PF_RING If a found version of libpcap supports PF_RING
find_path(PCAP_ROOT_DIR
NAMES include/pcap.h
)
find_path(PCAP_INCLUDE_DIR
NAMES pcap.h
HINTS ${PCAP_ROOT_DIR}/include
)
find_library(PCAP_LIBRARY
NAMES pcap
HINTS ${PCAP_ROOT_DIR}/lib
)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(PCAP DEFAULT_MSG
PCAP_LIBRARY
PCAP_INCLUDE_DIR
)
include(CheckCSourceCompiles)
set(CMAKE_REQUIRED_LIBRARIES ${PCAP_LIBRARY})
check_c_source_compiles("int main() { return 0; }" PCAP_LINKS_SOLO)
set(CMAKE_REQUIRED_LIBRARIES)
# check if linking against libpcap also needs to link against a thread library
if (NOT PCAP_LINKS_SOLO)
find_package(Threads)
if (THREADS_FOUND)
set(CMAKE_REQUIRED_LIBRARIES ${PCAP_LIBRARY} ${CMAKE_THREAD_LIBS_INIT})
check_c_source_compiles("int main() { return 0; }" PCAP_NEEDS_THREADS)
set(CMAKE_REQUIRED_LIBRARIES)
endif ()
if (THREADS_FOUND AND PCAP_NEEDS_THREADS)
set(_tmp ${PCAP_LIBRARY} ${CMAKE_THREAD_LIBS_INIT})
list(REMOVE_DUPLICATES _tmp)
set(PCAP_LIBRARY ${_tmp}
CACHE STRING "Libraries needed to link against libpcap" FORCE)
else ()
message(FATAL_ERROR "Couldn't determine how to link against libpcap")
endif ()
endif ()
include(CheckFunctionExists)
set(CMAKE_REQUIRED_LIBRARIES ${PCAP_LIBRARY})
check_function_exists(pcap_get_pfring_id HAVE_PF_RING)
check_function_exists(pcap_dump_open_append HAVE_PCAP_DUMP_OPEN_APPEND)
set(CMAKE_REQUIRED_LIBRARIES)
mark_as_advanced(
PCAP_ROOT_DIR
PCAP_INCLUDE_DIR
PCAP_LIBRARY
)

View File

@@ -0,0 +1,23 @@
description: >-
Using the "PacketSniffer" EVerest module it is possible to capture and
store the different packets on the PLC interface.
config:
device:
description: >-
The ethernet device on which the messages are to be captured
type: string
default: eth1
session_logging_path:
description: >-
Output directory for session capture dump files. Will be used only if not empty.
If empty, the logging path provided in the SessionStarted event will be used.
If no logging path is provided there either, no capture will be performed.
type: string
default: /tmp
requires:
evse_manager:
interface: evse_manager
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Sebastian Lukas

View File

@@ -0,0 +1,9 @@
load("//modules:module.bzl", "cc_everest_module")
cc_everest_module(
name = "PersistentStore",
impls = ["main"],
deps = [
"@sqlite3",
],
)

View File

@@ -0,0 +1,27 @@
#
# AUTO GENERATED - MARKED REGIONS WILL BE KEPT
# template version 3
#
# module setup:
# - ${MODULE_NAME}: module name
ev_setup_cpp_module()
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
# insert your custom targets and additional config variables here
find_package(SQLite3 REQUIRED)
target_link_libraries(${MODULE_NAME}
PRIVATE
SQLite::SQLite3
)
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
target_sources(${MODULE_NAME}
PRIVATE
"main/kvsImpl.cpp"
)
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
# insert other things like install cmds etc here
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1

View File

@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2022 - 2022 Pionix GmbH and Contributors to EVerest
#include "PersistentStore.hpp"
namespace module {
void PersistentStore::init() {
invoke_init(*p_main);
}
void PersistentStore::ready() {
invoke_ready(*p_main);
}
} // namespace module

View File

@@ -0,0 +1,60 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef PERSISTENT_STORE_HPP
#define PERSISTENT_STORE_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 2
//
#include "ld-ev.hpp"
// headers for provided interface implementations
#include <generated/interfaces/kvs/Implementation.hpp>
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
// insert your custom include headers here
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
namespace module {
struct Conf {
std::string sqlite_db_file_path;
};
class PersistentStore : public Everest::ModuleBase {
public:
PersistentStore() = delete;
PersistentStore(const ModuleInfo& info, std::unique_ptr<kvsImplBase> p_main, Conf& config) :
ModuleBase(info), p_main(std::move(p_main)), config(config){};
const std::unique_ptr<kvsImplBase> p_main;
const Conf& config;
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
// insert your public definitions here
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
protected:
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
// insert your protected definitions here
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
private:
friend class LdEverest;
void init();
void ready();
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
// insert your private definitions here
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
};
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
// insert other definitions here
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
} // namespace module
#endif // PERSISTENT_STORE_HPP

View File

@@ -0,0 +1,258 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2022 - 2022 Pionix GmbH and Contributors to EVerest
#include "kvsImpl.hpp"
#include <filesystem>
namespace fs = std::filesystem;
namespace module {
namespace main {
/**
* Wrapper class around a sqlite3_stmt pointer to ensure it is always
* finalised via sqlite3_finalize()
*/
class Sqlite3_Stmt {
private:
sqlite3_stmt* m_statement_ptr = nullptr;
public:
void finalize(const char* error_message = nullptr) {
const auto res = sqlite3_finalize(m_statement_ptr);
m_statement_ptr = nullptr; // prevent double free
if (res != SQLITE_OK) {
if (error_message != nullptr) {
EVLOG_error << error_message;
}
throw std::runtime_error("PersistentStore db access error");
}
}
~Sqlite3_Stmt() {
(void)sqlite3_finalize(m_statement_ptr);
}
constexpr operator sqlite3_stmt*() {
return m_statement_ptr;
}
constexpr operator sqlite3_stmt**() {
return &m_statement_ptr;
}
constexpr sqlite3_stmt** operator&() {
return &m_statement_ptr;
}
};
void kvsImpl::init() {
// open and initialize database
fs::path sqlite_db_path = fs::absolute(fs::path(mod->config.sqlite_db_file_path));
fs::path database_directory = sqlite_db_path.parent_path();
if (!fs::exists(database_directory)) {
fs::create_directories(database_directory);
}
int ret = sqlite3_open(sqlite_db_path.c_str(), &this->db);
if (ret != SQLITE_OK) {
EVLOG_error << "Error opening PersistentStore database '" << sqlite_db_path << "': " << sqlite3_errmsg(db);
throw std::runtime_error("Could not open PersistentStore database at provided path.");
}
EVLOG_debug << "Using SQLite version " << sqlite3_libversion();
// prepare the database
std::string create_sql = "CREATE TABLE IF NOT EXISTS KVS ("
"KEY TEXT UNIQUE,"
"VALUE TEXT,"
"TYPE TEXT);";
Sqlite3_Stmt create_statement;
sqlite3_prepare_v2(this->db, create_sql.c_str(), create_sql.size(), &create_statement, NULL);
int res = sqlite3_step(create_statement);
if (res != SQLITE_DONE) {
EVLOG_error << "Could not create KVS table: " << res << sqlite3_errmsg(this->db);
throw std::runtime_error("PersistentStore db access error");
}
create_statement.finalize("Error creating KVS table");
}
void kvsImpl::ready() {
}
class TypeNameVisitor {
public:
std::string operator()(std::nullptr_t t) const {
return "nullptr_t";
}
std::string operator()(const Array& t) const {
return "Array";
}
std::string operator()(const Object& t) const {
return "Object";
}
std::string operator()(const bool& t) const {
return "bool";
}
std::string operator()(const double& t) const {
return "double";
}
std::string operator()(const int& t) const {
return "int";
}
std::string operator()(const std::string& t) const {
return "std::string";
}
};
class StringValueVisitor {
public:
std::string operator()(std::nullptr_t t) const {
return "";
}
std::string operator()(Array t) const {
json a = t;
return a.dump();
}
std::string operator()(Object t) const {
json o = t;
return o.dump();
}
std::string operator()(bool t) const {
if (t) {
return "true";
}
return "false";
}
std::string operator()(double t) const {
return std::to_string(t);
}
std::string operator()(int t) const {
return std::to_string(t);
}
std::string operator()(std::string t) const {
return t;
}
};
void kvsImpl::handle_store(std::string& key,
std::variant<std::nullptr_t, Array, Object, bool, double, int, std::string>& value) {
std::string type = std::visit(TypeNameVisitor(), value);
std::string string_value = std::visit(StringValueVisitor(), value);
;
std::string insert_sql_str = "INSERT OR REPLACE INTO KVS (KEY, VALUE, TYPE) VALUES "
"(@key, @value, @type)";
Sqlite3_Stmt insert_statement;
sqlite3_prepare_v2(db, insert_sql_str.c_str(), insert_sql_str.size(), &insert_statement, NULL);
sqlite3_bind_text(insert_statement, 1, key.c_str(), -1, NULL);
sqlite3_bind_text(insert_statement, 2, string_value.c_str(), -1, NULL);
sqlite3_bind_text(insert_statement, 3, type.c_str(), -1, NULL);
int res = sqlite3_step(insert_statement);
if (res != SQLITE_DONE) {
EVLOG_error << "Could not insert into KVS table: " << res << sqlite3_errmsg(db);
throw std::runtime_error("PersistentStore db access error");
}
insert_statement.finalize("Error inserting into KVS table");
};
std::variant<std::nullptr_t, Array, Object, bool, double, int, std::string> kvsImpl::handle_load(std::string& key) {
std::string select_sql_str = "SELECT KEY, VALUE, TYPE FROM KVS WHERE KEY = @key";
Sqlite3_Stmt select_statement;
sqlite3_prepare_v2(db, select_sql_str.c_str(), select_sql_str.size(), &select_statement, NULL);
sqlite3_bind_text(select_statement, 1, key.c_str(), -1, NULL);
int res = sqlite3_step(select_statement);
if (res != SQLITE_ROW) {
// no key with that name exists in the database
return {};
}
std::variant<std::nullptr_t, Array, Object, bool, double, int, std::string> value;
auto value_ptr = sqlite3_column_text(select_statement, 1);
if (value_ptr != nullptr) {
std::string value_str = std::string(reinterpret_cast<const char*>(value_ptr));
auto type_ptr = sqlite3_column_text(select_statement, 2);
if (type_ptr != nullptr) {
std::string type_str = std::string(reinterpret_cast<const char*>(type_ptr));
if (type_str == "Array") {
Array value_array = json::parse(value_str);
value = value_array;
} else if (type_str == "Object") {
Object value_object = json::parse(value_str);
value = value_object;
} else if (type_str == "bool") {
if (value_str == "true") {
value = true;
} else {
value = false;
}
} else if (type_str == "double") {
value = std::stod(value_str);
} else if (type_str == "int") {
value = std::stoi(value_str);
} else if (type_str == "std::string") {
value = value_str;
}
}
}
select_statement.finalize("Error selecting from KVS table");
return value;
};
void kvsImpl::handle_delete(std::string& key) {
std::string delete_sql_str = "DELETE FROM KVS WHERE KEY = @key";
Sqlite3_Stmt delete_statement;
sqlite3_prepare_v2(db, delete_sql_str.c_str(), delete_sql_str.size(), &delete_statement, NULL);
sqlite3_bind_text(delete_statement, 1, key.c_str(), -1, NULL);
int res = sqlite3_step(delete_statement);
if (res != SQLITE_DONE) {
EVLOG_error << "Could not delete from KVS table: " << res << sqlite3_errmsg(db);
throw std::runtime_error("PersistentStore db access error");
}
delete_statement.finalize("Error deleting from KVS table");
};
bool kvsImpl::handle_exists(std::string& key) {
std::string select_sql_str = "SELECT KEY FROM KVS WHERE KEY = @key";
Sqlite3_Stmt select_statement;
sqlite3_prepare_v2(db, select_sql_str.c_str(), select_sql_str.size(), &select_statement, NULL);
sqlite3_bind_text(select_statement, 1, key.c_str(), -1, NULL);
int res = sqlite3_step(select_statement);
if (res != SQLITE_ROW) {
// no key with that name exists in the database
return false;
}
return true;
};
} // namespace main
} // namespace module

View File

@@ -0,0 +1,71 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef MAIN_KVS_IMPL_HPP
#define MAIN_KVS_IMPL_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 3
//
#include <generated/interfaces/kvs/Implementation.hpp>
#include "../PersistentStore.hpp"
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
// insert your custom include headers here
#include <mutex>
#include <sqlite3.h>
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
namespace module {
namespace main {
struct Conf {};
class kvsImpl : public kvsImplBase {
public:
kvsImpl() = delete;
kvsImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<PersistentStore>& mod, Conf& config) :
kvsImplBase(ev, "main"), mod(mod), config(config){};
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
// insert your public definitions here
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
protected:
// command handler functions (virtual)
virtual void
handle_store(std::string& key,
std::variant<std::nullptr_t, Array, Object, bool, double, int, std::string>& value) override;
virtual std::variant<std::nullptr_t, Array, Object, bool, double, int, std::string>
handle_load(std::string& key) override;
virtual void handle_delete(std::string& key) override;
virtual bool handle_exists(std::string& key) override;
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
// insert your protected definitions here
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
private:
const Everest::PtrContainer<PersistentStore>& mod;
const Conf& config;
virtual void init() override;
virtual void ready() override;
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
// insert your private definitions here
sqlite3* db;
std::mutex db_mutex;
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
};
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
// insert other definitions here
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
} // namespace main
} // namespace module
#endif // MAIN_KVS_IMPL_HPP

View File

@@ -0,0 +1,14 @@
description: Simple implementation of a SQLite backed persistent key-value store
config:
sqlite_db_file_path:
description: Path to the SQLite db file.
type: string
default: everest_persistent_store.db
provides:
main:
interface: kvs
description: This implements a persistent key-value store
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Kai-Uwe Hermann

View File

@@ -0,0 +1 @@
Miscellaneous modules implementing various supporting functionality

View File

@@ -0,0 +1,17 @@
load("//modules:module.bzl", "cc_everest_module")
IMPLS = [
"main",
]
cc_everest_module(
name = "SerialCommHub",
srcs = glob([
"*.cpp",
"*.hpp",
]),
impls = IMPLS,
deps = [
"//lib/everest/gpio",
],
)

View File

@@ -0,0 +1,33 @@
#
# AUTO GENERATED - MARKED REGIONS WILL BE KEPT
# template version 3
#
# module setup:
# - ${MODULE_NAME}: module name
ev_setup_cpp_module()
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
# insert your custom targets and additional config variables here
target_link_libraries(${MODULE_NAME}
PRIVATE
everest::gpio
)
target_sources(${MODULE_NAME}
PRIVATE
tiny_modbus_rtu.cpp
crc16.cpp
)
target_compile_features(${MODULE_NAME} PUBLIC cxx_std_17)
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
target_sources(${MODULE_NAME}
PRIVATE
"main/serial_communication_hubImpl.cpp"
)
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
# insert other things like install cmds etc here
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1

View File

@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2022 - 2022 Pionix GmbH and Contributors to EVerest
#include "SerialCommHub.hpp"
namespace module {
void SerialCommHub::init() {
invoke_init(*p_main);
}
void SerialCommHub::ready() {
invoke_ready(*p_main);
}
} // namespace module

View File

@@ -0,0 +1,58 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef SERIAL_COMM_HUB_HPP
#define SERIAL_COMM_HUB_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 2
//
#include "ld-ev.hpp"
// headers for provided interface implementations
#include <generated/interfaces/serial_communication_hub/Implementation.hpp>
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
// insert your custom include headers here
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
namespace module {
struct Conf {};
class SerialCommHub : public Everest::ModuleBase {
public:
SerialCommHub() = delete;
SerialCommHub(const ModuleInfo& info, std::unique_ptr<serial_communication_hubImplBase> p_main, Conf& config) :
ModuleBase(info), p_main(std::move(p_main)), config(config){};
const std::unique_ptr<serial_communication_hubImplBase> p_main;
const Conf& config;
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
// insert your public definitions here
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
protected:
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
// insert your protected definitions here
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
private:
friend class LdEverest;
void init();
void ready();
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
// insert your private definitions here
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
};
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
// insert other definitions here
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
} // namespace module
#endif // SERIAL_COMM_HUB_HPP

View File

@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
/*
MIT License
Copyright (c) 2019 Tiago Ventura
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "crc16.hpp"
uint16_t calculate_modbus_crc16(const uint8_t* buf, int len) {
uint16_t crc = 0xFFFF;
char i;
while (len--) {
crc ^= (*buf++);
for (i = 0; i < 8; i++) {
if (crc & 1) {
crc >>= 1;
crc ^= 0xA001;
} else {
crc >>= 1;
}
}
}
return crc;
}

View File

@@ -0,0 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef CRC16_HPP
#define CRC16_HPP
#include <stdint.h>
uint16_t calculate_modbus_crc16(const uint8_t* buf, int len);
#endif

View File

@@ -0,0 +1,246 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2022 - 2022 Pionix GmbH and Contributors to EVerest
#include "serial_communication_hubImpl.hpp"
#include <chrono>
#include <cstdint>
#include <date/date.h>
#include <date/tz.h>
#include <fmt/core.h>
#include <mutex>
#include <typeinfo>
namespace module {
namespace main {
template <typename T, typename U> static void append_array(std::vector<T>& m, const std::vector<U>& a) {
for (auto it = a.begin(); it != a.end(); ++it)
m.push_back(*it);
}
// Helper functions
static std::vector<int> vector_to_int(const std::vector<uint16_t>& response) {
std::vector<int> i;
i.reserve(response.size());
for (auto r : response) {
i.push_back((int)r);
}
return i;
}
/**
* @brief Converts a Result to a ResultBool by looking at each bit of the uint16_t values and converting them to
* bools in the right order. Used for Modbus read coils responses where the result is a bit-packed array of coil states.
* @param result The Result to convert
* @param number_of_coils The number of coils that were requested to read, used to limit the number of bools in the
* output
* @return The converted ResultBool
*/
static types::serial_comm_hub_requests::ResultBool
convert_read_coils_result(const types::serial_comm_hub_requests::Result& result, size_t number_of_coils) {
constexpr uint8_t BITS_PER_BYTE = 8;
constexpr uint16_t BYTE_MASK = 0xFF;
types::serial_comm_hub_requests::ResultBool out;
out.status_code = result.status_code;
if (result.value.has_value()) {
std::vector<bool> result_bool;
for (const uint16_t packed_bytes : result.value.value()) {
// Modbus read coils response packs bits into raw bytes, the modbus library uses big-endian to build uint16
// from those. Here we extract the original MSB and LSB from the BE uint16_t and process them in the correct
// order.
const auto msb = static_cast<uint8_t>((packed_bytes >> BITS_PER_BYTE) & BYTE_MASK);
const auto lsb = static_cast<uint8_t>(packed_bytes & BYTE_MASK);
for (const uint8_t byte : {msb, lsb}) {
for (int bit = 0; bit < BITS_PER_BYTE; bit++) {
if (result_bool.size() >= number_of_coils) {
break;
}
result_bool.push_back((byte & (1U << bit)) != 0);
}
}
}
out.value = std::move(result_bool);
}
return out;
}
// Implementation
void serial_communication_hubImpl::init() {
using namespace std::chrono;
Everest::GpioSettings rxtx_gpio_settings;
rxtx_gpio_settings.chip_name = config.rxtx_gpio_chip;
rxtx_gpio_settings.line_number = config.rxtx_gpio_line;
rxtx_gpio_settings.inverted = config.rxtx_gpio_tx_high;
system_error_logged = false;
if (!modbus.open_device(config.serial_port, config.baudrate, config.ignore_echo, rxtx_gpio_settings,
static_cast<tiny_modbus::Parity>(config.parity), config.rtscts,
milliseconds(config.initial_timeout_ms), milliseconds(config.within_message_timeout_ms))) {
EVLOG_error << fmt::format("Cannot open serial port {}, ModBus will not work.", config.serial_port);
}
}
void serial_communication_hubImpl::ready() {
}
types::serial_comm_hub_requests::Result
serial_communication_hubImpl::perform_modbus_request(uint8_t device_address, tiny_modbus::FunctionCode function,
uint16_t first_register_address, uint16_t register_quantity,
bool wait_for_reply, std::vector<uint16_t> request) {
std::scoped_lock lock(serial_mutex);
types::serial_comm_hub_requests::Result result;
std::vector<uint16_t> response;
auto retry_counter = config.retries + 1;
bool last_error_was_timeout = false;
while (retry_counter > 0) {
auto current_trial = config.retries + 1 - retry_counter + 1;
EVLOG_debug << fmt::format("Trial {}/{}: calling {}(id {} addr {}({:#06x}) len {})", current_trial,
config.retries + 1, tiny_modbus::FunctionCode_to_string_with_hex(function),
device_address, first_register_address, first_register_address, register_quantity);
last_error_was_timeout = false;
try {
response = modbus.txrx(device_address, function, first_register_address, register_quantity,
config.max_packet_size, wait_for_reply, request);
} catch (const tiny_modbus::TimeoutException& e) {
// TimeoutException is a specific type of communication error
last_error_was_timeout = true;
auto logmsg = fmt::format("Modbus call {} for device id {} addr {}({:#06x}) failed: {}",
tiny_modbus::FunctionCode_to_string_with_hex(function), device_address,
first_register_address, first_register_address, e.what());
if (retry_counter != 1) {
EVLOG_debug << logmsg;
} else {
EVLOG_warning << logmsg;
}
} catch (const tiny_modbus::TinyModbusException& e) {
auto logmsg = fmt::format("Modbus call {} for device id {} addr {}({:#06x}) failed: {}",
tiny_modbus::FunctionCode_to_string_with_hex(function), device_address,
first_register_address, first_register_address, e.what());
if (retry_counter != 1) {
EVLOG_debug << logmsg;
} else {
EVLOG_warning << logmsg;
}
} catch (const std::logic_error& e) {
EVLOG_warning << "Logic error in Modbus implementation: " << e.what();
} catch (const std::system_error& e) {
// FIXME: report this to the infrastructure, as soon as an error interface for this is available
// Log this only once, as we are convinced this will not go away
if (not system_error_logged) {
EVLOG_error << "System error in accessing Modbus: [" << e.code() << "] " << e.what();
system_error_logged = true;
}
}
if (response.size() > 0)
break;
retry_counter--;
}
if (response.size() > 0) {
EVLOG_debug << fmt::format("Process response (size {})", response.size());
result.status_code = types::serial_comm_hub_requests::StatusCodeEnum::Success;
result.value = vector_to_int(response);
system_error_logged = false; // reset after success
} else {
// If the last error was a timeout, return Timeout status, otherwise Error
if (last_error_was_timeout) {
result.status_code = types::serial_comm_hub_requests::StatusCodeEnum::Timeout;
} else {
result.status_code = types::serial_comm_hub_requests::StatusCodeEnum::Error;
}
}
return result;
}
// Commands
types::serial_comm_hub_requests::Result
serial_communication_hubImpl::handle_modbus_read_holding_registers(int& target_device_id, int& first_register_address,
int& num_registers_to_read) {
return perform_modbus_request(target_device_id, tiny_modbus::FunctionCode::READ_MULTIPLE_HOLDING_REGISTERS,
first_register_address, num_registers_to_read);
}
types::serial_comm_hub_requests::Result
serial_communication_hubImpl::handle_modbus_read_input_registers(int& target_device_id, int& first_register_address,
int& num_registers_to_read) {
return perform_modbus_request(target_device_id, tiny_modbus::FunctionCode::READ_INPUT_REGISTERS,
first_register_address, num_registers_to_read);
}
types::serial_comm_hub_requests::StatusCodeEnum serial_communication_hubImpl::handle_modbus_write_multiple_registers(
int& target_device_id, int& first_register_address, types::serial_comm_hub_requests::VectorUint16& data_raw) {
types::serial_comm_hub_requests::Result result;
std::vector<uint16_t> data;
append_array<uint16_t, int>(data, data_raw.data);
result = perform_modbus_request(target_device_id, tiny_modbus::FunctionCode::WRITE_MULTIPLE_HOLDING_REGISTERS,
first_register_address, data.size(), true, data);
return result.status_code;
}
types::serial_comm_hub_requests::StatusCodeEnum
serial_communication_hubImpl::handle_modbus_write_single_register(int& target_device_id, int& register_address,
int& data) {
types::serial_comm_hub_requests::Result result;
result = perform_modbus_request(target_device_id, tiny_modbus::FunctionCode::WRITE_SINGLE_HOLDING_REGISTER,
register_address, 1, true, {static_cast<uint16_t>(data)});
return result.status_code;
}
types::serial_comm_hub_requests::StatusCodeEnum
serial_communication_hubImpl::handle_modbus_write_single_coil(int& target_device_id, int& coil_address, bool& data) {
types::serial_comm_hub_requests::Result result;
result = perform_modbus_request(target_device_id, tiny_modbus::FunctionCode::WRITE_SINGLE_COIL, coil_address, 1,
true, {static_cast<uint16_t>(data ? 0xFF00 : 0x0000)});
return result.status_code;
}
types::serial_comm_hub_requests::ResultBool
serial_communication_hubImpl::handle_modbus_read_coils(int& target_device_id, int& first_coil_address,
int& num_coils_to_read) {
const auto result = perform_modbus_request(target_device_id, tiny_modbus::FunctionCode::READ_COILS,
first_coil_address, num_coils_to_read);
return convert_read_coils_result(result, num_coils_to_read);
}
void serial_communication_hubImpl::handle_nonstd_write(int& target_device_id, int& first_register_address,
int& num_registers_to_read) {
}
types::serial_comm_hub_requests::Result serial_communication_hubImpl::handle_nonstd_read(int& target_device_id,
int& first_register_address,
int& num_registers_to_read) {
types::serial_comm_hub_requests::Result result;
result.status_code = types::serial_comm_hub_requests::StatusCodeEnum::Error;
return result;
}
} // namespace main
} // namespace module

View File

@@ -0,0 +1,108 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef MAIN_SERIAL_COMMUNICATION_HUB_IMPL_HPP
#define MAIN_SERIAL_COMMUNICATION_HUB_IMPL_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 3
//
#include <generated/interfaces/serial_communication_hub/Implementation.hpp>
#include "../SerialCommHub.hpp"
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
// insert your custom include headers here
#include "tiny_modbus_rtu.hpp"
#include <chrono>
#include <cstdint>
#include <termios.h>
#include <utils/thread.hpp>
#include <vector>
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
namespace module {
namespace main {
struct Conf {
std::string serial_port;
int baudrate;
int parity;
bool rtscts;
bool ignore_echo;
std::string rxtx_gpio_chip;
int rxtx_gpio_line;
bool rxtx_gpio_tx_high;
int max_packet_size;
int initial_timeout_ms;
int within_message_timeout_ms;
int retries;
};
class serial_communication_hubImpl : public serial_communication_hubImplBase {
public:
serial_communication_hubImpl() = delete;
serial_communication_hubImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<SerialCommHub>& mod,
Conf& config) :
serial_communication_hubImplBase(ev, "main"), mod(mod), config(config){};
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
// insert your public definitions here
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
protected:
// command handler functions (virtual)
virtual types::serial_comm_hub_requests::Result
handle_modbus_read_holding_registers(int& target_device_id, int& first_register_address,
int& num_registers_to_read) override;
virtual types::serial_comm_hub_requests::Result
handle_modbus_read_input_registers(int& target_device_id, int& first_register_address,
int& num_registers_to_read) override;
virtual types::serial_comm_hub_requests::StatusCodeEnum
handle_modbus_write_multiple_registers(int& target_device_id, int& first_register_address,
types::serial_comm_hub_requests::VectorUint16& data_raw) override;
virtual types::serial_comm_hub_requests::StatusCodeEnum
handle_modbus_write_single_register(int& target_device_id, int& register_address, int& data) override;
virtual types::serial_comm_hub_requests::ResultBool
handle_modbus_read_coils(int& target_device_id, int& first_coil_address, int& num_coils_to_read) override;
virtual types::serial_comm_hub_requests::StatusCodeEnum
handle_modbus_write_single_coil(int& target_device_id, int& coil_address, bool& data) override;
virtual void handle_nonstd_write(int& target_device_id, int& first_register_address,
int& num_registers_to_read) override;
virtual types::serial_comm_hub_requests::Result
handle_nonstd_read(int& target_device_id, int& first_register_address, int& num_registers_to_read) override;
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
// insert your protected definitions here
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
private:
const Everest::PtrContainer<SerialCommHub>& mod;
const Conf& config;
virtual void init() override;
virtual void ready() override;
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
// insert your private definitions here
types::serial_comm_hub_requests::Result
perform_modbus_request(uint8_t device_address, tiny_modbus::FunctionCode function, uint16_t first_register_address,
uint16_t register_quantity, bool wait_for_reply = true,
std::vector<uint16_t> request = std::vector<uint16_t>());
tiny_modbus::TinyModbusRTU modbus;
std::mutex serial_mutex;
bool system_error_logged{false};
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
};
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
// insert other definitions here
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
} // namespace main
} // namespace module
#endif // MAIN_SERIAL_COMMUNICATION_HUB_IMPL_HPP

View File

@@ -0,0 +1,71 @@
description: Hub to communicate with attached serial devices
provides:
main:
description: Implementation of serial communication hub
interface: serial_communication_hub
config:
serial_port:
description: Serial port the hardware is connected to
type: string
default: /dev/ttyUSB0
baudrate:
description: Baudrate
type: integer
minimum: 0
maximum: 230400
default: 9600
parity:
description: 'Parity bit: 0: None, 1: Odd, 2: Even'
type: integer
minimum: 0
maximum: 2
default: 0
rtscts:
description: Use RTS/CTS hardware flow control
type: boolean
default: false
ignore_echo:
description: On some hardware every message that is sent is read back, this setting filters the sent message in the reply.
type: boolean
default: false
rxtx_gpio_chip:
description: GPIO chip to use to switch between RX/TX. An empty string disables GPIO usage.
type: string
default: ''
rxtx_gpio_line:
description: GPIO line to use to switch between RX/TX
type: integer
default: 0
rxtx_gpio_tx_high:
description: GPIO direction, false means low for TX, true means high for TX
type: boolean
default: false
max_packet_size:
description: >-
Maximum size of a packet to read/write in bytes. Payload exceeding the size will be chunked.
The APU size according to [wikipedia](https://en.wikipedia.org/wiki/Modbus) is 256 bytes,
which is used as default here.
type: integer
# 7 is a minimum packet size to transfer a response
minimum: 7
maximum: 65536
default: 256
initial_timeout_ms:
description: Timeout in ms for the first packet.
type: integer
default: 500
within_message_timeout_ms:
description: Timeout in ms for subsequent packets.
type: integer
default: 100
retries:
description: Count of retries in case of error in Modbus query.
type: integer
minimum: 0
maximum: 10
default: 2
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Lars Dieckmann
- Cornelius Claussen

View File

@@ -0,0 +1,526 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 Pionix GmbH and Contributors to EVerest
// TODOs:
// - sometimes we receive 0 bytes from sofar, find out why
// - implement echo removal for chargebyte
// - implement GPIO to switch rx/tx
#include "tiny_modbus_rtu.hpp"
#include "crc16.hpp"
#include <algorithm>
#include <cstring>
#include <endian.h>
#include <errno.h>
#include <fcntl.h>
#include <fmt/core.h>
#include <iomanip>
#include <ios>
#include <iostream>
#include <iterator>
#include <ostream>
#include <sstream>
#include <string>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <system_error>
#include <type_traits>
#include <unistd.h>
namespace tiny_modbus {
std::string FunctionCode_to_string(FunctionCode fc) {
switch (fc) {
case FunctionCode::READ_COILS:
return "READ_COILS";
case FunctionCode::READ_DISCRETE_INPUTS:
return "READ_DISCRETE_INPUTS";
case FunctionCode::READ_MULTIPLE_HOLDING_REGISTERS:
return "READ_MULTIPLE_HOLDING_REGISTERS";
case FunctionCode::READ_INPUT_REGISTERS:
return "READ_INPUT_REGISTERS";
case FunctionCode::WRITE_SINGLE_COIL:
return "WRITE_SINGLE_COIL";
case FunctionCode::WRITE_SINGLE_HOLDING_REGISTER:
return "WRITE_SINGLE_HOLDING_REGISTER";
case FunctionCode::WRITE_MULTIPLE_COILS:
return "WRITE_MULTIPLE_COILS";
case FunctionCode::WRITE_MULTIPLE_HOLDING_REGISTERS:
return "WRITE_MULTIPLE_HOLDING_REGISTERS";
default:
return "unknown";
}
}
std::string FunctionCode_to_string_with_hex(FunctionCode fc) {
return fmt::format("{}({:#04x})", FunctionCode_to_string(fc), (unsigned int)fc);
}
std::ostream& operator<<(std::ostream& os, const FunctionCode& fc) {
os << FunctionCode_to_string_with_hex(fc);
return os;
}
// This is a replacement for system library tcdrain().
// tcdrain() returns when all bytes are written to the UART, but it actually returns about 10msecs or more after the
// last byte has been written. This function tries to return as fast as possible instead.
static void fast_tcdrain(int fd) {
// in user space, the only way to find out if there are still bits to be shiftet out is to poll line status register
// as fast as we can
uint32_t lsr;
do {
ioctl(fd, TIOCSERGETLSR, &lsr);
} while (!(lsr & TIOCSER_TEMT));
}
static auto check_for_exception(uint8_t received_function_code) {
return received_function_code & (1 << 7);
}
static void clear_exception_bit(uint8_t& received_function_code) {
received_function_code &= ~(1 << 7);
}
static std::string hexdump(const uint8_t* msg, int msg_len) {
std::stringstream ss;
for (int i = 0; i < msg_len; i++) {
ss << "<" << std::nouppercase << std::setfill('0') << std::setw(2) << std::hex << (int)msg[i] << ">";
}
return ss.str();
}
static void append_checksum(uint8_t* msg, int msg_len) {
if (msg_len < 5)
return;
uint16_t crc_sum = calculate_modbus_crc16(msg, msg_len - 2);
memcpy(msg + msg_len - 2, &crc_sum, 2);
}
static bool validate_checksum(const uint8_t* msg, int msg_len) {
if (msg_len < 5)
return false;
// check crc
uint16_t crc_sum = calculate_modbus_crc16(msg, msg_len - 2);
uint16_t crc_msg;
memcpy(&crc_msg, msg + msg_len - 2, 2);
return (crc_msg == crc_sum);
}
static std::vector<uint16_t> decode_reply(const uint8_t* buf, int len, uint8_t expected_device_address,
FunctionCode function) {
std::vector<uint16_t> result;
if (len == 0) {
throw TimeoutException("Packet receive timeout");
} else if (len < MODBUS_MIN_REPLY_SIZE) {
throw ShortPacketException(fmt::format("Packet too small: only {} bytes", len));
}
if (expected_device_address != buf[DEVICE_ADDRESS_POS]) {
throw AddressMismatchException(fmt::format("Device address mismatch: expected: {} received: {}",
expected_device_address, buf[DEVICE_ADDRESS_POS]) +
": " + hexdump(buf, len));
}
bool exception = false;
uint8_t function_code_recvd = buf[FUNCTION_CODE_POS];
if (check_for_exception(function_code_recvd)) {
// highest bit is set for exception reply
exception = true;
// clear error bit
clear_exception_bit(function_code_recvd);
}
if (function != function_code_recvd) {
throw FunctionCodeMismatchException(fmt::format("Function code mismatch: expected: {} received: {}",
static_cast<std::underlying_type_t<FunctionCode>>(function),
function_code_recvd));
}
if (!validate_checksum(buf, len)) {
throw ChecksumErrorException("Retrieved Modbus checksum does not match calculated value.");
}
if (exception) {
// handle exception message
uint8_t err_code = buf[RES_EXCEPTION_CODE];
switch (err_code) {
case 0x01:
throw ModbusException("Modbus exception: Illegal function");
break;
case 0x02:
throw ModbusException("Modbus exception: Illegal data address");
break;
case 0x03:
throw ModbusException("Modbus exception: Illegal data value");
break;
case 0x04:
throw ModbusException("Modbus exception: Client device failure");
break;
case 0x05:
throw ModbusException("Modbus ACK");
break;
case 0x06:
throw ModbusException("Modbus exception: Client device busy");
break;
case 0x07:
throw ModbusException("Modbus exception: NACK");
break;
case 0x08:
throw ModbusException("Modbus exception: Memory parity error");
break;
case 0x09:
throw ModbusException("Modbus exception: Out of resources");
break;
case 0x0A:
throw ModbusException("Modbus exception: Gateway path unavailable");
break;
case 0x0B:
throw ModbusException("Modbus exception: Gateway target device failed to respond");
break;
default:
throw ModbusException("Modbus exception: Unknown");
}
}
// For a write reply we always get 4 bytes
uint8_t byte_cnt = 4;
int start_of_result = RES_TX_START_OF_PAYLOAD;
bool even_byte_cnt_expected = false;
// Was it a read reply?
switch (function) {
case FunctionCode::WRITE_SINGLE_COIL:
case FunctionCode::WRITE_SINGLE_HOLDING_REGISTER:
case FunctionCode::WRITE_MULTIPLE_COILS:
case FunctionCode::WRITE_MULTIPLE_HOLDING_REGISTERS:
// no - nothing to do
break;
case FunctionCode::READ_MULTIPLE_HOLDING_REGISTERS:
case FunctionCode::READ_INPUT_REGISTERS:
// yes - for 16-bit wide registers thus we can assume an even byte count
even_byte_cnt_expected = true;
[[fallthrough]];
case FunctionCode::READ_COILS:
case FunctionCode::READ_DISCRETE_INPUTS:
// yes
// adapt byte count and starting pos
byte_cnt = buf[RES_RX_LEN_POS];
start_of_result = RES_RX_START_OF_PAYLOAD;
break;
default:
throw std::logic_error("Missing implementation for function code " + FunctionCode_to_string_with_hex(function));
}
// check if result is completely in received data
if (start_of_result + byte_cnt > len) {
throw IncompletePacketException("Result data not completely in received message.");
}
// check even number of bytes
if (even_byte_cnt_expected && byte_cnt % 2 == 1) {
throw OddByteCountException("For " + FunctionCode_to_string_with_hex(function) +
" an even byte count is expected in the response.");
}
// ready to copy actual result data to output, so pre-allocate enough memory for the output
result.reserve((byte_cnt + 1) / 2);
for (int i = start_of_result; i < start_of_result + byte_cnt; i += 2) {
uint16_t t = 0;
const size_t num_bytes_to_copy = (i < len - 1) ? 2 : 1;
memcpy(&t, buf + i, num_bytes_to_copy);
t = be16toh(t);
result.push_back(t);
}
return result;
}
TinyModbusRTU::~TinyModbusRTU() {
if (fd != -1)
close(fd);
}
bool TinyModbusRTU::open_device(const std::string& device, int _baud, bool _ignore_echo,
const Everest::GpioSettings& rxtx_gpio_settings, const Parity parity, bool rtscts,
std::chrono::milliseconds _initial_timeout,
std::chrono::milliseconds _within_message_timeout) {
initial_timeout = _initial_timeout;
within_message_timeout = _within_message_timeout;
ignore_echo = _ignore_echo;
rxtx_gpio.open(rxtx_gpio_settings);
rxtx_gpio.set_output(true);
fd = open(device.c_str(), O_RDWR | O_NOCTTY | O_SYNC);
if (fd < 0) {
EVLOG_error << fmt::format("Serial: error {} opening {}: {}\n", errno, device, strerror(errno));
return false;
}
int baud;
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:
return false;
}
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] = 1; // read blocks
tty.c_cc[VTIME] = 1; // 0.1 seconds inter character read timeout after first byte was received
tty.c_cflag |= (CLOCAL | CREAD); // ignore modem controls,
// enable reading
if (parity == Parity::ODD) {
tty.c_cflag |= (PARENB | PARODD); // odd parity
} else if (parity == Parity::EVEN) { // even parity
tty.c_cflag &= ~PARODD;
tty.c_cflag |= PARENB;
} else {
tty.c_cflag &= ~(PARENB | PARODD); // shut off parity
}
tty.c_cflag &= ~CSTOPB; // 1 Stop bit
if (rtscts) {
tty.c_cflag |= CRTSCTS;
} else {
tty.c_cflag &= ~CRTSCTS;
}
if (tcsetattr(fd, TCSANOW, &tty) != 0) {
printf("Serial: error %d from tcsetattr\n", errno);
return false;
}
return true;
}
int TinyModbusRTU::read_reply(uint8_t* rxbuf, int rxbuf_len) {
if (fd == -1) {
return 0;
}
// Lambda to convert std::chrono to timeval.
auto to_timeval = [](const auto& time) {
using namespace std::chrono;
struct timeval timeout;
auto sec = duration_cast<seconds>(time);
timeout.tv_sec = sec.count();
timeout.tv_usec = duration_cast<microseconds>(time - sec).count();
return timeout;
};
auto timeout = to_timeval(initial_timeout);
const auto within_message_timeval = to_timeval(within_message_timeout);
fd_set set;
FD_ZERO(&set);
FD_SET(fd, &set);
int bytes_read_total = 0;
while (true) {
int rv = select(fd + 1, &set, NULL, NULL, &timeout);
timeout = within_message_timeval;
if (rv == -1) { // error in select function call
perror("txrx: select:");
break;
} else if (rv == 0) { // no more bytes to read within timeout, so transfer is complete
break;
} else { // received more bytes, add them to buffer
// do we have space in the rx buffer left?
if (bytes_read_total >= rxbuf_len) {
// no buffer space left, but more to read.
break;
}
int bytes_read = read(fd, rxbuf + bytes_read_total, rxbuf_len - bytes_read_total);
if (bytes_read > 0) {
bytes_read_total += bytes_read;
}
}
}
return bytes_read_total;
}
std::vector<uint16_t> TinyModbusRTU::txrx(uint8_t device_address, FunctionCode function,
uint16_t first_register_address, uint16_t register_quantity,
uint16_t max_packet_size, bool wait_for_reply,
std::vector<uint16_t> request) {
// This only supports chunking of the read-requests.
std::vector<uint16_t> out;
if (max_packet_size < MODBUS_MIN_REPLY_SIZE + 2) {
EVLOG_error << fmt::format("Max packet size too small: {}", max_packet_size);
return {};
}
const uint16_t register_chunk = (max_packet_size - MODBUS_MIN_REPLY_SIZE) / 2;
size_t written_elements = 0;
while (register_quantity) {
const auto current_register_quantity = std::min(register_quantity, register_chunk);
std::vector<uint16_t> current_request;
if (request.size() > written_elements + current_register_quantity) {
current_request = std::vector<uint16_t>(request.begin() + written_elements,
request.begin() + written_elements + current_register_quantity);
written_elements += current_register_quantity;
} else {
current_request = std::vector<uint16_t>(request.begin() + written_elements, request.end());
written_elements = request.size();
}
const auto res = txrx_impl(device_address, function, first_register_address, current_register_quantity,
wait_for_reply, current_request);
// We failed to read/write.
if (res.empty()) {
return res;
}
out.insert(out.end(), res.begin(), res.end());
first_register_address += current_register_quantity;
register_quantity -= current_register_quantity;
}
return out;
}
std::vector<uint8_t> _make_single_write_request(uint8_t device_address, FunctionCode function,
uint16_t register_address, bool wait_for_reply, uint16_t data) {
const int req_len = 8;
std::vector<uint8_t> req(req_len);
req[DEVICE_ADDRESS_POS] = device_address;
req[FUNCTION_CODE_POS] = static_cast<uint8_t>(function);
register_address = htobe16(register_address);
data = htobe16(data);
memcpy(req.data() + REQ_TX_FIRST_REGISTER_ADDR_POS, &register_address, 2);
memcpy(req.data() + REQ_TX_SINGLE_REG_PAYLOAD_POS, &data, 2);
append_checksum(req.data(), req_len);
return req;
}
std::vector<uint8_t> _make_generic_request(uint8_t device_address, FunctionCode function,
uint16_t first_register_address, uint16_t register_quantity,
std::vector<uint16_t> request) {
// size of request
int req_len = (request.size() == 0 ? 0 : 2 * request.size() + 1) + MODBUS_BASE_PAYLOAD_SIZE;
std::vector<uint8_t> req(req_len);
// add header
req[DEVICE_ADDRESS_POS] = device_address;
req[FUNCTION_CODE_POS] = function;
first_register_address = htobe16(first_register_address);
register_quantity = htobe16(register_quantity);
memcpy(req.data() + REQ_TX_FIRST_REGISTER_ADDR_POS, &first_register_address, 2);
memcpy(req.data() + REQ_TX_QUANTITY_POS, &register_quantity, 2);
if (function == FunctionCode::WRITE_MULTIPLE_HOLDING_REGISTERS) {
// write byte count
req[REQ_TX_MULTIPLE_REG_BYTE_COUNT_POS] = request.size() * 2;
// add request data
int i = REQ_TX_MULTIPLE_REG_BYTE_COUNT_POS + 1;
for (auto r : request) {
r = htobe16(r);
memcpy(req.data() + i, &r, 2);
i += 2;
}
}
// set checksum in the last 2 bytes
append_checksum(req.data(), req_len);
return req;
}
/*
This function transmits a modbus request and waits for the reply.
Parameter request is optional and is only used for writing multiple registers.
*/
std::vector<uint16_t> TinyModbusRTU::txrx_impl(uint8_t device_address, FunctionCode function,
uint16_t first_register_address, uint16_t register_quantity,
bool wait_for_reply, std::vector<uint16_t> request) {
{
if (fd == -1) {
return {};
}
auto req =
function == FunctionCode::WRITE_SINGLE_HOLDING_REGISTER or function == FunctionCode::WRITE_SINGLE_COIL
? _make_single_write_request(device_address, function, first_register_address, wait_for_reply,
request.at(0))
: _make_generic_request(device_address, function, first_register_address, register_quantity, request);
// clear input and output buffer
tcflush(fd, TCIOFLUSH);
// write to serial port
rxtx_gpio.set(false);
uint8_t* buffer = req.data();
ssize_t written = 0;
while (written < req.size()) {
ssize_t c = write(fd, &buffer[written], req.size() - written);
if (c == -1)
throw std::system_error(errno, std::generic_category(), "Could not send Modbus request");
written += c;
}
if (rxtx_gpio.is_ready()) {
// if we are using GPIO to switch between RX/TX, use the fast version of tcdrain with exact timing
fast_tcdrain(fd);
} else {
// without GPIO switching, use regular tcdrain as not all UART drivers implement the ioctl
tcdrain(fd);
}
rxtx_gpio.set(true);
if (ignore_echo) {
// read back echo of what we sent and ignore it
read_reply(req.data(), req.size());
}
}
if (wait_for_reply) {
// wait for reply
uint8_t rxbuf[MODBUS_MAX_REPLY_SIZE];
int bytes_read_total = read_reply(rxbuf, sizeof(rxbuf));
return decode_reply(rxbuf, bytes_read_total, device_address, function);
}
return std::vector<uint16_t>();
}
} // namespace tiny_modbus

View File

@@ -0,0 +1,118 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 Pionix GmbH and Contributors to EVerest
/*
This is a tiny and fast modbus RTU implementation
*/
#ifndef TINY_MODBUS_RTU
#define TINY_MODBUS_RTU
#include <chrono>
#include <ostream>
#include <stdexcept>
#include <stdint.h>
#include <termios.h>
#include <everest/gpio/gpio.hpp>
#include <everest/logging.hpp>
namespace tiny_modbus {
constexpr int DEVICE_ADDRESS_POS = 0x00;
constexpr int FUNCTION_CODE_POS = 0x01;
constexpr int REQ_TX_FIRST_REGISTER_ADDR_POS = 0x02;
constexpr int REQ_TX_QUANTITY_POS = 0x04;
constexpr int REQ_TX_SINGLE_REG_PAYLOAD_POS = 0x04;
constexpr int REQ_TX_MULTIPLE_REG_BYTE_COUNT_POS = 0x06;
constexpr int RES_RX_LEN_POS = 0x02;
constexpr int RES_RX_START_OF_PAYLOAD = 0x03;
constexpr int RES_TX_START_OF_PAYLOAD = 0x02;
constexpr int RES_EXCEPTION_CODE = 0x02;
constexpr int MODBUS_MAX_REPLY_SIZE = 255 + 6;
constexpr int MODBUS_MIN_REPLY_SIZE = 5;
constexpr int MODBUS_BASE_PAYLOAD_SIZE = 8;
enum class Parity : uint8_t {
NONE = 0,
ODD = 1,
EVEN = 2
};
enum FunctionCode : uint8_t {
READ_COILS = 0x01,
READ_DISCRETE_INPUTS = 0x02,
READ_MULTIPLE_HOLDING_REGISTERS = 0x03,
READ_INPUT_REGISTERS = 0x04,
WRITE_SINGLE_COIL = 0x05,
WRITE_SINGLE_HOLDING_REGISTER = 0x06,
WRITE_MULTIPLE_COILS = 0x0F,
WRITE_MULTIPLE_HOLDING_REGISTERS = 0x10,
};
std::string FunctionCode_to_string(FunctionCode fc);
std::string FunctionCode_to_string_with_hex(FunctionCode fc);
std::ostream& operator<<(std::ostream& os, const FunctionCode& fc);
class TinyModbusException : public std::runtime_error {
using std::runtime_error::runtime_error;
};
class TimeoutException : public TinyModbusException {
using TinyModbusException::TinyModbusException;
};
class ShortPacketException : public TinyModbusException {
using TinyModbusException::TinyModbusException;
};
class AddressMismatchException : public TinyModbusException {
using TinyModbusException::TinyModbusException;
};
class FunctionCodeMismatchException : public TinyModbusException {
using TinyModbusException::TinyModbusException;
};
class ChecksumErrorException : public TinyModbusException {
using TinyModbusException::TinyModbusException;
};
class IncompletePacketException : public TinyModbusException {
using TinyModbusException::TinyModbusException;
};
class OddByteCountException : public TinyModbusException {
using TinyModbusException::TinyModbusException;
};
class ModbusException : public TinyModbusException {
using TinyModbusException::TinyModbusException;
};
class TinyModbusRTU {
public:
~TinyModbusRTU();
bool open_device(const std::string& device, int baud, bool ignore_echo,
const Everest::GpioSettings& rxtx_gpio_settings, const Parity parity, bool rtscts,
std::chrono::milliseconds initial_timeout, std::chrono::milliseconds within_message_timeout);
std::vector<uint16_t> txrx(uint8_t device_address, FunctionCode function, uint16_t first_register_address,
uint16_t register_quantity, uint16_t chunk_size, bool wait_for_reply = true,
std::vector<uint16_t> request = std::vector<uint16_t>());
private:
// Serial interface
int fd{-1};
bool ignore_echo{false};
std::vector<uint16_t> txrx_impl(uint8_t device_address, FunctionCode function, uint16_t first_register_address,
uint16_t register_quantity, bool wait_for_reply = true,
std::vector<uint16_t> request = std::vector<uint16_t>());
int read_reply(uint8_t* rxbuf, int rxbuf_len);
Everest::Gpio rxtx_gpio;
std::chrono::milliseconds initial_timeout;
std::chrono::milliseconds within_message_timeout;
};
} // namespace tiny_modbus
#endif

View File

@@ -0,0 +1,32 @@
load("@rules_cc//cc:defs.bzl", "cc_test")
load("//modules:module.bzl", "cc_everest_module")
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_TEST_INCOMPATIBLE")
cc_everest_module(
name = "Setup",
srcs = glob([
"*.cpp",
"*.hpp",
]),
deps = [
"//lib/everest/run_application",
],
)
cc_test(
name = "Setup_test",
target_compatible_with = CROSS_TEST_INCOMPATIBLE,
srcs = glob(
[
"tests/*.cpp",
"tests/*.hpp",
"WiFiSetup.*",
],
),
includes = ["."],
deps = [
"//lib/everest/run_application",
"@com_github_nlohmann_json//:json",
"@googletest//:gtest_main",
],
)

View File

@@ -0,0 +1,29 @@
#
# AUTO GENERATED - MARKED REGIONS WILL BE KEPT
# template version 3
#
# module setup:
# - ${MODULE_NAME}: module name
ev_setup_cpp_module()
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
# insert your custom targets and additional config variables here
target_sources(${MODULE_NAME}
PRIVATE
"WiFiSetup.cpp"
)
target_link_libraries(${MODULE_NAME}
PRIVATE
everest::run_application
)
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
target_compile_features(${MODULE_NAME} PRIVATE cxx_std_17)
if(EVEREST_CORE_BUILD_TESTING)
include(CTest)
add_subdirectory(tests)
endif()
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1

View File

@@ -0,0 +1,245 @@
# Setup module API documentation
This module is responsible for setup tasks that might need privileged access, for example wifi configuration.
If not run as root user, set at least the following capabilities in your EVerest config file: CAP_NET_ADMIN, CAP_NET_RAW, CAP_DAC_OVERRIDE.
They will be passed on to the child processes such as wpa_cli etc.
## Periodically published variables
### everest_api/setup/var/supported_setup_features
This variable is published periodically and contains a JSON object with the supported features in the following form:
```json
{
"localization": true,
"setup_simulation": true,
"setup_wifi": true
}
```
### everest_api/setup/var/hostname
This variable is published periodically and contains the hostname.
## Commands and variables published in response
### everest_api/setup/cmd/scan_wifi
If any arbitrary payload is published to this topic a list of available wifi networks is published on the following topic:
__everest_api/setup/var/wifi_info__
with the following payload format:
```json
[
{
"bssid": "00:11:22:33:44:55",
"flags": [
"WPA2-PSK-CCMP",
"ESS"
],
"frequency": 2437,
"signal_level": -41,
"ssid": "Example"
},
{
"bssid": "66:77:88:99:aa:bb",
"flags": [
"WPA2-PSK-CCMP",
"ESS"
],
"frequency": 5180,
"signal_level": -56,
"ssid": "Example2"
}
]
```
additionally general network device information is published on the following topic:
__everest_api/setup/var/network_device_info__
with the following payload format:
```json
[
{
"blocked": false,
"interface": "wlan0",
"ipv4": ["192.0.2.23"],
"ipv6": ["2001:db8:0:0:0:0:0:23"],
"mac": "00:11:22:33:44:55",
"rfkill_id": "0",
"wireless": true
},
{
"blocked": false,
"interface": "eth0",
"ipv4": "192.0.2.42",
"ipv6": ["2001:db8:0:0:0:0:0:42"],
"mac": "11:22:33:44:55:66",
"rfkill_id": "",
"wireless": false
}
]
```
additionally the list of configured wifi networks is published to the following topic:
__everest_api/setup/var/configured_networks__
with the following payload format:
```json
[
{
"connected": true,
"interface": "wlan0",
"network_id": 0,
"signal_level": -56,
"ssid": "Example"
},
{
"connected": false,
"interface": "wlan0",
"network_id": 1,
"signal_level": -100,
"ssid": "Example2"
}
]
```
### everest_api/setup/cmd/enable_wifi_scanning
If any arbitrary payload is published to this topic the list of available wifi networks and general network device information just mentioned is published periodically.
### everest_api/setup/cmd/disable_wifi_scanning
If any arbitrary payload is published to this topic the list of available wifi networks and general network device information stops being periodically published.
### everest_api/setup/cmd/rfkill_unblock
If a rfkill_id is published to this topic the wifi interface with this id will be unblocked.
### everest_api/setup/cmd/rfkill_block
If a rfkill_id is published to this topic the wifi interface with this id will be blocked.
### everest_api/setup/cmd/list_configured_networks
If any arbitrary payload is published to this topic the list of configured wifi networks is published to the following topic:
__everest_api/setup/var/configured_networks__
with the following payload format:
```json
[
{
"connected": true,
"interface": "wlan0",
"network_id": 0,
"ssid": "Example"
},
{
"connected": false,
"interface": "wlan0",
"network_id": 1,
"ssid": "Example2"
}
]
```
### everest_api/setup/cmd/add_network
To add a wifi network a payload with the following format must be published to this topic:
```json
{
"interface": "wlan0",
"ssid": "Example",
"psk": "20fcb529dee0aad11b0568f553942850d06e4c4531c0d75b35345d580b300f78"
}
```
The PSK field can represent the passphrase instead using escaped quotes:
```json
{
"interface": "wlan0",
"ssid": "Example",
"psk": "\"A_valid_passphrase\""
}
```
For open WiFi networks the psk must be an empty string `"psk": ""`.
For hidden networks an optional item is needed:
```json
{
"interface": "wlan0",
"ssid": "Example",
"psk": "\"A_valid_passphrase\"",
"hidden": true
}
```
When `hidden` is not supplied then it is assumed to be false.
### everest_api/setup/cmd/enable_network
To enable a wifi network a payload with the following format must be published to this topic:
```json
{
"interface": "wlan0",
"network_id": 0
}
```
### everest_api/setup/cmd/disable_network
To disable a wifi network a payload with the following format must be published to this topic:
```json
{
"interface": "wlan0",
"network_id": 0
}
```
### everest_api/setup/cmd/select_network
To select a wifi network a payload with the following format must be published to this topic:
```json
{
"interface": "wlan0",
"network_id": 0
}
```
### everest_api/setup/cmd/remove_network
To remove a wifi network a payload with the following format must be published to this topic:
```json
{
"interface": "wlan0",
"network_id": 0
}
```
### everest_api/setup/cmd/remove_all_networks
If any arbitrary payload is published to this topic all wifi networks will be removed.
### everest_api/setup/cmd/enable_ap
If any arbitrary payload is published to this topic a wireless access point will be enabled on the interface configured in the module config.
### everest_api/setup/cmd/disable_ap
If any arbitrary payload is published to this topic the wireless access point will be disabled.
### everest_api/setup/cmd/check_online_status
If any arbitrary payload is published to this topic a ping will be sent to the host configured in the configuration key "online_check_host". Depending on the success of this ping a status of "online" or "offline" will be reported on the following topic:
__everest_api/setup/var/online_status__
### everest_api/setup/cmd/reboot
If any arbitrary payload is published to this topic the system will reboot.
## Application Info / Localization
### everest_api/setup/cmd/set_mode
If a mode _private_ or _public_ is published to this topic it will be stored permanently.
### everest_api/setup/cmd/set_initialized
If any arbitrary payload is published to this topic the system will be marked as "initialized" permanently.
### everest_api/setup/cmd/reset_initialized
If any arbitrary payload is published to this topic the system will be marked as "uninitialized" permanently.
### everest_api/setup/cmd/change_default_language
You can set a [three-letter language code](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) to be set as the default language which will be stored permanently.
### everest_api/setup/cmd/change_current_language
You can set a [three-letter language code](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) to be set as the current language.
### everest_api/setup/cmd/get_application_info
If any arbitrary payload is published to this topic a application info object is published to the following topic:
__everest_api/setup/var/application_info__
with the following payload format:
```json
{
"initialized": true,
"mode": "private",
"default_language": "eng",
"current_language": "ger"
}
```

View File

@@ -0,0 +1,707 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "Setup.hpp"
#include <algorithm>
#include <cstdlib>
#include <fstream>
#include <locale>
#include <everest/run_application/run_application.hpp>
#include <fmt/core.h>
using namespace everest::run_application;
namespace module {
// set WifiConfigureClass to the class to use for configuring WiFi
typedef WpaCliSetup WifiConfigureClass;
void to_json(json& j, const NetworkDeviceInfo& k) {
j = json::object({{"interface", k.interface},
{"wireless", k.wireless},
{"blocked", k.blocked},
{"rfkill_id", k.rfkill_id},
{"ipv4", k.ipv4},
{"ipv6", k.ipv6},
{"mac", k.mac},
{"link_type", k.link_type}});
}
void to_json(json& j, const WifiCredentials& k) {
j = json::object({{"interface", k.interface}, {"ssid", k.ssid}, {"psk", k.psk}, {"hidden", k.hidden}});
}
void from_json(const json& j, WifiCredentials& k) {
k.interface = j.at("interface");
k.ssid = j.at("ssid");
k.psk = j.at("psk");
k.hidden = false;
// optional item
auto it = j.find("hidden");
if ((it != j.end() && *it)) {
k.hidden = true;
}
}
void to_json(json& j, const InterfaceAndNetworkId& k) {
j = json::object({{"interface", k.interface}, {"network_id", k.network_id}});
}
void from_json(const json& j, InterfaceAndNetworkId& k) {
k.interface = j.at("interface");
k.network_id = j.at("network_id");
}
void to_json(json& j, const SupportedSetupFeatures& k) {
j = json::object(
{{"setup_wifi", k.setup_wifi}, {"localization", k.localization}, {"setup_simulation", k.setup_simulation}});
}
void to_json(json& j, const ApplicationInfo& k) {
j = json::object({{"initialized", k.initialized},
{"mode", k.mode},
{"default_language", k.default_language},
{"current_language", k.current_language},
{"release_metadata_file", k.release_metadata_file}});
}
//------------------------------------------------------------------------------
// JSON conversion for WifiConfigureClass types
static void to_json(json& j, const WifiConfigureClass::WifiNetworkStatus& k) {
j = json::object({{"interface", k.interface},
{"network_id", k.network_id},
{"ssid", k.ssid},
{"connected", k.connected},
{"signal_level", k.signal_level}});
}
static void to_json(json& j, const WifiConfigureClass::WifiScan& k) {
auto flags_array = json::array();
flags_array = k.flags;
j = json::object({{"bssid", k.bssid},
{"ssid", k.ssid},
{"frequency", k.frequency},
{"signal_level", k.signal_level},
{"flags", flags_array}});
}
//------------------------------------------------------------------------------
void Setup::init() {
// Set default locale "C" when no locale is set at all
try {
std::locale loc("");
} catch (const std::runtime_error& e) {
setenv("LC_ALL", "C", 1);
}
}
void Setup::ready() {
this->discover_network_thread = std::thread([this]() {
while (true) {
if ((this->config.setup_wifi) && (wifi_scan_enabled)) {
this->discover_network();
}
this->publish_hostname();
std::this_thread::sleep_for(std::chrono::seconds(5));
}
});
this->publish_application_info_thread = std::thread([this]() {
while (true) {
this->publish_supported_features();
this->publish_application_info();
this->publish_ap_state();
std::this_thread::sleep_for(std::chrono::seconds(1));
}
});
std::string set_mode_cmd = this->cmd_base + "set_mode";
this->mqtt.subscribe(set_mode_cmd, [this](const std::string& data) { this->set_mode(data); });
std::string set_initialized_cmd = this->cmd_base + "set_initialized";
this->mqtt.subscribe(set_initialized_cmd, [this](const std::string& data) { this->set_initialized(true); });
std::string reset_initialized_cmd = this->cmd_base + "reset_initialized";
this->mqtt.subscribe(reset_initialized_cmd, [this](const std::string& data) { this->set_initialized(false); });
std::string change_default_language_cmd = this->cmd_base + "change_default_language";
this->mqtt.subscribe(change_default_language_cmd,
[this](const std::string& data) { this->set_default_language(data); });
std::string change_current_language_cmd = this->cmd_base + "change_current_language";
this->mqtt.subscribe(change_current_language_cmd,
[this](const std::string& data) { this->set_current_language(data); });
std::string get_application_info_cmd = this->cmd_base + "get_application_info";
this->mqtt.subscribe(get_application_info_cmd,
[this](const std::string& data) { this->publish_application_info(); });
if (this->config.setup_wifi) {
std::string rfkill_unblock_cmd = this->cmd_base + "rfkill_unblock";
this->mqtt.subscribe(rfkill_unblock_cmd, [this](const std::string& data) { this->rfkill_unblock(data); });
std::string rfkill_block_cmd = this->cmd_base + "rfkill_block";
this->mqtt.subscribe(rfkill_block_cmd, [this](const std::string& data) { this->rfkill_block(data); });
std::string list_configured_networks_cmd = this->cmd_base + "list_configured_networks";
this->mqtt.subscribe(list_configured_networks_cmd,
[this](const std::string& data) { this->publish_configured_networks(); });
std::string add_network_cmd = this->cmd_base + "add_network";
this->mqtt.subscribe(add_network_cmd, [this](const std::string& data) {
WifiCredentials wifi_credentials = json::parse(data);
WifiConfigureClass wifi;
this->add_and_enable_network(wifi_credentials.interface, wifi_credentials.ssid, wifi_credentials.psk,
wifi_credentials.hidden);
wifi.save_config(wifi_credentials.interface);
this->publish_configured_networks();
});
std::string enable_network_cmd = this->cmd_base + "enable_network";
this->mqtt.subscribe(enable_network_cmd, [this](const std::string& data) {
InterfaceAndNetworkId wifi_details = json::parse(data);
WifiConfigureClass wifi;
wifi.enable_network(wifi_details.interface, wifi_details.network_id);
wifi.save_config(wifi_details.interface);
this->publish_configured_networks();
});
std::string disable_network_cmd = this->cmd_base + "disable_network";
this->mqtt.subscribe(disable_network_cmd, [this](const std::string& data) {
InterfaceAndNetworkId wifi_details = json::parse(data);
WifiConfigureClass wifi;
wifi.disable_network(wifi_details.interface, wifi_details.network_id);
wifi.save_config(wifi_details.interface);
this->publish_configured_networks();
});
std::string select_network_cmd = this->cmd_base + "select_network";
this->mqtt.subscribe(select_network_cmd, [this](const std::string& data) {
InterfaceAndNetworkId wifi_details = json::parse(data);
WifiConfigureClass wifi;
wifi.select_network(wifi_details.interface, wifi_details.network_id);
wifi.save_config(wifi_details.interface);
this->publish_configured_networks();
});
std::string remove_network_cmd = this->cmd_base + "remove_network";
this->mqtt.subscribe(remove_network_cmd, [this](const std::string& data) {
InterfaceAndNetworkId wifi_details = json::parse(data);
WifiConfigureClass wifi;
wifi.remove_network(wifi_details.interface, wifi_details.network_id);
wifi.save_config(wifi_details.interface);
this->publish_configured_networks();
});
std::string scan_wifi_cmd = this->cmd_base + "scan_wifi";
this->mqtt.subscribe(scan_wifi_cmd, [this](const std::string& data) { this->discover_network(); });
std::string enable_wifi_scanning_cmd = this->cmd_base + "enable_wifi_scanning";
this->mqtt.subscribe(enable_wifi_scanning_cmd,
[this](const std::string& data) { this->wifi_scan_enabled = true; });
std::string disable_wifi_scanning_cmd = this->cmd_base + "disable_wifi_scanning";
this->mqtt.subscribe(disable_wifi_scanning_cmd,
[this](const std::string& data) { this->wifi_scan_enabled = false; });
std::string remove_all_networks_cmd = this->cmd_base + "remove_all_networks";
this->mqtt.subscribe(remove_all_networks_cmd, [this](const std::string& data) {
this->remove_all_networks();
this->publish_configured_networks();
});
std::string reboot_cmd = this->cmd_base + "reboot";
this->mqtt.subscribe(reboot_cmd, [this](const std::string& data) { this->reboot(); });
std::string check_online_status_cmd = this->cmd_base + "check_online_status";
this->mqtt.subscribe(check_online_status_cmd, [this](const std::string& data) { this->check_online_status(); });
std::string enable_ap_cmd = this->cmd_base + "enable_ap";
this->mqtt.subscribe(enable_ap_cmd, [this](const std::string& data) { enable_ap(); });
std::string disable_ap_cmd = this->cmd_base + "disable_ap";
this->mqtt.subscribe(disable_ap_cmd, [this](const std::string& data) { disable_ap(); });
}
}
void Setup::publish_supported_features() {
SupportedSetupFeatures supported_setup_features;
supported_setup_features.setup_wifi = this->config.setup_wifi;
supported_setup_features.localization = this->config.localization;
supported_setup_features.setup_simulation = this->config.setup_simulation;
std::string supported_setup_features_var = this->var_base + "supported_setup_features";
json supported_setup_features_json = supported_setup_features;
this->mqtt.publish(supported_setup_features_var, supported_setup_features_json.dump());
}
void Setup::publish_application_info() {
ApplicationInfo application_info;
application_info.initialized = this->get_initialized();
application_info.mode = this->get_mode();
application_info.default_language = this->get_default_language();
application_info.current_language = this->get_current_language();
application_info.release_metadata_file = this->info.paths.etc / this->config.release_metadata_file;
std::string application_info_var = this->var_base + "application_info";
json application_info_json = application_info;
this->mqtt.publish(application_info_var, application_info_json.dump());
}
void Setup::publish_hostname() {
std::string hostname_var = this->var_base + "hostname";
this->mqtt.publish(hostname_var, this->get_hostname());
}
void Setup::publish_ap_state() {
std::string ap_state_var = this->var_base + "ap_state";
auto hostapd_enabled_output = run_application("systemctl", {"is-active", "--quiet", "hostapd"});
if (hostapd_enabled_output.exit_code == 0) {
this->ap_state = "enabled";
} else {
this->ap_state = "disabled";
}
this->mqtt.publish(ap_state_var, this->ap_state);
}
void Setup::set_default_language(std::string language) {
this->r_store->call_store("everest_localization_default_language", language);
}
std::string Setup::get_default_language() {
auto language = this->r_store->call_load("everest_localization_default_language");
if (!std::holds_alternative<std::string>(language)) {
return "unknown";
}
return std::get<std::string>(language);
}
void Setup::set_current_language(const std::string& language) {
this->current_language = language;
}
std::string Setup::get_current_language() {
if (this->current_language.empty()) {
this->current_language = this->get_default_language();
}
return this->current_language;
}
void Setup::set_mode(std::string mode) {
this->r_store->call_store("everest_mode", mode);
}
std::string Setup::get_mode() {
auto mode = this->r_store->call_load("everest_mode");
if (!std::holds_alternative<std::string>(mode)) {
return "unknown";
}
return std::get<std::string>(mode);
}
void Setup::set_initialized(bool initialized) {
this->r_store->call_store("everest_initialized", initialized);
}
bool Setup::get_initialized() {
if (this->config.initialized_by_default) {
return true;
}
auto initialized = this->r_store->call_load("everest_initialized");
if (!std::holds_alternative<bool>(initialized)) {
return false;
}
return std::get<bool>(initialized);
}
void Setup::discover_network() {
std::vector<NetworkDeviceInfo> device_info = this->get_network_devices();
this->populate_rfkill_status(device_info);
this->populate_ip_addresses(device_info);
std::string network_device_info_var = this->var_base + "network_device_info";
json device_info_json = json::array();
device_info_json = device_info;
this->mqtt.publish(network_device_info_var, device_info_json.dump());
auto wifi_info = this->scan_wifi(device_info);
std::string wifi_info_var = this->var_base + "wifi_info";
json wifi_info_json = json::array();
wifi_info_json = wifi_info;
this->mqtt.publish(wifi_info_var, wifi_info_json.dump());
this->publish_configured_networks();
}
std::string Setup::read_type_file(const fs::path& type_path) {
if (!fs::exists(type_path)) {
return "";
}
std::ifstream ifs(type_path.c_str());
std::string type_file((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>()));
// trim newlines
type_file.erase(std::remove(type_file.begin(), type_file.end(), '\n'), type_file.end());
return type_file;
}
std::vector<NetworkDeviceInfo> Setup::get_network_devices() {
auto sys_net_path = fs::path("/sys/class/net");
auto sys_virtual_net_path = fs::path("/sys/devices/virtual/net");
std::vector<NetworkDeviceInfo> device_info;
for (auto&& net_it : fs::directory_iterator(sys_net_path)) {
auto net_path = net_it.path();
auto type_path = net_path / "type";
if (!fs::exists(type_path)) {
continue;
}
std::string type_file = this->read_type_file(type_path);
auto interface = net_path.filename();
auto virtual_interface = sys_virtual_net_path / interface;
// check if type is ethernet:
if (type_file == "1") {
if (fs::exists(virtual_interface)) {
continue;
}
auto device = NetworkDeviceInfo();
device.interface = interface.string();
device.link_type = "ether";
// check if its wireless or not:
auto wireless_path = net_path / "wireless";
if (fs::exists(wireless_path)) {
device.wireless = true;
auto phy80211_path = net_path / "phy80211";
for (auto&& rfkill_it : fs::directory_iterator(phy80211_path)) {
auto phy_file_path = rfkill_it.path().filename().string();
std::string rfkill = "rfkill";
if (phy_file_path.find(rfkill) == 0) {
phy_file_path.erase(0, rfkill.size());
device.rfkill_id = phy_file_path;
break;
}
}
}
device_info.push_back(device);
} else if (type_file == "65534") {
if (!fs::exists(virtual_interface)) {
continue;
}
auto virtual_type_path = virtual_interface / "type";
if (!fs::exists(virtual_type_path)) {
continue;
}
std::string virtual_type_file = this->read_type_file(virtual_type_path);
if (virtual_type_file == type_file) {
// assume it's a vpn, but check ip link
auto ip_output = run_application("ip", {"--json", "-details", "link", "show", interface});
if (ip_output.exit_code != 0) {
continue;
}
const auto ip_json = json::parse(ip_output.output);
if (ip_json.size() < 1) {
continue;
}
const auto& entry = ip_json.at(0);
if (entry.contains("linkinfo") and entry.at("linkinfo").contains("info_kind")) {
auto device = NetworkDeviceInfo();
device.interface = interface.string();
device.link_type = entry.at("linkinfo").at("info_kind");
device_info.push_back(device);
}
}
}
}
return device_info;
}
void Setup::populate_rfkill_status(std::vector<NetworkDeviceInfo>& device_info) {
auto rfkill_output = run_application("rfkill", {"--json"});
if (rfkill_output.exit_code != 0) {
return;
}
auto rfkill_json = json::parse(rfkill_output.output);
for (auto rfkill_object : rfkill_json.items()) {
for (auto rfkill_device : rfkill_object.value()) {
for (auto& device : device_info) {
int device_id = rfkill_device.at("id");
if (!device.rfkill_id.empty() && std::to_string(device_id) == device.rfkill_id) {
if (rfkill_device.at("soft") == "blocked") {
device.blocked = true;
}
break;
}
}
}
}
}
bool Setup::rfkill_unblock(std::string rfkill_id) {
auto network_devices = this->get_network_devices();
this->populate_rfkill_status(network_devices);
bool found = false;
for (auto device : network_devices) {
if (device.rfkill_id == rfkill_id) {
if (!device.blocked) {
return true;
}
found = true;
break;
}
}
if (!found) {
return false;
}
auto rfkill_output = run_application("rfkill", {"unblock", rfkill_id});
if (rfkill_output.exit_code != 0) {
return false;
}
return true;
}
bool Setup::rfkill_block(std::string rfkill_id) {
auto network_devices = this->get_network_devices();
this->populate_rfkill_status(network_devices);
bool found = false;
for (auto device : network_devices) {
if (device.rfkill_id == rfkill_id) {
if (device.blocked) {
return true;
}
found = true;
break;
}
}
if (!found) {
return false;
}
auto rfkill_output = run_application("rfkill", {"block", rfkill_id});
if (rfkill_output.exit_code != 0) {
return false;
}
return true;
}
void Setup::publish_configured_networks() {
auto network_devices = this->get_network_devices();
WpaCliSetup::WifiNetworkStatusList all_wifi_networks;
for (auto device : network_devices) {
if (!device.wireless) {
continue;
}
WifiConfigureClass wifi;
auto network_list = wifi.list_networks_status(device.interface);
for (auto& i : network_list) {
all_wifi_networks.push_back(std::move(i));
}
}
std::string network_list_var = this->var_base + "configured_networks";
json configured_networks_json = json::array();
configured_networks_json = all_wifi_networks;
this->mqtt.publish(network_list_var, configured_networks_json.dump());
}
bool Setup::add_and_enable_network(const std::string& interface, const std::string& ssid, const std::string& psk,
bool hidden) {
WifiConfigureClass wifi;
std::string net_if = interface;
if (net_if.empty()) {
EVLOG_warning << "Attempting to add a network without an interface, attempting to use the first one";
auto network_devices = this->get_network_devices();
for (auto device : network_devices) {
if (device.wireless) {
net_if = device.interface;
break;
}
}
}
auto network_id = wifi.add_network(net_if);
bool bResult = network_id != -1;
bResult = bResult && wifi.set_network(net_if, network_id, ssid, psk, hidden);
bResult = bResult && wifi.enable_network(net_if, network_id);
return bResult;
}
bool Setup::remove_all_networks() {
auto network_devices = this->get_network_devices();
std::uint32_t remove_fail = 0;
for (auto device : network_devices) {
if (!device.wireless) {
continue;
}
WifiConfigureClass wifi;
auto networks = wifi.list_networks(device.interface);
for (auto network : networks) {
if (!wifi.remove_network(device.interface, network.network_id)) {
remove_fail++;
}
}
wifi.save_config(device.interface);
}
return remove_fail == 0;
}
bool Setup::reboot() {
bool success = true;
auto reboot_output = run_application("systemctl", {"reboot"});
if (reboot_output.exit_code != 0) {
success = false;
}
return success;
}
bool Setup::is_online() {
bool success = true;
auto reboot_output = run_application("ping", {"-c", "1", this->config.online_check_host});
if (reboot_output.exit_code != 0) {
success = false;
}
return success;
}
void Setup::check_online_status() {
std::string online_status_var = this->var_base + "online_status";
if (this->is_online()) {
this->mqtt.publish(online_status_var, "online");
} else {
this->mqtt.publish(online_status_var, "offline");
}
}
void Setup::enable_ap() {
auto wpa_cli_output = run_application("wpa_cli", {"-i", this->config.ap_interface, "disconnect"});
if (wpa_cli_output.exit_code != 0) {
EVLOG_error << "Could not disconnect from wireless LAN";
}
auto start_hostapd_output = run_application("systemctl", {"start", "hostapd"});
if (start_hostapd_output.exit_code != 0) {
EVLOG_error << "Could not start hostapd";
}
auto start_dnsmasq_output = run_application("systemctl", {"start", "dnsmasq"});
if (start_dnsmasq_output.exit_code != 0) {
EVLOG_error << "Could not start dnsmasq";
}
auto add_static_ip_output =
run_application("ip", {"addr", "add", this->config.ap_ipv4, "dev", this->config.ap_interface});
if (add_static_ip_output.exit_code != 0) {
EVLOG_error << "Could not add static ip to interface " << this->config.ap_interface;
}
}
void Setup::disable_ap() {
auto del_static_ip_output =
run_application("ip", {"addr", "del", this->config.ap_ipv4, "dev", this->config.ap_interface});
if (del_static_ip_output.exit_code != 0) {
EVLOG_error << "Could not del static ip " << this->config.ap_ipv4 << " from interface "
<< this->config.ap_interface;
}
auto stop_dnsmasq_output = run_application("systemctl", {"stop", "dnsmasq"});
if (stop_dnsmasq_output.exit_code != 0) {
EVLOG_error << "Could not stop dnsmasq";
}
auto stop_hostapd_output = run_application("systemctl", {"stop", "hostapd"});
if (stop_hostapd_output.exit_code != 0) {
EVLOG_error << "Could not stop hostapd";
}
auto wpa_cli_output = run_application("wpa_cli", {"-i", this->config.ap_interface, "reconnect"});
if (wpa_cli_output.exit_code != 0) {
EVLOG_error << "Could not reconnect to wireless LAN";
}
}
static void add_addr_infos_to_device(const json& addr_infos, NetworkDeviceInfo& device) {
for (const auto& addr_info : addr_infos) {
if (addr_info.at("family") == "inet") {
device.ipv4.push_back(addr_info.at("local"));
} else if (addr_info.at("family") == "inet6") {
device.ipv6.push_back(addr_info.at("local"));
}
}
}
void Setup::populate_ip_addresses(std::vector<NetworkDeviceInfo>& device_info) {
auto ip_output = run_application("ip", {"--json", "address", "show"});
if (ip_output.exit_code != 0) {
return;
}
const auto ip_json = json::parse(ip_output.output);
for (const auto& ip_object : ip_json) {
const std::string ifname = ip_object.at("ifname");
auto device = std::find_if(device_info.begin(), device_info.end(),
[&ifname](NetworkDeviceInfo& device) { return device.interface == ifname; });
if (device == device_info.end()) {
continue;
}
if (ip_object.contains("address")) {
device->mac = ip_object.at("address");
}
add_addr_infos_to_device(ip_object.at("addr_info"), *device);
}
}
WifiConfigureClass::WifiScanList Setup::scan_wifi(const std::vector<NetworkDeviceInfo>& device_info) {
WifiConfigureClass::WifiScanList wifi_info;
WifiConfigureClass wifi;
for (auto device : device_info) {
if (!device.wireless) {
continue;
}
auto dev_list = wifi.scan_wifi(device.interface);
wifi_info.insert(wifi_info.end(), dev_list.begin(), dev_list.end());
}
return wifi_info;
}
std::string Setup::get_hostname() {
auto hostname_output = run_application("hostname", {});
if (hostname_output.exit_code == 0 && hostname_output.split_output.size() > 0) {
return hostname_output.split_output.at(0);
}
return "";
}
} // namespace module

View File

@@ -0,0 +1,189 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef SETUP_HPP
#define SETUP_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 2
//
#include "ld-ev.hpp"
// headers for required interface implementations
#include <generated/interfaces/kvs/Interface.hpp>
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
// insert your custom include headers here
#include "WiFiSetup.hpp"
#include <regex>
namespace module {
namespace fs = std::filesystem;
struct WifiCredentials {
std::string interface;
std::string ssid;
std::string psk;
bool hidden;
operator std::string() {
json wifi_credentials = *this;
return wifi_credentials.dump();
}
};
void to_json(json& j, const WifiCredentials& k);
void from_json(const json& j, WifiCredentials& k);
struct InterfaceAndNetworkId {
std::string interface;
int network_id;
operator std::string() {
json remove_wifi = *this;
return remove_wifi.dump();
}
};
void to_json(json& j, const InterfaceAndNetworkId& k);
void from_json(const json& j, InterfaceAndNetworkId& k);
struct NetworkDeviceInfo {
std::string interface;
bool wireless = false;
bool blocked = false;
std::string rfkill_id;
std::vector<std::string> ipv4;
std::vector<std::string> ipv6;
std::string mac;
std::string link_type;
operator std::string() {
json device_info = *this;
return device_info.dump();
}
};
void to_json(json& j, const NetworkDeviceInfo& k);
struct SupportedSetupFeatures {
bool setup_wifi;
bool localization;
bool setup_simulation;
operator std::string() {
json supported_setup_features = *this;
return supported_setup_features.dump();
}
};
void to_json(json& j, const SupportedSetupFeatures& k);
struct ApplicationInfo {
bool initialized;
std::string mode;
std::string default_language;
std::string current_language;
std::string release_metadata_file;
operator std::string() {
json application_info = *this;
return application_info.dump();
}
};
void to_json(json& j, const ApplicationInfo& k);
} // namespace module
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
namespace module {
struct Conf {
bool setup_wifi;
bool localization;
bool setup_simulation;
std::string online_check_host;
bool initialized_by_default;
std::string release_metadata_file;
std::string ap_interface;
std::string ap_ipv4;
};
class Setup : public Everest::ModuleBase {
public:
Setup() = delete;
Setup(const ModuleInfo& info, Everest::MqttProvider& mqtt_provider, std::unique_ptr<kvsIntf> r_store,
Conf& config) :
ModuleBase(info), mqtt(mqtt_provider), r_store(std::move(r_store)), config(config){};
Everest::MqttProvider& mqtt;
const std::unique_ptr<kvsIntf> r_store;
const Conf& config;
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
// insert your public definitions here
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
protected:
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
// insert your protected definitions here
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
private:
friend class LdEverest;
void init();
void ready();
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
// insert your private definitions here
std::string api_base = "everest_api/setup/";
std::string var_base = api_base + "var/";
std::string cmd_base = api_base + "cmd/";
std::thread discover_network_thread;
std::thread publish_application_info_thread;
bool wifi_scan_enabled = false;
std::string ap_state = "unknown";
void publish_supported_features();
void publish_application_info();
void publish_hostname();
void publish_ap_state();
void set_default_language(std::string language);
std::string get_default_language();
std::string current_language;
void set_current_language(const std::string& language);
std::string get_current_language();
void set_mode(std::string mode);
std::string get_mode();
void set_initialized(bool initialized);
bool get_initialized();
void discover_network();
std::string read_type_file(const fs::path& type_path);
std::vector<NetworkDeviceInfo> get_network_devices();
void populate_rfkill_status(std::vector<NetworkDeviceInfo>& device_info);
bool rfkill_unblock(std::string rfkill_id);
bool rfkill_block(std::string rfkill_id);
void publish_configured_networks();
bool add_and_enable_network(const std::string& interface, const std::string& ssid, const std::string& psk,
bool hidden = false);
bool remove_all_networks();
bool reboot();
bool is_online();
void check_online_status();
void enable_ap();
void disable_ap();
void populate_ip_addresses(std::vector<NetworkDeviceInfo>& device_info);
WpaCliSetup::WifiScanList scan_wifi(const std::vector<NetworkDeviceInfo>& device_info);
std::string get_hostname();
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
};
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
// insert other definitions here
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
} // namespace module
#endif // SETUP_HPP

View File

@@ -0,0 +1,578 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "WiFiSetup.hpp"
#include <filesystem>
#include <functional>
#include <regex>
#include <sstream>
#include <thread>
#include <utility>
#include <everest/run_application/run_application.hpp>
using namespace everest::run_application;
/**
* @file
* @brief wpa_cli command failure detection
*
* `wpa_cli` sets an exit code of 0 unless the command is malformed.
* Failures are presented via text to stdout.
* Hence checking for failure to remove a network would mean checking
* the output for OK or FAIL.
*
* This is common across all calls to `wpa_cli`.
*/
namespace {
inline int hex_digit_to_nibble(std::uint8_t c) {
int result{-1};
if ((c >= '0') && (c <= '9')) {
result = static_cast<std::uint8_t>(c - '0');
} else if ((c >= 'a') && (c <= 'f')) {
result = static_cast<std::uint8_t>(c - 'a') + 10;
} else if ((c >= 'A') && (c <= 'F')) {
result = static_cast<std::uint8_t>(c - 'A') + 10;
}
return result;
}
inline int hex_to_int(std::uint8_t high, std::uint8_t low) {
int result{-1};
const auto h = hex_digit_to_nibble(high);
const auto l = hex_digit_to_nibble(low);
if ((h != -1) && (l != -1)) {
const auto hc = static_cast<std::uint8_t>(h) << 4U;
const auto lc = static_cast<std::uint8_t>(l);
result = static_cast<int>(hc | lc);
}
return result;
}
inline std::uint8_t nibble_to_hex_digit(std::uint8_t c) {
std::uint8_t result{};
c &= 0x0fU;
if (c <= 9) {
result = c + '0';
} else {
result = (c - 10) + 'a';
}
return result;
}
inline void int_to_hex(std::uint8_t& high, std::uint8_t& low, std::uint8_t c) {
high = nibble_to_hex_digit(c >> 4U);
low = nibble_to_hex_digit(c);
}
} // namespace
namespace module {
constexpr const char* wpa_cli = "/usr/sbin/wpa_cli";
constexpr const int not_connected_rssi = -100; // -100 dBm is the minimum for wifi
bool WpaCliSetup::do_scan(const std::string& interface) {
if (!is_wifi_interface(interface)) {
return false;
}
auto output = run_application(wpa_cli, {"-i", interface, "scan"});
return output.exit_code == 0;
}
WpaCliSetup::WifiScanList WpaCliSetup::do_scan_results(const std::string& interface) {
WifiScanList result = {};
auto output = run_application(wpa_cli, {"-i", interface, "scan_results"});
if (output.exit_code == 0) {
auto scan_results = output.split_output;
if (scan_results.size() >= 2) {
// skip header
for (auto scan_results_it = std::next(scan_results.begin()); scan_results_it != scan_results.end();
++scan_results_it) {
std::vector<std::string> columns;
std::istringstream stream(*scan_results_it);
for (std::string value; std::getline(stream, value, '\t');) {
columns.push_back(std::move(value));
}
if (columns.size() >= 5) {
WifiScan info;
info.bssid = columns[0];
info.ssid = columns[4];
info.frequency = std::stoi(columns[1]);
info.signal_level = std::stoi(columns[2]);
info.flags = std::move(parse_flags(columns[3]));
result.push_back(std::move(info));
}
}
}
}
return result;
}
WpaCliSetup::Status WpaCliSetup::do_status(const std::string& interface) {
Status result = {};
if (is_wifi_interface(interface)) {
auto output = run_application(wpa_cli, {"-i", interface, "status"});
if (output.exit_code == 0) {
auto scan_results = output.split_output;
for (auto& scan_result : scan_results) {
std::vector<std::string> columns;
std::istringstream ss(scan_result);
for (std::string value; std::getline(ss, value, '=');) {
columns.push_back(std::move(value));
}
if (columns.size() == 2) {
result[columns[0]] = columns[1];
}
}
}
}
return result;
}
WpaCliSetup::Poll WpaCliSetup::do_signal_poll(const std::string& interface) {
Poll result = {};
if (is_wifi_interface(interface)) {
auto output = run_application(wpa_cli, {"-i", interface, "signal_poll"});
if (output.exit_code == 0) {
auto scan_results = output.split_output;
for (auto& scan_result : scan_results) {
std::vector<std::string> columns;
std::istringstream ss(scan_result);
for (std::string value; std::getline(ss, value, '=');) {
columns.push_back(std::move(value));
}
if (columns.size() == 2) {
result[columns[0]] = columns[1];
}
}
}
}
return result;
}
WpaCliSetup::flags_t WpaCliSetup::parse_flags(const std::string& flags) {
const std::regex flags_regex("\\[(.*?)\\]");
flags_t parsed_flags;
for (auto it = std::sregex_iterator(flags.begin(), flags.end(), flags_regex); it != std::sregex_iterator(); ++it) {
parsed_flags.push_back((*it).str(1));
}
return parsed_flags;
}
int WpaCliSetup::add_network(const std::string& interface) {
if (!is_wifi_interface(interface)) {
return -1;
}
auto output = run_application(wpa_cli, {"-i", interface, "add_network"});
if ((output.exit_code != 0) || (output.split_output.size() != 1)) {
return -1;
}
return std::stoi(output.split_output.at(0));
}
bool WpaCliSetup::set_network(const std::string& interface, int network_id, const std::string& ssid,
const std::string& psk, network_security_t mode, bool hidden) {
/*
* configuring a network needs:
* - ssid "<SSID>"
* - psk "<Passphrase>" or ABCDEF0123456789... (for WPA2)
* - sae_password "<Passphrase>" (for WPA3)
* - key_mgmt NONE (for open networks)
* - scan_ssid 1 (for hidden networks)
*
* Support for WPA3 requires:
* - key_mgmt WPA-PSK WPA-PSK-SHA256 SAE or SAE if WPA3 only
* - sae_password replaces psk, WPA3 doesn't support PreSharedKey (64 hex digits)
* the passphrase is required
* - for interworking WPA2 and WPA3 the passphrase is needed
* - psk with hex digits (PreSharedKey) doesn't work
*/
/*
* From wpa_supplicant/hostapd
* ieee80211w: Whether management frame protection (MFP) is enabled
* 0 = disabled (default)
* 1 = optional
* 2 = required
* The most common configuration options for this based on the PMF (protected
* management frames) certification program are:
* PMF enabled: ieee80211w=1 and wpa_key_mgmt=WPA-EAP WPA-EAP-SHA256
* PMF required: ieee80211w=2 and wpa_key_mgmt=WPA-EAP-SHA256
* (and similarly for WPA-PSK and WPA-PSK-SHA256 if WPA2-Personal is used)
* WPA3-Personal-only mode: ieee80211w=2 and wpa_key_mgmt=SAE
*/
constexpr std::uint8_t wpa2_psk_size = 64U;
if (!is_wifi_interface(interface)) {
return false;
}
if (psk.empty()) {
// force security mode to none
mode = network_security_t::none;
}
const char* key_mgt = nullptr;
const char* psk_name = nullptr;
const char* ieee80211w = nullptr;
switch (mode) {
case network_security_t::none:
key_mgt = "NONE";
break;
case network_security_t::wpa2_only:
key_mgt = "WPA-PSK";
psk_name = "psk";
break;
case network_security_t::wpa3_only:
key_mgt = "SAE";
psk_name = "sae_password";
ieee80211w = "2";
break;
case network_security_t::wpa2_and_wpa3:
default:
if (psk.size() == wpa2_psk_size) {
// WPA3 doesn't support PSK (hex digits), it needs a passphrase
key_mgt = "WPA-PSK";
psk_name = "psk";
} else if (psk.size() > wpa2_psk_size) {
// WPA2 doesn't support passphrases > 63 characters
key_mgt = "SAE";
psk_name = "sae_password";
ieee80211w = "2";
} else {
key_mgt = "WPA-PSK WPA-PSK-SHA256 SAE";
psk_name = "psk";
ieee80211w = "1";
}
break;
}
auto network_id_string = std::to_string(network_id);
// de-escaping SSID strings in wpa_supplicant is not reliable.
// hence providing the SSID as a string of hex digits
auto ssid_parameter = ssid_to_hex(ssid);
auto output = run_application(wpa_cli, {"-i", interface, "set_network", network_id_string, "ssid", ssid_parameter});
if ((output.exit_code == 0) && (psk_name != nullptr)) {
output = run_application(wpa_cli, {"-i", interface, "set_network", network_id_string, psk_name, psk});
}
if (output.exit_code == 0) {
output = run_application(wpa_cli, {"-i", interface, "set_network", network_id_string, "key_mgmt", key_mgt});
}
if ((output.exit_code == 0) && (ieee80211w != nullptr)) {
output =
run_application(wpa_cli, {"-i", interface, "set_network", network_id_string, "ieee80211w", ieee80211w});
}
if (hidden && (output.exit_code == 0)) {
output = run_application(wpa_cli, {"-i", interface, "set_network", network_id_string, "scan_ssid", "1"});
}
return output.exit_code == 0;
}
bool WpaCliSetup::enable_network(const std::string& interface, int network_id) {
if (!is_wifi_interface(interface)) {
return false;
}
auto network_id_string = std::to_string(network_id);
auto output = run_application(wpa_cli, {"-i", interface, "enable_network", network_id_string});
return output.exit_code == 0;
}
bool WpaCliSetup::disable_network(const std::string& interface, int network_id) {
if (!is_wifi_interface(interface)) {
return false;
}
auto network_id_string = std::to_string(network_id);
auto output = run_application(wpa_cli, {"-i", interface, "disable_network", network_id_string});
return output.exit_code == 0;
}
bool WpaCliSetup::select_network(const std::string& interface, int network_id) {
if (!is_wifi_interface(interface)) {
return false;
}
auto network_id_string = std::to_string(network_id);
auto output = run_application(wpa_cli, {"-i", interface, "select_network", network_id_string});
return output.exit_code == 0;
}
bool WpaCliSetup::remove_network(const std::string& interface, int network_id) {
if (!is_wifi_interface(interface)) {
return false;
}
auto network_id_string = std::to_string(network_id);
auto output = run_application(wpa_cli, {"-i", interface, "remove_network", network_id_string});
return output.exit_code == 0;
}
bool WpaCliSetup::save_config(const std::string& interface) {
if (!is_wifi_interface(interface)) {
return false;
}
auto output = run_application(wpa_cli, {"-i", interface, "save_config"});
return output.exit_code == 0;
}
WpaCliSetup::WifiScanList WpaCliSetup::scan_wifi(const std::string& interface) {
WifiScanList result = {};
if (do_scan(interface)) {
// FIXME: is there a proper signal to check if the scan is ready? Maybe in the socket based interface
std::this_thread::sleep_for(std::chrono::seconds(3));
result = std::move(do_scan_results(interface));
}
return result;
}
WpaCliSetup::WifiNetworkList WpaCliSetup::list_networks(const std::string& interface) {
WifiNetworkList result = {};
if (is_wifi_interface(interface)) {
auto output = run_application(wpa_cli, {"-i", interface, "list_networks"});
if (output.exit_code == 0) {
auto scan_results = output.split_output;
if (scan_results.size() >= 2) {
// skip header
for (auto scan_results_it = std::next(scan_results.begin()); scan_results_it != scan_results.end();
++scan_results_it) {
std::vector<std::string> columns;
std::istringstream ss(*scan_results_it);
for (std::string value; std::getline(ss, value, '\t');) {
columns.push_back(std::move(value));
}
if (columns.size() >= 2) {
WifiNetwork info;
info.network_id = std::stoi(columns[0]);
info.ssid = columns[1];
result.push_back(std::move(info));
}
}
}
}
}
return result;
}
WpaCliSetup::WifiNetworkStatusList WpaCliSetup::list_networks_status(const std::string& interface) {
WifiNetworkStatusList result = {};
if (is_wifi_interface(interface)) {
auto network_list = list_networks(interface);
auto status_map = do_status(interface);
int connected_rssi = not_connected_rssi;
// signal_poll raises errors when not connected
if (status_map["wpa_state"] == "COMPLETED") {
auto signal_map = do_signal_poll(interface);
if (auto it = signal_map.find("RSSI"); it != signal_map.end()) {
connected_rssi = std::stoi(it->second);
}
}
for (auto& i : network_list) {
WifiNetworkStatus net;
net.interface = interface;
net.network_id = i.network_id;
net.ssid = i.ssid;
net.connected = false;
net.signal_level = not_connected_rssi;
auto id_it = status_map.find("id");
auto ssid_it = status_map.find("ssid");
if ((id_it != status_map.end()) && (ssid_it != status_map.end()) &&
(std::stoi(id_it->second) == i.network_id) && (ssid_it->second == i.ssid)) {
net.connected = true;
net.signal_level = connected_rssi;
}
result.push_back(net);
}
}
return result;
}
bool WpaCliSetup::is_wifi_interface(const std::string& interface) {
// check if /sys/class/net/<interface>/wireless exists
auto path = std::filesystem::path("/sys/class/net");
path /= interface;
path /= "wireless";
return std::filesystem::exists(path);
}
int Ssid::standard(std::uint8_t c) {
int output{-1};
if (c == '\\') {
handler = &Ssid::backslash;
} else {
output = static_cast<int>(c);
}
return output;
}
int Ssid::backslash(std::uint8_t c) {
int output{static_cast<int>(c)};
handler = &Ssid::standard;
switch (c) {
case '"':
case '\\':
break;
case 'n':
output = '\n';
break;
case 'r':
output = '\r';
break;
case 't':
output = '\t';
break;
case 'e':
output = '\033';
break;
case 'x':
handler = &Ssid::hex_1;
output = -1;
break;
default:
// malformed
error = true;
output = -1;
break;
}
return output;
}
int Ssid::hex_1(std::uint8_t c) {
tmp = static_cast<std::uint8_t>(c);
handler = &Ssid::hex_2;
return -1;
}
int Ssid::hex_2(std::uint8_t c) {
int output{-1};
handler = &Ssid::standard;
const auto res = hex_to_int(tmp, c);
if (res != -1) {
output = res;
} else {
error = true;
// malformed
}
return output;
}
std::string Ssid::to_hex(const std::string& ssid) {
std::ostringstream ss;
handler = &Ssid::standard;
error = false;
for (const auto& c : ssid) {
const auto output = std::invoke(handler, this, static_cast<std::uint8_t>(c));
if (error) {
break;
}
if (output != -1) {
ss << std::hex << std::setw(2) << std::setfill('0') << output;
}
}
return (error) ? std::string{} : ss.str();
}
void Ssid::encode(int c, std::ostringstream& ss) {
if ((c < 0) || (c > 255)) {
error = true;
} else {
switch (c) {
case '"':
ss << R"(\")";
break;
case '\\':
ss << R"(\\)";
break;
case '\033':
ss << R"(\e)";
break;
case '\n':
ss << R"(\n)";
break;
case '\r':
ss << R"(\r)";
break;
case '\t':
ss << R"(\t)";
break;
default: {
if ((c >= 32) && (c <= 126)) {
ss << static_cast<char>(c);
} else {
std::uint8_t high{};
std::uint8_t low{};
int_to_hex(high, low, c);
ss << R"(\x)" << static_cast<char>(high) << static_cast<char>(low);
}
break;
}
}
}
}
std::string Ssid::from_hex(const std::string& hex) {
std::ostringstream ss;
bool high{true};
error = (hex.size() % 2) != 0;
for (const auto& c : hex) {
if (error) {
break;
}
if (high) {
tmp = static_cast<std::uint8_t>(c);
high = false;
} else {
const auto output = hex_to_int(tmp, static_cast<std::uint8_t>(c));
encode(output, ss);
high = true;
}
}
return (error) ? std::string{} : ss.str();
}
std::string WpaCliSetup::hex_to_ssid(const std::string& hex) {
Ssid converter;
return converter.from_hex(hex);
}
std::string WpaCliSetup::ssid_to_hex(const std::string& ssid) {
Ssid converter;
return converter.to_hex(ssid);
}
} // namespace module

View File

@@ -0,0 +1,122 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef WIFISETUP_HPP
#define WIFISETUP_HPP
#include <cstdint>
#include <map>
#include <sstream>
#include <string>
#include <vector>
/**
* SSID encoding
* From Wikipedia:
* "SSIDs can be zero to 32 octets long, and are, for convenience, usually
* in a natural language, such as English"
*
* wpa-cli escapes SSID strings in scan results; character values 32..126 are
* not converted. The following conversions are applied:
* - \" double quote
* - \\ backslash
* - \e escape (\033)
* - \n newline
* - \r return
* - \t tab
* - \xnn for other values
*
* JSON strings are UTF-8 with \ converted to \\
*/
namespace module {
class Ssid {
private:
using fn_p = int (Ssid::*)(std::uint8_t c);
fn_p handler{nullptr};
std::uint8_t tmp{0};
bool error{false};
int standard(std::uint8_t c);
int backslash(std::uint8_t c);
int hex_1(std::uint8_t c);
int hex_2(std::uint8_t c);
void encode(int c, std::ostringstream& ss);
public:
std::string to_hex(const std::string& ssid);
std::string from_hex(const std::string& hex);
};
class WpaCliSetup {
public:
using flags_t = std::vector<std::string>;
enum class network_security_t : std::uint8_t {
none,
wpa2_only,
wpa3_only,
wpa2_and_wpa3,
};
struct WifiScan {
std::string bssid;
std::string ssid;
flags_t flags;
int frequency;
int signal_level;
};
using WifiScanList = std::vector<WifiScan>;
struct WifiNetworkStatus {
std::string interface;
std::string ssid;
int network_id;
int signal_level;
bool connected;
};
using WifiNetworkStatusList = std::vector<WifiNetworkStatus>;
struct WifiNetwork {
std::string ssid;
int network_id;
};
using WifiNetworkList = std::vector<WifiNetwork>;
using Status = std::map<std::string, std::string>;
using Poll = std::map<std::string, std::string>;
protected:
virtual bool do_scan(const std::string& interface);
virtual WifiScanList do_scan_results(const std::string& interface);
virtual Status do_status(const std::string& interface);
virtual Poll do_signal_poll(const std::string& interface);
virtual flags_t parse_flags(const std::string& flags);
public:
virtual ~WpaCliSetup() = default;
virtual int add_network(const std::string& interface);
virtual bool set_network(const std::string& interface, int network_id, const std::string& ssid,
const std::string& psk, network_security_t mode, bool hidden);
virtual bool set_network(const std::string& interface, int network_id, const std::string& ssid,
const std::string& psk, bool hidden) {
return set_network(interface, network_id, ssid, psk, network_security_t::wpa2_and_wpa3, hidden);
}
virtual bool enable_network(const std::string& interface, int network_id);
virtual bool disable_network(const std::string& interface, int network_id);
virtual bool select_network(const std::string& interface, int network_id);
virtual bool remove_network(const std::string& interface, int network_id);
virtual bool save_config(const std::string& interface);
virtual WifiScanList scan_wifi(const std::string& interface);
virtual WifiNetworkList list_networks(const std::string& interface);
virtual WifiNetworkStatusList list_networks_status(const std::string& interface);
virtual bool is_wifi_interface(const std::string& interface);
static std::string hex_to_ssid(const std::string& hex);
static std::string ssid_to_hex(const std::string& ssid);
};
} // namespace module
#endif // WIFISETUP_HPP

View File

@@ -0,0 +1,44 @@
description: >-
The EVerest Setup module for setting up a LAN or WIFI network connection. This module needs privileged access and
should not run during normal operations
config:
setup_wifi:
description: Allow wifi setup
type: boolean
default: false
localization:
description: Enable localization support
type: boolean
default: false
setup_simulation:
description: Allow simulation setup
type: boolean
default: false
online_check_host:
description: Hostname or IP to use to check for internet connectivity
type: string
default: lfenergy.org
initialized_by_default:
description: Always report as if the charger was initialized
type: boolean
default: true
release_metadata_file:
description: Location of the release metadata file relative to the EVerest prefix
type: string
default: "release.json"
ap_interface:
description: Wifi interface for AP mode
type: string
default: wlan0
ap_ipv4:
description: IPv4 address of the AP
type: string
default: "192.168.1.1/24"
requires:
store:
interface: kvs
enable_external_mqtt: true
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Kai-Uwe Hermann

View File

@@ -0,0 +1,19 @@
set(TEST_TARGET_NAME ${PROJECT_NAME}_setup_tests)
add_executable(${TEST_TARGET_NAME})
target_include_directories(${TEST_TARGET_NAME} PUBLIC ${GTEST_INCLUDE_DIRS} . ..)
target_sources(${TEST_TARGET_NAME} PRIVATE
RunApplicationStub.cpp
WiFiSetupTest.cpp
../WiFiSetup.cpp
)
target_link_libraries(${TEST_TARGET_NAME} PRIVATE
GTest::gtest_main
nlohmann_json::nlohmann_json
everest::run_application
)
add_test(${TEST_TARGET_NAME} ${TEST_TARGET_NAME})
ev_register_test_target(${TEST_TARGET_NAME})

View File

@@ -0,0 +1,108 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "RunApplicationStub.hpp"
#include <gtest/gtest.h>
#include <utility>
using namespace everest::run_application;
namespace stub {
RunApplication* RunApplication::active_p = nullptr;
RunApplication::RunApplication() :
results({
{"add_network", {{}, {{"0"}}, 0}},
{"set_network", {{}, {{"OK"}}, 0}},
{"enable_network", {{}, {{"OK"}}, 0}},
{"disable_network", {{}, {{"OK"}}, 0}},
{"select_network", {{}, {{"OK"}}, 0}},
{"remove_network", {{}, {{"OK"}}, 0}},
{"save_config", {{}, {{"OK"}}, 0}},
// scan_wifi uses scan and scan_results
{"scan", {{}, {{"OK"}}, 0}},
{"scan_results",
{{},
{
{"bssid / frequency / signal level / flags / ssid"},
},
0}},
{"list_networks",
{{},
{
{"network id / ssid / bssid / flags"},
},
0}},
// list_networks_status uses list_networks status signal_poll
{"status",
{{},
{
{"wpa_state=INACTIVE"},
{"p2p_device_address=c2:ee:40:b0:57:b8"},
{"address=c0:ee:40:b0:57:b8"},
{"uuid=7dd9abf8-53f0-532b-a763-2f43537e4234"},
},
0}},
{"signal_poll", {{}, {{"FAIL"}}, 0}},
}),
signal_poll_called(false),
psk_called(false),
sae_password_called(false),
key_mgmt_called(false),
scan_ssid_called(false),
ieee80211w_called(false),
key_mgmt_value(),
ieee80211w_value() {
active_p = this;
}
RunApplication::~RunApplication() {
active_p = nullptr;
}
CmdOutput RunApplication::run_application(const std::string& name, std::vector<std::string> args) {
CmdOutput result = {{}, {}, -1};
EXPECT_EQ(name, "/usr/sbin/wpa_cli");
EXPECT_EQ(args[0], "-i");
if (args[2] == "signal_poll") {
signal_poll_called = true;
} else if (args[2] == "set_network") {
if (args[4] == "psk") {
psk_called = true;
} else if (args[4] == "sae_password") {
sae_password_called = true;
} else if (args[4] == "key_mgmt") {
key_mgmt_called = true;
key_mgmt_value = args[5];
} else if (args[4] == "ieee80211w") {
ieee80211w_called = true;
ieee80211w_value = args[5];
} else if (args[4] == "scan_ssid") {
scan_ssid_called = true;
}
}
auto it = results.find(args[2]);
if (it != results.end()) {
result = it->second;
if (!result.split_output.empty() && result.output.empty()) {
for (auto& line : result.output) {
result.output += line + "\n";
}
}
}
return result;
}
} // namespace stub
namespace everest::run_application {
CmdOutput run_application(const std::string& name, std::vector<std::string> args,
const std::function<CmdControl(const std::string& output_line)> output_callback) {
CmdOutput result = {{}, {}, -1};
if (stub::RunApplication::active_p != nullptr) {
result = std::move(stub::RunApplication::active_p->run_application(name, args));
}
return result;
}
} // namespace everest::run_application

View File

@@ -0,0 +1,39 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef RUNAPPLICATIONSTUB_HPP
#define RUNAPPLICATIONSTUB_HPP
#include <everest/run_application/run_application.hpp>
#include <map>
#include <string>
namespace stub {
class RunApplication {
public:
static RunApplication* active_p;
std::map<std::string, everest::run_application::CmdOutput> results;
bool signal_poll_called;
bool psk_called;
bool sae_password_called;
bool key_mgmt_called;
bool scan_ssid_called;
bool ieee80211w_called;
std::string key_mgmt_value;
std::string ieee80211w_value;
RunApplication();
virtual ~RunApplication();
virtual everest::run_application::CmdOutput run_application(const std::string& name, std::vector<std::string> args);
};
} // namespace stub
namespace everest::run_application {
CmdOutput run_application(const std::string& name, std::vector<std::string> args,
const std::function<CmdControl(const std::string& output_line)> output_callback);
}
#endif // RUNAPPLICATIONSTUB_HPP

View File

@@ -0,0 +1,726 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "RunApplicationStub.hpp"
#include <WiFiSetup.hpp>
#include <gtest/gtest.h>
#include <nlohmann/json.hpp>
#include <string>
namespace {
using namespace module;
using nlohmann::json;
constexpr const char* example_psk = "e3003974af901976485f3e655b455791dcc20a5380f42a7839de3bfdc9d70d71";
constexpr const char* example_password = "LetMeIn2";
constexpr const char* example_long_password = "e3003974af901976485f3e655b455791dcc20a5380f42a7839de3bfdc9d70d71X";
class WpaCliSetupTest : public WpaCliSetup {
public:
// override to support testing
virtual bool is_wifi_interface(const std::string& interface) override {
if (interface == "ap0") {
return false;
} else if (interface == "eth0") {
return false;
}
return true;
};
};
struct WifiCredentials {
std::string interface;
std::string ssid;
std::string psk;
bool hidden;
operator std::string() {
json wifi_credentials = *this;
return wifi_credentials.dump();
}
};
void to_json(json& j, const WifiCredentials& k) {
j = json::object({{"interface", k.interface}, {"ssid", k.ssid}, {"psk", k.psk}, {"hidden", k.hidden}});
}
void from_json(const json& j, WifiCredentials& k) {
k.interface = j.at("interface");
k.ssid = j.at("ssid");
k.psk = j.at("psk");
k.hidden = false;
// optional item
auto it = j.find("hidden");
if ((it != j.end() && *it)) {
k.hidden = true;
}
}
//-----------------------------------------------------------------------------
// SSID conversions
TEST(Ssid, toHex) {
Ssid ssid;
EXPECT_EQ(ssid.to_hex("0123456789"), "30313233343536373839");
EXPECT_EQ(ssid.to_hex("abcdef"), "616263646566");
EXPECT_EQ(ssid.to_hex("ABCDEF"), "414243444546");
EXPECT_EQ(ssid.to_hex(R"(\" \\ \e\n\r\t)"), "22205c201b0a0d09");
EXPECT_EQ(ssid.to_hex(R"(\x00\x01\xfd)"), "0001fd");
}
TEST(Ssid, fromHex) {
Ssid ssid;
EXPECT_EQ(ssid.from_hex("30313233343536373839"), "0123456789");
EXPECT_EQ(ssid.from_hex("616263646566"), "abcdef");
EXPECT_EQ(ssid.from_hex("414243444546"), "ABCDEF");
EXPECT_EQ(ssid.from_hex("22205c201b0a0d09"), R"(\" \\ \e\n\r\t)");
EXPECT_EQ(ssid.from_hex("0001fd"), R"(\x00\x01\xfd)");
}
TEST(Ssid, complete) {
Ssid ssid;
std::stringstream ss;
for (std::uint16_t i = 0; i < 256; i++) {
ss << std::hex << std::setw(2) << std::setfill('0') << i;
}
const std::string values_str(ss.str());
const auto result_str = ssid.from_hex(values_str);
const auto result_hex = ssid.to_hex(result_str);
// std::cout << result_str << std::endl;
// std::cout << result_hex << std::endl;
EXPECT_EQ(values_str, result_hex);
}
TEST(Ssid, unusual) {
Ssid ssid;
// allowing upper case hex digits
EXPECT_EQ(ssid.from_hex("E0E1E2E3E4E5E6E7E8E9EaEbEcEdEeEfEAEBECEDEEEF"),
R"(\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xea\xeb\xec\xed\xee\xef)");
EXPECT_EQ(
ssid.to_hex(R"(\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xEA\xEB\xEC\xED\xEE\xEF)"),
"e0e1e2e3e4e5e6e7e8e9eaebecedeeefeaebecedeeef");
// parsing errors
EXPECT_EQ(ssid.to_hex(R"(123\?456)"), "");
EXPECT_EQ(ssid.to_hex(R"(123\?)"), "");
EXPECT_EQ(ssid.to_hex(R"(\?456)"), "");
EXPECT_EQ(ssid.to_hex(R"(\034)"), "");
EXPECT_EQ(ssid.to_hex(R"(\03)"), "");
EXPECT_EQ(ssid.to_hex(R"(\0)"), "");
EXPECT_EQ(ssid.to_hex(R"(\x)"), "");
EXPECT_EQ(ssid.to_hex(R"(\xz)"), "");
EXPECT_EQ(ssid.to_hex(R"(\xaz)"), "");
EXPECT_EQ(ssid.from_hex(R"(G123)"), "");
EXPECT_EQ(ssid.from_hex(R"(12G3)"), "");
EXPECT_EQ(ssid.from_hex(R"(123G)"), "");
EXPECT_EQ(ssid.from_hex(R"(1234568)"), "");
}
TEST(Ssid, json) {
/*
* worked example
* ssid=PP€-310034
* scan_results
* bssid / frequency / signal level / flags / ssid
* c2:ee:40:10:57:b8 2417 -45 [WPA2-PSK-CCMP][ESS] PP\xe2\x82\xac-310034
*
* MQTT
* everest_api/setup/var/wifi_info: [
* {"bssid":"c2:ee:40:10:57:b8","flags":["WPA2-PSK-CCMP","ESS"],"frequency":2417,"signal_level":-46,"ssid":"PP\\xe2\\x82\\xac-310034"}]
*/
Ssid conv;
std::string ssid_hex{"535349443de282ac3132"};
std::string ssid = conv.from_hex(ssid_hex);
WifiCredentials wifi = {"eth0", ssid, "\"psk\"", false};
nlohmann::json j;
to_json(j, wifi);
const auto transmitted = j.dump();
WifiCredentials received = json::parse(transmitted);
EXPECT_EQ(received.ssid, ssid);
const auto rec_ssid_hex = conv.to_hex(received.ssid);
EXPECT_EQ(rec_ssid_hex, ssid_hex);
}
//-----------------------------------------------------------------------------
// add_network()
TEST(add_network, wired) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_EQ(obj.add_network("eth0"), -1);
}
TEST(add_network, wireless) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_NE(obj.add_network("wlan0"), -1);
}
TEST(add_network, access_point) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_EQ(obj.add_network("ap0"), -1);
}
//-----------------------------------------------------------------------------
// set_network()
TEST(set_network, none_no_psk) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "PlusnetWireless", "", WpaCliSetup::network_security_t::none, false));
ASSERT_FALSE(ra.psk_called);
ASSERT_FALSE(ra.sae_password_called);
ASSERT_FALSE(ra.ieee80211w_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "NONE");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, none_psk) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(
obj.set_network("wlan0", 0, "PlusnetWireless", example_psk, WpaCliSetup::network_security_t::none, false));
ASSERT_FALSE(ra.psk_called);
ASSERT_FALSE(ra.sae_password_called);
ASSERT_FALSE(ra.ieee80211w_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "NONE");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, none_password) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(
obj.set_network("wlan0", 0, "PlusnetWireless", example_password, WpaCliSetup::network_security_t::none, false));
ASSERT_FALSE(ra.psk_called);
ASSERT_FALSE(ra.sae_password_called);
ASSERT_FALSE(ra.ieee80211w_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "NONE");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa2_no_psk) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "PlusnetWireless", "", WpaCliSetup::network_security_t::wpa2_only, false));
ASSERT_FALSE(ra.psk_called);
ASSERT_FALSE(ra.sae_password_called);
ASSERT_FALSE(ra.ieee80211w_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "NONE");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa2_psk) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(
obj.set_network("wlan0", 0, "PlusnetWireless", example_psk, WpaCliSetup::network_security_t::wpa2_only, false));
ASSERT_TRUE(ra.psk_called);
ASSERT_FALSE(ra.sae_password_called);
ASSERT_FALSE(ra.ieee80211w_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "WPA-PSK");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa2_password) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "PlusnetWireless", example_password,
WpaCliSetup::network_security_t::wpa2_only, false));
ASSERT_TRUE(ra.psk_called);
ASSERT_FALSE(ra.sae_password_called);
ASSERT_FALSE(ra.ieee80211w_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "WPA-PSK");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa3_no_psk) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "PlusnetWireless", "", WpaCliSetup::network_security_t::wpa3_only, false));
ASSERT_FALSE(ra.psk_called);
ASSERT_FALSE(ra.sae_password_called);
ASSERT_FALSE(ra.ieee80211w_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "NONE");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa3_psk) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(
obj.set_network("wlan0", 0, "PlusnetWireless", example_psk, WpaCliSetup::network_security_t::wpa3_only, false));
ASSERT_FALSE(ra.psk_called);
ASSERT_TRUE(ra.sae_password_called);
ASSERT_TRUE(ra.ieee80211w_called);
ASSERT_EQ(ra.ieee80211w_value, "2");
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "SAE");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa3_password) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "PlusnetWireless", example_password,
WpaCliSetup::network_security_t::wpa3_only, false));
ASSERT_FALSE(ra.psk_called);
ASSERT_TRUE(ra.sae_password_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_TRUE(ra.ieee80211w_called);
ASSERT_EQ(ra.ieee80211w_value, "2");
ASSERT_EQ(ra.key_mgmt_value, "SAE");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa2_and_wpa3_no_psk) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(
obj.set_network("wlan0", 0, "PlusnetWireless", "", WpaCliSetup::network_security_t::wpa2_and_wpa3, false));
ASSERT_FALSE(ra.psk_called);
ASSERT_FALSE(ra.sae_password_called);
ASSERT_FALSE(ra.ieee80211w_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "NONE");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa2_and_wpa3_psk) {
// with a PSK result is same as wpa2_only
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "PlusnetWireless", example_psk,
WpaCliSetup::network_security_t::wpa2_and_wpa3, false));
ASSERT_TRUE(ra.psk_called);
ASSERT_FALSE(ra.sae_password_called);
ASSERT_FALSE(ra.ieee80211w_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "WPA-PSK");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa2_and_wpa3_password) {
// configure for both
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "PlusnetWireless", example_password,
WpaCliSetup::network_security_t::wpa2_and_wpa3, false));
ASSERT_TRUE(ra.psk_called);
ASSERT_FALSE(ra.sae_password_called);
ASSERT_TRUE(ra.ieee80211w_called);
ASSERT_EQ(ra.ieee80211w_value, "1");
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "WPA-PSK WPA-PSK-SHA256 SAE");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa2_and_wpa3_password_long) {
// configure for WPA3 only
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "PlusnetWireless", example_long_password,
WpaCliSetup::network_security_t::wpa2_and_wpa3, false));
ASSERT_FALSE(ra.psk_called);
ASSERT_TRUE(ra.sae_password_called);
ASSERT_TRUE(ra.ieee80211w_called);
ASSERT_EQ(ra.ieee80211w_value, "2");
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_EQ(ra.key_mgmt_value, "SAE");
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, wpa2) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "PlusnetWireless", "LetMeIn2", false));
ASSERT_TRUE(ra.psk_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, open) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "OpenNet", "", false));
ASSERT_FALSE(ra.psk_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_FALSE(ra.scan_ssid_called);
}
TEST(set_network, hidden_wpa2) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "Hidden", "LetMeIn3", true));
ASSERT_TRUE(ra.psk_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_TRUE(ra.scan_ssid_called);
}
TEST(set_network, hidden_open) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.set_network("wlan0", 0, "Hidden", "", true));
ASSERT_FALSE(ra.psk_called);
ASSERT_TRUE(ra.key_mgmt_called);
ASSERT_TRUE(ra.scan_ssid_called);
}
//-----------------------------------------------------------------------------
// enable_network()
TEST(enable_network, exists) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.enable_network("wlan0", 0));
}
TEST(enable_network, doesnt_exist) {
stub::RunApplication ra;
ra.results["enable_network"] = {{}, {{"FAIL"}}, 0};
WpaCliSetupTest obj;
// still returns an exit code of 0
ASSERT_TRUE(obj.enable_network("wlan0", 1));
}
//-----------------------------------------------------------------------------
// disable_network()
TEST(disable_network, exists) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.disable_network("wlan0", 0));
}
TEST(disable_network, doesnt_exist) {
stub::RunApplication ra;
ra.results["disable_network"] = {{}, {{"FAIL"}}, 0};
WpaCliSetupTest obj;
// still returns an exit code of 0
ASSERT_TRUE(obj.disable_network("wlan0", 1));
}
//-----------------------------------------------------------------------------
// select_network()
TEST(select_network, exists) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.select_network("wlan0", 0));
}
TEST(select_network, doesnt_exist) {
stub::RunApplication ra;
ra.results["select_network"] = {{}, {{"FAIL"}}, 0};
WpaCliSetupTest obj;
// still returns an exit code of 0
ASSERT_TRUE(obj.select_network("wlan0", 1));
}
//-----------------------------------------------------------------------------
// remove_network()
TEST(remove_network, exists) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.remove_network("wlan0", 0));
}
TEST(remove_network, doesnt_exist) {
stub::RunApplication ra;
ra.results["remove_network"] = {{}, {{"FAIL"}}, 0};
WpaCliSetupTest obj;
// still returns an exit code of 0
ASSERT_TRUE(obj.remove_network("wlan0", 1));
}
TEST(remove_network, fail) {
stub::RunApplication ra;
ra.results["remove_network"] = {{}, {{"Invalid REMOVE_NETWORK command - at least 1 argument is required."}}, 255};
WpaCliSetupTest obj;
ASSERT_FALSE(obj.remove_network("wlan0", -99));
}
//-----------------------------------------------------------------------------
// save_config()
TEST(save_config, success) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_TRUE(obj.save_config("wlan0"));
}
TEST(save_config, fail) {
stub::RunApplication ra;
WpaCliSetupTest obj;
ASSERT_FALSE(obj.save_config("ap0"));
}
//-----------------------------------------------------------------------------
// scan_wifi()
TEST(scan_wifi, none) {
stub::RunApplication ra;
WpaCliSetupTest obj;
auto res = obj.scan_wifi("wlan0");
ASSERT_TRUE(res.empty());
}
TEST(scan_wifi, some) {
stub::RunApplication ra;
ra.results["scan_results"] = {
{},
{
{"bssid / frequency / signal level / flags / ssid"},
{"14:49:bc:06:81:19\t2412\t-72\t[WPA2-PSK-CCMP][ESS]\tPlusnetWireless"},
{"6a:82:8c:38:b2:a1\t2412\t-93\t[WPA2-PSK-CCMP][ESS]\t\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00"},
},
0};
WpaCliSetupTest obj;
auto res = obj.scan_wifi("wlan0");
ASSERT_FALSE(res.empty());
ASSERT_EQ(res.size(), 2);
module::WpaCliSetup::flags_t expected = {"WPA2-PSK-CCMP", "ESS"};
EXPECT_EQ(res[0].bssid, "14:49:bc:06:81:19");
EXPECT_EQ(res[0].ssid, "PlusnetWireless");
EXPECT_EQ(res[0].frequency, 2412);
EXPECT_EQ(res[0].signal_level, -72);
EXPECT_EQ(res[0].flags, expected);
EXPECT_EQ(res[1].bssid, "6a:82:8c:38:b2:a1");
EXPECT_EQ(res[1].ssid, "\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00");
EXPECT_EQ(res[1].frequency, 2412);
EXPECT_EQ(res[1].signal_level, -93);
EXPECT_EQ(res[1].flags, expected);
}
TEST(scan_wifi, more) {
stub::RunApplication ra;
ra.results["scan_results"] = {
{},
{
{"bssid / frequency / signal level / flags / ssid"},
{"14:49:bc:06:81:19\t2412\t-71\t[WPA2-PSK-CCMP][ESS]\tPlusnetWireless"},
{"14:49:bc:06:81:1b\t2412\t-71\t[WPA2-PSK-CCMP][ESS]\t\\x00\\x00\\x00\\x00\\x00\\x00\\x00"},
{"00:1e:42:33:62:07\t2462\t-89\t[WPA2-PSK-CCMP+TKIP][ESS][UTF-8]\tRUT950_6207"},
{"b4:ba:9d:16:e2:ba\t2437\t-92\t[WPA2-PSK-CCMP][WPS][ESS]\tSKYLZMEY"},
{"14:49:bc:06:81:1c\t2412\t-72\t[ESS]\tTesting123"},
{"36:49:5b:f8:e1:07\t2412\t-92\t[ESS]\tEE WiFi"},
{"14:49:bc:06:81:18\t2412\t-73\t[WPA2-PSK-CCMP][ESS]\t\\x00\\x00\\x00\\x00\\x00\\x00\\x00"},
{"6a:82:8c:38:b2:a1\t2412\t-88\t[WPA2-PSK-CCMP][ESS]\t\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00"},
{"18:82:8c:38:b2:a5\t2412\t-92\t[WPA2-PSK-CCMP][WPS][ESS]\tBT-3GAG3M"},
{"6a:82:8c:38:b2:a6\t2412\t-92\t[ESS]\tEE WiFi"},
},
0};
WpaCliSetupTest obj;
auto res = obj.scan_wifi("wlan0");
ASSERT_FALSE(res.empty());
ASSERT_EQ(res.size(), 10);
module::WpaCliSetup::flags_t expected1 = {"WPA2-PSK-CCMP", "ESS"};
module::WpaCliSetup::flags_t expected2 = {"WPA2-PSK-CCMP+TKIP", "ESS", "UTF-8"};
module::WpaCliSetup::flags_t expected3 = {"WPA2-PSK-CCMP", "WPS", "ESS"};
module::WpaCliSetup::flags_t expected4 = {"ESS"};
EXPECT_EQ(res[0].bssid, "14:49:bc:06:81:19");
EXPECT_EQ(res[0].ssid, "PlusnetWireless");
EXPECT_EQ(res[0].frequency, 2412);
EXPECT_EQ(res[0].signal_level, -71);
EXPECT_EQ(res[0].flags, expected1);
EXPECT_EQ(res[1].bssid, "14:49:bc:06:81:1b");
EXPECT_EQ(res[1].ssid, "\\x00\\x00\\x00\\x00\\x00\\x00\\x00");
EXPECT_EQ(res[1].frequency, 2412);
EXPECT_EQ(res[1].signal_level, -71);
EXPECT_EQ(res[1].flags, expected1);
EXPECT_EQ(res[2].bssid, "00:1e:42:33:62:07");
EXPECT_EQ(res[2].ssid, "RUT950_6207");
EXPECT_EQ(res[2].frequency, 2462);
EXPECT_EQ(res[2].signal_level, -89);
EXPECT_EQ(res[2].flags, expected2);
EXPECT_EQ(res[3].bssid, "b4:ba:9d:16:e2:ba");
EXPECT_EQ(res[3].ssid, "SKYLZMEY");
EXPECT_EQ(res[3].frequency, 2437);
EXPECT_EQ(res[3].signal_level, -92);
EXPECT_EQ(res[3].flags, expected3);
EXPECT_EQ(res[4].bssid, "14:49:bc:06:81:1c");
EXPECT_EQ(res[4].ssid, "Testing123");
EXPECT_EQ(res[4].frequency, 2412);
EXPECT_EQ(res[4].signal_level, -72);
EXPECT_EQ(res[4].flags, expected4);
EXPECT_EQ(res[5].bssid, "36:49:5b:f8:e1:07");
EXPECT_EQ(res[5].ssid, "EE WiFi");
EXPECT_EQ(res[5].frequency, 2412);
EXPECT_EQ(res[5].signal_level, -92);
EXPECT_EQ(res[5].flags, expected4);
EXPECT_EQ(res[6].bssid, "14:49:bc:06:81:18");
EXPECT_EQ(res[6].ssid, "\\x00\\x00\\x00\\x00\\x00\\x00\\x00");
EXPECT_EQ(res[6].frequency, 2412);
EXPECT_EQ(res[6].signal_level, -73);
EXPECT_EQ(res[6].flags, expected1);
EXPECT_EQ(res[7].bssid, "6a:82:8c:38:b2:a1");
EXPECT_EQ(res[7].ssid, "\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00");
EXPECT_EQ(res[7].frequency, 2412);
EXPECT_EQ(res[7].signal_level, -88);
EXPECT_EQ(res[7].flags, expected1);
EXPECT_EQ(res[8].bssid, "18:82:8c:38:b2:a5");
EXPECT_EQ(res[8].ssid, "BT-3GAG3M");
EXPECT_EQ(res[8].frequency, 2412);
EXPECT_EQ(res[8].signal_level, -92);
EXPECT_EQ(res[8].flags, expected3);
EXPECT_EQ(res[9].bssid, "6a:82:8c:38:b2:a6");
EXPECT_EQ(res[9].ssid, "EE WiFi");
EXPECT_EQ(res[9].frequency, 2412);
EXPECT_EQ(res[9].signal_level, -92);
EXPECT_EQ(res[9].flags, expected4);
}
//-----------------------------------------------------------------------------
// list_networks()
TEST(list_networks, none) {
stub::RunApplication ra;
WpaCliSetupTest obj;
auto res = obj.list_networks("wlan0");
ASSERT_TRUE(res.empty());
}
TEST(list_networks, one) {
stub::RunApplication ra;
ra.results["list_networks"] = {{},
{
{"network id / ssid / bssid / flags"},
{"0\t\tany\t[DISABLED]"},
},
0};
WpaCliSetupTest obj;
auto res = obj.list_networks("wlan0");
ASSERT_FALSE(res.empty());
ASSERT_EQ(res.size(), 1);
module::WpaCliSetup::flags_t expected = {"WPA2-PSK-CCMP", "ESS"};
EXPECT_EQ(res[0].network_id, 0);
EXPECT_EQ(res[0].ssid, "");
}
TEST(list_networks, two) {
stub::RunApplication ra;
ra.results["list_networks"] = {{},
{
{"network id / ssid / bssid / flags"},
{"0\t\tany\t[DISABLED]"},
{"1\tPlusnetWireless\tany\t[CURRENT]"},
},
0};
WpaCliSetupTest obj;
auto res = obj.list_networks("wlan0");
ASSERT_FALSE(res.empty());
ASSERT_EQ(res.size(), 2);
module::WpaCliSetup::flags_t expected = {"WPA2-PSK-CCMP", "ESS"};
EXPECT_EQ(res[0].network_id, 0);
EXPECT_EQ(res[0].ssid, "");
EXPECT_EQ(res[1].network_id, 1);
EXPECT_EQ(res[1].ssid, "PlusnetWireless");
}
//-----------------------------------------------------------------------------
// list_networks_status()
TEST(list_networks, not_connected) {
stub::RunApplication ra;
WpaCliSetupTest obj;
auto res = obj.list_networks_status("wlan0");
ASSERT_TRUE(res.empty());
ASSERT_FALSE(ra.signal_poll_called);
}
TEST(list_networks, connected) {
stub::RunApplication ra;
ra.results["list_networks"] = {{},
{
{"network id / ssid / bssid / flags"},
{"0\t\tany\t[DISABLED]"},
{"1\tPlusnetWireless\tany\t[CURRENT]"},
{"2\tHiddenNet\tany\t[DISABLED]"},
},
0};
ra.results["status"] = {{},
{
{"bssid=14:49:bc:06:81:19"},
{"freq=2412"},
{"ssid=PlusnetWireless"},
{"id=1"},
{"mode=station"},
{"wifi_generation=4"},
{"pairwise_cipher=CCMP"},
{"group_cipher=CCMP"},
{"key_mgmt=WPA2-PSK"},
{"wpa_state=COMPLETED"},
{"ip_address=172.25.1.11"},
{"p2p_device_address=c2:ee:40:b0:57:b8"},
{"address=c0:ee:40:b0:57:b8"},
{"uuid=7dd9abf8-53f0-532b-a763-2f43537e4234"},
},
0};
ra.results["signal_poll"] = {{},
{
{"RSSI=-73"},
{"LINKSPEED=54"},
{"NOISE=9999"},
{"FREQUENCY=2412"},
{"WIDTH=20 MHz"},
{"CENTER_FRQ1=2412"},
},
0};
WpaCliSetupTest obj;
auto res = obj.list_networks_status("wlan0");
ASSERT_FALSE(res.empty());
ASSERT_TRUE(ra.signal_poll_called);
ASSERT_EQ(res.size(), 3);
EXPECT_EQ(res[0].interface, "wlan0");
EXPECT_EQ(res[0].network_id, 0);
EXPECT_EQ(res[0].ssid, "");
EXPECT_FALSE(res[0].connected);
EXPECT_EQ(res[0].signal_level, -100);
EXPECT_EQ(res[1].interface, "wlan0");
EXPECT_EQ(res[1].network_id, 1);
EXPECT_EQ(res[1].ssid, "PlusnetWireless");
EXPECT_TRUE(res[1].connected);
EXPECT_EQ(res[1].signal_level, -73);
EXPECT_EQ(res[2].interface, "wlan0");
EXPECT_EQ(res[2].network_id, 2);
EXPECT_EQ(res[2].ssid, "HiddenNet");
EXPECT_FALSE(res[2].connected);
EXPECT_EQ(res[2].signal_level, -100);
}
//-----------------------------------------------------------------------------
// is_wifi_interface()
// not tested as it is checking for files in /proc so depends on the
// machine the tests are running on
} // namespace

View File

@@ -0,0 +1,21 @@
#
# AUTO GENERATED - MARKED REGIONS WILL BE KEPT
# template version 3
#
# module setup:
# - ${MODULE_NAME}: module name
ev_setup_cpp_module()
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
# insert your custom targets and additional config variables here
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
target_sources(${MODULE_NAME}
PRIVATE
"main/kvsImpl.cpp"
)
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
# insert other things like install cmds etc here
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1

View File

@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest
#include "Store.hpp"
namespace module {
void Store::init() {
invoke_init(*p_main);
}
void Store::ready() {
invoke_ready(*p_main);
}
} // namespace module

View File

@@ -0,0 +1,58 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef STORE_HPP
#define STORE_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 2
//
#include "ld-ev.hpp"
// headers for provided interface implementations
#include <generated/interfaces/kvs/Implementation.hpp>
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
// insert your custom include headers here
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
namespace module {
struct Conf {};
class Store : public Everest::ModuleBase {
public:
Store() = delete;
Store(const ModuleInfo& info, std::unique_ptr<kvsImplBase> p_main, Conf& config) :
ModuleBase(info), p_main(std::move(p_main)), config(config){};
const std::unique_ptr<kvsImplBase> p_main;
const Conf& config;
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
// insert your public definitions here
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
protected:
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
// insert your protected definitions here
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
private:
friend class LdEverest;
void init();
void ready();
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
// insert your private definitions here
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
};
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
// insert other definitions here
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
} // namespace module
#endif // STORE_HPP

View File

@@ -0,0 +1,32 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest
#include "kvsImpl.hpp"
namespace module {
namespace main {
void kvsImpl::init() {
}
void kvsImpl::ready() {
}
void kvsImpl::handle_store(std::string& key,
std::variant<std::nullptr_t, Array, Object, bool, double, int, std::string>& value) {
kvs[key] = value;
};
std::variant<std::nullptr_t, Array, Object, bool, double, int, std::string> kvsImpl::handle_load(std::string& key) {
return kvs[key];
};
void kvsImpl::handle_delete(std::string& key) {
kvs.erase(key);
};
bool kvsImpl::handle_exists(std::string& key) {
return kvs.count(key) != 0;
};
} // namespace main
} // namespace module

View File

@@ -0,0 +1,67 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef MAIN_KVS_IMPL_HPP
#define MAIN_KVS_IMPL_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 3
//
#include <generated/interfaces/kvs/Implementation.hpp>
#include "../Store.hpp"
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
#include <map>
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
namespace module {
namespace main {
struct Conf {};
class kvsImpl : public kvsImplBase {
public:
kvsImpl() = delete;
kvsImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<Store>& mod, Conf& config) :
kvsImplBase(ev, "main"), mod(mod), config(config){};
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
// insert your public definitions here
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
protected:
// command handler functions (virtual)
virtual void
handle_store(std::string& key,
std::variant<std::nullptr_t, Array, Object, bool, double, int, std::string>& value) override;
virtual std::variant<std::nullptr_t, Array, Object, bool, double, int, std::string>
handle_load(std::string& key) override;
virtual void handle_delete(std::string& key) override;
virtual bool handle_exists(std::string& key) override;
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
// insert your protected definitions here
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
private:
const Everest::PtrContainer<Store>& mod;
const Conf& config;
virtual void init() override;
virtual void ready() override;
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
std::map<std::string, std::variant<std::nullptr_t, Array, Object, bool, double, int, std::string>> kvs{};
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
};
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
// insert other definitions here
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
} // namespace main
} // namespace module
#endif // MAIN_KVS_IMPL_HPP

View File

@@ -0,0 +1,10 @@
description: Simple implementation of a memory-backed key-value store
provides:
main:
interface: kvs
description: This implements a key-value store
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Kai-Uwe Hermann
- Thilo Molitor

View File

@@ -0,0 +1,37 @@
load("//modules:module.bzl", "cc_everest_module")
IMPLS = [
"main",
]
cc_everest_module(
name = "System",
impls = IMPLS,
deps = [
"//lib:run_application",
"//lib/everest/timer:libtimer",
"@com_github_HowardHinnant_date//:date",
],
)
# Install script files alongside the binary
genrule(
name = "system_scripts",
srcs = [
"constants.env",
"diagnostics_uploader.sh",
"firmware_updater.sh",
"signed_firmware_downloader.sh",
"signed_firmware_installer.sh",
],
outs = [
"System/constants.env",
"System/diagnostics_uploader.sh",
"System/firmware_updater.sh",
"System/signed_firmware_downloader.sh",
"System/signed_firmware_installer.sh",
],
cmd = "mkdir -p $(RULEDIR)/System && " +
"cp $(SRCS) $(RULEDIR)/System/",
visibility = ["//visibility:public"],
)

View File

@@ -0,0 +1,39 @@
#
# AUTO GENERATED - MARKED REGIONS WILL BE KEPT
# template version 3
#
# module setup:
# - ${MODULE_NAME}: module name
ev_setup_cpp_module()
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
# insert your custom targets and additional config variables here
target_link_libraries(${MODULE_NAME}
PRIVATE
date::date
date::date-tz
everest::timer
everest::run_application
)
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
target_sources(${MODULE_NAME}
PRIVATE
"main/systemImpl.cpp"
)
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
# insert other things like install cmds etc here
target_compile_features(${MODULE_NAME} PRIVATE cxx_std_17)
install(
PROGRAMS
constants.env
diagnostics_uploader.sh
firmware_updater.sh
signed_firmware_downloader.sh
signed_firmware_installer.sh
DESTINATION "${EVEREST_MODULE_INSTALL_PREFIX}/${MODULE_NAME}"
)
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1

View File

@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "System.hpp"
namespace module {
void System::init() {
invoke_init(*p_main);
}
void System::ready() {
invoke_ready(*p_main);
}
} // namespace module

View File

@@ -0,0 +1,67 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef SYSTEM_HPP
#define SYSTEM_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 2
//
#include "ld-ev.hpp"
// headers for provided interface implementations
#include <generated/interfaces/system/Implementation.hpp>
// headers for required interface implementations
#include <generated/interfaces/kvs/Interface.hpp>
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
// insert your custom include headers here
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
namespace module {
struct Conf {
double DefaultRetries;
double DefaultRetryInterval;
int ResetDelay;
};
class System : public Everest::ModuleBase {
public:
System() = delete;
System(const ModuleInfo& info, std::unique_ptr<systemImplBase> p_main,
std::vector<std::unique_ptr<kvsIntf>> r_store, Conf& config) :
ModuleBase(info), p_main(std::move(p_main)), r_store(std::move(r_store)), config(config){};
const std::unique_ptr<systemImplBase> p_main;
const std::vector<std::unique_ptr<kvsIntf>> r_store;
const Conf& config;
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
// insert your public definitions here
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
protected:
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
// insert your protected definitions here
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
private:
friend class LdEverest;
void init();
void ready();
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
// insert your private definitions here
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
};
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
// insert other definitions here
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
} // namespace module
#endif // SYSTEM_HPP

View File

@@ -0,0 +1,27 @@
CONNECTION_TIMEOUT=20
DOWNLOADED="Downloaded"
DOWNLOADING="Downloading"
DOWNLOAD_FAILED="DownloadFailed"
DOWNLOAD_SCHEDULED="DownloadScheduled"
DOWNLOAD_PAUSED="DownloadPaused"
IDLE="Idle"
INSTALLATION_FAILED="InstallationFailed"
INSTALLING="Installing"
INSTALLED="Installed"
INSTALL_REBOOTING="InstallRebooting"
INSTALL_SCHEDULED="InstallScheduled"
INSTALL_VERIFICATION_FAILED="InstallVerificationFailed"
PERMISSION_DENIED="PermissionDenied"
NOT_SUPPORTED_OPERATION="NotSupportedOperation"
BAD_MESSAGE="BadMessage"
INVALID_SIGNATURE="InvalidSignature"
SIGNATURE_VERIFIED="SignatureVerified"
UPLOADED="Uploaded"
UPLOAD_FAILURE="UploadFailure"
UPLOADING="Uploading"

View File

@@ -0,0 +1,21 @@
#!/bin/bash
. "${1}"
echo "$UPLOADING"
sleep 2
curl --progress-bar --ssl --connect-timeout "$CONNECTION_TIMEOUT" -T "${4}" "${2}"
curl_exit_code=$?
if [[ $curl_exit_code -eq 0 ]]; then
echo "$UPLOADED"
elif [[ $curl_exit_code -eq 67 ]] || [[ $curl_exit_code -eq 35 ]] || [[ $curl_exit_code -eq 69 ]] ||
[[ $curl_exit_code -eq 9 ]]; then
echo "$PERMISSION_DENIED"
elif [[ $curl_exit_code -eq 3 ]] || [[ $curl_exit_code -eq 6 ]] || [[ $curl_exit_code -eq 10 ]] ||
[[ $curl_exit_code -eq 87 ]]; then
echo "$BAD_MESSAGE"
elif [[ $curl_exit_code -eq 1 ]]; then
echo "$NOT_SUPPORTED_OPERATION"
else
echo "$UPLOAD_FAILURE"
fi

View File

@@ -0,0 +1,20 @@
.. _everest_modules_handwritten_System:
.. ******
.. System
.. ******
This module implements system wide operations.
Currently this includes the following commands:
- Log Uploads
- Firmware Updates
- Setting of System time
Corresponding variables signal the state of Log Uploads and Firmware Updates.
Integration in EVerest
======================
This module provides implementation for the system interface. It does not require any other modules.

View File

@@ -0,0 +1,21 @@
#!/bin/bash
. "${1}"
echo "$DOWNLOADING"
curl --progress-bar --ssl --connect-timeout "$CONNECTION_TIMEOUT" "${2}" -o "${3}"
curl_exit_code=$?
sleep 2
if [[ $curl_exit_code -eq 0 ]]; then
echo "$DOWNLOADED"
else
echo "$DOWNLOAD_FAILED"
fi
sleep 2
if [[ $curl_exit_code -eq 0 ]]; then
echo "$INSTALLING"
sleep 2
echo "$INSTALLED"
sleep 2
fi

View File

@@ -0,0 +1,475 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "systemImpl.hpp"
#include <chrono>
#include <cstdlib>
#include <fstream>
#include <thread>
#include <vector>
#include <unistd.h>
#include <utils/date.hpp>
#include <everest/run_application/run_application.hpp>
using namespace everest::run_application;
namespace module {
namespace main {
const std::string CONSTANTS = "constants.env";
const std::string DIAGNOSTICS_UPLOADER = "diagnostics_uploader.sh";
const std::string FIRMWARE_UPDATER = "firmware_updater.sh";
const std::string SIGNED_FIRMWARE_DOWNLOADER = "signed_firmware_downloader.sh";
const std::string SIGNED_FIRMWARE_INSTALLER = "signed_firmware_installer.sh";
namespace fs = std::filesystem;
// FIXME (aw): this function needs to be refactored into some kind of utility library
fs::path create_temp_file(const fs::path& dir, const std::string& prefix) {
const std::string fn_template = (dir / prefix).string() + "XXXXXX" + std::string(1, '\0');
std::vector<char> fn_template_buffer{fn_template.begin(), fn_template.end()};
// mkstemp needs to have at least 6 XXXXXX at the end and it will replace these
// with a valid file name
auto fd = mkstemp(fn_template_buffer.data());
if (fd == -1) {
EVLOG_error << "Failed to create temporary file at: " << fn_template;
return {};
}
// close the file descriptor
close(fd);
return fn_template_buffer.data();
}
void systemImpl::init() {
this->scripts_path = mod->info.paths.libexec;
this->log_upload_running = false;
this->firmware_download_running = false;
this->firmware_installation_running = false;
this->standard_firmware_update_running = false;
this->boot_reason_key = "ocpp_boot_reason";
}
void systemImpl::ready() {
}
void systemImpl::standard_firmware_update(const types::system::FirmwareUpdateRequest& firmware_update_request) {
this->standard_firmware_update_running = true;
EVLOG_info << "Starting firmware update";
// create temporary file
const auto date_time = Everest::Date::to_rfc3339(date::utc_clock::now());
const auto firmware_file_path = create_temp_file(fs::temp_directory_path(), "firmware-" + date_time);
if (firmware_file_path.empty()) {
EVLOG_error << "Firmware update ignored, cannot write temporary file.";
publish_firmware_update_status({types::system::FirmwareUpdateStatusEnum::DownloadFailed});
return;
}
const auto constants = this->scripts_path / CONSTANTS;
this->update_firmware_thread = std::thread([this, firmware_update_request, firmware_file_path, constants]() {
const auto firmware_updater = this->scripts_path / FIRMWARE_UPDATER;
const std::vector<std::string> args = {constants.string(), firmware_update_request.location,
firmware_file_path.string()};
int32_t retries = 0;
const auto total_retries = firmware_update_request.retries.value_or(this->mod->config.DefaultRetries);
const auto retry_interval =
firmware_update_request.retry_interval_s.value_or(this->mod->config.DefaultRetryInterval);
auto firmware_status_enum = types::system::FirmwareUpdateStatusEnum::DownloadFailed;
types::system::FirmwareUpdateStatus firmware_status;
firmware_status.request_id = -1;
firmware_status.firmware_update_status = firmware_status_enum;
while (firmware_status.firmware_update_status == types::system::FirmwareUpdateStatusEnum::DownloadFailed &&
retries < total_retries) {
retries += 1;
run_application(firmware_updater.string(), args, [this, &firmware_status](const std::string& output_line) {
firmware_status.firmware_update_status =
types::system::string_to_firmware_update_status_enum(output_line);
this->publish_firmware_update_status(firmware_status);
return CmdControl::Continue;
});
if (firmware_status.firmware_update_status == types::system::FirmwareUpdateStatusEnum::DownloadFailed &&
retries < total_retries) {
std::this_thread::sleep_for(std::chrono::seconds(retry_interval));
}
if (firmware_status.firmware_update_status == types::system::FirmwareUpdateStatusEnum::Installed and
!this->mod->r_store.empty()) {
this->mod->r_store.at(0)->call_store(boot_reason_key,
boot_reason_to_string(types::system::BootReason::FirmwareUpdate));
}
}
this->standard_firmware_update_running = false;
});
this->update_firmware_thread.detach();
}
types::system::UpdateFirmwareResponse
systemImpl::handle_standard_firmware_update(const types::system::FirmwareUpdateRequest& firmware_update_request) {
if (!this->standard_firmware_update_running) {
if (firmware_update_request.retrieve_timestamp.has_value() &&
Everest::Date::from_rfc3339(firmware_update_request.retrieve_timestamp.value()) > date::utc_clock::now()) {
const auto retrieve_timestamp =
Everest::Date::from_rfc3339(firmware_update_request.retrieve_timestamp.value());
this->standard_update_firmware_timer.at(
[this, retrieve_timestamp, firmware_update_request]() {
this->standard_firmware_update(firmware_update_request);
},
retrieve_timestamp);
EVLOG_info << "Download for firmware scheduled for: " << Everest::Date::to_rfc3339(retrieve_timestamp);
} else {
// start download immediately
this->update_firmware_thread = std::thread(
[this, firmware_update_request]() { this->standard_firmware_update(firmware_update_request); });
this->update_firmware_thread.detach();
}
return types::system::UpdateFirmwareResponse::Accepted;
} else {
EVLOG_info << "Not starting firmware update because firmware update process already running";
return types::system::UpdateFirmwareResponse::Rejected;
}
}
types::system::UpdateFirmwareResponse
systemImpl::handle_signed_fimware_update(const types::system::FirmwareUpdateRequest& firmware_update_request) {
if (!firmware_update_request.signing_certificate.has_value()) {
EVLOG_warning << "Signing certificate is missing in FirmwareUpdateRequest";
return types::system::UpdateFirmwareResponse::Rejected;
}
if (!firmware_update_request.signature.has_value()) {
EVLOG_warning << "Signature is missing in FirmwareUpdateRequest";
return types::system::UpdateFirmwareResponse::Rejected;
}
EVLOG_info << "Executing signed firmware update download callback";
if (firmware_update_request.retrieve_timestamp.has_value() &&
Everest::Date::from_rfc3339(firmware_update_request.retrieve_timestamp.value()) > date::utc_clock::now()) {
const auto retrieve_timestamp = Everest::Date::from_rfc3339(firmware_update_request.retrieve_timestamp.value());
this->signed_firmware_update_download_timer.at(
[this, retrieve_timestamp, firmware_update_request]() {
this->download_signed_firmware(firmware_update_request);
},
retrieve_timestamp);
EVLOG_info << "Download for firmware scheduled for: " << Everest::Date::to_rfc3339(retrieve_timestamp);
types::system::FirmwareUpdateStatus firmware_update_status;
firmware_update_status.request_id = firmware_update_request.request_id;
firmware_update_status.firmware_update_status = types::system::FirmwareUpdateStatusEnum::DownloadScheduled;
this->publish_firmware_update_status(firmware_update_status);
} else {
// start download immediately
this->update_firmware_thread =
std::thread([this, firmware_update_request]() { this->download_signed_firmware(firmware_update_request); });
this->update_firmware_thread.detach();
}
if (this->firmware_download_running) {
return types::system::UpdateFirmwareResponse::AcceptedCanceled;
} else if (this->firmware_installation_running) {
return types::system::UpdateFirmwareResponse::Rejected;
} else {
return types::system::UpdateFirmwareResponse::Accepted;
}
}
void systemImpl::download_signed_firmware(const types::system::FirmwareUpdateRequest& firmware_update_request) {
if (!firmware_update_request.signing_certificate.has_value()) {
EVLOG_warning << "Signing certificate is missing in FirmwareUpdateRequest";
this->publish_firmware_update_status(
{types::system::FirmwareUpdateStatusEnum::DownloadFailed, firmware_update_request.request_id});
return;
}
if (!firmware_update_request.signature.has_value()) {
EVLOG_warning << "Signature is missing in FirmwareUpdateRequest";
this->publish_firmware_update_status(
{types::system::FirmwareUpdateStatusEnum::DownloadFailed, firmware_update_request.request_id});
return;
}
if (this->firmware_download_running) {
EVLOG_info
<< "Received Firmware update request and firmware update already running - cancelling firmware update";
this->interrupt_firmware_download.exchange(true);
EVLOG_info << "Waiting for other firmware download to finish...";
std::unique_lock<std::mutex> lk(this->firmware_update_mutex);
this->firmware_update_cv.wait(lk, [this]() { return !this->firmware_download_running; });
EVLOG_info << "Previous Firmware download finished!";
}
std::lock_guard<std::mutex> lg(this->firmware_update_mutex);
EVLOG_info << "Starting Firmware update";
this->interrupt_firmware_download.exchange(false);
this->firmware_download_running = true;
// // create temporary file
const auto date_time = Everest::Date::to_rfc3339(date::utc_clock::now());
const auto firmware_file_path = create_temp_file(fs::temp_directory_path(), "signed_firmware-" + date_time);
const auto firmware_downloader = this->scripts_path / SIGNED_FIRMWARE_DOWNLOADER;
const auto constants = this->scripts_path / CONSTANTS;
const std::vector<std::string> download_args = {
constants.string(), firmware_update_request.location, firmware_file_path.string(),
firmware_update_request.signature.value(), firmware_update_request.signing_certificate.value()};
int32_t retries = 0;
const auto total_retries = firmware_update_request.retries.value_or(this->mod->config.DefaultRetries);
const auto retry_interval =
firmware_update_request.retry_interval_s.value_or(this->mod->config.DefaultRetryInterval);
auto firmware_status_enum = types::system::FirmwareUpdateStatusEnum::DownloadFailed;
types::system::FirmwareUpdateStatus firmware_status;
firmware_status.request_id = firmware_update_request.request_id;
firmware_status.firmware_update_status = firmware_status_enum;
while (firmware_status.firmware_update_status == types::system::FirmwareUpdateStatusEnum::DownloadFailed &&
retries < total_retries && !this->interrupt_firmware_download) {
run_application(
firmware_downloader.string(), download_args, [this, &firmware_status](const std::string& output_line) {
firmware_status.firmware_update_status =
types::system::string_to_firmware_update_status_enum(output_line);
this->publish_firmware_update_status(firmware_status);
if (this->interrupt_firmware_download) {
EVLOG_info << "Updating firmware was interrupted, terminating firmware update script, requestId: "
<< firmware_status.request_id;
return CmdControl::Terminate;
}
return CmdControl::Continue;
});
retries += 1;
if (firmware_status.firmware_update_status == types::system::FirmwareUpdateStatusEnum::DownloadFailed &&
retries < total_retries) {
std::this_thread::sleep_for(std::chrono::seconds(retry_interval));
}
}
if (firmware_status.firmware_update_status == types::system::FirmwareUpdateStatusEnum::SignatureVerified) {
this->initialize_firmware_installation(firmware_update_request, firmware_file_path);
}
this->firmware_download_running = false;
this->firmware_update_cv.notify_one();
EVLOG_info << "Firmware update thread finished";
}
void systemImpl::initialize_firmware_installation(const types::system::FirmwareUpdateRequest& firmware_update_request,
const fs::path& firmware_file_path) {
if (firmware_update_request.install_timestamp.has_value() &&
Everest::Date::from_rfc3339(firmware_update_request.install_timestamp.value()) > date::utc_clock::now()) {
const auto install_timestamp = Everest::Date::from_rfc3339(firmware_update_request.install_timestamp.value());
this->signed_firmware_update_install_timer.at(
[this, firmware_update_request, firmware_file_path]() {
this->install_signed_firmware(firmware_update_request, firmware_file_path);
},
install_timestamp);
EVLOG_info << "Installation for firmware scheduled for: " << Everest::Date::to_rfc3339(install_timestamp);
types::system::FirmwareUpdateStatus firmware_update_status;
firmware_update_status.request_id = firmware_update_request.request_id;
firmware_update_status.firmware_update_status = types::system::FirmwareUpdateStatusEnum::InstallScheduled;
this->publish_firmware_update_status(firmware_update_status);
} else {
// start installation immediately
this->update_firmware_thread = std::thread([this, firmware_update_request, firmware_file_path]() {
this->install_signed_firmware(firmware_update_request, firmware_file_path);
});
this->update_firmware_thread.detach();
}
}
void systemImpl::install_signed_firmware(const types::system::FirmwareUpdateRequest& firmware_update_request,
const fs::path& firmware_file_path) {
auto firmware_status_enum = types::system::FirmwareUpdateStatusEnum::Installing;
types::system::FirmwareUpdateStatus firmware_status;
firmware_status.request_id = firmware_update_request.request_id;
firmware_status.firmware_update_status = firmware_status_enum;
if (!this->firmware_installation_running) {
this->firmware_installation_running = true;
const auto firmware_installer = this->scripts_path / SIGNED_FIRMWARE_INSTALLER;
const auto constants = this->scripts_path / CONSTANTS;
const std::vector<std::string> install_args = {constants.string()};
run_application(firmware_installer.string(), install_args,
[this, &firmware_status](const std::string& output_line) {
firmware_status.firmware_update_status =
types::system::string_to_firmware_update_status_enum(output_line);
this->publish_firmware_update_status(firmware_status);
return CmdControl::Continue;
});
if (firmware_status.firmware_update_status == types::system::FirmwareUpdateStatusEnum::Installed) {
if (!this->mod->r_store.empty()) {
this->mod->r_store.at(0)->call_store(boot_reason_key,
boot_reason_to_string(types::system::BootReason::FirmwareUpdate));
}
auto reset_type = types::system::ResetType::Hard;
bool firmware_installation_running_copy = this->firmware_installation_running;
this->handle_reset(reset_type, firmware_installation_running_copy);
}
} else {
firmware_status.firmware_update_status = types::system::FirmwareUpdateStatusEnum::InstallationFailed;
this->publish_firmware_update_status(firmware_status);
}
}
types::system::UpdateFirmwareResponse
systemImpl::handle_update_firmware(types::system::FirmwareUpdateRequest& firmware_update_request) {
if (firmware_update_request.request_id == -1) {
return this->handle_standard_firmware_update(firmware_update_request);
} else {
return this->handle_signed_fimware_update(firmware_update_request);
}
};
void systemImpl::handle_allow_firmware_installation() {
// TODO: implement me
}
types::system::UploadLogsResponse
systemImpl::handle_upload_logs(types::system::UploadLogsRequest& upload_logs_request) {
types::system::UploadLogsResponse response;
if (this->log_upload_running) {
response.upload_logs_status = types::system::UploadLogsStatus::AcceptedCanceled;
} else {
response.upload_logs_status = types::system::UploadLogsStatus::Accepted;
}
const auto date_time = Everest::Date::to_rfc3339(date::utc_clock::now());
// TODO(piet): consider start time and end time
const auto diagnostics_file_path = create_temp_file(fs::temp_directory_path(), "diagnostics-" + date_time);
const auto diagnostics_file_name = diagnostics_file_path.filename().string();
response.file_name = diagnostics_file_name;
const auto fake_diagnostics_file = json({{"diagnostics", {{"key", "value"}}}});
std::ofstream diagnostics_file(diagnostics_file_path.c_str());
diagnostics_file << fake_diagnostics_file.dump();
this->upload_logs_thread = std::thread([this, upload_logs_request, diagnostics_file_name, diagnostics_file_path]() {
if (this->log_upload_running) {
EVLOG_info << "Received Log upload request and log upload already running - cancelling current upload";
this->interrupt_log_upload.exchange(true);
EVLOG_info << "Waiting for other log upload to finish...";
std::unique_lock<std::mutex> lk(this->log_upload_mutex);
this->log_upload_cv.wait(lk, [this]() { return !this->log_upload_running; });
EVLOG_info << "Previous Log upload finished!";
}
std::lock_guard<std::mutex> lg(this->log_upload_mutex);
EVLOG_info << "Starting upload of log file";
this->interrupt_log_upload.exchange(false);
this->log_upload_running = true;
const auto diagnostics_uploader = this->scripts_path / DIAGNOSTICS_UPLOADER;
const auto constants = this->scripts_path / CONSTANTS;
std::vector<std::string> args = {constants.string(), upload_logs_request.location, diagnostics_file_name,
diagnostics_file_path.string()};
bool uploaded = false;
int32_t retries = 0;
const auto total_retries = upload_logs_request.retries.value_or(this->mod->config.DefaultRetries);
const auto retry_interval =
upload_logs_request.retry_interval_s.value_or(this->mod->config.DefaultRetryInterval);
types::system::LogStatus log_status;
while (!uploaded && retries < total_retries && !this->interrupt_log_upload) {
retries += 1;
log_status.request_id = upload_logs_request.request_id.value_or(-1);
run_application(diagnostics_uploader.string(), args, [this, &log_status](const std::string& output_line) {
if (output_line == "Uploaded") {
log_status.log_status = types::system::string_to_log_status_enum(output_line);
} else if (output_line == "UploadFailure" || output_line == "PermissionDenied" ||
output_line == "BadMessage" || output_line == "NotSupportedOperation") {
log_status.log_status = types::system::LogStatusEnum::UploadFailure;
} else {
log_status.log_status = types::system::LogStatusEnum::Uploading;
}
this->publish_log_status(log_status);
if (this->interrupt_log_upload) {
return CmdControl::Terminate;
}
return CmdControl::Continue;
});
if (this->interrupt_log_upload) {
EVLOG_info << "Uploading Logs was interrupted, terminating upload script, requestId: "
<< log_status.request_id;
// N01.FR.20
log_status.log_status = types::system::LogStatusEnum::AcceptedCanceled;
this->publish_log_status(log_status);
} else if (log_status.log_status != types::system::LogStatusEnum::Uploaded && retries < total_retries) {
// command finished, but neither interrupted nor uploaded
std::this_thread::sleep_for(std::chrono::seconds(retry_interval));
} else {
uploaded = true;
}
}
this->log_upload_running = false;
this->log_upload_cv.notify_one();
EVLOG_info << "Log upload thread finished";
});
this->upload_logs_thread.detach();
return response;
};
bool systemImpl::handle_is_reset_allowed(types::system::ResetType& type) {
// Right now we dont want to reject a reset ever
return true;
}
void systemImpl::handle_reset(types::system::ResetType& type, bool& scheduled) {
// let the actual work be done by a worker thread, which can also delay it
// a little bit (if configured) to allow e.g. clean shutdown of communication
// channels in parallel when this call returns
std::thread([this, type, scheduled] {
EVLOG_info << "Reset request received: " << type << ", " << (scheduled ? "" : "not ") << "scheduled";
if (!this->mod->r_store.empty() and !this->mod->r_store.at(0)->call_exists(boot_reason_key)) {
this->mod->r_store.at(0)->call_store(boot_reason_key,
boot_reason_to_string(types::system::BootReason::RemoteReset));
}
std::this_thread::sleep_for(std::chrono::seconds(this->mod->config.ResetDelay));
if (type == types::system::ResetType::Soft) {
EVLOG_info << "Performing soft reset now.";
kill(getpid(), SIGINT);
} else {
EVLOG_info << "Performing hard reset now.";
kill(getpid(), SIGINT); // FIXME(piet): Define appropriate behavior for hard reset
}
}).detach();
}
bool systemImpl::handle_set_system_time(std::string& timestamp) {
// your code for cmd set_system_time goes here
return true;
};
types::system::BootReason systemImpl::handle_get_boot_reason() {
if (this->mod->r_store.empty()) {
return types::system::BootReason::PowerUp;
}
auto reason_variant = this->mod->r_store.at(0)->call_load(boot_reason_key);
auto* reason = std::get_if<std::string>(&reason_variant);
std::string final_reason{boot_reason_to_string(types::system::BootReason::PowerUp)};
if (reason != nullptr) {
final_reason = *reason;
}
this->mod->r_store.at(0)->call_delete(boot_reason_key);
return types::system::string_to_boot_reason(final_reason);
}
} // namespace main
} // namespace module

View File

@@ -0,0 +1,152 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef MAIN_SYSTEM_IMPL_HPP
#define MAIN_SYSTEM_IMPL_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 3
//
#include <generated/interfaces/system/Implementation.hpp>
#include "../System.hpp"
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
// insert your custom include headers here
#include <filesystem>
#include <everest/timer.hpp>
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
namespace module {
namespace main {
struct Conf {};
class systemImpl : public systemImplBase {
public:
systemImpl() = delete;
systemImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer<System>& mod, Conf& config) :
systemImplBase(ev, "main"), mod(mod), config(config){};
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
// insert your public definitions here
// ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1
protected:
// command handler functions (virtual)
virtual types::system::UpdateFirmwareResponse
handle_update_firmware(types::system::FirmwareUpdateRequest& firmware_update_request) override;
virtual void handle_allow_firmware_installation() override;
virtual types::system::UploadLogsResponse
handle_upload_logs(types::system::UploadLogsRequest& upload_logs_request) override;
virtual bool handle_is_reset_allowed(types::system::ResetType& type) override;
virtual void handle_reset(types::system::ResetType& type, bool& scheduled) override;
virtual bool handle_set_system_time(std::string& timestamp) override;
virtual types::system::BootReason handle_get_boot_reason() override;
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
// insert your protected definitions here
// ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1
private:
const Everest::PtrContainer<System>& mod;
const Conf& config;
virtual void init() override;
virtual void ready() override;
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
// insert your private definitions here
std::filesystem::path scripts_path;
std::atomic<bool> interrupt_firmware_download;
std::atomic<bool> interrupt_log_upload;
bool log_upload_running;
bool standard_firmware_update_running;
bool firmware_download_running;
std::atomic<bool> firmware_installation_running;
std::condition_variable log_upload_cv;
std::condition_variable firmware_update_cv;
std::mutex log_upload_mutex;
std::mutex firmware_update_mutex;
std::thread update_firmware_thread;
std::thread upload_logs_thread;
Everest::SteadyTimer standard_update_firmware_timer;
Everest::SteadyTimer signed_firmware_update_download_timer;
Everest::SteadyTimer signed_firmware_update_install_timer;
std::string boot_reason_key;
/**
* @brief Executes a standard firmware update using the given \p firmware_update_request
*
* @param firmware_update_request
*/
void standard_firmware_update(const types::system::FirmwareUpdateRequest& firmware_update_request);
/**
* @brief Handles the given \p firmware_update_request . If firmware update is already running, the request will be
* rejected. If the download should not be started in the future it starts the download and installation of the
* firmware immediately, otherwise this method sets a timer for the download accordingly.
*
* @param firmware_update_request
* @return types::system::UpdateFirmwareResponse
*/
types::system::UpdateFirmwareResponse
handle_standard_firmware_update(const types::system::FirmwareUpdateRequest& firmware_update_request);
/**
* @brief Handles the given \p firmware_update_request. If the download should not be started in the future it
* starts the download and installation of the firmware immediately, otherwise this method sets a timer for the
* download accordingly.
*
* @param firmware_update_request
*/
types::system::UpdateFirmwareResponse
handle_signed_fimware_update(const types::system::FirmwareUpdateRequest& firmware_update_request);
/**
* @brief Handles the download of the firmware specified in the given \p firmware_update_request . If a download is
* already running, this method will interrupt the download process and restart it.
*
* @param firmware_update_request
*/
void download_signed_firmware(const types::system::FirmwareUpdateRequest& firmware_update_request);
/**
* @brief Initializes the firmware installation by starting it immediately or if specified in the \p
* firmware_update_request it schedules it for the future.
*
* @param firmware_update_request
* @param firmware_file_path
*/
void initialize_firmware_installation(const types::system::FirmwareUpdateRequest& firmware_update_request,
const std::filesystem::path& firmware_file_path);
/**
* @brief Executes the installation of the firmware specified in the given \p firmware_update_request .
*
* @param firmware_update_reqeust
* @param firmware_file_path
*/
void install_signed_firmware(const types::system::FirmwareUpdateRequest& firmware_update_reqeust,
const std::filesystem::path& firmware_file_path);
// ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1
};
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
// insert other definitions here
// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1
} // namespace main
} // namespace module
#endif // MAIN_SYSTEM_IMPL_HPP

View File

@@ -0,0 +1,33 @@
description: This module implements system wide operations
config:
DefaultRetries:
description: Specifies how many times Charge Point tries to upload or download files on previous failure.
type: number
default: 1
DefaultRetryInterval:
description: >-
Specifies in seconds after which time a retry of an upload or download on previous failure may be attempted.
type: number
default: 1
ResetDelay:
description: >-
When receiving a reset request, then the actual execution can be delayed by this amount of time (given in seconds).
This might be necessary, for example, when the reset request arrives via the network and the call acknowledgement
should be given some time to travel the return path to the caller.
Defaults to zero, which means that the reset is executed directly without delay.
type: integer
minimum: 0
default: 0
provides:
main:
description: Implements the system interface
interface: system
requires:
store:
interface: kvs
min_connections: 0
max_connections: 1
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Piet Gömpel

View File

@@ -0,0 +1,30 @@
#!/bin/bash
. "${1}"
SIGNATURE_VALIDATION_DIR=$(mktemp -d /tmp/signature_validation_XXXXX)
sleep 2
echo "$DOWNLOADING"
sleep 2
curl --progress-bar --ssl --connect-timeout "$CONNECTION_TIMEOUT" "${2}" -o "${3}"
curl_exit_code=$?
sleep 2
if [[ $curl_exit_code -eq 0 ]]; then
echo "$DOWNLOADED"
echo -e "${4}" >"$SIGNATURE_VALIDATION_DIR/firmware_signature.base64"
echo -e "${5}" >"$SIGNATURE_VALIDATION_DIR/firmware_cert.pem"
openssl x509 -pubkey -noout -in "$SIGNATURE_VALIDATION_DIR/firmware_cert.pem" >"$SIGNATURE_VALIDATION_DIR/pubkey.pem"
openssl base64 -d -in "$SIGNATURE_VALIDATION_DIR/firmware_signature.base64" -out "$SIGNATURE_VALIDATION_DIR/firmware_signature.sha256"
r=$(openssl dgst -sha256 -verify "$SIGNATURE_VALIDATION_DIR/pubkey.pem" -signature "$SIGNATURE_VALIDATION_DIR/firmware_signature.sha256" "${3}")
if [ "$r" = "Verified OK" ]; then
echo "$SIGNATURE_VERIFIED"
else
echo "$INVALID_SIGNATURE"
fi
else
echo "$DOWNLOAD_FAILED"
fi
rm -rf "$SIGNATURE_VALIDATION_DIR"

View File

@@ -0,0 +1,7 @@
#!/bin/bash
. "${1}"
echo "$INSTALLING"
sleep 2
echo "$INSTALLED"

View File

@@ -0,0 +1,25 @@
#
# AUTO GENERATED - MARKED REGIONS WILL BE KEPT
# template version 3
#
# module setup:
# - ${MODULE_NAME}: module name
ev_setup_cpp_module()
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
# insert your custom targets and additional config variables here
target_link_libraries(${MODULE_NAME}
PRIVATE
everest::yaml
)
# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1
target_sources(${MODULE_NAME}
PRIVATE
"main/kvsImpl.cpp"
)
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1
# insert other things like install cmds etc here
# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1

View File

@@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "YamlStore.hpp"
namespace module {
void YamlStore::init() {
invoke_init(*p_main);
}
void YamlStore::ready() {
invoke_ready(*p_main);
}
} // namespace module

View File

@@ -0,0 +1,60 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#ifndef YAML_STORE_HPP
#define YAML_STORE_HPP
//
// AUTO GENERATED - MARKED REGIONS WILL BE KEPT
// template version 2
//
#include "ld-ev.hpp"
// headers for provided interface implementations
#include <generated/interfaces/kvs/Implementation.hpp>
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
// insert your custom include headers here
// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1
namespace module {
struct Conf {
std::string file;
};
class YamlStore : public Everest::ModuleBase {
public:
YamlStore() = delete;
YamlStore(const ModuleInfo& info, std::unique_ptr<kvsImplBase> p_main, Conf& config) :
ModuleBase(info), p_main(std::move(p_main)), config(config){};
const std::unique_ptr<kvsImplBase> p_main;
const Conf& config;
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
// insert your public definitions here
// ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1
protected:
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
// insert your protected definitions here
// ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1
private:
friend class LdEverest;
void init();
void ready();
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
// insert your private definitions here
// ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1
};
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
// insert other definitions here
// ev@087e516b-124c-48df-94fb-109508c7cda9:v1
} // namespace module
#endif // YAML_STORE_HPP

View File

@@ -0,0 +1,51 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "kvsImpl.hpp"
#include <filesystem>
#include <string>
#include <utils/yaml_loader.hpp>
#include <variant>
namespace module {
namespace main {
void kvsImpl::init() {
auto kv_file_path = std::filesystem::path(mod->config.file);
try {
data = Everest::load_yaml(kv_file_path);
} catch (const std::exception& err) {
EVLOG_error << "Error parsing YAML file at " << mod->config.file << ": " << err.what();
}
}
void kvsImpl::ready() {
}
void kvsImpl::handle_store(std::string& key,
std::variant<std::nullptr_t, Array, Object, bool, double, int, std::string>& value) {
// this is a read-only kvs - do nothing but prevent compiler warnings about unused parameters
(void)key;
(void)value;
}
std::variant<std::nullptr_t, Array, Object, bool, double, int, std::string> kvsImpl::handle_load(std::string& key) {
if (data.contains(key)) {
std::string value{data[key]};
return value;
}
return nullptr;
}
void kvsImpl::handle_delete(std::string& key) {
// this is a read-only kvs - do nothing but prevent compiler warnings about unused parameters
(void)key;
}
bool kvsImpl::handle_exists(std::string& key) {
return data.contains(key);
}
} // namespace main
} // namespace module

Some files were not shown because too many files have changed in this diff Show More