Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter
- CitrineOS core extracted (CSMS OCPP 2.0.1) - OpenOCPP extracted (firmware OCPP 1.6J/2.0.1) - ShapeShifter library installed (pip install -e) - ShapeShifter specification extracted - EVerest extracted TODO updated with progress
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
add_library(everest_api_module_helpers OBJECT)
|
||||
add_library(everest::everest_api_module_helpers ALIAS everest_api_module_helpers)
|
||||
ev_register_library_target(everest_api_module_helpers)
|
||||
|
||||
target_compile_options(everest_api_module_helpers
|
||||
PUBLIC -Wall -Wextra -pedantic -Werror=switch)
|
||||
|
||||
target_include_directories(everest_api_module_helpers
|
||||
PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
"$<TARGET_PROPERTY:generate_cpp_files,EVEREST_GENERATED_INCLUDE_DIR>"
|
||||
)
|
||||
|
||||
target_sources(everest_api_module_helpers
|
||||
PRIVATE
|
||||
src/ApiHelper.cpp
|
||||
src/ValidatingMqttProxy.cpp
|
||||
)
|
||||
|
||||
add_dependencies(everest_api_module_helpers generate_cpp_files)
|
||||
|
||||
target_link_libraries(everest_api_module_helpers
|
||||
PUBLIC
|
||||
everest::everest_api_types
|
||||
everest::framework
|
||||
)
|
||||
|
||||
install(TARGETS everest_api_module_helpers
|
||||
EXPORT everest_api_module_helpers-targets
|
||||
LIBRARY
|
||||
)
|
||||
|
||||
install(
|
||||
DIRECTORY include/everest_api_module_helpers
|
||||
DESTINATION include
|
||||
)
|
||||
@@ -0,0 +1,95 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <everest/logging.hpp>
|
||||
#include <utils/config_service.hpp>
|
||||
|
||||
#include <everest_api_module_helpers/ValidatingMqttProxy.hpp>
|
||||
#include <everest_api_types/entrypoint/API.hpp>
|
||||
#include <everest_api_types/generic/codec.hpp>
|
||||
#include <everest_api_types/utilities/CommCheckHandler.hpp>
|
||||
#include <everest_api_types/utilities/MqttProviderInterface.hpp>
|
||||
#include <everest_api_types/utilities/Topics.hpp>
|
||||
#include <everest_api_types/utilities/codec.hpp>
|
||||
|
||||
namespace everest::lib::API {
|
||||
|
||||
inline constexpr std::string_view bridge_connection_lost_message{"Bridge to implementation connection lost"};
|
||||
|
||||
class ApiHelper {
|
||||
public:
|
||||
using ParseAndPublishFtor = std::function<bool(std::string const&)>;
|
||||
|
||||
ApiHelper(const ModuleInfo& info_, Mqtt::MqttProviderInterface& mqtt_provider_,
|
||||
std::map<std::string, size_t> implemented_apis_,
|
||||
std::shared_ptr<Everest::config::ConfigServiceClient> config_service_client_) :
|
||||
info(info_),
|
||||
mqtt(mqtt_provider_),
|
||||
implemented_apis(std::move(implemented_apis_)),
|
||||
config_service_client(std::move(config_service_client_)) {
|
||||
}
|
||||
|
||||
const ModuleInfo& info;
|
||||
Mqtt::MqttProviderInterface& mqtt;
|
||||
|
||||
const Topics& get_topics() const;
|
||||
|
||||
// Initialization helpers (called from module::init())
|
||||
void init(V1_0::types::entrypoint::CommunicationParameters const& comm_parameters);
|
||||
|
||||
void publish_ready_beacon();
|
||||
|
||||
// Topic subscription helpers
|
||||
void subscribe_api_topic(std::string const& topic, ParseAndPublishFtor parse_and_publish);
|
||||
void subscribe_entrypoint_var(std::string const& var, ParseAndPublishFtor parse_and_publish);
|
||||
|
||||
template <typename CommCheckT> void generate_api_var_communication_check(CommCheckT* comm_check) {
|
||||
subscribe_api_topic("communication_check", [comm_check](std::string const& data) {
|
||||
bool val = false;
|
||||
if (deserialize(data, val)) {
|
||||
comm_check->set_value(val);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
template <typename CommCheckT> void setup_heartbeat_generator(CommCheckT* comm_check, int interval_ms) {
|
||||
auto topic = topics.everest_to_extern("heartbeat");
|
||||
auto action = [this, topic]() {
|
||||
using V1_0::types::generic::serialize;
|
||||
mqtt.publish(topic, serialize(hb_id++));
|
||||
return true;
|
||||
};
|
||||
comm_check->heartbeat(interval_ms, action);
|
||||
}
|
||||
|
||||
private:
|
||||
void subscribe_mqtt_topic(std::string const& topic, ParseAndPublishFtor parse_and_publish);
|
||||
void init_entrypoint_API(V1_0::types::entrypoint::CommunicationParameters const& comm_parameters);
|
||||
void init_topics();
|
||||
void generate_api_entrypoint_cmd_discover();
|
||||
void generate_api_entrypoint_cmd_query_module();
|
||||
|
||||
Topics topics;
|
||||
std::atomic<size_t> hb_id{0};
|
||||
V1_0::types::entrypoint::ApiDiscoverResponse discover_response{};
|
||||
std::map<std::string, V1_0::types::entrypoint::ApiDiscoverResponse> module_query_response{};
|
||||
std::map<std::string, size_t> implemented_apis;
|
||||
std::atomic<bool> responsible_for_sending_ready_beacon{false};
|
||||
std::shared_ptr<Everest::config::ConfigServiceClient> config_service_client;
|
||||
};
|
||||
|
||||
} // namespace everest::lib::API
|
||||
@@ -0,0 +1,31 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include <framework/ModuleAdapter.hpp>
|
||||
|
||||
#include <everest_api_types/utilities/MqttProviderInterface.hpp>
|
||||
|
||||
namespace everest::lib::API::Mqtt {
|
||||
|
||||
class ValidatingMqttProxy : public MqttProviderInterface {
|
||||
public:
|
||||
explicit ValidatingMqttProxy(Everest::MqttProvider& provider_);
|
||||
|
||||
void publish(const std::string& topic, const std::string& data) override;
|
||||
void publish(const std::string& topic, bool data) override;
|
||||
|
||||
Everest::UnsubscribeToken subscribe(const std::string& topic,
|
||||
std::function<void(std::string)> handler) const override;
|
||||
|
||||
private:
|
||||
Everest::MqttProvider& provider;
|
||||
|
||||
[[nodiscard]] static bool is_topic_valid(std::string_view topic);
|
||||
};
|
||||
|
||||
} // namespace everest::lib::API::Mqtt
|
||||
@@ -0,0 +1,139 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <everest_api_module_helpers/ApiHelper.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
|
||||
#include <everest_api_types/entrypoint/codec.hpp>
|
||||
#include <everest_api_types/generic/string.hpp>
|
||||
|
||||
#include <everest/logging.hpp>
|
||||
#include <generated/version_information.hpp>
|
||||
|
||||
namespace {
|
||||
inline bool ends_with(std::string_view str, std::string_view suffix) {
|
||||
return str.size() >= suffix.size() && str.substr(str.size() - suffix.size()) == suffix;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace everest::lib::API {
|
||||
|
||||
void ApiHelper::init(V1_0::types::entrypoint::CommunicationParameters const& comm_parameters) {
|
||||
init_entrypoint_API(comm_parameters);
|
||||
init_topics();
|
||||
generate_api_entrypoint_cmd_query_module();
|
||||
generate_api_entrypoint_cmd_discover();
|
||||
}
|
||||
|
||||
void ApiHelper::init_entrypoint_API(V1_0::types::entrypoint::CommunicationParameters const& comm_parameters) {
|
||||
const auto& module_configs = config_service_client->get_module_configs();
|
||||
|
||||
const std::string api_module_type_ending = "_API";
|
||||
std::vector<std::string> active_api_modules;
|
||||
|
||||
for (const auto& [id, _] : module_configs) {
|
||||
// Heuristics for figuring out if this module is an API module: type ends with "_API"
|
||||
if (ends_with(id.module_type, "_API")) {
|
||||
active_api_modules.push_back(id.module_id);
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(active_api_modules.begin(), active_api_modules.end());
|
||||
if (active_api_modules.size() > 0 && info.id == active_api_modules[0]) {
|
||||
// each module will figure out if it happens to be the one with the
|
||||
// lowest-ordering alphabetical id if so, it shall be the one which sends
|
||||
// out a ready beacon
|
||||
responsible_for_sending_ready_beacon = true;
|
||||
}
|
||||
|
||||
for (const auto& [api_type, version] : implemented_apis) {
|
||||
V1_0::types::entrypoint::ApiParameter api_params;
|
||||
api_params.type = api_type;
|
||||
api_params.module_id = info.id;
|
||||
api_params.version = version;
|
||||
api_params.communication_monitoring = comm_parameters;
|
||||
|
||||
discover_response.apis.push_back(api_params);
|
||||
|
||||
if (module_query_response.count(api_type) == 0) {
|
||||
module_query_response[api_type] = V1_0::types::entrypoint::ApiDiscoverResponse{};
|
||||
}
|
||||
module_query_response[api_type].apis.push_back(std::move(api_params));
|
||||
}
|
||||
}
|
||||
|
||||
void ApiHelper::init_topics() {
|
||||
for (const auto& [api_type, version] : implemented_apis) {
|
||||
topics.setup(info.id, api_type, version);
|
||||
// Extend here to support multiple api implementations per module
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ApiHelper::generate_api_entrypoint_cmd_discover() {
|
||||
subscribe_entrypoint_var("discover", [this](std::string const& data) {
|
||||
V1_0::types::generic::RequestReply msg;
|
||||
if (deserialize(data, msg)) {
|
||||
mqtt.publish(msg.replyTo, V1_0::types::entrypoint::serialize(discover_response));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
void ApiHelper::generate_api_entrypoint_cmd_query_module() {
|
||||
for (const auto& [api_type, _] : implemented_apis) {
|
||||
std::stringstream topic;
|
||||
topic << "query-modules/" << api_type;
|
||||
|
||||
subscribe_entrypoint_var(topic.str(), [api_type = api_type, this](std::string const& data) {
|
||||
V1_0::types::generic::RequestReply msg;
|
||||
if (deserialize(data, msg)) {
|
||||
if (module_query_response.count(api_type) > 0) {
|
||||
mqtt.publish(msg.replyTo, V1_0::types::entrypoint::serialize(module_query_response[api_type]));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ApiHelper::publish_ready_beacon() {
|
||||
if (responsible_for_sending_ready_beacon) {
|
||||
mqtt.publish(topics.entrypoint("ready_beacon"), std::string{"{}"});
|
||||
}
|
||||
}
|
||||
|
||||
void ApiHelper::subscribe_entrypoint_var(std::string const& var, ParseAndPublishFtor parse_and_publish) {
|
||||
subscribe_mqtt_topic(topics.entrypoint(var), std::move(parse_and_publish));
|
||||
}
|
||||
|
||||
void ApiHelper::subscribe_api_topic(std::string const& topic, ParseAndPublishFtor parse_and_publish) {
|
||||
subscribe_mqtt_topic(topics.extern_to_everest(topic), std::move(parse_and_publish));
|
||||
}
|
||||
|
||||
void ApiHelper::subscribe_mqtt_topic(std::string const& topic, ParseAndPublishFtor parse_and_publish) {
|
||||
mqtt.subscribe(topic, [topic = topic, f = std::move(parse_and_publish)](std::string const& data) {
|
||||
try {
|
||||
if (not f(data)) {
|
||||
EVLOG_warning << "Invalid data: Deserialization failed.\n" << topic << "\n" << data;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
EVLOG_warning << "Topic: '" << topic << "' failed with -> " << e.what() << "\n => " << data;
|
||||
} catch (...) {
|
||||
EVLOG_warning << "Invalid data: Failed to parse JSON or to get data from it.\n" << topic;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const Topics& ApiHelper::get_topics() const {
|
||||
return topics;
|
||||
}
|
||||
|
||||
} // namespace everest::lib::API
|
||||
@@ -0,0 +1,49 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <everest_api_module_helpers/ValidatingMqttProxy.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
#include <everest/logging.hpp>
|
||||
|
||||
namespace everest::lib::API::Mqtt {
|
||||
|
||||
ValidatingMqttProxy::ValidatingMqttProxy(Everest::MqttProvider& provider_) : provider(provider_) {
|
||||
}
|
||||
|
||||
void ValidatingMqttProxy::publish(const std::string& topic, const std::string& data) {
|
||||
if (is_topic_valid(topic)) {
|
||||
provider.publish(topic, data);
|
||||
} else {
|
||||
EVLOG_warning << "ValidatingMqttProxy: Droped '" << data << "' as topic '" << topic << "' invalid";
|
||||
}
|
||||
}
|
||||
|
||||
void ValidatingMqttProxy::publish(const std::string& topic, bool data) {
|
||||
if (is_topic_valid(topic)) {
|
||||
provider.publish(topic, data);
|
||||
} else {
|
||||
EVLOG_warning << "ValidatingMqttProxy: Droped '" << data << "' as topic '" << topic << "' invalid";
|
||||
}
|
||||
}
|
||||
|
||||
Everest::UnsubscribeToken ValidatingMqttProxy::subscribe(const std::string& topic,
|
||||
std::function<void(std::string)> handler) const {
|
||||
return provider.subscribe(topic, std::move(handler));
|
||||
}
|
||||
|
||||
bool ValidatingMqttProxy::is_topic_valid(std::string_view topic) {
|
||||
static constexpr std::size_t max_topic_length = 65535;
|
||||
if (topic.empty() || topic.length() > max_topic_length) {
|
||||
return false;
|
||||
}
|
||||
if (topic.find('\0') != std::string_view::npos || topic.find_first_of("+#") != std::string_view::npos) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace everest::lib::API::Mqtt
|
||||
Reference in New Issue
Block a user