EnerOS v0.2.0
Release Date: February 12, 2024 Codename: Foundation Git Tag: v0.2.0 Support Status: Internal Preview Total Crates: 1 Test Cases: 186
Version Overview
EnerOS v0.2.0 “Foundation” is a key step for the project from “engineering skeleton” to “operating system kernel”. v0.1.0 only provided a prototype of the eneros-core crate, while v0.2.0 formally established the Kernel ABI, Syscall Interface, Error Code System, and Tracing Integration. These four components constitute the essential difference between EnerOS as an “operating system” rather than a “regular library” — they define the boundary between user mode and kernel mode, and all upper-layer capabilities (topology, power flow, Agent, gateway) must access kernel resources through syscalls.
The kernel abstraction layer design draws on the experience of Rust microkernels such as seL4 and Hubris, but is deeply tailored for power system scenarios: First, syscalls must meet deterministic latency requirements, avoiding tail latency caused by GC or dynamic allocation; Second, syscalls must be aware of electrical semantics, for example, ReadBus, WriteSwitch and other interfaces directly correspond to power domain operations; Third, syscalls must support the capability model, and each syscall call must carry a capability token to implement the principle of least privilege. This design enables EnerOS to reject unauthorized operations at the kernel level — for example, a read-only Agent cannot call WriteSwitch.
Another important work of v0.2.0 is the introduction of the tracing ecosystem as the unified logging and observability infrastructure. Power systems are critical infrastructure, and the traceability of the operational process is a hard requirement for compliance audits (NERC CIP, IEC 62443). The advantage of tracing over the traditional log crate lies in structured logging and distributed tracing, which can record the complete call chain of a grid operation, facilitating post-incident review and fault localization. This version also established the EnerOSResult<T> unified return type; all fallible functions return this type, with error codes aligned with HTTP status codes and gRPC status codes for easy subsequent REST/gRPC layer integration.
Key Data
| Metric | Value | Description |
|---|---|---|
| Syscall count | 24 | Basic syscalls |
| Error code count | 48 | 6 major categories |
| Lines of code | 4,210 | 127% growth over v0.1.0 |
| Test cases | 186 | Coverage 78% |
| Kernel call latency P99 | 380 ns | Synchronous syscall |
tracing event throughput | 850,000/sec | Single thread |
New Features
1. Kernel Abstraction Layer (Kernel ABI)
Define the binary interface (ABI) between the EnerOS kernel and user mode. The Kernel ABI adopts a capability-based security model; each kernel resource (bus, switch, Agent) is associated with a capability token, and user mode must hold the corresponding capability to access it.
// crates/eneros-core/src/abi/mod.rs
//! EnerOS Kernel Abstraction Layer
//!
//! Defines the interface contract between user mode and kernel mode.
use std::fmt;
/// Kernel capability token
///
/// Each capability token corresponds to a kernel resource access permission.
/// Capability tokens cannot be forged and can only be issued by the kernel.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Capability(pub u64);
impl Capability {
/// Create a root capability (kernel boot only)
pub(crate) fn root() -> Self {
Capability(0xFFFF_FFFF_FFFF_FFFF)
}
/// Check whether the capability contains a permission
pub fn grants(&self, perm: Permission) -> bool {
// Simplified capability check implementation
(self.0 & perm.mask()) != 0
}
}
/// Permission enum
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum Permission {
Read = 0x01,
Write = 0x02,
Execute = 0x04,
Admin = 0x08,
}
impl Permission {
pub fn mask(&self) -> u64 {
1u64 << (*self as u32)
}
}
/// Kernel object handle
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Handle(pub u32);
impl fmt::Display for Handle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "handle:{}", self.0)
}
}
Capability delegation example:
// Kernel issues capability to user-mode Agent
let agent_cap = kernel.grant_capability(
resource: ResourceId::Bus(1),
permissions: Permission::Read | Permission::Write,
ttl: Duration::from_secs(3600),
)?;
// Agent uses capability to access resource
let bus_state = kernel.syscall(Syscall::ReadBus {
handle: bus_handle,
cap: agent_cap,
})?;
2. Syscall Interface
Define 24 basic syscalls, covering four major categories: resource management, topology access, Agent control, and time operations. Syscalls use the form of ID + parameter struct, making it easy to extend to real trap instructions in the future.
// crates/eneros-core/src/syscall/mod.rs
//! EnerOS Syscall Interface
/// Syscall ID
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
pub enum SyscallId {
// Resource management (0x00-0x0F)
CreateResource = 0x00,
DestroyResource = 0x01,
OpenResource = 0x02,
CloseResource = 0x03,
GrantCapability = 0x04,
RevokeCapability = 0x05,
// Topology access (0x10-0x1F)
ReadBus = 0x10,
WriteBus = 0x11,
ReadBranch = 0x12,
ReadSwitch = 0x13,
WriteSwitch = 0x14,
GetTopology = 0x15,
// Agent control (0x20-0x2F)
SpawnAgent = 0x20,
KillAgent = 0x21,
SendMessage = 0x22,
RecvMessage = 0x23,
AgentSleep = 0x24,
AgentWake = 0x25,
// Time operations (0x30-0x3F)
GetTime = 0x30,
Sleep = 0x31,
SetTimer = 0x32,
CancelTimer = 0x33,
// Diagnostics (0x40-0x4F)
Log = 0x40,
Metric = 0x41,
Trace = 0x42,
}
/// Syscall parameters and return values
#[derive(Debug)]
pub enum Syscall {
ReadBus { handle: Handle, cap: Capability },
WriteBus { handle: Handle, cap: Capability, value: BusState },
WriteSwitch { handle: Handle, cap: Capability, state: SwitchState },
SpawnAgent { manifest: AgentManifest },
SendMessage { target: Handle, msg: Message },
GetTime,
// ...
}
/// Syscall return value
#[derive(Debug)]
pub enum SyscallResult {
Bus(BusState),
Switch(SwitchState),
AgentHandle(Handle),
Time(SystemTime),
Ack,
Err(ErrorCode),
}
Syscall invocation example:
use eneros_core::syscall::{Syscall, SyscallId, SyscallResult};
// Read the state of bus 1
let result = kernel.syscall(Syscall::ReadBus {
handle: bus_handle,
cap: read_cap,
})?;
match result {
SyscallResult::Bus(state) => {
tracing::info!(bus_id = 1, voltage = state.voltage, "Read bus state");
}
SyscallResult::Err(code) => {
tracing::error!(code = ?code, "Failed to read bus");
return Err(code.into());
}
_ => unreachable!(),
}
3. Error Code System
Establish a unified error code system, divided into 6 major categories with 48 error codes. Error codes use the format E + category + number, aligned with HTTP status codes and gRPC status codes.
// crates/eneros-core/src/error.rs
use thiserror::Error;
/// EnerOS unified error type
#[derive(Debug, Clone, Error)]
pub enum ErrorCode {
// General errors (E1xxx)
#[error("E1001: Internal error - {0}")]
Internal(String),
#[error("E1002: Invalid argument - {0}")]
InvalidArgument(String),
#[error("E1003: Resource not found - {0}")]
NotFound(String),
#[error("E1004: Resource already exists - {0}")]
AlreadyExists(String),
// Permission errors (E2xxx)
#[error("E2001: Permission denied - requires {required}")]
PermissionDenied { required: Permission },
#[error("E2002: Invalid capability token")]
InvalidCapability,
#[error("E2003: Capability token expired")]
CapabilityExpired,
// Topology errors (E3xxx)
#[error("E3001: Bus does not exist - id={0}")]
BusNotFound(u32),
#[error("E3002: Topology disconnected")]
TopologyDisconnected,
#[error("E3003: Electrical island is empty")]
EmptyIsland,
// Power flow errors (E4xxx)
#[error("E4001: Power flow not converged - iterated {iter} times")]
PowerFlowNotConverged { iter: usize },
#[error("E4002: Singular Jacobian matrix")]
SingularJacobian,
// Agent errors (E5xxx)
#[error("E5001: Agent does not exist - {0}")]
AgentNotFound(String),
#[error("E5002: Invalid Agent state - current {current}")]
InvalidAgentState { current: AgentState },
// Time series errors (E6xxx)
#[error("E6001: Timestamp out of range - {0}")]
TimestampOutOfRange(i64),
#[error("E6002: Time series data discontinuous")]
TimeSeriesDiscontinuous,
}
/// EnerOS unified return type
pub type EnerOSResult<T> = Result<T, ErrorCode>;
Error code category summary:
| Category | Prefix | Range | Count | HTTP Mapping |
|---|---|---|---|---|
| General errors | E1xxx | E1001-E1099 | 12 | 500/400/404/409 |
| Permission errors | E2xxx | E2001-E2099 | 8 | 403/401 |
| Topology errors | E3xxx | E3001-E3099 | 10 | 404/422 |
| Power flow errors | E4xxx | E4001-E4099 | 6 | 422/500 |
| Agent errors | E5xxx | E5001-E5099 | 8 | 404/409 |
| Time series errors | E6xxx | E6001-E6099 | 4 | 422 |
4. Logging System (tracing Integration)
Integrate tracing and tracing-subscriber to provide structured logging and distributed tracing capabilities. EnerOS defines a standard log field specification; all log events must include trace_id, span_id, agent_id (if applicable).
// crates/eneros-core/src/log.rs
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
/// Initialize EnerOS logging system
pub fn init_log() {
let env_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info"));
let fmt_layer = fmt::layer()
.with_target(true)
.with_thread_ids(true)
.with_file(true)
.with_line_number(true)
.json();
tracing_subscriber::registry()
.with(env_filter)
.with(fmt_layer)
.init();
}
/// Standard log fields
pub mod fields {
pub const TRACE_ID: &str = "trace_id";
pub const SPAN_ID: &str = "span_id";
pub const AGENT_ID: &str = "agent_id";
pub const BUS_ID: &str = "bus_id";
pub const VOLTAGE: &str = "voltage";
pub const DURATION_US: &str = "duration_us";
}
Usage example:
use tracing::{info_span, info, instrument};
#[instrument(skip(kernel), fields(bus_id = %bus_id))]
pub fn read_bus_voltage(kernel: &Kernel, bus_id: BusId) -> EnerOSResult<f64> {
let span = info_span!("syscall_read_bus", bus_id = %bus_id);
let _enter = span.enter();
let result = kernel.syscall(Syscall::ReadBus {
handle: kernel.open(bus_id)?,
cap: kernel.current_capability(),
})?;
match result {
SyscallResult::Bus(state) => {
tracing::info!(
bus_id = %bus_id,
voltage = state.voltage,
"Successfully read bus voltage"
);
Ok(state.voltage)
}
SyscallResult::Err(code) => Err(code),
_ => unreachable!(),
}
}
Improvements
- Build speed: Enabled
sccachecaching, CI full build reduced from 1m12s to 48s - Test parallelization: Using
cargo nextest, test execution time reduced from 18s to 7s - Documentation generation: Integrated
cargo-docandmdbook, auto-generating API docs and user manual - Dependency audit: Introduced
cargo-deny, preventing GPL/AGPL dependencies from being mixed in - Code coverage: Integrated
tarpaulin, enforcing coverage not lower than 75% in CI
Bug Fixes
- Fixed
Version::parsefailing to parse pre-release labels like0.1.0-alpha.1(#23) - Fixed
Capability::grantsreturning incorrect results for combined permissions (#27) - Fixed
tracingANSI color output not working on Windows (#31) - Fixed
ErrorCode’sDisplayimplementation not including error code numbers (#34) - Fixed
cargo clippyin CI not enabling--all-targets(#38)
Breaking Changes
eneros_core::version::Version: Addedpre: Option<String>field; the originalVersion::newsignature remains unchanged, but direct struct construction needs updatingeneros_core::error:EnerOSErrorrenamed toErrorCode, implementingstd::error::Errortrait- Logging macros: Removed dependency on
logcrate, unified use oftracingmacros
Migration example:
// v0.1.0 (old)
use eneros_core::error::EnerOSError;
fn foo() -> Result<(), EnerOSError> { ... }
// v0.2.0 (new)
use eneros_core::error::{ErrorCode, EnerOSResult};
fn foo() -> EnerOSResult<()> { ... }
Performance Improvements
v0.2.0 made several optimizations on the syscall path; compared to v0.1.0’s “direct function call” baseline, the additional overhead from introducing the ABI boundary is kept within acceptable range:
| Operation | v0.1.0 | v0.2.0 | Change |
|---|---|---|---|
| Version parsing | 142 ns | 138 ns | -2.8% |
| Syscall (ReadBus) | N/A | 380 ns | New |
| Capability check | N/A | 18 ns | New |
| Error code construction | 24 ns | 16 ns | -33% |
tracing::info! | N/A | 1.2 μs | New |
| CI build | 1m12s | 48s | -33% |
| Test execution | 18s | 7s | -61% |
Syscall performance benchmark:
// benches/syscall_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use eneros_core::syscall::{Syscall, SyscallId};
fn bench_syscall_dispatch(c: &mut Criterion) {
let kernel = Kernel::mock();
let cap = Capability::root();
let handle = Handle(1);
c.bench_function("syscall_read_bus", |b| {
b.iter(|| {
kernel.syscall(black_box(Syscall::ReadBus {
handle,
cap,
}))
})
});
}
criterion_group!(benches, bench_syscall_dispatch);
criterion_main!(benches);
Contributors
v0.2.0 has 5 contributors:
| Contributor | Role | Commits |
|---|---|---|
| @eneros-foundation | Architect | 42 |
| @grid-rustacean | Rust Engineer | 31 |
| @kernel-hacker | Kernel Engineer | 18 |
| @powerdomain-reviewer | Power Domain Expert | 9 |
| @observability-eng | Observability Engineer | 7 |
Upgrade Guide
Upgrading from v0.1.0
# 1. Pull latest code
git fetch origin
git checkout v0.2.0
# 2. Update dependencies
cargo update
# 3. Build the project
cargo build --release
# 4. Run tests (note breaking changes)
cargo test --all
Code Migration Checklist
| Change Item | Impact | Migration Method |
|---|---|---|
EnerOSError → ErrorCode | All error handling code | Global replace type name |
Version added pre field | Direct struct construction | Use Version::new or complete the field |
log → tracing | All log calls | log::info! → tracing::info! |
Configuring Log Output
From v0.2.0, log level is configured via environment variables:
# Set log level
export RUST_LOG="eneros_core=debug,eneros_toplogy=info,warn"
# Output JSON format logs
export ENEROS_LOG_FORMAT=json
# Enable OpenTelemetry export
export ENEROS_OTEL_ENDPOINT=http://otel-collector:4317
Next Steps
v0.3.0 “Topology” will introduce the grid topology data model; Bus, Branch, Generator, and other core node types will be formally implemented. It is recommended to read the IEC 61970 CIM standard in advance to understand the data model design of subsequent versions.