Files
quic_ecs_dt/simulator/src/commands.rs
2026-05-13 15:03:23 -04:00

97 lines
3.5 KiB
Rust

//! Substrate → simulator T3 receiver.
//!
//! The substrate is the brain: when its `automation_system` decides to
//! actuate, it opens a QUIC bidirectional stream to one of its connected
//! devices. The simulator side accepts those streams here, decodes the
//! 39-byte command, applies it to local actuator state, and writes a 39-byte
//! ack back. This closes the loop the paper's three-tier model describes.
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use substrate::transport::{QuicMessage, SensorType};
/// Convenience constructor used by `main.rs` and integration tests.
/// `true` means the simulated engine is running normally.
pub fn new_engine_state() -> Arc<AtomicBool> {
Arc::new(AtomicBool::new(true))
}
/// Loop accepting substrate-initiated bidirectional streams until the
/// connection drops. Each stream is one (command, ack) round-trip:
/// the simulator reads a 39-byte `QuicMessage`, mutates `engine_running` if
/// the command targets the Relay actuator, then writes a 39-byte ack back
/// (echoes the command with the simulator's local timestamp).
pub async fn run_command_receiver(conn: quinn::Connection, engine_running: Arc<AtomicBool>) {
let remote = conn.remote_address();
let mut streams_seen: u64 = 0;
loop {
let (send, recv) = match conn.accept_bi().await {
Ok(s) => s,
Err(e) => {
tracing::debug!(
?remote,
streams_seen,
error = %e,
"command receiver: accept_bi loop ended"
);
return;
}
};
streams_seen += 1;
let engine_running = engine_running.clone();
tokio::spawn(handle_one_command(remote, send, recv, engine_running));
}
}
async fn handle_one_command(
remote: std::net::SocketAddr,
mut send: quinn::SendStream,
mut recv: quinn::RecvStream,
engine_running: Arc<AtomicBool>,
) {
let mut buf = [0u8; QuicMessage::WIRE_SIZE];
if let Err(e) = recv.read_exact(&mut buf).await {
tracing::trace!(?remote, error = %e, "command receiver: short read; closing stream");
return;
}
let cmd = match QuicMessage::decode(&buf) {
Ok(m) => m,
Err(e) => {
tracing::warn!(?remote, error = %e, "command receiver: decode failed");
let _ = send.reset(0u32.into());
return;
}
};
if cmd.typ() == SensorType::Relay {
// raw_value == 1.0 ⇒ stop the engine; 0.0 ⇒ resume.
let now_running = cmd.raw_value < 0.5;
let was_running = engine_running.swap(now_running, Ordering::SeqCst);
if now_running != was_running {
if now_running {
tracing::info!(device = %cmd.device_id, "Relay=0 received — engine resuming");
} else {
tracing::info!(device = %cmd.device_id, "Relay=1 received — engine stopping");
}
}
} else {
tracing::debug!(
?remote,
sensor_type = cmd.sensor_type,
"command receiver: ignoring non-Relay command"
);
}
// Ack by echoing the command — the substrate's outbound drain measures
// latency from open_bi() to ack receipt.
if let Err(e) = send.write_all(&cmd.to_bytes()).await {
tracing::warn!(?remote, error = %e, "command receiver: ack write failed");
return;
}
if let Err(e) = send.finish() {
tracing::warn!(?remote, error = %e, "command receiver: ack finish failed");
}
}