EnerOS v0.18.0
Release Date: April 20, 2025 Codename: Scada Git Tag: v0.18.0 Support Status: Stable Total Crates: 50 (4 new) Test Cases: 6920+ (540 new)
Version Overview
EnerOS v0.18.0 “Scada” is the core release for EnerOS in the “Field Device Access” direction. The primary goal is to deeply integrate SCADA (Supervisory Control and Data Acquisition) systems, with enhanced focus on the IEC 60870-5-104 protocol stack, enabling real-time telemetry collection, remote control command issuance, and SOE (Sequence of Events) recording with substation RTUs, plant station measurement and control devices, and protection devices—allowing EnerOS to directly serve as a provincial dispatch master station communicating with field devices.
SCADA is the “nerve endings” of power dispatch, responsible for collecting operational status and measurement data from field devices and dispatching instructions to actuators. IEC 60870-5-104 is an internationally standardized telecontrol protocol (based on TCP/IP), widely used in dispatch automation systems in China, Europe, Asia, and other regions. v0.9.0 introduced basic Modbus and IEC 104 access capabilities, but only supported simple telemetry reads. v0.18.0 upgrades the IEC 104 protocol stack to production-grade implementation, supporting all types of telemetry (TI), telesignaling (DI), integrated totals (CI), telecontrol (TC), and setpoint (TI_setpoint), with built-in SOE event sequence recording and debounce processing.
This version introduces four new crates: eneros-scada (SCADA core), eneros-scada-iec104 (IEC 104 protocol stack), eneros-scada-rtu (RTU management), and eneros-scada-soe (SOE event recording). The design philosophy is “protocol-native, data-native”—the SCADA protocol stack is implemented directly in the Rust kernel, avoiding the security and performance issues of C library bindings, and collected data flows directly into EnerOS time series storage and pipelines without intermediary transfer.
Key Metrics
| Metric | v0.9.0 (Basic) | v0.18.0 (Enhanced) | Improvement |
|---|---|---|---|
| IEC 104 connections | 32 | 1024 | 32x |
| Telemetry collection latency | 2s | 80ms | 25x |
| Telecontrol execution latency | 5s | 150ms | 33x |
| SOE resolution | 10ms | 1ms | 10x |
| Protocol coverage | 30% | 95% | +65pp |
New Features
1. SCADA System Integration
Introduced the eneros-scada crate as the unified entry point for the SCADA subsystem, managing all RTU/IED connections, point table configurations, data collection tasks, and control command queues. The SCADA subsystem is deeply integrated with EnerOS pipelines, time series storage, and digital twin, with collected data automatically flowing into the downstream processing chain.
SCADA Master Station Configuration
use eneros_scada::{ScadaMaster, ScadaConfig, RtuConnection};
let mut master = ScadaMaster::new(ScadaConfig {
max_connections: 1024,
poll_interval: Duration::milliseconds(500),
command_timeout: Duration::seconds(5),
soe_buffer_size: 100_000,
auto_reconnect: true,
reconnect_backoff: Backoff::exponential(Duration::seconds(1), Duration::minutes(5)),
})?;
// Add RTU connection
master.add_rtu(RtuConnection::iec104()
.id("rtu-substation-1")
.address("10.20.30.1:2404")
.common_address(1)
.timeout(Duration::seconds(10))
.time_sync(true))?;
// Start SCADA master station
master.start().await?;
Point Table Configuration
use eneros_scada::{PointTable, PointType, PointId};
// Configure point table (telemetry/telesignaling/integrated totals)
let table = PointTable::new()
// Telemetry points (TI)
.point(PointId::ti(1, 1), PointType::Float, "Bus 1 Voltage", "kV")
.point(PointId::ti(1, 2), PointType::Float, "Bus 1 Frequency", "Hz")
.point(PointId::ti(1, 3), PointType::Float, "Line 12 Active Power", "MW")
// Telesignaling points (DI)
.point(PointId::di(1, 1), PointType::Single, "Breaker 1 Status", "")
.point(PointId::di(1, 2), PointType::Single, "Disconnector 2 Status", "")
// Integrated total points (CI)
.point(PointId::ci(1, 1), PointType::Integrated, "Total Active Energy", "kWh")
// Telecontrol points (TC)
.point(PointId::tc(1, 1), PointType::Single, "Breaker 1 Control", "")
// Setpoint points
.point(PointId::sp(1, 1), PointType::Float, "AVR Voltage Setpoint", "kV");
master.load_point_table(table)?;
Point Type Mapping
| IEC Type | Abbreviation | Description | Data Format | Direction |
|---|---|---|---|---|
| Telemetry | TI | Measured value | float | Uplink |
| Telesignaling | DI | Status | bool | Uplink |
| Integrated total | CI | Accumulated value | int64 | Uplink |
| Telecontrol | TC | Single-point control | bool | Downlink |
| Setpoint | SP | Setpoint value | float | Downlink |
2. IEC 60870-5-104 Protocol Enhancement
Introduced the eneros-scada-iec104 crate, implementing a production-grade IEC 60870-5-104 protocol stack with full support for APCI frame format, ASDU (Application Service Data Unit), and I-frame / S-frame / U-frame types, with both balanced and unbalanced transmission modes.
Protocol Frame Structure
use eneros_scada_iec104::{Frame, FrameType};
// I-frame (Information frame): carries data
let i_frame = Frame::i(
send_seq: 1024,
recv_seq: 512,
asdu: Asdu::single_point_info(
common_address: 1,
ioa: 1,
value: true,
quality: Quality::ok(),
time: Some(now()),
),
);
// S-frame (Supervisory frame): acknowledge reception
let s_frame = Frame::s(recv_seq: 1024);
// U-frame (Control frame): start/stop/test
let u_frame = Frame::u(FrameType::StartDt);
ASDU Type Support
| Type Code | Name | Direction | Description |
|---|---|---|---|
| <1> | Single-point telesignaling | Uplink | DI single-point |
| <3> | Double-point telesignaling | Uplink | DI double-point |
| <9> | Normalized telemetry | Uplink | TI scaled value |
| <13> | Float telemetry | Uplink | TI IEEE 754 |
| <15> | Integrated total | Uplink | CI |
| <30> | Single-point SOE | Uplink | Time-stamped DI |
| <45> | Single-point telecontrol | Downlink | TC |
| <50> | Setpoint | Downlink | SP |
| <70> | Clock sync | Downlink | Time alignment |
Link Layer Management
use eneros_scada_iec104::{LinkLayer, LinkConfig};
let link = LinkLayer::new(LinkConfig {
t1: Duration::seconds(15), // Wait ACK timeout
t2: Duration::seconds(10), // Send ACK delay
t3: Duration::seconds(20), // Idle test interval
k: 12, // Max unacknowledged I-frames
w: 8, // S-frame threshold after receiving I-frames
});
// Automatic link maintenance
link.on_disconnect(|rtu_id| {
log::warn!("RTU {} link disconnected", rtu_id);
alert::notify(format!("RTU {} communication interrupted", rtu_id));
});
link.on_reconnect(|rtu_id| {
log::info!("RTU {} link restored", rtu_id);
});
3. Real-Time Telemetry Data Collection
The SCADA master station continuously polls RTU telemetry data and publishes it in real-time through the EnerOS pipeline, with synchronous consumption by time series storage and digital twin. Collection supports both periodic polling and spontaneous reporting modes, balancing real-time performance with bandwidth efficiency.
Data Collection Flow
use eneros_scada::collector::Collector;
let collector = Collector::new(&master)
// Periodic collection: poll all telemetry every 500ms
.poll(PollConfig {
interval: Duration::milliseconds(500),
strategy: PollStrategy::grouped_by_rtu,
})
// Spontaneous reporting: immediately report when change exceeds threshold
.spontaneous(SpontaneousConfig {
enable: true,
deadband: Deadband::relative(0.01), // 1% change
min_interval: Duration::milliseconds(100),
})
// Collection callback
.on_telemetry(|point| {
// Publish to pipeline
pipeline.publish(
Topic::of(format!("telemetry.rtu.{}.{}", point.rtu_id, point.ioa)),
Event::telemetry(point),
)?;
Ok(())
});
collector.start().await?;
Collection Performance Metrics
| Collection Mode | RTU Count | Points | Collection Latency | Bandwidth |
|---|---|---|---|---|
| Periodic 500ms | 1024 | 500K | 80ms | 12 Mbps |
| Periodic 1s | 1024 | 500K | 120ms | 6 Mbps |
| Spontaneous | 1024 | 500K | 20ms | 2 Mbps |
| Hybrid mode | 1024 | 500K | 50ms | 4 Mbps |
Data Quality Flags
use eneros_scada::Quality;
// Each telemetry point carries quality flags
match point.quality {
Quality::Ok => { /* Data valid */ }
Quality::Invalid => { /* Data invalid */ }
Quality::NonTopical => { /* Data not current */ }
Quality::Substituted => { /* Data substituted */ }
Quality::Blocked => { /* Data blocked */ }
Quality::Overflow => { /* Data overflow */ }
}
4. Telecontrol Command Issuance
Supports issuing telecontrol commands (single-point telecontrol, double-point telecontrol, setpoints) to RTUs via the IEC 104 protocol. All commands pass through Guardian validation and support “Select-Before-Operate” two-step operations to prevent misoperations.
Telecontrol Operation
use eneros_scada::command::{Command, SelectOperate};
// Single-point telecontrol: close
let cmd = Command::single_point(
rtu_id: "rtu-substation-1",
common_address: 1,
ioa: 1,
value: true, // Close
);
// Select-Before-Operate mode (SBO)
let so = SelectOperate::new(&master)
.select(&cmd).await? // Select
.confirm_within(Duration::seconds(10)) // Confirm within 10 seconds
.operate().await?; // Operate
match so.result {
Ok(()) => log::info!("Telecontrol success: {}", cmd),
Err(e) => log::error!("Telecontrol failed: {}", e),
}
Direct Execution Mode
// Direct execution (non-critical operations only, requires Guardian authorization)
let cmd = Command::set_point(
rtu_id: "rtu-substation-1",
common_address: 1,
ioa: 1,
value: 110.0, // Voltage setpoint 110kV
);
// Direct issuance after Guardian validation
ctx.guardian().check(&cmd).await?;
master.execute_direct(&cmd).await?;
Telecontrol Mode Comparison
| Mode | Steps | Safety | Use Case |
|---|---|---|---|
| SBO | Select→Confirm→Operate | High | Critical operations |
| Direct execution | Direct issuance | Medium | Setpoint adjustment |
| Pre-set execution | Pre-set→Execute | Higher | Batch operations |
5. SOE Event Recording
Introduced the eneros-scada-soe crate, recording grid event sequences (SOE) with 1ms-level event resolution for fault analysis and accident tracing. SOE events carry GPS timestamps, ensuring event time consistency across the station.
SOE Recording
use eneros_scada_soe::{SoeLog, SoeEvent};
let soe = SoeLog::new(SoeConfig {
buffer_size: 1_000_000,
resolution: Duration::milliseconds(1),
time_source: TimeSource::gps(),
debounce: DebounceConfig::disabled(), // SOE no debounce
});
// Automatically record events
soe.on_event(|event| {
let evt = SoeEvent {
timestamp: event.time, // GPS timestamp
rtu_id: event.rtu_id,
ioa: event.ioa,
value: event.value,
description: point_table.describe(event.rtu_id, event.ioa),
};
// Write to persistent storage
soe.persist(&evt).await?;
// Publish to pipeline
pipeline.publish("soe.event", Event::soe(evt))?;
});
SOE Query
// Query event sequence during fault period
let events = soe.query()
.rtu("rtu-substation-1")
.range("2025-04-20T14:30:00Z", "2025-04-20T14:35:00Z")
.order(OrderBy::Ascending)
.execute()?;
for evt in events {
println!("{:?} RTU={} IOA={} {} = {}",
evt.timestamp, evt.rtu_id, evt.ioa, evt.description, evt.value);
}
Event Sequence Example
| Timestamp | RTU | IOA | Description | Value |
|---|---|---|---|---|
| 14:30:00.123 | rtu-1 | 5 | Main transformer differential protection | Trip |
| 14:30:00.125 | rtu-1 | 12 | Main transformer HV side breaker | Open |
| 14:30:00.128 | rtu-2 | 3 | Bus transfer device | Operated |
| 14:30:00.135 | rtu-2 | 7 | Bus tie breaker | Closed |
Improvements
- Time Series Storage: Write path optimized for high-frequency SCADA writes (500K points/second)
- Digital Twin: Twin directly subscribes to SCADA pipeline, state sync latency reduced to 80ms
- Guardian System: Telecontrol commands automatically pass through Guardian validation, intercepting dangerous operations
- Dashboard: New SCADA monitoring panel, real-time display of RTU connection status and data quality
Bug Fixes
- Fixed
eneros-scada-iec104connection leak when RTU count exceeds 256 (#1809) - Fixed
eneros-scadatelecontrol command not releasing select lock on SBO timeout (#1815) - Fixed
eneros-scada-soeevent disorder during GPS clock jumps (#1821) - Fixed
eneros-scada-iec104ASDU sequence number disorder after link reconnection (#1827)
Breaking Changes
Gateway::add_iec104: Migrated toScadaMaster::add_rtu, original method deprecatedPointTable::load: Changed to async, requires.awaitCommand::execute: Defaults to SBO mode; direct execution requiresexecute_direct
Performance Improvements
- IEC 104 connections increased from 32 to 1024 (32x improvement)
- Telemetry collection latency reduced from 2s to 80ms (25x improvement)
- Telecontrol execution latency reduced from 5s to 150ms (33x improvement)
- SOE event resolution improved from 10ms to 1ms (10x improvement)
Contributors
This version was jointly completed by 22 contributors with 388 commits. Special thanks to:
- @scada-core: SCADA master station and point table management
- @iec104-implementer: IEC 104 protocol stack Rust implementation
- @rtu-manager: RTU connection and link maintenance
- @soe-master: SOE event sequence and GPS time synchronization
Upgrade Guide
Upgrading from v0.17.0
1. Update Dependencies
# Cargo.toml
[dependencies]
eneros-scada = { version = "0.18" }
eneros-scada-iec104 = { version = "0.18" }
eneros-scada-rtu = { version = "0.18" }
eneros-scada-soe = { version = "0.18" }
2. Migrate SCADA Configuration
// v0.9.0 old approach
gateway.add_iec104("rtu-1", "10.20.30.1:2404")?;
// v0.18.0 new approach
master.add_rtu(RtuConnection::iec104()
.id("rtu-1")
.address("10.20.30.1:2404")
.common_address(1))?;
3. Configure Point Table
# eneros.toml
[scada]
enabled = true
max_connections = 1024
poll_interval_ms = 500
[[scada.rtu]]
id = "rtu-substation-1"
address = "10.20.30.1:2404"
common_address = 1
time_sync = true
[[scada.points]]
id = "ti:1:1"
type = "float"
name = "Bus 1 Voltage"
unit = "kV"
4. Enable SOE Recording
For production environments, enable SOE recording and configure a GPS clock source to ensure event sequence precision meets accident analysis requirements:
[scada.soe]
enabled = true
buffer_size = 1000000
resolution_ms = 1
time_source = "gps"
5. Telecontrol Safety Configuration
All telecontrol operations should enable SBO mode and integrate Guardian validation:
let so = SelectOperate::new(&master)
.guardian(ctx.guardian()) // Integrate guardian
.select(&cmd).await?
.confirm_within(Duration::seconds(10))
.operate().await?;