plato-engine-block 0.1.0

Atomic room runtime for the Plato Matrix — universal agent-space interface
Documentation
# Tutorial — plato-engine-block

> **By the end of this tutorial, you will have built a complete monitoring room** for a fishing vessel's engine compartment — with real sensor simulation, overheat alarms, actuator control, and a text protocol you can interact with over TCP.

---

## Prerequisites

- Rust 1.70+ (`rustup update`)
- `netcat` (for TCP testing, optional)
- 20 minutes

## Step 1: Create the Project

```bash
cargo new engine-room-monitor
cd engine-room-monitor
```

Add the dependency to `Cargo.toml`:

```toml
[dependencies]
plato-engine-block = { version = "0.1", features = ["server"] }
tokio = { version = "1", features = ["full"] }
```

We enable the `server` feature so we can connect over TCP later.

## Step 2: Build a Basic Engine

Replace `src/main.rs` with:

```rust
use plato_engine_block::{PlatoEngine, PlatoEngineBuilder};

fn main() {
    let mut engine = PlatoEngine::builder()
        .sensor("coolant_temp_c", Box::new(|| 85.0))
        .sensor("oil_pressure_psi", Box::new(|| 55.0))
        .sensor("engine_rpm", Box::new(|| 1800.0))
        .tick_hz(1.0)          // 1 tick per second
        .history_capacity(60)  // 1 minute of history
        .build();

    // Take 3 ticks
    for _ in 0..3 {
        let tick = engine.tick();
        println!(
            "Tick {}: coolant={:.1}°C, oil={:.1} psi, rpm={:.0}",
            tick.index,
            tick.get("coolant_temp_c").unwrap(),
            tick.get("oil_pressure_psi").unwrap(),
            tick.get("engine_rpm").unwrap(),
        );
    }
}
```

Run it:

```bash
cargo run
```

You should see:

```
Tick 0: coolant=85.0°C, oil=55.0 psi, rpm=1800
Tick 1: coolant=85.0°C, oil=55.0 psi, rpm=1800
Tick 2: coolant=85.0°C, oil=55.0 psi, rpm=1800
```

**What happened:** The builder created an engine with 3 sensors (all returning fixed values), running at 1 Hz, with a 60-tick circular buffer. Each `tick()` call reads all sensors and records a snapshot.

## Step 3: Add Realistic Sensor Simulation

Static values are boring. Let's simulate a coolant temperature that slowly rises:

```rust
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use plato_engine_block::{PlatoEngine, PlatoEngineBuilder};

fn main() {
    let tick_count = Arc::new(AtomicU64::new(0));
    let tick_clone = tick_count.clone();

    let mut engine = PlatoEngine::builder()
        .sensor("coolant_temp_c", Box::new(move || {
            let t = tick_clone.fetch_add(1, Ordering::SeqCst);
            80.0 + (t as f64 * 0.5) // Rises 0.5°C per tick
        }))
        .sensor("oil_pressure_psi", Box::new(|| 55.0))
        .sensor("engine_rpm", Box::new(|| 1800.0))
        .tick_hz(1.0)
        .history_capacity(60)
        .build();

    for _ in 0..10 {
        let tick = engine.tick();
        let temp = tick.get("coolant_temp_c").unwrap();
        println!("Tick {}: coolant = {:.1}°C", tick.index, temp);
    }
}
```

Output:

```
Tick 0: coolant = 80.0°C
Tick 1: coolant = 80.5°C
Tick 2: coolant = 81.0°C
...
Tick 9: coolant = 84.5°C
```

**What happened:** We wrapped an `AtomicU64` in `Arc` to create a stateful sensor. Each call increments the counter and computes a rising temperature. This pattern — `Arc<Mutex<>>` or `Arc<Atomic*>` — is how you bridge async I/O or hardware reads into the engine's synchronous sensor model.

## Step 4: Add an Overheat Alarm

Now let's trigger an alarm when coolant exceeds 90°C:

```rust
.alarm(
    "coolant_overtemp",
    Box::new(|data| {
        data.iter().any(|(name, val)| name == "coolant_temp_c" && *val > 90.0)
    }),
    5,  // Cooldown: 5 ticks between fires
)
```

