Custom Plugin Development
This tutorial demonstrates how to develop a custom plugin for EnerOS to support private protocol parsing or third-party algorithm integration. EnerOS plugins are based on dynamic libraries (cdylib) and trait interfaces for hot-loading, allowing system capabilities to be extended without restarting the main process. This tutorial will develop a complete IEC 60870-5-103 (relay protection device protocol) plugin, from scratch to publishing on the plugin market.
Plugin Architecture Overview
The EnerOS plugin system consists of the following components:
┌─────────────────────────────────────────────────────────────┐
│ EnerOS main process │
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌─────────────┐ │
│ │ PluginRegistry │──→│ PluginLoader │──→│ Sandbox │ │
│ │ (registry) │ │ (dynamic load) │ │ (seccomp) │ │
│ └────────────────┘ └────────────────┘ └─────────────┘ │
│ │ │ │ │
│ │ │ │ │
│ ┌──────▼───────┐ ┌────────▼─────────┐ ┌──────▼──────┐ │
│ │ Protocol │ │ Agent Strategy │ │ Analysis │ │
│ │ Plugin Slot │ │ Plugin Slot │ │ Plugin Slot │ │
│ └──────┬───────┘ └────────┬─────────┘ └──────┬──────┘ │
└─────────┼──────────────────────┼───────────────────┼────────┘
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ libiec103.so │ │ libmystrat.so│ │ liboptflow.so│
│ (this │ │ │ │ │
│ tutorial) │ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
EnerOS supports four types of plugins:
| Plugin Type | Implemented Interface | Purpose |
|---|---|---|
Protocol | ProtocolPlugin trait | Protocol adapters (IEC 103, Modbus, private protocols) |
Agent | AgentPlugin trait | Agent decision strategies (voltage control, fault recovery) |
Analysis | AnalysisPlugin trait | Analysis modules (state estimation, harmonic analysis) |
Strategy | StrategyPackRef | Strategy packs (hot-swappable Agent decision packs) |
Prerequisites
- Install Rust 1.78+ and
cargo - Read the Project Structure to understand crate division
- Be familiar with the basic concepts of dynamic libraries (cdylib)
This tutorial involves the following crates:
| Crate | Purpose |
|---|---|
eneros-plugin | Plugin framework core (trait, loader, registry, sandbox) |
eneros-plugin-macros | Procedural macros (simplify plugin declaration) |
eneros-plugin-api | Stable plugin API (ABI compatibility layer) |
Step 1: Create the Plugin Crate
Use cargo new to create a dynamic library crate:
cargo new --lib iec103-driver
cd iec103-driver
Declare the plugin interface dependencies in 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"] # Generate both dynamic and static libraries
[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 selection
cdylib generates .so (Linux) / .dll (Windows) / .dylib (macOS) for dynamic loading by the main process. rlib is used for static linking during unit testing.
:::
Step 2: Write the Plugin Manifest
Each plugin must provide a TOML-format manifest describing metadata, dependencies, and security information:
# iec103-driver/plugin.toml
[plugin]
name = "iec103-driver"
version = "0.1.0"
api_version = "0.47.0" # Must be compatible with the main program API version
plugin_type = "Protocol" # Protocol / Agent / Analysis / Strategy
description = "IEC 60870-5-103 relay protection device communication protocol driver"
author = "EnerOS Community"
[dependencies]
plugins = [] # Does not depend on other plugins
[security]
signer = "eneros-trusted" # Signer identifier
public_key = "ed25519:Base64..." # Public key (for signature verification)
Manifest field descriptions:
| Field | Type | Required | Description |
|---|---|---|---|
plugin.name | String | Yes | Plugin unique identifier (matches crate name) |
plugin.version | String | Yes | Semantic version |
plugin.api_version | String | Yes | EnerOS API version used at compile time |
plugin.plugin_type | Enum | Yes | Plugin type (Protocol/Agent/Analysis/Strategy) |
plugin.description | String | No | Human-readable description |
plugin.author | String | No | Author information |
dependencies.plugins | Vec | No | Names of other plugins it depends on |
security.signer | String | No | Signer identifier |
security.public_key | String | No | Ed25519 public key |
Step 3: Implement the Protocol Plugin Trait
Implement the ProtocolPlugin trait to handle upstream messages and produce time-series points. Below is the complete IEC 103 driver implementation:
// 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 protocol plugin
#[derive(Debug)]
pub struct Iec103Plugin {
metadata: PluginMetadata,
/// Number of adapter instances created (for diagnostics)
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 relay protection device communication protocol driver".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 relay protection device communication protocol, used to access protection and control devices"
}
/// Create a protocol adapter instance (called once per device connection)
async fn create_adapter(
&self,
config: &ProtocolPluginConfig,
) -> Result<Box<dyn ProtocolAdapterInstance>, PluginError> {
info!(
addr = %config.address,
timeout_ms = config.timeout_ms,
"Creating IEC 103 adapter instance"
);
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 protocol adapter instance (corresponds to one device connection)
pub struct Iec103Adapter {
address: String,
timeout_ms: u64,
/// Device address (ASDU address in IEC 103)
device_address: u8,
/// TCP connection (only set after connecting)
stream: Option<tokio::net::TcpStream>,
/// Receive callback
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 {
/// Connect to the device
async fn connect(&mut self) -> Result<(), PluginError> {
debug!(addr = %self.address, "Connecting to IEC 103 device");
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!("Connection timeout: {}", self.address)))?
.map_err(|e| PluginError::Connection(format!("Connection failed: {}", e)))?;
// Set read/write timeout
let _ = stream.set_nodelay(true);
self.stream = Some(stream);
info!(addr = %self.address, device = self.device_address, "IEC 103 device connected");
Ok(())
}
/// Disconnect
async fn disconnect(&mut self) -> Result<(), PluginError> {
if let Some(stream) = self.stream.take() {
debug!(addr = %self.address, "Disconnecting from IEC 103 device");
// Explicitly drop to close the connection
drop(stream);
}
Ok(())
}
/// Send a message (raw bytes)
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 not connected".into()))?;
stream.write_all(data).await
.map_err(|e| PluginError::Io(format!("Send failed: {}", e)))?;
debug!(len = data.len(), "Message sent");
Ok(())
}
/// Receive a message (returns raw bytes and parsed points)
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 not connected".into()))?;
// Read IEC 103 frame header: fixed 4 bytes (10H + LEN + LEN + 68H)
let mut header = [0u8; 4];
stream.read_exact(&mut header).await
.map_err(|e| PluginError::Io(format!("Failed to read frame header: {}", e)))?;
if header[0] != 0x10 || header[3] != 0x68 {
return Err(PluginError::Protocol(format!(
"Invalid frame header: {:02X?} (expected 10 .. 68)", header
)));
}
let data_len = header[1] as usize;
if data_len == 0 || data_len > 253 {
return Err(PluginError::Protocol(format!(
"Abnormal data length: {}", data_len
)));
}
// Read data segment + checksum + end marker
let mut payload = vec![0u8; data_len + 2];
stream.read_exact(&mut payload).await
.map_err(|e| PluginError::Io(format!("Failed to read data segment: {}", e)))?;
// Verify end marker
if payload[data_len + 1] != 0x16 {
return Err(PluginError::Protocol(
format!("End marker error: {:02X}", payload[data_len + 1])
));
}
// Verify checksum
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], "Checksum error");
return Err(PluginError::Protocol("Checksum mismatch".into()));
}
// Parse ASDU
let points = self.parse_asdu(&payload[..data_len])?;
debug!(points = points.len(), "Parsed points");
Ok(points)
}
/// Query device status (synchronous scan)
async fn scan(&mut self) -> Result<Vec<(String, f64)>, PluginError> {
// IEC 103 scanning is implemented by sending the C_RD 8-0 command
let cmd = self.build_read_command(self.device_address, 0)?;
self.send(&cmd).await?;
// Wait for response (may be returned in multiple frames)
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, // Timeout, end scan
}
}
Ok(all_points)
}
}
impl Iec103Adapter {
/// Parse ASDU (Application Service Data Unit)
fn parse_asdu(&self, data: &[u8]) -> Result<Vec<(String, f64)>, PluginError> {
if data.len() < 4 {
return Err(PluginError::Protocol("ASDU too short".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 {
// Type 1: Single-point information with time tag (breaker state)
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));
}
// Type 4: Measured value with time tag
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));
}
}
// Type 7: Fault record
7 => {
warn!(dev = asdu_addr, "Received fault record ASDU");
let point_name = format!("iec103.dev{}.fault_flag", asdu_addr);
points.push((point_name, 1.0));
}
// Type 23: Ground fault
23 => {
warn!(dev = asdu_addr, "Ground fault alarm");
let point_name = format!("iec103.dev{}.ground_fault", asdu_addr);
points.push((point_name, 1.0));
}
_ => {
debug!(type_id, "Unknown ASDU type, skipping");
}
}
Ok(points)
}
/// Build a read command (C_RD 8-0)
fn build_read_command(&self, asdu_addr: u8, record: u8) -> Result<Vec<u8>, PluginError> {
// Fixed-length frame format: 10H + LEN + LEN + 68H + C_RD + 0 + ASDU_ADDR + 16H
let cmd = vec![
0x10, 0x03, 0x03, 0x68,
0x08, // C_RD: read command
record,
asdu_addr,
0x08 ^ record ^ asdu_addr, // Checksum
0x16, // End marker
];
Ok(cmd)
}
}
// ============================================================================
// C ABI entry functions — the main process loads these via dlopen
// ============================================================================
/// Plugin creation entry (must be extern "C")
/// Returns a raw pointer to 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))
}
/// Plugin destruction entry (for safe release)
#[no_mangle]
pub extern "C" fn eneros_plugin_destroy(ptr: *mut dyn ProtocolPlugin) {
if !ptr.is_null() {
unsafe { drop(Box::from_raw(ptr)) };
}
}
/// Plugin metadata entry (for the loader to query info without creating an instance)
#[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 relay protection device communication protocol driver".into(),
author: "EnerOS Community".into(),
}
}
// ============================================================================
// Unit tests
// ============================================================================
#[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,
};
// Run async function synchronously
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();
// Simulate 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();
// Simulate 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); // Frame header
assert_eq!(cmd[3], 0x68); // Frame header
assert_eq!(cmd[4], 0x08); // C_RD
assert_eq!(cmd[8], 0x16); // End marker
}
#[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();
// Checksum should be 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]; // Unknown type_id
let points = adapter.parse_asdu(&data).unwrap();
assert!(points.is_empty()); // Unknown types return empty
}
}
Step 4: Register the Plugin
Place the build artifact in the plugin directory and configure eneros.toml:
# /etc/eneros/plugin.toml
[[plugins]]
name = "iec103-driver"
path = "/opt/eneros/plugins/libiec103.so"
type = "Protocol"
enabled = true
auto_load = true # Auto-load when the main process starts
[plugins.config]
device_address = 1
timeout_ms = 3000
reconnect_interval_ms = 5000
Plugin configuration fields:
| Field | Type | Default | Description |
|---|---|---|---|
name | String | — | Plugin unique identifier |
path | String | — | Absolute path to the dynamic library |
type | Enum | — | Plugin type |
enabled | bool | true | Whether enabled |
auto_load | bool | false | Auto-load on startup |
config | Object | {} | Configuration passed to the plugin (JSON) |
Step 5: Build and Sign
# 1. Build the plugin (generates libiec103.so)
cargo build --release
# 2. Verify ABI entry functions
nm -D target/release/libiec103.so | grep eneros_plugin
# 3. Sign the plugin (mandatory in production)
eneros-cli plugin sign \
--plugin libiec103.so \
--key ~/.eneros/ed25519.key \
--output libiec103.so.sig
# 4. Verify the signature
eneros-cli plugin verify \
--plugin libiec103.so \
--sig libiec103.so.sig \
--pubkey ~/.eneros/ed25519.pub
Example output:
$ 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
✓ Signature verification passed
Plugin: libiec103.so (124KB)
Signer: eneros-trusted
Algorithm: Ed25519
Step 6: Hot Loading and Debugging
Loading a plugin does not require restarting the main process:
use eneros_plugin::{PluginRegistry, loader::PluginLoader};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Create the plugin registry
let registry = PluginRegistry::new();
let loader = PluginLoader::new(®istry);
// 2. Load the plugin (with signature verification)
loader.load("/opt/eneros/plugins/libiec103.so").await?;
println!("✓ Plugin loaded");
// 3. Query registered plugins
let plugins = registry.list();
for p in &plugins {
println!(" - {} v{} ({:?})",
p.metadata.name, p.metadata.version, p.metadata.plugin_type);
}
// 4. Create a protocol adapter instance
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. Connect to the device and scan
adapter.connect().await?;
let points = adapter.scan().await?;
println!("Scanned {} points:", points.len());
for (name, value) in &points {
println!(" {} = {:.4}", name, value);
}
// 6. Unload the plugin (hot unload)
loader.unload("iec103-driver").await?;
println!("✓ Plugin unloaded");
Ok(())
}
During development, use RUST_LOG=eneros_plugin=debug to view plugin logs without restarting the main process:
# View plugin logs in real time
RUST_LOG=eneros_plugin=debug,iec103=trace eneros-os --config /etc/eneros/eneros.toml
# Reload a plugin (without restarting the main process)
eneros-cli plugin reload iec103-driver
# View plugin status
eneros-cli plugin status iec103-driver
Example output:
$ eneros-cli plugin status iec103-driver
Plugin: iec103-driver
Version: 0.1.0
Type: Protocol
Status: Running
Loaded at: 2026-07-06 14:23:01
Instances: 2
Connections: 2
Last heartbeat: 2026-07-06 14:25:30
Step 7: Plugin Sandbox
Production environments enforce the sandbox to restrict plugin system calls and resource usage:
# /etc/eneros/plugin.toml — sandbox configuration
[[plugins]]
name = "iec103-driver"
path = "/opt/eneros/plugins/libiec103.so"
type = "Protocol"
[plugins.sandbox]
enabled = true
# seccomp filter: only allow necessary system calls
seccomp_profile = "protocol-default"
# Filesystem isolation
fs_isolation = "/var/lib/eneros/plugins/iec103-driver"
allowed_paths = [
"/var/lib/eneros/plugins/iec103-driver/",
"/etc/eneros/certs/iec103/",
]
# Resource limits
max_memory_mb = 64
max_cpu_percent = 10
max_fds = 32
max_threads = 4
# Crash isolation (catch_unwind)
crash_isolation = true
max_crashes_per_hour = 5
Isolation guarantees provided by the sandbox:
| Dimension | Isolation Mechanism | Description |
|---|---|---|
| System calls | seccomp BPF | Only allows necessary calls like read/write/accept |
| Filesystem | chroot + whitelist | Restricts accessible paths |
| Memory | cgroups v2 | Limits max memory, OOM kill on exceed |
| CPU | cgroups cpu quota | Limits CPU usage percentage |
| Network | Network namespace (optional) | Restricts accessible ports |
| Crashes | catch_unwind | Plugin panic does not affect the main process |
Step 8: Publish to the Plugin Market
EnerOS provides a Plugin Market for plugin distribution and version management:
# 1. Package the plugin (including dynamic library, manifest, signature)
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. Upload to the plugin market
eneros-cli plugin publish \
--file iec103-driver-0.1.0.tar.gz \
--repo https://market.openeneros.com
# 3. Other users install
eneros-cli plugin install iec103-driver --version 0.1.0
Plugin market configuration:
# ~/.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"]
Step 9: Integration Testing
End-to-End Testing
#[cfg(test)]
mod integration_tests {
use super::*;
use std::io::Write;
/// Start a TCP server that simulates an IEC 103 device
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 {
// Wait to read a command
let mut buf = [0u8; 9];
use tokio::io::AsyncReadExt;
stream.read_exact(&mut buf).await.unwrap();
// Reply with one frame of breaker state (type=1, asdu=1, value=1)
let response = vec![
0x10, 0x05, 0x05, 0x68, // Frame header
1, 0, 1, 0, 1, // ASDU
1, // Checksum
0x16, // End marker
];
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")));
}
}
Sandbox Testing
# Run the plugin in the sandbox and check resource usage
eneros-cli plugin test iec103-driver \
--sandbox \
--device 127.0.0.1:2405 \
--duration 60s
# Example output:
# ✓ Test passed
# Memory peak: 8.2 MB / 64 MB
# CPU usage: 2.3% / 10%
# Crashes: 0 / 5
# Syscall violations: 0
Validation
Validate the correctness of the plugin against the following metrics:
| Metric | Expected | Description |
|---|---|---|
| Build artifact | libiec103.so | cdylib format |
| ABI entry | 3 exported symbols | eneros_plugin_create/destroy/metadata |
| Load time | < 100ms | From dlopen to init completion |
| Signature verification | Pass | Ed25519 signature |
| Unit tests | 100% pass | cargo test -p iec103-driver |
| Sandbox isolation | 0 violations | seccomp/cgroups/fs all normal |
| Hot loading | < 1s | Does not affect other protocol channels |
| Crash isolation | Main process survives | Plugin panic does not affect the main process |
Debugging Tips
# View plugin loading details
RUST_LOG=eneros_plugin=trace eneros-os --config /etc/eneros/eneros.toml
# Check dynamic library dependencies
ldd /opt/eneros/plugins/libiec103.so
# Observe plugin syscalls with strace
strace -f -e trace=network -p <eneros-os-pid>
# Check memory leaks with valgrind
valgrind --leak-check=full target/release/iec103-driver-test
# Force-trigger a plugin crash (test isolation)
eneros-cli plugin crash iec103-driver --type panic
Common Issue Troubleshooting
| Symptom | Possible Cause | Solution |
|---|---|---|
| Load failure | api_version mismatch | Check the api_version field in the manifest |
| Symbol not found | eneros_plugin_create not exported | Confirm #[no_mangle] and extern "C" |
| Signature failure | Public key mismatch | Check market.toml trusted_signers |
| Frequent crashes | Null pointer or out-of-bounds | Investigate with cargo test and valgrind |
| Memory exceeded | Resources not released | Check drop implementations, use Rc<RefCell<>> instead of raw pointers |
| Syscall rejected | seccomp config too strict | Adjust seccomp_profile or contact the security team |
Next Steps
- IoT Ubiquitous Access — built-in protocol overview
- Development Environment — plugin development guidelines
- Plugin Framework — in-depth analysis of the plugin framework
- Strategy Market — strategy plugin distribution mechanism