本指南面向第三方开发者,说明如何为 EnerOS 开发协议适配器、Agent 策略与分析模块插件。EnerOS v0.27.0 引入插件框架,v0.28.0 增加 plugin-daemon 进程隔离模式。
EnerOS 插件系统支持三类插件,以动态库(.so/.dll/.dylib)形式接入系统:
| 插件类型 | 用途 | 实现 trait | 权限上限 |
|---|---|---|---|
| Protocol | 协议适配器(IEC 104/61850/Modbus 等扩展) | ProtocolPlugin |
设备访问 |
| Agent | Agent 策略(调度、预测、操作等扩展) | AgentPlugin |
Operator |
| Analysis | 分析模块(潮流、状态估计等扩展) | AnalysisPlugin |
只读分析 |
插件通过 Ed25519 签名验证保障来源可信,通过 seccomp 沙箱与 cgroups 资源配额实现隔离。v0.28.0 起,插件默认在 plugin-daemon 独立进程中加载(Daemon 模式),崩溃不影响主进程;开发环境可使用 Inline 模式(同进程加载)。
插件开发依赖以下 crate:
eneros-sdk:开发者 SDK,封装常用类型与辅助函数eneros-plugin:插件框架核心,提供 trait 定义与错误类型eneros-plugin-macros:#[eneros_plugin]过程宏,自动生成 C ABI 入口函数
在插件的 Cargo.toml 中添加依赖:
[dependencies]
eneros-plugin = { path = "../eneros-plugin" }
eneros-plugin-macros = { path = "../eneros-plugin-macros" }
eneros-sdk = { path = "../eneros-sdk", features = ["plugin"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"- Rust 1.75+,edition 2021
- 编译为动态库(
crate-type = ["cdylib"])
使用 #[eneros_plugin] 宏创建第一个插件。以下示例实现一个最小的分析插件:
// src/lib.rs
use std::sync::OnceLock;
use eneros_plugin::{Plugin, PluginMetadata, PluginResult, PluginType};
use eneros_plugin_macros::eneros_plugin;
struct MyAnalysisPlugin;
/// 静态元数据存储(OnceLock 保证线程安全初始化)
static METADATA: OnceLock<PluginMetadata> = OnceLock::new();
#[eneros_plugin(
name = "my-analysis",
version = "1.0.0",
api_version = "0.28.0",
plugin_type = "analysis",
author = "Your Name",
description = "My first analysis plugin"
)]
#[async_trait::async_trait]
impl Plugin for MyAnalysisPlugin {
fn metadata(&self) -> &PluginMetadata {
METADATA.get_or_init(|| PluginMetadata {
name: "my-analysis".to_string(),
version: "1.0.0".to_string(),
api_version: "0.28.0".to_string(),
plugin_type: PluginType::Analysis,
description: "My first analysis plugin".to_string(),
})
}
fn plugin_type(&self) -> PluginType {
PluginType::Analysis
}
async fn init(&mut self) -> PluginResult<()> {
Ok(())
}
async fn start(&mut self) -> PluginResult<()> {
Ok(())
}
async fn stop(&mut self) -> PluginResult<()> {
Ok(())
}
}Cargo.toml:
[package]
name = "my-analysis-plugin"
version = "1.0.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
eneros-plugin = { path = "../../crates/eneros-plugin" }
eneros-plugin-macros = { path = "../../crates/eneros-plugin-macros" }
async-trait = "0.1"
tokio = { version = "1", features = ["full"] }#[eneros_plugin] 宏会自动生成 C ABI 入口函数(eneros_plugin_create / eneros_plugin_destroy / eneros_plugin_metadata)及 vtable 全局变量,无需手动编写。
编译:
cargo build --release
# 产物:target/release/libmy_analysis_plugin.so(Linux)
# target/release/my_analysis_plugin.dll(Windows)
# target/release/libmy_analysis_plugin.dylib(macOS)协议插件实现 ProtocolPlugin trait,用于扩展 EnerOS 支持的设备协议。
use std::sync::OnceLock;
use async_trait::async_trait;
use eneros_plugin::protocol::{
PluginDataPoint, PluginDataQuality, PluginDataValue, ProtocolAdapterInstance, ProtocolPlugin,
ProtocolPluginConfig,
};
use eneros_plugin::{Plugin, PluginError, PluginMetadata, PluginResult, PluginType};
use eneros_plugin_macros::eneros_plugin;
struct Iec103Plugin;
static METADATA: OnceLock<PluginMetadata> = OnceLock::new();
#[eneros_plugin(
name = "iec103-driver",
version = "1.2.0",
api_version = "0.28.0",
plugin_type = "protocol",
author = "EnerOS",
description = "IEC 60870-5-103 protocol driver"
)]
#[async_trait::async_trait]
impl Plugin for Iec103Plugin {
fn metadata(&self) -> &PluginMetadata {
METADATA.get_or_init(|| PluginMetadata {
name: "iec103-driver".to_string(),
version: "1.2.0".to_string(),
api_version: "0.28.0".to_string(),
plugin_type: PluginType::Protocol,
description: "IEC 60870-5-103 protocol driver".to_string(),
})
}
fn plugin_type(&self) -> PluginType {
PluginType::Protocol
}
async fn init(&mut self) -> PluginResult<()> {
Ok(())
}
async fn start(&mut self) -> PluginResult<()> {
Ok(())
}
async fn stop(&mut self) -> PluginResult<()> {
Ok(())
}
}
#[async_trait]
impl ProtocolPlugin for Iec103Plugin {
fn protocol_name(&self) -> &str {
"iec103"
}
fn protocol_type_str(&self) -> String {
// 默认实现返回 "custom:<protocol_name>"
// 与 eneros_device::ProtocolType::Custom(name) 的 serde 表示一致
format!("custom:{}", self.protocol_name())
}
fn description(&self) -> &str {
"IEC 60870-5-103 protocol driver"
}
async fn create_adapter(
&self,
_config: &ProtocolPluginConfig,
) -> Result<Box<dyn ProtocolAdapterInstance>, PluginError> {
// 每次调用返回独立的适配器实例(对应一次设备连接)
Ok(Box::new(Iec103Adapter::new()))
}
}
struct Iec103Adapter {
connected: bool,
}
impl Iec103Adapter {
fn new() -> Self {
Self { connected: false }
}
}
#[async_trait]
impl ProtocolAdapterInstance for Iec103Adapter {
async fn connect(&mut self) -> Result<(), PluginError> {
// 建立连接
self.connected = true;
Ok(())
}
async fn disconnect(&mut self) -> Result<(), PluginError> {
// 断开连接
self.connected = false;
Ok(())
}
async fn read(&self, address: &str) -> Result<PluginDataPoint, PluginError> {
// 读取数据点
Ok(PluginDataPoint {
address: address.to_string(),
value: PluginDataValue::Float32(0.0),
timestamp: 0,
quality: if self.connected {
PluginDataQuality::Good
} else {
PluginDataQuality::Offline
},
})
}
async fn write(
&mut self,
_address: &str,
_value: &PluginDataValue,
) -> Result<(), PluginError> {
// 写入数据
Ok(())
}
fn name(&self) -> &str {
"iec103-adapter"
}
fn is_connected(&self) -> bool {
self.connected
}
}设备层通过协议类型字符串(custom:iec103)查找对应插件。
Agent 插件实现 AgentPlugin trait,用于扩展 Agent 的策略逻辑。Agent 插件的权限上限为 Operator,即使声明更高权限也会被系统强制降级。
use std::sync::OnceLock;
use async_trait::async_trait;
use eneros_core::AuthorityLevel;
use eneros_plugin::agent::{
AgentPlugin, AgentPluginAction, AgentPluginConfig, AgentPluginEvent, AgentStrategyInstance,
StrategyPriority,
};
use eneros_plugin::{Plugin, PluginError, PluginMetadata, PluginResult, PluginType};
use eneros_plugin_macros::eneros_plugin;
struct CustomDispatchStrategy;
static METADATA: OnceLock<PluginMetadata> = OnceLock::new();
#[eneros_plugin(
name = "custom-dispatch",
version = "0.1.0",
api_version = "0.28.0",
plugin_type = "agent",
author = "Your Name",
description = "Custom dispatch strategy"
)]
#[async_trait::async_trait]
impl Plugin for CustomDispatchStrategy {
fn metadata(&self) -> &PluginMetadata {
METADATA.get_or_init(|| PluginMetadata {
name: "custom-dispatch".to_string(),
version: "0.1.0".to_string(),
api_version: "0.28.0".to_string(),
plugin_type: PluginType::Agent,
description: "Custom dispatch strategy".to_string(),
})
}
fn plugin_type(&self) -> PluginType {
PluginType::Agent
}
async fn init(&mut self) -> PluginResult<()> {
Ok(())
}
async fn start(&mut self) -> PluginResult<()> {
Ok(())
}
async fn stop(&mut self) -> PluginResult<()> {
Ok(())
}
}
#[async_trait]
impl AgentPlugin for CustomDispatchStrategy {
fn strategy_name(&self) -> &str {
"custom-dispatch"
}
fn description(&self) -> &str {
"Custom dispatch strategy"
}
fn authority_level(&self) -> AuthorityLevel {
// 系统会通过 enforce_authority_limit 强制降级到 Operator 上限
// 插件即使声明 Emergency 或 Supervisor 也无法获得高于 Operator 的权限
AuthorityLevel::Operator
}
fn priority(&self) -> StrategyPriority {
// 用于多插件冲突解决,高优先级插件的动作优先执行
StrategyPriority::Normal
}
async fn create_agent(
&self,
config: &AgentPluginConfig,
) -> Result<Box<dyn AgentStrategyInstance>, PluginError> {
Ok(Box::new(CustomDispatchAgent {
agent_id: config.agent_id.clone(),
agent_type: config.agent_type.clone(),
}))
}
}
struct CustomDispatchAgent {
agent_id: String,
agent_type: String,
}
#[async_trait]
impl AgentStrategyInstance for CustomDispatchAgent {
fn agent_id(&self) -> &str {
&self.agent_id
}
fn agent_type(&self) -> &str {
&self.agent_type
}
async fn handle_event(
&mut self,
_event: &AgentPluginEvent,
) -> Result<Vec<AgentPluginAction>, PluginError> {
Ok(vec![AgentPluginAction::NoOp])
}
async fn tick(&mut self) -> Result<Vec<AgentPluginAction>, PluginError> {
Ok(vec![])
}
}分析插件实现 AnalysisPlugin trait,输入输出为 serde_json::Value,用于扩展分析能力。
use std::sync::OnceLock;
use async_trait::async_trait;
use eneros_plugin::analysis::{AnalysisPlugin, AnalysisResult};
use eneros_plugin::{Plugin, PluginError, PluginMetadata, PluginResult, PluginType};
use eneros_plugin_macros::eneros_plugin;
struct ReliabilityAnalysis;
static METADATA: OnceLock<PluginMetadata> = OnceLock::new();
#[eneros_plugin(
name = "reliability-analysis",
version = "1.0.0",
api_version = "0.28.0",
plugin_type = "analysis",
author = "Your Name",
description = "Power grid reliability analysis"
)]
#[async_trait::async_trait]
impl Plugin for ReliabilityAnalysis {
fn metadata(&self) -> &PluginMetadata {
METADATA.get_or_init(|| PluginMetadata {
name: "reliability-analysis".to_string(),
version: "1.0.0".to_string(),
api_version: "0.28.0".to_string(),
plugin_type: PluginType::Analysis,
description: "Power grid reliability analysis".to_string(),
})
}
fn plugin_type(&self) -> PluginType {
PluginType::Analysis
}
async fn init(&mut self) -> PluginResult<()> {
Ok(())
}
async fn start(&mut self) -> PluginResult<()> {
Ok(())
}
async fn stop(&mut self) -> PluginResult<()> {
Ok(())
}
}
impl AnalysisPlugin for ReliabilityAnalysis {
fn analyze_type(&self) -> &str {
"reliability"
}
fn description(&self) -> &str {
"Power grid reliability analysis"
}
fn analyze(
&self,
input: &serde_json::Value,
) -> Result<AnalysisResult<serde_json::Value>, PluginError> {
// 解析输入字段
let load_level = input["load_level"]
.as_f64()
.ok_or_else(|| PluginError::InvalidManifest("missing load_level".into()))?;
// 执行分析逻辑
let sai = 1.0 - load_level * 0.01;
let caidi = 2.5;
// 构造输出 JSON
let output = serde_json::json!({
"sai": sai,
"caidi": caidi,
"assessment": if sai > 0.999 { "good" } else { "needs_attention" }
});
// AnalysisResult::new 创建收敛结果(converged=true, iterations=1, warnings=[])
Ok(AnalysisResult::new(output))
}
}每个插件需附带 manifest.toml 清单文件,描述元数据、依赖与安全信息:
[plugin]
name = "iec104-driver"
version = "1.2.0"
api_version = "0.28.0"
plugin_type = "Protocol"
description = "IEC 104 protocol driver"
author = "EnerOS"
[dependencies]
plugins = ["core-mbus"] # 依赖的其他插件名列表
[security]
signer = "eneros-trusted" # 签名者标识| 段 | 字段 | 类型 | 说明 |
|---|---|---|---|
[plugin] |
name |
String | 插件名称(唯一标识) |
version |
String | 插件版本(语义化版本) | |
api_version |
String | 插件 API 版本(与 EnerOS API 版本兼容性检查) | |
plugin_type |
String | 插件类型(Protocol / Agent / Analysis) | |
description |
String | 插件描述 | |
author |
String | 插件作者 | |
[dependencies] |
plugins |
Vec<String> | 依赖的其他插件名列表 |
[security] |
signer |
String | 签名者标识 |
- 0.x 版本:比较次版本号(MINOR),次版本号相同即兼容
- 1.x+ 版本:比较主版本号(MAJOR),主版本号相同即兼容
插件依赖通过 Kahn 算法进行拓扑排序,确定加载顺序。若检测到循环依赖,加载将被拒绝。
生产环境中插件必须经过 Ed25519 签名验证。签名流程复用 v0.22.0 OTA 签名基础设施(ed25519-dalek)。
enerosctl plugin gen-keys --output /etc/eneros/keys/生成两个文件:
private.key:私钥(base64 编码,妥善保管)public.pub:公钥(base64 编码,部署到设备)
enerosctl plugin sign ./libiec104_driver.so /etc/eneros/keys/private.key生成签名文件 ./libiec104_driver.so.sig。
# 验证签名(不加载插件)
enerosctl plugin verify ./libiec104_driver.so
# 指定签名文件路径
enerosctl plugin verify ./libiec104_driver.so --sig ./custom.sig验证结果:
Valid:签名有效,签名者匹配可信公钥Invalid:签名无效(文件被篡改)Missing:未找到签名文件UntrustedSigner:签名有效但签名者不在可信公钥列表中
可信公钥部署在 /etc/eneros/keys/ 目录。plugin.toml 中 require_signature = true 时,未签名或签名不可信的插件将被拒绝加载。
开发环境可使用 --skip-signature 跳过验证:
enerosctl plugin load ./my_plugin.so --skip-signature插件在 plugin-daemon 进程中运行,受 seccomp 与 cgroups 双重约束。
以下 syscall 被 seccomp BPF 规则禁止:
| syscall | 禁止原因 |
|---|---|
mount |
禁止挂载文件系统 |
reboot |
禁止重启系统 |
kexec_load |
禁止加载新内核 |
init_module / finit_module |
禁止加载内核模块 |
ptrace |
禁止进程追踪 |
setuid / setgid |
禁止权限提升 |
通过 plugin.toml 配置资源上限:
[plugin.sandbox]
enable_seccomp = true
enable_quota = true
default_cpu_percent = 50 # CPU 上限(百分比)
default_memory_mb = 256 # 内存上限(MB)
allowed_paths = ["/var/lib/eneros/data"] # 允许访问的路径
denied_paths = ["/etc/shadow"] # 禁止访问的路径
allowed_network = ["tcp:2404"] # 允许的网络访问插件 panic 或段错误时,plugin-daemon 进程崩溃,主进程不受影响。主进程检测到 plugin-daemon 退出后可自动重启并重新加载插件。
| 模式 | 说明 | 适用场景 |
|---|---|---|
| Daemon(默认) | 插件在 plugin-daemon 独立进程中加载,通过 IPC 通信 | 生产环境 |
| Inline | 插件在同进程加载,直接函数调用 | 开发/测试,低延迟场景 |
通过 plugin.toml 配置默认模式:
[plugin]
default_mode = "daemon"单个插件可在 manifest 中指定模式。
Daemon 模式下,主进程通过 PluginDaemonClient(eneros-plugin/src/ipc.rs)与 plugin-daemon 通信:
- 主进程发送
DaemonRequest(JSON 行协议 over Unix socket / TCP) - plugin-daemon 返回
DaemonResponse
# 加载插件(验证签名 → 加载库 → 初始化 → 启动)
enerosctl plugin load ./libiec104_driver.so
# 卸载插件(停止 → 卸载库)
enerosctl plugin unload iec104-driver
# 查看插件详情
enerosctl plugin info iec104-driver
# 启用/禁用插件
enerosctl plugin enable iec104-driver
enerosctl plugin disable iec104-driver插件状态机遵循以下转换:
Loaded → Initialized → Starting → Running → Stopping → Stopped
↘ Crashed
↘ Failed
非法状态转换会返回 InvalidStateTransition 错误。
将插件动态库、manifest.toml、签名文件打包为 tar.gz:
tar czf iec104-driver-1.2.0.tar.gz \
libiec104_driver.so \
manifest.toml \
libiec104_driver.so.sig将打包文件上传至插件市场索引服务器,附带公钥信息以便消费者验证。
插件市场维护插件索引(名称、版本、签名者、下载地址),消费者通过 enerosctl plugin list 查看可用插件。
以下示例从零开发一个简单的自定义协议插件,包含完整代码、清单、签名与部署流程。
mkdir -p my-protocol-plugin/src
cd my-protocol-plugin[package]
name = "my-protocol-plugin"
version = "1.0.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
eneros-plugin = { path = "../eneros/crates/eneros-plugin" }
eneros-plugin-macros = { path = "../eneros/crates/eneros-plugin-macros" }
async-trait = "0.1"
tokio = { version = "1", features = ["full"] }use std::collections::HashMap;
use std::sync::OnceLock;
use async_trait::async_trait;
use eneros_plugin::protocol::{
PluginDataPoint, PluginDataQuality, PluginDataValue, ProtocolAdapterInstance, ProtocolPlugin,
ProtocolPluginConfig,
};
use eneros_plugin::{Plugin, PluginError, PluginMetadata, PluginResult, PluginType};
use eneros_plugin_macros::eneros_plugin;
struct MyProtocolPlugin;
static METADATA: OnceLock<PluginMetadata> = OnceLock::new();
#[eneros_plugin(
name = "my-protocol",
version = "1.0.0",
api_version = "0.28.0",
plugin_type = "protocol",
author = "Example Author",
description = "Example custom protocol adapter"
)]
#[async_trait::async_trait]
impl Plugin for MyProtocolPlugin {
fn metadata(&self) -> &PluginMetadata {
METADATA.get_or_init(|| PluginMetadata {
name: "my-protocol".to_string(),
version: "1.0.0".to_string(),
api_version: "0.28.0".to_string(),
plugin_type: PluginType::Protocol,
description: "Example custom protocol adapter".to_string(),
})
}
fn plugin_type(&self) -> PluginType {
PluginType::Protocol
}
async fn init(&mut self) -> PluginResult<()> {
Ok(())
}
async fn start(&mut self) -> PluginResult<()> {
Ok(())
}
async fn stop(&mut self) -> PluginResult<()> {
Ok(())
}
}
#[async_trait]
impl ProtocolPlugin for MyProtocolPlugin {
fn protocol_name(&self) -> &str {
"my-protocol"
}
fn description(&self) -> &str {
"Example custom protocol adapter"
}
async fn create_adapter(
&self,
_config: &ProtocolPluginConfig,
) -> Result<Box<dyn ProtocolAdapterInstance>, PluginError> {
Ok(Box::new(MyAdapter::new()))
}
}
struct MyAdapter {
connected: bool,
data: HashMap<String, PluginDataValue>,
}
impl MyAdapter {
fn new() -> Self {
Self {
connected: false,
data: HashMap::new(),
}
}
}
#[async_trait]
impl ProtocolAdapterInstance for MyAdapter {
async fn connect(&mut self) -> Result<(), PluginError> {
self.connected = true;
Ok(())
}
async fn disconnect(&mut self) -> Result<(), PluginError> {
self.connected = false;
Ok(())
}
async fn read(&self, address: &str) -> Result<PluginDataPoint, PluginError> {
if !self.connected {
return Err(PluginError::InitFailed("not connected".into()));
}
let value = self
.data
.get(address)
.cloned()
.unwrap_or(PluginDataValue::Float32(0.0));
Ok(PluginDataPoint {
address: address.to_string(),
value,
timestamp: 0,
quality: PluginDataQuality::Good,
})
}
async fn write(
&mut self,
address: &str,
value: &PluginDataValue,
) -> Result<(), PluginError> {
if !self.connected {
return Err(PluginError::InitFailed("not connected".into()));
}
self.data.insert(address.to_string(), value.clone());
Ok(())
}
fn name(&self) -> &str {
"my-adapter"
}
fn is_connected(&self) -> bool {
self.connected
}
}[plugin]
name = "my-protocol"
version = "1.0.0"
api_version = "0.28.0"
plugin_type = "Protocol"
description = "Example custom protocol adapter"
author = "Example Author"
[dependencies]
plugins = []
[security]
signer = "example-author"cargo build --release产物:target/release/libmy_protocol_plugin.so
# 生成密钥对(首次)
enerosctl plugin gen-keys --output /etc/eneros/keys/
# 签名
enerosctl plugin sign ./target/release/libmy_protocol_plugin.so /etc/eneros/keys/private.key
# 验证
enerosctl plugin verify ./target/release/libmy_protocol_plugin.so# 复制到插件目录
cp ./target/release/libmy_protocol_plugin.so /var/lib/eneros/plugins/
cp ./target/release/libmy_protocol_plugin.so.sig /var/lib/eneros/plugins/
cp manifest.toml /var/lib/eneros/plugins/my-protocol.toml
# 加载
enerosctl plugin load /var/lib/eneros/plugins/libmy_protocol_plugin.so
# 验证加载状态
enerosctl plugin list
enerosctl plugin info my-protocol编写单元测试验证插件逻辑:
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_adapter_connect_read_write() {
let mut adapter = MyAdapter::new();
assert!(!adapter.connected);
adapter.connect().await.unwrap();
assert!(adapter.connected);
adapter.write("reg.1", &PluginDataValue::Int32(42)).await.unwrap();
let dp = adapter.read("reg.1").await.unwrap();
assert_eq!(dp.value, PluginDataValue::Int32(42));
adapter.disconnect().await.unwrap();
assert!(!adapter.connected);
}
#[test]
fn test_protocol_type_str() {
let plugin = MyProtocolPlugin;
assert_eq!(plugin.protocol_name(), "my-protocol");
assert_eq!(plugin.protocol_type_str(), "custom:my-protocol");
}
}