Add this to the builder chain. Then modify the loop to check for alarm fires:

```rust
for _ in 0..25 {
    let tick = engine.tick();
    let temp = tick.get("coolant_temp_c").unwrap();

    if !engine.alarm_fires.is_empty() {
        println!("🚨 ALARM: {:?} (coolant = {:.1}°C)", engine.alarm_fires, temp);
    } else {
        println!("Tick {}: coolant = {:.1}°C ✓", tick.index, temp);
    }
}
```

You'll see the alarm fire when the temperature crosses 90°C, then enter cooldown:

```
Tick 0: coolant = 80.0°C ✓
...
Tick 20: coolant = 90.0°C ✓
🚨 ALARM: ["coolant_overtemp"] (coolant = 90.5°C)
...
```

**What happened:** The alarm condition checks if `coolant_temp_c > 90.0`. On first detection, it transitions `Idle → Active` and records a fire. While `Active`, it won't re-fire. If the condition clears, it enters `Cooldown` for 5 ticks before re-arming.

## Step 5: Add Actuator Control

Add a cooling fan actuator:

```rust
.actuator("cooling_fan", Box::new(|v| {
    println!("   → Cooling fan set to {:.0}%", v);
    v >= 0.0 && v <= 100.0  // Validate range
}))
```

Control it via the text protocol:

```rust
// In the loop, after alarm detection:
if !engine.alarm_fires.is_empty() && tick.get("coolant_temp_c").unwrap() > 90.0 {
    engine.handle_command("cooling_fan 100.0");
}
```

## Step 6: Use the Text Protocol

The text protocol lets you interact with the engine through simple commands:

```rust
// Outside the loop, try some commands:
println!("\n--- Protocol Demo ---");
println!("{}", engine.handle_command("history 3"));
println!("{}", engine.handle_command("alarm list"));
println!("{}", engine.handle_command("cooling_fan 50.0"));
println!("{}", engine.handle_command("help"));
```

## Step 7: Expose via TCP Server

Replace `src/main.rs` with the full server version:

```rust
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use plato_engine_block::{PlatoEngine, PlatoEngineBuilder};
use plato_engine_block::server::run_server;

#[tokio::main]
async fn main() {
    let tick_count = Arc::new(AtomicU64::new(0));
    let tc = tick_count.clone();

    let engine = PlatoEngine::builder()
        .sensor("coolant_temp_c", Box::new(move || {
            let t = tc.fetch_add(1, Ordering::SeqCst);
            80.0 + (t as f64 * 0.3)
        }))
        .sensor("oil_pressure_psi", Box::new(|| 55.0))
        .actuator("cooling_fan", Box::new(|v| {
            println!("Fan set to {:.0}%", v);
            v >= 0.0 && v <= 100.0
        }))
        .alarm(
            "overheat",
            Box::new(|d| d.iter().any(|(n, v)| n == "coolant_temp_c" && *v > 90.0)),
            5,
        )
        .tick_hz(1.0)
        .history_capacity(100)
        .build();

    let (handle, join) = run_server(engine, "0.0.0.0:7070").await.unwrap();

    handle.broadcast("Engine room monitor online".to_string());
    println!("Server running on :7070. Connect with: nc localhost 7070");

    join.await.unwrap();
}
```

Run it, then in another terminal:

```bash
$ nc localhost 7070
tick
history 5
cooling_fan 75.0
alarm list
subscribe
help
```

**Congratulations!** You've built a complete engine room monitor with simulated sensors, alarm detection, actuator control, and a TCP interface. This is the same pattern used in production Plato deployments on fishing vessels, server rooms, and factory floors.

## What's Next?

- Connect real hardware sensors by replacing the closure with GPIO/I2C reads
- Add more alarms for oil pressure, RPM thresholds, and multi-sensor conditions
- Compose multiple engine blocks with `plato-fleet-manager` for fleet-wide monitoring
- Convert sensor data to ternary with `plato-ternary-bridge` for compressed history and consensus
- Compile alarm conditions to FLUX bytecode with `plato-flux-compiler` for deterministic cross-platform execution