18 KiB
| date | updated | tags | link | share | publish | ||
|---|---|---|---|---|---|---|---|
| 2025-03-05 10:35 | 2025-03-11 14:46 |
|
false | true | true |
项目概述
EVerest-Framework 是一个支持多语言模块化开发的框架,主要用于电动汽车充电基础设施的开发。从代码中可以看出,它支持 C++、JavaScript、Rust 和 Python 编程语言的模块。
项目结构
EVerest-Framework/
│
├── cmake/ # CMake 模块和辅助函数
│ ├── NodeApiVersion.cmake
│ └── find-mqttc.cmake
│
├── everestjs/ # JavaScript 模块支持
│ ├── CMakeLists.txt
│ ├── conversions.cpp
│ └── ...
│
├── everestpy/ # Python 模块支持
│ ├── CMakeLists.txt
│ ├── setup.cfg
│ └── src/
│ └── everest/
│
├── everestrs/ # Rust 模块支持(可选)
│ └── everestrs_sys/
│
├── include/ # 头文件
│ ├── compile_time_settings.hpp.in
│ └── ...
│
├── lib/ # 核心库实现
│ ├── config.cpp # 配置系统实现
│ └── ...
│
├── schemas/ # JSON Schema 定义
│ ├── config.yaml
│ ├── manifest.yaml
│ ├── interface.yaml
│ └── ...
│
├── src/ # 源代码
│ ├── manager.cpp # 主管理器实现
│ ├── controller/
│ └── ...
│
└── tests/ # 测试代码
├── CMakeLists.txt
├── test_config.cpp
├── test_configs/
├── test_modules/
└── test_interfaces/
graph LR
A[everest-framework] --> B[src]
A --> C[tests]
A --> D[schemas]
B --> B1[manager.cpp]
B --> B2[其他源文件]
C --> C1[test_config.cpp]
C --> C2[test_configs]
C --> C3[test_modules]
C --> C4[test_interfaces]
C2 --> C21[valid_config.yaml]
C2 --> C22[broken_config.yaml]
C2 --> C23[missing_module_config.yaml]
C2 --> C24[broken_manifest_config.yaml]
C2 --> C25[broken_manifest2_config.yaml]
C2 --> C26[valid_manifest_missing_interface_config.yaml]
核心组件
模块系统
从代码中可以看出,EVerest 框架支持多种语言的模块
classDiagram
class ModuleStartInfo {
+string name
+string printable_name
+Language language
+fs::path path
}
class Language {
<<enumeration>>
cpp
javascript
python
rust
}
ModuleStartInfo --> Language
配置系统
EVerest-Framework 的配置系统主要由 Config 类实现,负责加载、验证和处理配置文件,以及管理模块之间的依赖关系。配置系统使用 YAML 格式的配置文件来描述系统中的模块、它们的配置参数以及模块之间的连接关系。
graph TD
A[Config] --> B[active_modules]
B --> C[模块1]
B --> D[模块2]
C --> C1[module: 模块名称]
D --> D1[module: 模块名称]
配置系统工作流程
配置文件加载
配置系统首先从指定路径加载主配置文件(通常是 config.yaml ):
// 在 Config 构造函数中加载配置文件
fs::path config_path = rs->config_file;
try {
if (manager) {
EVLOG_info << fmt::format("Loading config file at: {}", fs::canonical(config_path).string());
}
auto complete_config = load_yaml(config_path);
// 尝试加载用户配置(如果存在)
auto user_config_path = config_path.parent_path() / "user-config" / config_path.filename();
if (fs::exists(user_config_path)) {
// 加载用户配置并合并到主配置
auto user_config = load_yaml(user_config_path);
complete_config.merge_patch(user_config);
}
// 其他处理...
}
配置验证
加载配置后,系统会使用 JSON Schema 验证配置的有效性:
json_validator validator(Config::loader, Config::format_checker);
validator.set_root_schema(this->_schemas.config);
auto patch = validator.validate(complete_config);
if (!patch.is_null()) {
// 使用默认值扩展配置
complete_config = complete_config.patch(patch);
}
类型和接口加载
配置系统还会加载类型定义和接口定义文件
// 加载类型文件
for (auto const& types_entry : fs::recursive_directory_iterator(this->rs->types_dir)) {
// 处理每个类型文件...
}
// 加载接口文件(在其他地方实现)
模块依赖解析
一个关键功能是解析模块之间的依赖关系,通过 resolve_all_requirements 方法实现:
void Config::resolve_all_requirements() {
// 遍历所有模块
for (auto& element : this->main.items()) {
const auto& module_id = element.key();
auto& module_config = element.value();
// 检查模块的需求是否在清单中定义
// ...
// 解析每个需求
for (auto& element : this->manifests[module_config["module"].get<std::string>()]["requires"].items()) {
const auto& requirement_id = element.key();
auto& requirement = element.value();
// 检查需求是否满足
// ...
// 检查连接数量是否符合要求
// ...
// 验证每个连接
for (uint64_t connection_num = 0; connection_num < connections.size(); connection_num++) {
// 验证连接的模块是否存在
// 验证接口是否匹配
// ...
}
}
}
}
配置访问
配置系统提供了多种方法来访问配置数据:
// 获取模块配置
ModuleConfigs Config::get_module_configs(const std::string& module_id) {
// ...
}
// 解析模块需求
json Config::resolve_requirement(const std::string& module_id, const std::string& requirement_id) {
// ...
}
// 获取接口定义
json Config::get_interface_definition(const std::string& interface_name) {
// ...
}
配置文件结构
EVerest 的主配置文件结构如下:
active_modules:
module_id_1:
module: "ModuleName1"
config_maps:
!module: { /* 模块级配置 */ }
implementation_id_1: { /* 实现级配置 */ }
connections:
requirement_id_1:
- module_id: "module_id_2"
implementation_id: "implementation_id_2"
module_id_2:
# 类似结构...
配置系统与模块执行
配置系统与模块执行紧密相关。在 manager.cpp 中,系统根据配置启动相应的模块:
// 根据模块语言类型执行不同的启动逻辑
static SubprocessHandle exec_cpp_module(const ModuleStartInfo& module_info, std::shared_ptr<RuntimeSettings> rs) {
// 启动 C++ 模块...
}
static SubprocessHandle exec_javascript_module(const ModuleStartInfo& module_info, std::shared_ptr<RuntimeSettings> rs) {
// 启动 JavaScript 模块...
}
// 还有 Python 和 Rust 模块的启动逻辑...
多语言支持
EVerest-Framework 支持多种编程语言的模块,通过特定的包装器实现:
- C++ 模块直接使用框架库
- JavaScript 模块通过 everestjs 包装器
- Python 模块通过 everestpy 包装器
- Rust 模块通过 everestrs 包装器(可选功能)
每种语言的包装器都提供了访问配置系统的接口,例如 Python 的 ModuleSetup 类:
class ModuleSetup:
@property
def configs(self) -> ModuleSetupConfigurations: ...
@property
def connections(self) -> dict[str, list[Fulfillment]]: ...
总结
EVerest-Framework 的配置系统是一个复杂而强大的系统,它通过以下步骤工作:
- 加载并验证 YAML 格式的配置文件
- 加载类型定义和接口定义
- 解析模块之间的依赖关系
- 验证模块连接的有效性
- 提供配置数据访问接口
- 根据配置启动相应的模块
这种设计使得 EVerest 能够支持模块化、多语言的开发方式,同时确保模块之间的依赖关系正确无误。
模块执行机制
EVerest-Framework 是一个支持多语言模块化开发的框架,它通过配置文件定义模块及其依赖关系,然后启动并管理这些模块。
graph TD
A[配置加载] --> B[配置验证]
B --> C[模块依赖解析]
C --> D[模块启动]
D --> E1[C++模块执行]
D --> E2[JavaScript模块执行]
D --> E3[Python模块执行]
D --> E4[Rust模块执行]
E1 --> F[模块间通信]
E2 --> F
E3 --> F
E4 --> F
模块执行流程图
sequenceDiagram
participant Manager as 管理器
participant Config as 配置系统
participant MQTT as MQTT抽象层
participant Module as 模块进程
Manager->>Config: 加载配置文件
Config->>Config: 验证配置
Config->>Config: 加载模块清单
Config->>Config: 解析模块依赖
Manager->>Manager: 准备模块启动信息
loop 对每个模块
Manager->>Module: 创建子进程
alt C++模块
Manager->>Module: exec_cpp_module()
else JavaScript模块
Manager->>Module: exec_javascript_module()
else Python模块
Manager->>Module: exec_python_module()
end
Module->>MQTT: 连接MQTT代理
Module->>Config: 获取模块配置
Module->>Module: 初始化模块
Module->>Manager: 报告就绪状态
end
Manager->>Manager: 监控模块运
模块启动流程
从代码中可以看出,模块启动主要由 start_modules 和 spawn_modules 函数处理:
sequenceDiagram
participant Manager
participant Config
participant ModuleStartInfo
participant SubprocessHandle
Manager->>Config: get_main_config()
Config-->>Manager: 返回配置信息
loop 遍历每个模块
Manager->>ModuleStartInfo: 创建模块启动信息
Manager->>+SubprocessHandle: spawn_modules()
SubprocessHandle->>SubprocessHandle: 根据语言类型选择执行方式
SubprocessHandle-->>-Manager: 返回进程句柄
end
模块执行代码分析
模块启动信息准备
首先,系统会从配置中获取所有需要启动的模块信息:
static std::map<pid_t, std::string> start_modules(Config& config, MQTTAbstraction& mqtt_abstraction,
const std::vector<std::string>& ignored_modules,
const std::vector<std::string>& standalone_modules,
std::shared_ptr<RuntimeSettings> rs, StatusFifo& status_fifo,
error::ErrorManager& err_manager) {
// ...
auto main_config = config.get_main_config();
modules_to_spawn.reserve(main_config.size());
for (const auto& module : main_config.items()) {
std::string module_name = module.key();
// ... 准备模块启动信息
}
// ...
}
模块执行函数
系统根据模块的语言类型选择不同的执行方式:
static std::map<pid_t, std::string> spawn_modules(const std::vector<ModuleStartInfo>& modules,
std::shared_ptr<RuntimeSettings> rs) {
std::map<pid_t, std::string> started_modules;
for (const auto& module : modules) {
auto handle = [&module, &rs]() -> SubprocessHandle {
switch (module.language) {
case ModuleStartInfo::Language::cpp:
return exec_cpp_module(module, rs);
case ModuleStartInfo::Language::javascript:
return exec_javascript_module(module, rs);
case ModuleStartInfo::Language::python:
return exec_python_module(module, rs);
default:
throw std::logic_error("Module language not in enum");
}
}();
// ...
}
// ...
}
不同语言模块的执行
C++ 模块执行
static SubprocessHandle exec_cpp_module(const ModuleStartInfo& module_info, std::shared_ptr<RuntimeSettings> rs) {
const auto exec_binary = module_info.path.c_str();
std::vector<std::string> arguments = {module_info.printable_name, "--prefix", rs->prefix.string(), "--conf",
rs->config_file.string(), "--module", module_info.name};
auto handle = create_subprocess();
if (handle.is_child()) {
auto argv_list = arguments_to_exec_argv(arguments);
execv(exec_binary, argv_list.data());
// ...
}
return handle;
}
JavaScript 模块执行
static SubprocessHandle exec_javascript_module(const ModuleStartInfo& module_info,
std::shared_ptr<RuntimeSettings> rs) {
// 设置环境变量
const auto node_modules_path = rs->prefix / defaults::LIB_DIR / defaults::NAMESPACE / "node_modules";
setenv("NODE_PATH", node_modules_path.c_str(), 0);
setenv("EV_MODULE", module_info.name.c_str(), 1);
setenv("EV_PREFIX", rs->prefix.c_str(), 0);
setenv("EV_CONF_FILE", rs->config_file.c_str(), 0);
// ...
const auto node_binary = "node";
std::vector<std::string> arguments = {
"node",
"--unhandled-rejections=strict",
module_info.path.string(),
};
auto handle = create_subprocess();
if (handle.is_child()) {
auto argv_list = arguments_to_exec_argv(arguments);
execvp(node_binary, argv_list.data());
// ...
}
return handle;
}
Python 模块执行
static SubprocessHandle exec_python_module(const ModuleStartInfo& module_info, std::shared_ptr<RuntimeSettings> rs) {
// 设置环境变量
const auto pythonpath = rs->prefix / defaults::LIB_DIR / defaults::NAMESPACE / "everestpy";
setenv("EV_MODULE", module_info.name.c_str(), 1);
setenv("EV_PREFIX", rs->prefix.c_str(), 0);
setenv("EV_CONF_FILE", rs->config_file.c_str(), 0);
setenv("PYTHONPATH", pythonpath.c_str(), 0);
// ...
const auto python_binary = "python3";
std::vector<std::string> arguments = {python_binary, module_info.path.c_str()};
auto handle = create_subprocess();
if (handle.is_child()) {
auto argv_list = arguments_to_exec_argv(arguments);
execvp(python_binary, argv_list.data());
// ...
}
return handle;
}
模块初始化和通信
模块启动后,会进行初始化并与其他模块通信:
graph LR
A[模块启动] --> B[创建模块实例]
B --> C[say_hello 获取模块设置]
C --> D[实现命令处理器]
D --> E[init_done 信号准备就绪]
E --> F[模块间通信]
Python 模块示例
Python 模块的初始化和通信流程:
class Module:
def __init__(self, module_id: str, session: RuntimeSession) -> None:
# 初始化模块
pass
def say_hello(self) -> ModuleSetup:
# 获取模块设置信息
pass
def implement_command(self, implementation_id: str, command_name: str,
handler: Callable[[dict], dict]) -> None:
# 实现命令处理器
pass
def init_done(self) -> None:
# 信号模块准备就绪
pass
def call_command(self, fulfillment: Fulfillment,
command_name: str, args: dict) -> None:
# 调用其他模块的命令
pass
模块间依赖和通信
模块间的依赖关系通过配置文件中的 connections 部分定义:
graph LR
A[模块A] -->|需求1| B[模块B]
A -->|需求2| C[模块C]
D[模块D] -->|需求3| A
在代码中,这些依赖关系通过 resolve_requirement 函数解析:
json Config::resolve_requirement(const std::string& module_id, const std::string& requirement_id) {
// 检查模块是否存在
auto module_name_it = this->module_names.find(module_id);
if (module_name_it == this->module_names.end()) {
EVLOG_AND_THROW(EverestApiError(fmt::format("Requested requirement id '{}' of module {} not found in config!",
requirement_id, printable_identifier(module_id))));
}
// 检查连接是否存在
auto& module_config = this->main[module_id];
std::string module_name = module_name_it->second;
auto& requirement = this->manifests[module_name]["requires"][requirement_id];
if (!module_config["connections"].contains(requirement_id)) {
return json::array(); // 如果配置中没有此需求的连接,返回空数组
}
// 根据连接数量返回不同格式
if (requirement["min_connections"] == 1 && requirement["max_connections"] == 1) {
return module_config["connections"][requirement_id].at(0);
}
return module_config["connections"][requirement_id];
}
总结
EVerest-Framework 的模块执行流程包括以下几个关键步骤:
- 加载和验证配置文件
- 解析模块依赖关系
- 准备模块启动信息
- 根据模块语言类型选择执行方式
- 创建子进程执行模块
- 模块初始化并建立通信
这种设计使得 EVerest 能够支持多语言模块化开发,并通过配置文件灵活地定义模块间的依赖关系。
测试系统
项目使用 Catch2 测试框架进行单元测试,主要测试配置解析功能:
graph LR
A[test_config.cpp] --> B[测试空配置]
A --> C[测试无效主目录]
A --> D[测试不存在的配置文件]
A --> E[测试损坏的配置文件]
A --> F[测试引用不存在模块的配置]
A --> G[测试使用损坏清单的模块]
A --> H[测试有效清单但引用无效接口]
A --> I[测试有效配置]
项目依赖
项目依赖了一些第三方库:
- CodeCoverage.cmake (用于代码覆盖率测试)
- 元编程宏库 (来自 libextobjc)
- ./EVerest-timer
- EVerest-log