phoxal 0.7.0

Phoxal — production-oriented autonomous robot framework (engine, model, typed bus, contracts).
Documentation
use std::time::Duration;

use crate::api::v1::simulation::clock::Clock;
use crate::api::v1::topic;
use crate::bus::Bus;
use crate::bus::typed::TypedTopicSubscriber;
use anyhow::{Result, anyhow};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Step {
    pub tick: Clock,
}

impl Step {
    pub const fn new(tick: Clock) -> Self {
        Self { tick }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SchedulePolicy {
    CatchUp,
    Collapse,
    Skip,
}

#[derive(Debug, Clone)]
pub struct Schedule {
    period_ns: u64,
    policy: SchedulePolicy,
    next_deadline_ns: u64,
}

impl Schedule {
    pub const fn new(period_ns: u64, policy: SchedulePolicy) -> Self {
        Self {
            period_ns,
            policy,
            next_deadline_ns: period_ns,
        }
    }

    pub const fn from_publish_hz(publish_hz: f64, policy: SchedulePolicy) -> Self {
        Self::new((1_000_000_000_f64 / publish_hz) as u64, policy)
    }

    pub fn due_steps(&mut self, time_ns: u64) -> u64 {
        if self.period_ns == 0 || time_ns < self.next_deadline_ns {
            return 0;
        }

        let overdue = time_ns - self.next_deadline_ns;
        let missed = overdue / self.period_ns;
        let due = match self.policy {
            SchedulePolicy::CatchUp => missed + 1,
            SchedulePolicy::Collapse | SchedulePolicy::Skip => 1,
        };

        self.next_deadline_ns = match self.policy {
            SchedulePolicy::CatchUp | SchedulePolicy::Collapse => {
                self.next_deadline_ns + ((missed + 1) * self.period_ns)
            }
            SchedulePolicy::Skip => time_ns + self.period_ns,
        };

        due
    }
}

#[derive(Debug)]
struct RealClock {
    step: u64,
    time_ns: u64,
    dt_ns: u64,
    interval: tokio::time::Interval,
}

impl RealClock {
    fn new(period: Duration) -> Self {
        let mut interval = tokio::time::interval(period);
        interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
        Self {
            step: 0,
            time_ns: 0,
            dt_ns: period.as_nanos() as u64,
            interval,
        }
    }

    async fn tick(&mut self) -> Step {
        self.interval.tick().await;
        self.step = self.step.saturating_add(1);
        self.time_ns = self.time_ns.saturating_add(self.dt_ns);
        Step::new(Clock::new(0, self.step, self.time_ns, self.dt_ns))
    }
}

enum StepSource {
    Local(RealClock),
    Simulation(TypedTopicSubscriber<Clock>),
}

pub(crate) struct StepStream {
    source: StepSource,
    bound_epoch: Option<u64>,
    last_step: Option<u64>,
}

impl StepStream {
    pub(crate) async fn new(bus: &Bus, simulation: bool, period: Duration) -> Result<Self> {
        Ok(Self {
            source: if simulation {
                StepSource::Simulation(
                    bus.subscriber(&topic::new().v1().simulation().clock())
                        .await?,
                )
            } else {
                StepSource::Local(RealClock::new(period))
            },
            bound_epoch: None,
            last_step: None,
        })
    }

    pub(crate) async fn next(&mut self) -> Result<Step> {
        loop {
            return match &mut self.source {
                StepSource::Local(clock) => Ok(clock.tick().await),
                StepSource::Simulation(subscriber) => match subscriber.recv().await {
                    Ok(received) => {
                        let tick = received.value;
                        if self.bound_epoch != Some(tick.epoch()) {
                            self.bound_epoch = Some(tick.epoch());
                            self.last_step = None;
                        }

                        if let Some(last_step) = self.last_step
                            && tick.step() <= last_step
                        {
                            continue;
                        }

                        self.last_step = Some(tick.step());
                        Ok(Step::new(tick))
                    }
                    Err(error) => Err(anyhow!("simulation clock subscription failed: {error}")),
                },
            };
        }
    }
}