docs.rs failed to build simengine-0.2.2
Please check the
build logs for more information.
See
Builds for ideas on how to fix a failed build,
or
Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault,
open an issue.
SimEngine
SimEngine is a plugin-based simulation runtime for composing independent simulations through typed instruments, instrument flows, and runtime state transitions.
This repository is packaged as a single Rust crate named simengine. It includes:
- CLI/runtime
- Manifest, instrument, and flow validation
- Plugin ABI/API
- TCP instrument flow between simulations
- Engine and simulation state management
Install
cargo install simengine
Commands
Run a manifest:
simengine run config.json
Validate a manifest without running it:
simengine check config.json
When framework.max_frames is omitted, the engine runs indefinitely unless it enters a terminal state.
Runtime States
SimEngine has fixed engine states:
START
READY
RUNNING
FINISHED
ERROR
Each simulation has fixed internal states:
_START
_READY
_RUNNING
_FINISHED
_ERROR
These states are built into the engine. They are not declared or customized in config.json.
The engine starts in START. Each simulation starts in _START. During Simulation::create, a plugin should report whether it initialized successfully:
ctx.set_state("_READY");
or:
ctx.set_state("_ERROR");
The engine evaluates state_transitions after simulations are loaded and during the run loop. pre_step, step, and post_step are called only while the engine state is RUNNING. ERROR and FINISHED are terminal runtime states.
Minimum One-Machine Example
SimEngine loads three files from the same directory:
config.json
instruments.json
instrument_flows.json
This example runs two simulations on one machine:
temperature-reader produces temperature_c
fan-controller consumes temperature_c
config.json declares the runtime, simulations, plugins, and state transitions:
{
"framework": {
"fps": 60
},
"simulations": [
{
"name": "temperature-reader",
"endpoint": "127.0.0.1:7001",
"plugin": "temperature_reader.dll"
},
{
"name": "fan-controller",
"endpoint": "127.0.0.1:7002",
"plugin": "fan_controller.dll"
}
],
"state_transitions": [
{
"when": {
"all": [
{ "sim": "temperature-reader", "state": "_READY" },
{ "sim": "fan-controller", "state": "_READY" }
]
},
"engine": "RUNNING"
},
{
"when": {
"any": [
{ "sim": "temperature-reader", "state": "_ERROR" },
{ "sim": "fan-controller", "state": "_ERROR" }
]
},
"engine": "ERROR"
}
]
}
instruments.json declares typed values once:
[
{
"value": "temperature_c",
"type": "float32"
}
]
instrument_flows.json declares where instrument values move:
[
{
"instrument": "temperature_c",
"from": "127.0.0.1:7001",
"to": "127.0.0.1:7002"
}
]
Currently supported primitive types:
float32
State Transitions
state_transitions is a top-level manifest field. Each rule has:
when: a condition using either all or any
engine: the engine state to enter when the condition matches
Example:
{
"when": {
"all": [
{ "sim": "producer", "state": "_READY" },
{ "sim": "consumer", "state": "_READY" }
]
},
"engine": "RUNNING"
}
Validation rejects:
- unknown simulation names in state transitions
- empty transition conditions
- transition conditions that use both
all and any
- unknown engine or simulation states
State transitions are one-way orchestration rules:
simulation states -> engine state
The engine uses its state to decide whether lifecycle methods should run. It does not normally force simulation states.
Plugin API v2
SimEngine plugin ABI version is currently:
pub const SIMENGINE_API_VERSION: u32 = 2;
Plugins implement Simulation and are exported with simengine_plugin!.
Example simulation: XPlane DataRef Sender
use simengine::plugin_api::{Simulation, SimulationContext};
use simengine::simengine_plugin;
use std::net::{SocketAddr, UdpSocket};
const DREF_HEADER: &[u8; 5] = b"DREF\0";
const DATAREF_NAME_LEN: usize = 500;
const XPLANE_ADDR: &str = "127.0.0.1:49000";
const SENDER_BIND_ADDR: &str = "0.0.0.0:49006";
struct WritableDatarefSpec {
input_name: &'static str,
dataref: &'static str,
min_value: f32,
max_value: f32,
}
struct XplaneDatarefsSender {
ctx: SimulationContext,
socket: UdpSocket,
xplane_addr: SocketAddr,
has_entered_running: bool,
}
impl XplaneDatarefsSender {
fn writable_datarefs() -> Vec<WritableDatarefSpec> {
vec![
WritableDatarefSpec {
input_name: "yoke_pitch_ratio",
dataref: "sim/cockpit2/controls/yoke_pitch_ratio",
min_value: -1.0,
max_value: 1.0,
},
WritableDatarefSpec {
input_name: "yoke_roll_ratio",
dataref: "sim/cockpit2/controls/yoke_roll_ratio",
min_value: -1.0,
max_value: 1.0,
},
WritableDatarefSpec {
input_name: "throttle_ratio",
dataref: "sim/cockpit2/engine/actuators/throttle_ratio_all",
min_value: 0.0,
max_value: 1.0,
},
]
}
fn clamp(value: f32, min_value: f32, max_value: f32) -> f32 {
value.max(min_value).min(max_value)
}
fn build_dref_packet(value: f32, dataref: &str) -> Vec<u8> {
let mut packet = Vec::with_capacity(5 + 4 + DATAREF_NAME_LEN);
packet.extend_from_slice(DREF_HEADER);
packet.extend_from_slice(&value.to_le_bytes());
let mut name = [0u8; DATAREF_NAME_LEN];
let bytes = dataref.as_bytes();
let len = bytes.len().min(DATAREF_NAME_LEN - 1);
name[..len].copy_from_slice(&bytes[..len]);
packet.extend_from_slice(&name);
packet
}
fn send_dataref(&self, dataref: &str, value: f32) -> bool {
let packet = Self::build_dref_packet(value, dataref);
match self.socket.send_to(&packet, self.xplane_addr) {
Ok(_) => true,
Err(err) => {
self.ctx.info(format!(
"failed to send DREF {} value={}: {}",
dataref, value, err
));
false
}
}
}
}
impl Simulation for XplaneDatarefsSender {
fn create(ctx: SimulationContext, _config_json: &str) -> Self {
let xplane_addr: SocketAddr = match XPLANE_ADDR.parse() {
Ok(addr) => addr,
Err(err) => {
ctx.set_state("_ERROR");
panic!("invalid hardcoded XPLANE_ADDR: {err}");
}
};
let socket = match UdpSocket::bind(SENDER_BIND_ADDR) {
Ok(socket) => socket,
Err(err) => {
ctx.set_state("_ERROR");
panic!("failed to bind X-Plane DREF sender UDP socket: {err}");
}
};
ctx.info(format!(
"xplane-datarefs-sender created: bind={} xplane={}",
SENDER_BIND_ADDR, XPLANE_ADDR
));
ctx.set_state("_READY");
Self {
ctx,
socket,
xplane_addr,
has_entered_running: false,
}
}
fn step(&mut self, _dt_seconds: f64) {
if !self.has_entered_running {
self.ctx.set_state("_RUNNING");
self.has_entered_running = true;
}
for spec in Self::writable_datarefs() {
let Some(value) = self.ctx.get_input_f32(spec.input_name) else {
continue;
};
let value = Self::clamp(value, spec.min_value, spec.max_value);
if !self.send_dataref(spec.dataref, value) {
self.ctx.set_state("_ERROR");
return;
}
}
}
fn destroy(&mut self) {
self.send_dataref("sim/cockpit2/controls/yoke_pitch_ratio", 0.0);
self.send_dataref("sim/cockpit2/controls/yoke_roll_ratio", 0.0);
self.send_dataref("sim/cockpit2/engine/actuators/throttle_ratio_all", 0.0);
self.ctx.info("xplane-datarefs-sender destroyed");
}
}
simengine_plugin!(XplaneDatarefsSender);
Available context helpers include:
ctx.info("message");
ctx.set_state("_READY");
ctx.set_output_f32("output_name", value);
ctx.get_input_f32("input_name");
Instrument Flows
Instrument flows can target simulations in the same engine process or endpoints hosted by another simengine process.
Local flow:
{
"instrument": "temperature_c",
"from": "127.0.0.1:7001",
"to": "127.0.0.1:7002"
}
Remote flow:
{
"instrument": "temperature_c",
"from": "127.0.0.1:7001",
"to": "127.0.0.2:7001"
}
Flow sources must be local to config.json. Flow targets may be local or remote. The instrument must exist in instruments.json, and the same instrument name is used at both ends of the flow.