Skip to main content

v0.18.0 Release Notes

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

Metricv0.9.0 (Basic)v0.18.0 (Enhanced)Improvement
IEC 104 connections32102432x
Telemetry collection latency2s80ms25x
Telecontrol execution latency5s150ms33x
SOE resolution10ms1ms10x
Protocol coverage30%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 TypeAbbreviationDescriptionData FormatDirection
TelemetryTIMeasured valuefloatUplink
TelesignalingDIStatusboolUplink
Integrated totalCIAccumulated valueint64Uplink
TelecontrolTCSingle-point controlboolDownlink
SetpointSPSetpoint valuefloatDownlink

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 CodeNameDirectionDescription
<1>Single-point telesignalingUplinkDI single-point
<3>Double-point telesignalingUplinkDI double-point
<9>Normalized telemetryUplinkTI scaled value
<13>Float telemetryUplinkTI IEEE 754
<15>Integrated totalUplinkCI
<30>Single-point SOEUplinkTime-stamped DI
<45>Single-point telecontrolDownlinkTC
<50>SetpointDownlinkSP
<70>Clock syncDownlinkTime alignment
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 ModeRTU CountPointsCollection LatencyBandwidth
Periodic 500ms1024500K80ms12 Mbps
Periodic 1s1024500K120ms6 Mbps
Spontaneous1024500K20ms2 Mbps
Hybrid mode1024500K50ms4 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

ModeStepsSafetyUse Case
SBOSelect→Confirm→OperateHighCritical operations
Direct executionDirect issuanceMediumSetpoint adjustment
Pre-set executionPre-set→ExecuteHigherBatch 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

TimestampRTUIOADescriptionValue
14:30:00.123rtu-15Main transformer differential protectionTrip
14:30:00.125rtu-112Main transformer HV side breakerOpen
14:30:00.128rtu-23Bus transfer deviceOperated
14:30:00.135rtu-27Bus tie breakerClosed

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-iec104 connection leak when RTU count exceeds 256 (#1809)
  • Fixed eneros-scada telecontrol command not releasing select lock on SBO timeout (#1815)
  • Fixed eneros-scada-soe event disorder during GPS clock jumps (#1821)
  • Fixed eneros-scada-iec104 ASDU sequence number disorder after link reconnection (#1827)

Breaking Changes

  • Gateway::add_iec104: Migrated to ScadaMaster::add_rtu, original method deprecated
  • PointTable::load: Changed to async, requires .await
  • Command::execute: Defaults to SBO mode; direct execution requires execute_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?;