use std::collections::BTreeMap;
use crate::bus::handle::publisher::WorldClockPublisher;
use crate::bus::handle::stamp::TimelineAuthority;
use crate::bus::session::{BusConfig, BusOwner};
use crate::bus::{
BusCloseReport, BusError, BusHandle, Endpoint, ParticipantReadyEvents, ParticipantReadyToken,
Publish, RobotEndpoint, Sample, SamplePublisher, Setpoint, SetpointReceiver, SourceLabel,
SourceLabelError, State, StatePublisher, Subscribe, Topic, WorldStepToken,
};
use crate::identity::{ExecutionId, ParticipantId, TimelineId};
use crate::runtime::api::simulation::Clock;
#[derive(Debug, thiserror::Error)]
pub enum SimulatorError {
#[error(
"no Phoxal execution is reachable at {connect}; start the supervisor before the simulation"
)]
NoExecution { connect: String },
#[error(
"{count} Phoxal executions are reachable at {connect}, which must identify exactly one: {executions:?}"
)]
MultipleExecutions {
connect: String,
count: usize,
executions: Vec<ExecutionId>,
},
#[error(transparent)]
SourceLabel(#[from] SourceLabelError),
#[error(transparent)]
Bus(#[from] BusError),
#[error("this session's world time has already been taken")]
WorldTimeTaken,
}
#[derive(Debug, thiserror::Error)]
#[error("the simulator session did not close cleanly: {report}")]
pub struct SimulatorCloseError {
pub report: BusCloseReport,
}
#[derive(Clone, Debug)]
pub struct SimulatorConnectOptions {
pub connect: String,
pub label: String,
}
impl SimulatorConnectOptions {
#[must_use]
pub fn new(connect: impl Into<String>, label: impl Into<String>) -> Self {
Self {
connect: connect.into(),
label: label.into(),
}
}
}
pub struct SimulatorSession {
presence: BTreeMap<ParticipantId, ParticipantReadyToken>,
world_time: Option<WorldTime>,
bus: BusHandle,
execution: ExecutionId,
owner: Option<BusOwner>,
}
impl std::fmt::Debug for SimulatorSession {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("SimulatorSession")
.field("execution", &self.execution)
.field("presented", &self.presence.len())
.field("world_time_taken", &self.world_time.is_none())
.finish_non_exhaustive()
}
}
impl SimulatorSession {
pub async fn probe(connect: &str) -> Result<Vec<ExecutionId>, SimulatorError> {
Ok(BusOwner::probe_routers(connect).await?)
}
pub async fn connect(options: SimulatorConnectOptions) -> Result<Self, SimulatorError> {
let executions = Self::probe(&options.connect).await?;
let execution = match executions.as_slice() {
[only] => *only,
[] => {
return Err(SimulatorError::NoExecution {
connect: options.connect,
});
}
many => {
let mut executions = many.to_vec();
executions.sort_by_key(ToString::to_string);
return Err(SimulatorError::MultipleExecutions {
connect: options.connect,
count: executions.len(),
executions,
});
}
};
let label = SourceLabel::new(options.label)?;
Self::open(
BusConfig::for_external(execution, Some(label), vec![options.connect]),
execution,
)
.await
}
pub async fn in_process(label: &str) -> Result<Self, SimulatorError> {
let execution = ExecutionId::mint();
Self::open(
BusConfig::for_external(execution, Some(SourceLabel::new(label)?), Vec::new()),
execution,
)
.await
}
async fn open(config: BusConfig, execution: ExecutionId) -> Result<Self, SimulatorError> {
let (owner, bus) = BusOwner::open(config).await?;
let world_time = match WorldTime::open(&bus) {
Ok(world_time) => world_time,
Err(error) => {
let _ = owner.close().await;
return Err(error);
}
};
Ok(Self {
owner: Some(owner),
bus,
execution,
presence: BTreeMap::new(),
world_time: Some(world_time),
})
}
#[must_use]
pub fn execution(&self) -> ExecutionId {
self.execution
}
pub fn sample_publisher<E>(
&self,
topic: Topic<Publish<E>>,
) -> Result<SamplePublisher<E>, SimulatorError>
where
E: RobotEndpoint + Endpoint<Semantics = Sample>,
{
Ok(SamplePublisher::new(self.bus.clone(), &topic)?)
}
pub fn state_publisher<E>(
&self,
topic: Topic<Publish<E>>,
) -> Result<StatePublisher<E>, SimulatorError>
where
E: RobotEndpoint + Endpoint<Semantics = State>,
{
Ok(StatePublisher::new(self.bus.clone(), &topic)?)
}
pub async fn setpoint_receiver<E>(
&self,
topic: Topic<Subscribe<E>>,
) -> Result<SetpointReceiver<E>, SimulatorError>
where
E: RobotEndpoint + Endpoint<Semantics = Setpoint>,
{
Ok(SetpointReceiver::new(&self.bus, &topic).await?)
}
pub async fn participant_ready_events(
&self,
participant: &ParticipantId,
) -> Result<ParticipantReadyEvents, SimulatorError> {
Ok(self.bus.participant_ready_events_for(participant).await?)
}
pub async fn present(&mut self, participant: &ParticipantId) -> Result<(), SimulatorError> {
if self.presence.contains_key(participant) {
return Ok(());
}
let Some(owner) = self.owner.as_ref() else {
return Ok(());
};
let token = owner.declare_participant_ready_as(participant).await?;
self.presence.insert(participant.clone(), token);
Ok(())
}
pub fn take_world_time(&mut self) -> Result<WorldTime, SimulatorError> {
self.world_time.take().ok_or(SimulatorError::WorldTimeTaken)
}
pub async fn close(mut self) -> Result<(), SimulatorCloseError> {
self.presence.clear();
self.world_time = None;
let Some(owner) = self.owner.take() else {
return Ok(());
};
let report = owner.close().await;
if report.is_clean() {
Ok(())
} else {
Err(SimulatorCloseError { report })
}
}
}
pub struct WorldTime {
authority: TimelineAuthority,
clock: WorldClockPublisher<Clock>,
}
impl std::fmt::Debug for WorldTime {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("WorldTime")
.field("timeline", &self.authority.timeline())
.finish_non_exhaustive()
}
}
impl WorldTime {
fn open(bus: &BusHandle) -> Result<Self, SimulatorError> {
let authority = TimelineAuthority::mint(TimelineId::mint())?;
let clock = WorldClockPublisher::mint(
bus.clone(),
&crate::runtime::api::topics().simulation().clock().owner(),
)?;
Ok(Self { authority, clock })
}
pub fn completed_step(&mut self, time_ns: u64) -> WorldStepToken {
self.authority.completed_step(time_ns)
}
pub fn replace_timeline(&mut self) {
self.authority.replace_timeline(TimelineId::mint());
}
#[must_use]
pub fn timeline(&self) -> TimelineId {
self.authority.timeline()
}
pub fn publish_clock(
&mut self,
step: &WorldStepToken,
clock: Clock,
) -> Result<(), SimulatorError> {
Ok(self.clock.publish(step, clock)?)
}
}
#[cfg(test)]
mod world_session_tests;