自定义插件开发
本教程演示如何为 EnerOS 开发一个自定义插件,使其支持私有协议解析或第三方算法接入。EnerOS 插件基于动态库(cdylib)与 trait 接口实现热加载,无需重启主进程即可扩展系统能力。本教程将开发一个完整的 IEC 60870-5-103(继电保护设备协议)插件,从零开始直至发布到插件市场。
插件架构概览
EnerOS 插件系统由以下组件构成:
┌─────────────────────────────────────────────────────────────┐
│ EnerOS 主进程 │
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌─────────────┐ │
│ │ PluginRegistry │──→│ PluginLoader │──→│ Sandbox │ │
│ │ (注册表) │ │ (动态库加载) │ │ (seccomp) │ │
│ └────────────────┘ └────────────────┘ └─────────────┘ │
│ │ │ │ │
│ │ │ │ │
│ ┌──────▼───────┐ ┌────────▼─────────┐ ┌──────▼──────┐ │
│ │ Protocol │ │ Agent Strategy │ │ Analysis │ │
│ │ Plugin Slot │ │ Plugin Slot │ │ Plugin Slot │ │
│ └──────┬───────┘ └────────┬─────────┘ └──────┬──────┘ │
└─────────┼──────────────────────┼───────────────────┼────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ libiec103.so │ │ libmystrat.so│ │ liboptflow.so│
│ (本教程产物) │ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
EnerOS 支持四类插件:
| 插件类型 | 实现接口 | 用途 |
|---|---|---|
Protocol | ProtocolPlugin trait | 协议适配器(IEC 103、Modbus、私有协议) |
Agent | AgentPlugin trait | Agent 决策策略(电压控制、故障恢复) |
Analysis | AnalysisPlugin trait | 分析模块(状态估计、谐波分析) |
Strategy | StrategyPackRef | 策略包(可热替换的 Agent 决策包) |
准备工作
- 安装 Rust 1.78+ 与
cargo - 阅读 项目结构,了解 crate 划分
- 熟悉动态库(cdylib)的基本概念
本教程涉及以下 crate:
| Crate | 作用 |
|---|---|
eneros-plugin | 插件框架核心(trait、loader、registry、sandbox) |
eneros-plugin-macros | 过程宏(简化插件声明) |
eneros-plugin-api | 稳定的插件 API(ABI 兼容层) |
步骤 1:创建插件 crate
使用 cargo new 创建一个动态库 crate:
cargo new --lib iec103-driver
cd iec103-driver
在 Cargo.toml 中声明插件接口依赖:
[package]
name = "iec103-driver"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <you@example.com>"]
description = "IEC 60870-5-103 protocol driver for EnerOS"
license = "Apache-2.0"
[lib]
name = "iec103"
crate-type = ["cdylib", "rlib"] # 同时生成动态库与静态库
[dependencies]
eneros-plugin = { path = "../eneros/crates/eneros-plugin", version = "0.47" }
async-trait = "0.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["rt", "net", "io-util"] }
tracing = "0.1"
[dev-dependencies]
tokio = { version = "1", features = ["full", "test-util"] }
:::tip crate-type 选择
cdylib 生成 .so(Linux)/ .dll(Windows)/ .dylib(macOS),供主进程动态加载。rlib 用于单元测试时静态链接。
:::
步骤 2:编写插件清单
每个插件必须提供一个 TOML 格式的清单(manifest),描述元数据、依赖与安全信息:
# iec103-driver/plugin.toml
[plugin]
name = "iec103-driver"
version = "0.1.0"
api_version = "0.47.0" # 必须与主程序 API 版本兼容
plugin_type = "Protocol" # Protocol / Agent / Analysis / Strategy
description = "IEC 60870-5-103 继电保护设备通信协议驱动"
author = "EnerOS Community"
[dependencies]
plugins = [] # 不依赖其他插件
[security]
signer = "eneros-trusted" # 签名者标识
public_key = "ed25519:Base64..." # 公钥(用于验证签名)
清单字段说明:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
plugin.name | String | 是 | 插件唯一标识(与 crate 名一致) |
plugin.version | String | 是 | 语义化版本 |
plugin.api_version | String | 是 | 编译时使用的 EnerOS API 版本 |
plugin.plugin_type | Enum | 是 | 插件类型(Protocol/Agent/Analysis/Strategy) |
plugin.description | String | 否 | 人类可读描述 |
plugin.author | String | 否 | 作者信息 |
dependencies.plugins | Vec | 否 | 依赖的其他插件名 |
security.signer | String | 否 | 签名者标识 |
security.public_key | String | 否 | Ed25519 公钥 |
步骤 3:实现协议插件 trait
实现 ProtocolPlugin trait,处理上行报文并产出时序点位。下面是完整的 IEC 103 驱动实现:
// src/lib.rs
use eneros_plugin::protocol::{ProtocolPlugin, ProtocolAdapterInstance, ProtocolPluginConfig};
use eneros_plugin::error::{PluginError, PluginResult};
use eneros_plugin::manifest::{PluginManifest, PluginMetadata, PluginType};
use async_trait::async_trait;
use std::sync::Arc;
use parking_lot::Mutex;
use tracing::{info, debug, warn};
/// IEC 103 协议插件
#[derive(Debug)]
pub struct Iec103Plugin {
metadata: PluginMetadata,
/// 已创建的适配器实例数(用于诊断)
instance_count: Arc<Mutex<u32>>,
}
impl Iec103Plugin {
pub fn new() -> Self {
Self {
metadata: PluginMetadata {
name: "iec103-driver".into(),
version: "0.1.0".into(),
api_version: "0.47.0".into(),
plugin_type: PluginType::Protocol,
description: "IEC 60870-5-103 继电保护设备通信协议驱动".into(),
author: "EnerOS Community".into(),
},
instance_count: Arc::new(Mutex::new(0)),
}
}
pub fn instance_count(&self) -> u32 {
*self.instance_count.lock()
}
}
#[async_trait]
impl ProtocolPlugin for Iec103Plugin {
fn protocol_name(&self) -> &str {
"iec103"
}
fn protocol_type_str(&self) -> String {
"custom:iec103".into()
}
fn description(&self) -> &str {
"IEC 60870-5-103 继电保护设备通信协议,用于接入保护测控装置"
}
/// 创建协议适配器实例(每次设备连接调用一次)
async fn create_adapter(
&self,
config: &ProtocolPluginConfig,
) -> Result<Box<dyn ProtocolAdapterInstance>, PluginError> {
info!(
addr = %config.address,
timeout_ms = config.timeout_ms,
"创建 IEC 103 适配器实例"
);
let mut count = self.instance_count.lock();
*count += 1;
let adapter = Iec103Adapter::new(
config.address.clone(),
config.timeout_ms,
config.protocol_config.clone(),
)?;
Ok(Box::new(adapter))
}
}
/// IEC 103 协议适配器实例(对应一次设备连接)
pub struct Iec103Adapter {
address: String,
timeout_ms: u64,
/// 设备地址(IEC 103 中的 ASDU 地址)
device_address: u8,
/// TCP 连接(连接后才有值)
stream: Option<tokio::net::TcpStream>,
/// 接收回调
point_callback: Option<Arc<dyn Fn(&str, f64) + Send + Sync>>,
}
impl Iec103Adapter {
fn new(
address: String,
timeout_ms: u64,
protocol_config: serde_json::Value,
) -> Result<Self, PluginError> {
let device_address = protocol_config
.get("device_address")
.and_then(|v| v.as_u64())
.unwrap_or(1) as u8;
Ok(Self {
address,
timeout_ms,
device_address,
stream: None,
point_callback: None,
})
}
}
#[async_trait]
impl ProtocolAdapterInstance for Iec103Adapter {
/// 连接设备
async fn connect(&mut self) -> Result<(), PluginError> {
debug!(addr = %self.address, "连接 IEC 103 设备");
let stream = tokio::time::timeout(
std::time::Duration::from_millis(self.timeout_ms),
tokio::net::TcpStream::connect(&self.address),
)
.await
.map_err(|_| PluginError::Timeout(format!("连接超时: {}", self.address)))?
.map_err(|e| PluginError::Connection(format!("连接失败: {}", e)))?;
// 设置读写超时
let _ = stream.set_nodelay(true);
self.stream = Some(stream);
info!(addr = %self.address, device = self.device_address, "IEC 103 设备已连接");
Ok(())
}
/// 断开连接
async fn disconnect(&mut self) -> Result<(), PluginError> {
if let Some(stream) = self.stream.take() {
debug!(addr = %self.address, "断开 IEC 103 设备");
// 显式 drop 关闭连接
drop(stream);
}
Ok(())
}
/// 发送报文(原始字节)
async fn send(&mut self, data: &[u8]) -> Result<(), PluginError> {
use tokio::io::AsyncWriteExt;
let stream = self.stream.as_mut()
.ok_or_else(|| PluginError::NotConnected("IEC 103 未连接".into()))?;
stream.write_all(data).await
.map_err(|e| PluginError::Io(format!("发送失败: {}", e)))?;
debug!(len = data.len(), "已发送报文");
Ok(())
}
/// 接收报文(返回原始字节与解析后的点位)
async fn receive(&mut self) -> Result<Vec<(String, f64)>, PluginError> {
use tokio::io::AsyncReadExt;
let stream = self.stream.as_mut()
.ok_or_else(|| PluginError::NotConnected("IEC 103 未连接".into()))?;
// 读取 IEC 103 帧头:固定 4 字节(10H + LEN + LEN + 68H)
let mut header = [0u8; 4];
stream.read_exact(&mut header).await
.map_err(|e| PluginError::Io(format!("读取帧头失败: {}", e)))?;
if header[0] != 0x10 || header[3] != 0x68 {
return Err(PluginError::Protocol(format!(
"无效帧头: {:02X?} (期望 10 .. 68)", header
)));
}
let data_len = header[1] as usize;
if data_len == 0 || data_len > 253 {
return Err(PluginError::Protocol(format!(
"数据长度异常: {}", data_len
)));
}
// 读取数据段 + 校验和 + 结束符
let mut payload = vec![0u8; data_len + 2];
stream.read_exact(&mut payload).await
.map_err(|e| PluginError::Io(format!("读取数据段失败: {}", e)))?;
// 验证结束符
if payload[data_len + 1] != 0x16 {
return Err(PluginError::Protocol(
format!("结束符错误: {:02X}", payload[data_len + 1])
));
}
// 验证校验和
let checksum: u8 = payload[..data_len].iter().fold(0u8, |a, &b| a.wrapping_add(b));
if checksum != payload[data_len] {
warn!(expected = checksum, got = payload[data_len], "校验和错误");
return Err(PluginError::Protocol("校验和不匹配".into()));
}
// 解析 ASDU
let points = self.parse_asdu(&payload[..data_len])?;
debug!(points = points.len(), "解析得到测点");
Ok(points)
}
/// 查询设备状态(同步扫描)
async fn scan(&mut self) -> Result<Vec<(String, f64)>, PluginError> {
// IEC 103 的扫描通过发送 C_RD 8-0 命令实现
let cmd = self.build_read_command(self.device_address, 0)?;
self.send(&cmd).await?;
// 等待响应(可能分多帧返回)
let mut all_points = Vec::new();
for _ in 0..10 {
match tokio::time::timeout(
std::time::Duration::from_millis(self.timeout_ms),
self.receive(),
).await {
Ok(Ok(points)) => {
if points.is_empty() { break; }
all_points.extend(points);
}
Ok(Err(PluginError::Timeout(_))) => break,
Ok(Err(e)) => return Err(e),
Err(_) => break, // 超时,结束扫描
}
}
Ok(all_points)
}
}
impl Iec103Adapter {
/// 解析 ASDU(应用服务数据单元)
fn parse_asdu(&self, data: &[u8]) -> Result<Vec<(String, f64)>, PluginError> {
if data.len() < 4 {
return Err(PluginError::Protocol("ASDU 过短".into()));
}
let type_id = data[0];
let asdu_addr = u16::from_le_bytes([data[2], data[3]]);
let mut points = Vec::new();
match type_id {
// 类型 1:时间标记的单点信息(断路器状态)
1 => {
let point_name = format!("iec103.dev{}.breaker_state", asdu_addr);
let value = if data.len() > 4 { data[4] as f64 } else { 0.0 };
points.push((point_name, value));
}
// 类型 4:时间标记的测量值
4 => {
if data.len() >= 9 {
let value = i16::from_le_bytes([data[4], data[5]]) as f64;
let point_name = format!("iec103.dev{}.meas_{}", asdu_addr, data[6]);
points.push((point_name, value));
}
}
// 类型 7:故障记录
7 => {
warn!(dev = asdu_addr, "收到故障记录 ASDU");
let point_name = format!("iec103.dev{}.fault_flag", asdu_addr);
points.push((point_name, 1.0));
}
// 类型 23:接地故障
23 => {
warn!(dev = asdu_addr, "接地故障告警");
let point_name = format!("iec103.dev{}.ground_fault", asdu_addr);
points.push((point_name, 1.0));
}
_ => {
debug!(type_id, "未知 ASDU 类型,跳过");
}
}
Ok(points)
}
/// 构造读取命令(C_RD 8-0)
fn build_read_command(&self, asdu_addr: u8, record: u8) -> Result<Vec<u8>, PluginError> {
// 固定长度帧格式:10H + LEN + LEN + 68H + C_RD + 0 + ASDU_ADDR + 16H
let cmd = vec![
0x10, 0x03, 0x03, 0x68,
0x08, // C_RD: 读命令
record,
asdu_addr,
0x08 ^ record ^ asdu_addr, // 校验和
0x16, // 结束符
];
Ok(cmd)
}
}
// ============================================================================
// C ABI 入口函数 — 主进程通过 dlopen 加载此函数
// ============================================================================
/// 插件创建入口(必须为 extern "C")
/// 返回 Box<dyn ProtocolPlugin> 的裸指针
#[no_mangle]
pub extern "C" fn eneros_plugin_create() -> *mut dyn ProtocolPlugin {
let plugin = Iec103Plugin::new();
Box::into_raw(Box::new(plugin))
}
/// 插件销毁入口(用于安全释放)
#[no_mangle]
pub extern "C" fn eneros_plugin_destroy(ptr: *mut dyn ProtocolPlugin) {
if !ptr.is_null() {
unsafe { drop(Box::from_raw(ptr)) };
}
}
/// 插件元数据入口(供加载器在不创建实例时查询信息)
#[no_mangle]
pub extern "C" fn eneros_plugin_metadata() -> PluginMetadata {
PluginMetadata {
name: "iec103-driver".into(),
version: "0.1.0".into(),
api_version: "0.47.0".into(),
plugin_type: PluginType::Protocol,
description: "IEC 60870-5-103 继电保护设备通信协议驱动".into(),
author: "EnerOS Community".into(),
}
}
// ============================================================================
// 单元测试
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plugin_metadata() {
let plugin = Iec103Plugin::new();
assert_eq!(plugin.protocol_name(), "iec103");
assert_eq!(plugin.protocol_type_str(), "custom:iec103");
assert!(!plugin.description().is_empty());
}
#[test]
fn test_create_adapter() {
let plugin = Iec103Plugin::new();
let config = ProtocolPluginConfig {
address: "127.0.0.1:2405".into(),
protocol_config: serde_json::json!({"device_address": 1}),
timeout_ms: 1000,
};
// 同步运行异步函数
let rt = tokio::runtime::Runtime::new().unwrap();
let adapter = rt.block_on(plugin.create_adapter(&config)).unwrap();
assert_eq!(plugin.instance_count(), 1);
drop(adapter);
}
#[test]
fn test_parse_breaker_state() {
let adapter = Iec103Adapter::new(
"127.0.0.1:0".into(), 100, serde_json::json!({}),
).unwrap();
// 模拟 ASDU: type=1, asdu_addr=1, value=1
let data = [1, 0, 1, 0, 1];
let points = adapter.parse_asdu(&data).unwrap();
assert_eq!(points.len(), 1);
assert_eq!(points[0].0, "iec103.dev1.breaker_state");
assert_eq!(points[0].1, 1.0);
}
#[test]
fn test_parse_measurement() {
let adapter = Iec103Adapter::new(
"127.0.0.1:0".into(), 100, serde_json::json!({}),
).unwrap();
// 模拟 ASDU: type=4, asdu_addr=2, value=12345 (i16 LE), meas_id=5
let value: i16 = 12345;
let bytes = value.to_le_bytes();
let data = [4, 0, 2, 0, bytes[0], bytes[1], 5];
let points = adapter.parse_asdu(&data).unwrap();
assert_eq!(points.len(), 1);
assert_eq!(points[0].0, "iec103.dev2.meas_5");
assert!((points[0].1 - 12345.0).abs() < 0.1);
}
#[test]
fn test_build_read_command() {
let adapter = Iec103Adapter::new(
"127.0.0.1:0".into(), 100, serde_json::json!({}),
).unwrap();
let cmd = adapter.build_read_command(1, 0).unwrap();
assert_eq!(cmd[0], 0x10); // 帧头
assert_eq!(cmd[3], 0x68); // 帧头
assert_eq!(cmd[4], 0x08); // C_RD
assert_eq!(cmd[8], 0x16); // 结束符
}
#[test]
fn test_checksum_calculation() {
let adapter = Iec103Adapter::new(
"127.0.0.1:0".into(), 100, serde_json::json!({}),
).unwrap();
let cmd = adapter.build_read_command(1, 0).unwrap();
// 校验和应为 cmd[4] ^ cmd[5] ^ cmd[6]
let expected = cmd[4] ^ cmd[5] ^ cmd[6];
assert_eq!(cmd[7], expected);
}
#[test]
fn test_unknown_asdu_type() {
let adapter = Iec103Adapter::new(
"127.0.0.1:0".into(), 100, serde_json::json!({}),
).unwrap();
let data = [99, 0, 1, 0]; // 未知 type_id
let points = adapter.parse_asdu(&data).unwrap();
assert!(points.is_empty()); // 未知类型返回空
}
}
步骤 4:注册插件
将编译产物放入插件目录并配置 eneros.toml:
# /etc/eneros/plugin.toml
[[plugins]]
name = "iec103-driver"
path = "/opt/eneros/plugins/libiec103.so"
type = "Protocol"
enabled = true
auto_load = true # 主进程启动时自动加载
[plugins.config]
device_address = 1
timeout_ms = 3000
reconnect_interval_ms = 5000
插件配置字段:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name | String | — | 插件唯一标识 |
path | String | — | 动态库绝对路径 |
type | Enum | — | 插件类型 |
enabled | bool | true | 是否启用 |
auto_load | bool | false | 启动时自动加载 |
config | Object | {} | 传递给插件的配置(JSON) |
步骤 5:构建与签名
# 1. 编译插件(生成 libiec103.so)
cargo build --release
# 2. 验证 ABI 入口函数
nm -D target/release/libiec103.so | grep eneros_plugin
# 3. 签名插件(生产环境强制要求)
eneros-cli plugin sign \
--plugin libiec103.so \
--key ~/.eneros/ed25519.key \
--output libiec103.so.sig
# 4. 验证签名
eneros-cli plugin verify \
--plugin libiec103.so \
--sig libiec103.so.sig \
--pubkey ~/.eneros/ed25519.pub
输出示例:
$ nm -D target/release/libiec103.so | grep eneros_plugin
0000000000012340 T eneros_plugin_create
0000000000012350 T eneros_plugin_destroy
0000000000012360 T eneros_plugin_metadata
$ eneros-cli plugin verify --plugin libiec103.so --sig libiec103.so.sig --pubkey ~/.eneros/ed25519.pub
✓ 签名验证通过
插件: libiec103.so (124KB)
签名者: eneros-trusted
算法: Ed25519
步骤 6:热加载与调试
加载插件无需重启主进程:
use eneros_plugin::{PluginRegistry, loader::PluginLoader};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. 创建插件注册表
let registry = PluginRegistry::new();
let loader = PluginLoader::new(®istry);
// 2. 加载插件(带签名验证)
loader.load("/opt/eneros/plugins/libiec103.so").await?;
println!("✓ 插件已加载");
// 3. 查询已注册插件
let plugins = registry.list();
for p in &plugins {
println!(" - {} v{} ({:?})",
p.metadata.name, p.metadata.version, p.metadata.plugin_type);
}
// 4. 创建协议适配器实例
let config = ProtocolPluginConfig {
address: "10.0.0.30:2405".into(),
protocol_config: serde_json::json!({"device_address": 1}),
timeout_ms: 3000,
};
let adapter = registry
.create_protocol_adapter("iec103-driver", &config)
.await?;
// 5. 连接设备并扫描
adapter.connect().await?;
let points = adapter.scan().await?;
println!("扫描到 {} 个测点:", points.len());
for (name, value) in &points {
println!(" {} = {:.4}", name, value);
}
// 6. 卸载插件(热卸载)
loader.unload("iec103-driver").await?;
println!("✓ 插件已卸载");
Ok(())
}
开发期使用 RUST_LOG=eneros_plugin=debug 查看插件日志,无需重启主进程:
# 实时查看插件日志
RUST_LOG=eneros_plugin=debug,iec103=trace eneros-os --config /etc/eneros/eneros.toml
# 重新加载插件(不重启主进程)
eneros-cli plugin reload iec103-driver
# 查看插件状态
eneros-cli plugin status iec103-driver
输出示例:
$ eneros-cli plugin status iec103-driver
插件: iec103-driver
版本: 0.1.0
类型: Protocol
状态: Running
加载时间: 2026-07-06 14:23:01
实例数: 2
连接数: 2
最后心跳: 2026-07-06 14:25:30
步骤 7:插件沙箱
生产环境强制启用沙箱,限制插件系统调用与资源使用:
# /etc/eneros/plugin.toml —— 沙箱配置
[[plugins]]
name = "iec103-driver"
path = "/opt/eneros/plugins/libiec103.so"
type = "Protocol"
[plugins.sandbox]
enabled = true
# seccomp 过滤器:仅允许必要的系统调用
seccomp_profile = "protocol-default"
# 文件系统隔离
fs_isolation = "/var/lib/eneros/plugins/iec103-driver"
allowed_paths = [
"/var/lib/eneros/plugins/iec103-driver/",
"/etc/eneros/certs/iec103/",
]
# 资源限制
max_memory_mb = 64
max_cpu_percent = 10
max_fds = 32
max_threads = 4
# 崩溃隔离(catch_unwind)
crash_isolation = true
max_crashes_per_hour = 5
沙箱提供的隔离保障:
| 维度 | 隔离机制 | 说明 |
|---|---|---|
| 系统调用 | seccomp BPF | 仅允许 read/write/accept 等必要调用 |
| 文件系统 | chroot + 白名单 | 限制可访问路径 |
| 内存 | cgroups v2 | 限制最大内存,超限 OOM kill |
| CPU | cgroups cpu quota | 限制 CPU 占用百分比 |
| 网络 | 网络命名空间(可选) | 限制可访问的端口 |
| 崩溃 | catch_unwind | 插件 panic 不影响主进程 |
步骤 8:发布到插件市场
EnerOS 提供插件市场(Plugin Market)用于插件分发与版本管理:
# 1. 打包插件(含动态库、清单、签名)
eneros-cli plugin pack \
--lib target/release/libiec103.so \
--manifest plugin.toml \
--sig libiec103.so.sig \
--output iec103-driver-0.1.0.tar.gz
# 2. 上传到插件市场
eneros-cli plugin publish \
--file iec103-driver-0.1.0.tar.gz \
--repo https://market.openeneros.com
# 3. 其他用户安装
eneros-cli plugin install iec103-driver --version 0.1.0
插件市场配置:
# ~/.eneros/market.toml
[[repos]]
name = "official"
url = "https://market.openeneros.com"
trusted_signers = ["eneros-trusted"]
[[repos]]
name = "internal"
url = "https://internal.eneros.local/market"
trusted_signers = ["corp-ops"]
步骤 9:集成测试
端到端测试
#[cfg(test)]
mod integration_tests {
use super::*;
use std::io::Write;
/// 启动一个模拟 IEC 103 设备的 TCP 服务器
async fn start_mock_device() -> (String, tokio::task::JoinHandle<()>) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let handle = tokio::spawn(async move {
if let Ok((mut stream, _)) = listener.accept().await {
// 等待读取命令
let mut buf = [0u8; 9];
use tokio::io::AsyncReadExt;
stream.read_exact(&mut buf).await.unwrap();
// 回复一帧断路器状态(type=1, asdu=1, value=1)
let response = vec![
0x10, 0x05, 0x05, 0x68, // 帧头
1, 0, 1, 0, 1, // ASDU
1, // 校验和
0x16, // 结束符
];
use tokio::io::AsyncWriteExt;
stream.write_all(&response).await.unwrap();
}
});
(addr.to_string(), handle)
}
#[tokio::test]
async fn test_end_to_end_scan() {
let (addr, _handle) = start_mock_device().await;
let plugin = Iec103Plugin::new();
let config = ProtocolPluginConfig {
address: addr,
protocol_config: serde_json::json!({"device_address": 1}),
timeout_ms: 1000,
};
let mut adapter = plugin.create_adapter(&config).await.unwrap();
adapter.connect().await.unwrap();
let points = adapter.scan().await.unwrap();
assert!(!points.is_empty());
assert!(points.iter().any(|(n, _)| n.contains("breaker_state")));
}
}
沙箱测试
# 在沙箱中运行插件,检查资源使用
eneros-cli plugin test iec103-driver \
--sandbox \
--device 127.0.0.1:2405 \
--duration 60s
# 输出示例:
# ✓ 测试通过
# 内存峰值: 8.2 MB / 64 MB
# CPU 占用: 2.3% / 10%
# 崩溃次数: 0 / 5
# 系统调用违规: 0
验证
按以下指标验证插件的正确性:
| 指标 | 期望值 | 说明 |
|---|---|---|
| 编译产物 | libiec103.so | cdylib 格式 |
| ABI 入口 | 3 个导出符号 | eneros_plugin_create/destroy/metadata |
| 加载时间 | < 100ms | 从 dlopen 到 init 完成 |
| 签名验证 | 通过 | Ed25519 签名 |
| 单元测试 | 100% 通过 | cargo test -p iec103-driver |
| 沙箱隔离 | 0 次违规 | seccomp/cgroups/fs 均正常 |
| 热加载 | < 1s | 不影响其他协议通道 |
| 崩溃隔离 | 主进程存活 | 插件 panic 不影响主进程 |
调试技巧
# 查看插件加载详情
RUST_LOG=eneros_plugin=trace eneros-os --config /etc/eneros/eneros.toml
# 检查动态库依赖
ldd /opt/eneros/plugins/libiec103.so
# 用 strace 观察插件系统调用
strace -f -e trace=network -p <eneros-os-pid>
# 用 valgrind 检查内存泄漏
valgrind --leak-check=full target/release/iec103-driver-test
# 强制触发插件崩溃(测试隔离)
eneros-cli plugin crash iec103-driver --type panic
常见问题排查
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 加载失败 | api_version 不匹配 | 检查清单中的 api_version 字段 |
| 符号未找到 | 未导出 eneros_plugin_create | 确认 #[no_mangle] 与 extern "C" |
| 签名失败 | 公钥不匹配 | 检查 market.toml 的 trusted_signers |
| 崩溃频繁 | 空指针或越界 | 用 cargo test 与 valgrind 排查 |
| 内存超限 | 未释放资源 | 检查 drop 实现,使用 Rc<RefCell<>> 替代裸指针 |
| 系统调用被拒 | seccomp 配置过严 | 调整 seccomp_profile 或联系安全团队 |