Flip T3 to substrate-initiated actuator commands

This commit is contained in:
Valère Plantevin
2026-05-13 15:03:23 -04:00
parent 272d3b3c59
commit baa075fe0f
22 changed files with 1003 additions and 749 deletions

View File

@@ -6,26 +6,41 @@ use tokio::runtime::Handle;
use tokio::sync::mpsc;
use crate::config::AppConfig;
use crate::transport::{QuicMessage, T1Sender, T2Sender, T3Inbound, T3Sender};
use crate::transport::server::{accept_loop, bind_endpoint};
use crate::transport::{OutboundT3, QuicMessage, T1Sender, T2Sender, T3OutboundSender};
use crate::transport::server::{ConnectionRegistry, accept_loop, bind_endpoint, new_connection_registry};
use crate::transport::state::ServerState;
pub struct EcsQuicTransportPlugin;
/// Receive halves of the three tier channels, wrapped so they can sit in a
/// Bevy `Resource`. The `world` module's ingest system is the sole reader.
/// Receive halves of the inbound tier channels (T1 datagrams, T2 uni
/// streams). The `world` module's ingest system is the sole reader.
/// T3 is substrate-initiated and lives on the tokio side via the outbound
/// drain task — no inbound T3 receiver exists here.
#[derive(Resource)]
pub(crate) struct BridgeReceivers {
pub(crate) t1: Mutex<mpsc::Receiver<QuicMessage>>,
pub(crate) t2: Mutex<mpsc::Receiver<QuicMessage>>,
pub(crate) t3: Mutex<mpsc::Receiver<T3Inbound>>,
}
#[derive(Resource, Clone)]
pub(crate) struct BridgeSenders {
pub(crate) t1: T1Sender,
pub(crate) t2: T2Sender,
pub(crate) t3: T3Sender,
/// Outbound actuator-command sender — `automation_system` enqueues
/// `OutboundT3` items here; the tokio drain task routes them to the
/// originating device's connection.
pub(crate) t3_out: T3OutboundSender,
}
/// Holds the receiver half of the outbound-T3 channel until the listener
/// starts, plus the connection registry and a sender clone for the optional
/// synthetic T3 driver. All pass into `accept_loop` once at the
/// `Starting → Started` transition.
#[derive(Resource)]
pub(crate) struct OutboundT3Plumbing {
pub(crate) rx: Mutex<Option<mpsc::Receiver<OutboundT3>>>,
pub(crate) tx: mpsc::Sender<OutboundT3>,
pub(crate) registry: ConnectionRegistry,
}
#[derive(Resource, Clone)]
@@ -37,6 +52,7 @@ fn start_quic_server(
config: Res<AppConfig>,
senders: Res<BridgeSenders>,
runtime: Res<TokioHandle>,
outbound: Res<OutboundT3Plumbing>,
mut next: ResMut<NextState<ServerState>>,
) {
tracing::info!("entering ServerState::Starting — bringing up QUIC listener");
@@ -50,8 +66,29 @@ fn start_quic_server(
tracing::info!(local = ?endpoint.local_addr().ok(), "QUIC listener bound");
// Move the outbound receiver into the tokio side; accept_loop owns it for
// the rest of the listener's life. The registry is cloned (it's already an
// `Arc`) so the ECS-side resource can still observe the routes if needed.
let outbound_rx = outbound
.rx
.lock()
.unwrap()
.take()
.expect("OutboundT3 receiver consumed twice");
let outbound_tx = outbound.tx.clone();
let registry = outbound.registry.clone();
let synthetic_rate = config.network.synthetic_t3_rate_hz;
let s = senders.clone();
runtime.0.spawn(accept_loop(endpoint, s.t1, s.t2, s.t3));
runtime.0.spawn(accept_loop(
endpoint,
s.t1,
s.t2,
registry,
outbound_rx,
outbound_tx,
synthetic_rate,
));
next.set(ServerState::Started);
tracing::info!("ServerState::Started");
@@ -60,11 +97,15 @@ fn start_quic_server(
impl Plugin for EcsQuicTransportPlugin {
fn build(&self, app: &mut App) {
let config = app.world_mut().resource::<AppConfig>();
// Three-tier bridge between the tokio-side QUIC accept loop and the
// Inbound bridge: T1 datagrams + T2 uni streams from devices into the
// ECS PreUpdate ingest system (in the `world` module).
let (t1_tx, t1_rx) = mpsc::channel::<QuicMessage>(config.network.t1_capacity);
let (t2_tx, t2_rx) = mpsc::channel::<QuicMessage>(config.network.t2_capacity);
let (t3_tx, t3_rx) = mpsc::channel::<T3Inbound>(config.network.t3_capacity);
// Outbound-T3: substrate → device actuator-command path. Capacity
// budget tracks automation cadence, not per-sample throughput.
let (t3_out_tx, t3_out_rx) = mpsc::channel::<OutboundT3>(config.network.t3_capacity);
let registry = new_connection_registry();
// Spawn a tokio runtime on a dedicated OS thread, ship its Handle back
// to the ECS, and keep the runtime alive for the lifetime of the app
@@ -96,12 +137,16 @@ impl Plugin for EcsQuicTransportPlugin {
.insert_resource(BridgeSenders {
t1: T1Sender::new(t1_tx),
t2: T2Sender::new(t2_tx),
t3: T3Sender::new(t3_tx),
t3_out: T3OutboundSender::new(t3_out_tx.clone()),
})
.insert_resource(BridgeReceivers {
t1: Mutex::new(t1_rx),
t2: Mutex::new(t2_rx),
t3: Mutex::new(t3_rx),
})
.insert_resource(OutboundT3Plumbing {
rx: Mutex::new(Some(t3_out_rx)),
tx: t3_out_tx,
registry,
})
.add_systems(OnEnter(ServerState::Starting), start_quic_server);
}