diff --git a/content/Obsidian/OCPP/Everest/EVerest-framework.md b/content/Obsidian/OCPP/Everest/EVerest-framework.md new file mode 100644 index 000000000..60efb2d0e --- /dev/null +++ b/content/Obsidian/OCPP/Everest/EVerest-framework.md @@ -0,0 +1,572 @@ +--- +date: 2025-03-05 10:35 +updated: 2025-03-05 13:28 +tags: ocpp,Everest +link: false +share: true +publish: true +--- + +# 项目概述 + +EVerest-Framework 是一个支持多语言模块化开发的框架,主要用于电动汽车充电基础设施的开发。从代码中可以看出,它支持 C++、JavaScript、Rust 和 Python 编程语言的模块。 + +# 项目结构 + +```mermaid +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 框架支持多种语言的模块 + +```mermaid +classDiagram + class ModuleStartInfo { + +string name + +string printable_name + +Language language + +fs::path path + } + + class Language { + <> + cpp + javascript + python + } + + ModuleStartInfo --> Language +``` + +## 配置系统 + +EVerest-Framework 的配置系统主要由 Config 类实现,负责加载、验证和处理配置文件,以及管理模块之间的依赖关系。配置系统使用 YAML 格式的配置文件来描述系统中的模块、它们的配置参数以及模块之间的连接关系。 + +```mermaid +graph TD + A[Config] --> B[active_modules] + B --> C[模块1] + B --> D[模块2] + C --> C1[module: 模块名称] + D --> D1[module: 模块名称] +``` + +### 配置系统工作流程 + +#### 配置文件加载 + +配置系统首先从指定路径加载主配置文件(通常是 config.yaml ): + +```cpp +// 在 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 验证配置的有效性: + +```cpp +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); +} +``` + +#### 类型和接口加载 + +配置系统还会加载类型定义和接口定义文件 + +```cpp +// 加载类型文件 +for (auto const& types_entry : fs::recursive_directory_iterator(this->rs->types_dir)) { + // 处理每个类型文件... +} + +// 加载接口文件(在其他地方实现) +``` + +#### 模块依赖解析 + +一个关键功能是解析模块之间的依赖关系,通过 resolve_all_requirements 方法实现: + +```cpp +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()]["requires"].items()) { + const auto& requirement_id = element.key(); + auto& requirement = element.value(); + + // 检查需求是否满足 + // ... + + // 检查连接数量是否符合要求 + // ... + + // 验证每个连接 + for (uint64_t connection_num = 0; connection_num < connections.size(); connection_num++) { + // 验证连接的模块是否存在 + // 验证接口是否匹配 + // ... + } + } + } +} +``` + +#### 配置访问 + +配置系统提供了多种方法来访问配置数据: + +```cpp +// 获取模块配置 +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 的主配置文件结构如下: + +```yaml +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 中,系统根据配置启动相应的模块: + +```cpp +// 根据模块语言类型执行不同的启动逻辑 +static SubprocessHandle exec_cpp_module(const ModuleStartInfo& module_info, std::shared_ptr rs) { + // 启动 C++ 模块... +} + +static SubprocessHandle exec_javascript_module(const ModuleStartInfo& module_info, std::shared_ptr rs) { + // 启动 JavaScript 模块... +} + +// 还有 Python 和 Rust 模块的启动逻辑... +``` + +### 多语言支持 + +EVerest-Framework 支持多种编程语言的模块,通过特定的包装器实现: + +1. C++ 模块直接使用框架库 +2. JavaScript 模块通过 everestjs 包装器 +3. Python 模块通过 everestpy 包装器 +4. Rust 模块通过 everestrs 包装器(可选功能) + +每种语言的包装器都提供了访问配置系统的接口,例如 Python 的 ModuleSetup 类: + +```py +class ModuleSetup: + @property + def configs(self) -> ModuleSetupConfigurations: ... + + @property + def connections(self) -> dict[str, list[Fulfillment]]: ... +``` + +### 总结 + +EVerest-Framework 的配置系统是一个复杂而强大的系统,它通过以下步骤工作: + +1. 加载并验证 YAML 格式的配置文件 +2. 加载类型定义和接口定义 +3. 解析模块之间的依赖关系 +4. 验证模块连接的有效性 +5. 提供配置数据访问接口 +6. 根据配置启动相应的模块 + +这种设计使得 EVerest 能够支持模块化、多语言的开发方式,同时确保模块之间的依赖关系正确无误。 + +## 模块执行机制 + +EVerest-Framework 是一个支持多语言模块化开发的框架,它通过配置文件定义模块及其依赖关系,然后启动并管理这些模块。 + +```mermaid +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 +``` + +### 模块执行流程图 + +```mermaid +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` 函数处理: + +```mermaid +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 +``` + +### 模块执行代码分析 + +#### 模块启动信息准备 + +首先,系统会从配置中获取所有需要启动的模块信息: + +```cpp +static std::map start_modules(Config& config, MQTTAbstraction& mqtt_abstraction, + const std::vector& ignored_modules, + const std::vector& standalone_modules, + std::shared_ptr 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(); + // ... 准备模块启动信息 + } + // ... +} +``` + +#### 模块执行函数 + +系统根据模块的语言类型选择不同的执行方式: + +```cpp +static std::map spawn_modules(const std::vector& modules, + std::shared_ptr rs) { + std::map 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++ 模块执行 + +```cpp +static SubprocessHandle exec_cpp_module(const ModuleStartInfo& module_info, std::shared_ptr rs) { + const auto exec_binary = module_info.path.c_str(); + std::vector 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 模块执行 + +```cpp +static SubprocessHandle exec_javascript_module(const ModuleStartInfo& module_info, + std::shared_ptr 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 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 模块执行 + +```cpp +static SubprocessHandle exec_python_module(const ModuleStartInfo& module_info, std::shared_ptr 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 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; +} +``` + +### 模块初始化和通信 + +模块启动后,会进行初始化并与其他模块通信: + +```mermaid +graph LR + A[模块启动] --> B[创建模块实例] + B --> C[say_hello 获取模块设置] + C --> D[实现命令处理器] + D --> E[init_done 信号准备就绪] + E --> F[模块间通信] +``` + +#### Python 模块示例 + +Python 模块的初始化和通信流程: + +```py +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 部分定义: + +```mermaid +graph LR + A[模块A] -->|需求1| B[模块B] + A -->|需求2| C[模块C] + D[模块D] -->|需求3| A +``` + +在代码中,这些依赖关系通过 `resolve_requirement` 函数解析: + +```cpp +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 的模块执行流程包括以下几个关键步骤: + +1. 加载和验证配置文件 +2. 解析模块依赖关系 +3. 准备模块启动信息 +4. 根据模块语言类型选择执行方式 +5. 创建子进程执行模块 +6. 模块初始化并建立通信 + +这种设计使得 EVerest 能够支持多语言模块化开发,并通过配置文件灵活地定义模块间的依赖关系。 + +# 测试系统 + +项目使用 Catch2 测试框架进行单元测试,主要测试配置解析功能: + +```mermaid +graph LR + A[test_config.cpp] --> B[测试空配置] + A --> C[测试无效主目录] + A --> D[测试不存在的配置文件] + A --> E[测试损坏的配置文件] + A --> F[测试引用不存在模块的配置] + A --> G[测试使用损坏清单的模块] + A --> H[测试有效清单但引用无效接口] + A --> I[测试有效配置] +``` + +# 项目依赖 + +项目依赖了一些第三方库: + +- CodeCoverage.cmake (用于代码覆盖率测试) +- 元编程宏库 (来自 libextobjc)