prns-runtime-embassy 0.3.4

Embassy host runtime for Personal Reticulum
Documentation
use core::cell::RefCell;

use embassy_sync::blocking_mutex::raw::RawMutex;
use embassy_sync::blocking_mutex::Mutex;
use embassy_sync::signal::Signal;
use heapless::FnvIndexMap;

use crate::engine::InterfaceCounts;
use crate::interfaces::{InterfaceId, PacketPhyStats};
use crate::routing::dedup::PacketHash;

use prns_runtime::runtime::packet_phy_retention::{
    fixed_packet_phy_retention, FixedPacketPhyRetention,
};

#[must_use]
pub const fn minimum_interface_store_capacity(interface_capacity: usize) -> usize {
    assert!(interface_capacity > 0);
    interface_capacity.next_power_of_two()
}

pub(crate) trait InterfaceInspectionStore: Sync {
    const RETAINS_COUNTS: bool;
    const RETAINS_PACKET_PHY: bool;

    fn set_interface_counts(&self, interface: InterfaceId, counts: InterfaceCounts);
    fn forget_interface(&self, interface: InterfaceId);
    fn signal_interface_counts_changed(&self);
    fn remember_packet_phy(&self, packet_hash: PacketHash, stats: PacketPhyStats);
}

pub(crate) struct NoInterfaceInspectionStore;

impl InterfaceInspectionStore for NoInterfaceInspectionStore {
    const RETAINS_COUNTS: bool = false;
    const RETAINS_PACKET_PHY: bool = false;

    fn set_interface_counts(&self, _interface: InterfaceId, _counts: InterfaceCounts) {}

    fn forget_interface(&self, _interface: InterfaceId) {}

    fn signal_interface_counts_changed(&self) {}

    fn remember_packet_phy(&self, _packet_hash: PacketHash, _stats: PacketPhyStats) {}
}

pub struct EmbassyInterfaceStore<
    M: RawMutex,
    const INTERFACES: usize,
    const PACKET_PHY_CAPACITY: usize,
    const PACKET_PHY_INDEX_BUCKETS: usize,
> {
    counts: Mutex<M, RefCell<FnvIndexMap<InterfaceId, InterfaceCounts, INTERFACES>>>,
    packet_phy:
        Mutex<M, RefCell<FixedPacketPhyRetention<PACKET_PHY_CAPACITY, PACKET_PHY_INDEX_BUCKETS>>>,
    signal: Signal<M, ()>,
}

impl<
        M: RawMutex,
        const INTERFACES: usize,
        const PACKET_PHY_CAPACITY: usize,
        const PACKET_PHY_INDEX_BUCKETS: usize,
    > Default
    for EmbassyInterfaceStore<M, INTERFACES, PACKET_PHY_CAPACITY, PACKET_PHY_INDEX_BUCKETS>
{
    fn default() -> Self {
        Self::new()
    }
}

impl<
        M: RawMutex,
        const INTERFACES: usize,
        const PACKET_PHY_CAPACITY: usize,
        const PACKET_PHY_INDEX_BUCKETS: usize,
    > EmbassyInterfaceStore<M, INTERFACES, PACKET_PHY_CAPACITY, PACKET_PHY_INDEX_BUCKETS>
{
    #[must_use]
    pub const fn new() -> Self {
        const {
            assert!(
                INTERFACES.is_power_of_two(),
                "EmbassyInterfaceStore INTERFACES must be a power of two: heapless::FnvIndexMap requires it"
            )
        };
        Self {
            counts: Mutex::new(RefCell::new(FnvIndexMap::new())),
            packet_phy: Mutex::new(RefCell::new(fixed_packet_phy_retention())),
            signal: Signal::new(),
        }
    }

    #[must_use]
    pub fn counts(&self, interface: InterfaceId) -> InterfaceCounts {
        self.counts
            .lock(|cell| cell.borrow().get(&interface).copied().unwrap_or_default())
    }

    #[must_use]
    pub fn packet_phy(&self, packet_hash: PacketHash) -> Option<PacketPhyStats> {
        self.packet_phy.lock(|cell| cell.borrow().get(packet_hash))
    }

    pub async fn changed(&self) {
        self.signal.wait().await;
    }
}

impl<
        M: RawMutex + Sync,
        const INTERFACES: usize,
        const PACKET_PHY_CAPACITY: usize,
        const PACKET_PHY_INDEX_BUCKETS: usize,
    > InterfaceInspectionStore
    for EmbassyInterfaceStore<M, INTERFACES, PACKET_PHY_CAPACITY, PACKET_PHY_INDEX_BUCKETS>
{
    const RETAINS_COUNTS: bool = true;
    const RETAINS_PACKET_PHY: bool = true;

    fn set_interface_counts(&self, interface: InterfaceId, counts: InterfaceCounts) {
        self.counts.lock(|cell| {
            let stored = cell.borrow_mut().insert(interface, counts);
            assert!(
                stored.is_ok(),
                "EmbassyInterfaceStore INTERFACES is smaller than the live interface count"
            );
        });
    }

    fn forget_interface(&self, interface: InterfaceId) {
        self.counts.lock(|cell| {
            let _ = cell.borrow_mut().remove(&interface);
        });
    }

    fn signal_interface_counts_changed(&self) {
        self.signal.signal(());
    }

    fn remember_packet_phy(&self, packet_hash: PacketHash, stats: PacketPhyStats) {
        if stats.is_empty() {
            return;
        }
        self.packet_phy
            .lock(|cell| cell.borrow_mut().remember(packet_hash, stats));
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::interfaces::{RssiDbm, INTERFACE_ID_LEN};
    use crate::routing::dedup::dedup_index_buckets;
    use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;

    #[test]
    fn interface_store_capacity_covers_the_interface_ceiling_with_a_power_of_two() {
        assert_eq!(minimum_interface_store_capacity(1), 1);
        assert_eq!(minimum_interface_store_capacity(7), 8);
        assert_eq!(minimum_interface_store_capacity(24), 32);
    }

    #[test]
    fn fixed_store_reads_interface_counts_and_packet_phy() {
        const PACKET_PHY_CAPACITY: usize = 8;
        const PACKET_PHY_INDEX_BUCKETS: usize = dedup_index_buckets(PACKET_PHY_CAPACITY);

        let store = EmbassyInterfaceStore::<
            CriticalSectionRawMutex,
            8,
            PACKET_PHY_CAPACITY,
            PACKET_PHY_INDEX_BUCKETS,
        >::new();
        let interface = InterfaceId::new([5; INTERFACE_ID_LEN]);
        let packet_hash = PacketHash::new([7; 32]);
        let packet_phy = PacketPhyStats {
            rssi: Some(RssiDbm::new(-87)),
            snr: None,
            quality: None,
        };

        assert_eq!(store.counts(interface), InterfaceCounts::default());
        assert_eq!(store.packet_phy(packet_hash), None);

        store.set_interface_counts(
            interface,
            InterfaceCounts {
                destinations: 2,
                links: 1,
                transported_links: 4,
            },
        );
        store.remember_packet_phy(packet_hash, packet_phy);

        assert_eq!(store.counts(interface).transported_links, 4);
        assert_eq!(store.packet_phy(packet_hash), Some(packet_phy));
    }
}