lxmf-embedded-mini 0.7.0

No-alloc mini LXMF runtime for embedded targets.
Documentation
use heapless::{Deque, Vec};
use lxmf_core::{Payload, WireMessage};

use crate::{
    LinkState, MiniError, MiniMessage, MiniNodeRuntime, MiniRuntimeConfig, MiniStore,
    MiniTransport, RamReplayStore,
};

const TITLE: usize = 32;
const CONTENT: usize = 128;
const FRAME: usize = 256;

type DefaultRuntime = MiniNodeRuntime<TITLE, CONTENT, FRAME, 4, 8>;
type TinyEventRuntime = MiniNodeRuntime<TITLE, CONTENT, FRAME, 4, 1>;

#[derive(Debug)]
struct MockTransport<const IN: usize, const OUT: usize> {
    state: LinkState,
    inbound: Deque<Vec<u8, FRAME>, IN>,
    sent: Deque<Vec<u8, FRAME>, OUT>,
    fail_send: Option<MiniError>,
}

impl<const IN: usize, const OUT: usize> MockTransport<IN, OUT> {
    fn new() -> Self {
        Self { state: LinkState::Up, inbound: Deque::new(), sent: Deque::new(), fail_send: None }
    }

    fn enqueue_inbound(&mut self, bytes: &[u8]) {
        let mut frame = Vec::<u8, FRAME>::new();
        frame.extend_from_slice(bytes).expect("test frame fits");
        self.inbound.push_back(frame).expect("test inbound capacity");
    }
}

impl<const IN: usize, const OUT: usize> MiniTransport for MockTransport<IN, OUT> {
    fn link_state(&self) -> LinkState {
        self.state
    }

    fn send_frame(&mut self, frame: &[u8]) -> crate::MiniResult<()> {
        if self.state != LinkState::Up {
            return Err(MiniError::Disconnected);
        }
        if let Some(error) = self.fail_send.take() {
            return Err(error);
        }
        let mut out = Vec::<u8, FRAME>::new();
        out.extend_from_slice(frame).map_err(|_| MiniError::CapacityExceeded)?;
        self.sent.push_back(out).map_err(|_| MiniError::Backpressure)
    }

    fn poll_frame(&mut self, out: &mut [u8]) -> crate::MiniResult<Option<usize>> {
        let Some(frame) = self.inbound.pop_front() else {
            return Ok(None);
        };
        if out.len() < frame.len() {
            return Err(MiniError::BufferTooSmall);
        }
        out[..frame.len()].copy_from_slice(&frame);
        Ok(Some(frame.len()))
    }
}

fn config() -> MiniRuntimeConfig {
    MiniRuntimeConfig { local_identity: [0x22; 16] }
}

fn signature() -> [u8; 64] {
    [0xA5; 64]
}

#[test]
fn mini_wire_roundtrips_without_alloc() {
    let message = MiniMessage::<TITLE, CONTENT>::new(
        [0x11; 16],
        [0x22; 16],
        signature(),
        1234.5,
        b"status",
        b"hello",
    )
    .expect("message");
    let mut out = [0u8; FRAME];
    let len = message.encode(&mut out).expect("encode");
    let decoded = MiniMessage::<TITLE, CONTENT>::decode(&out[..len]).expect("decode");

    assert_eq!(decoded.destination, [0x11; 16]);
    assert_eq!(decoded.source, [0x22; 16]);
    assert_eq!(decoded.signature, signature());
    assert_eq!(decoded.timestamp(), 1234.5);
    assert_eq!(decoded.title.as_slice(), b"status");
    assert_eq!(decoded.content.as_slice(), b"hello");
}

#[test]
fn mini_decode_accepts_lxmf_core_basic_wire_subset() {
    let payload =
        Payload::new(42.0, Some(std::vec![0xCA, 0xFE]), Some(std::vec![0xBA, 0xBE]), None, None);
    let mut wire = WireMessage::new([0x11; 16], [0x22; 16], payload);
    wire.signature = Some(signature());
    let packed = wire.pack().expect("pack");

    let decoded = MiniMessage::<TITLE, CONTENT>::decode(&packed).expect("mini decode");
    assert_eq!(decoded.destination, [0x11; 16]);
    assert_eq!(decoded.source, [0x22; 16]);
    assert_eq!(decoded.title.as_slice(), &[0xBA, 0xBE]);
    assert_eq!(decoded.content.as_slice(), &[0xCA, 0xFE]);
}

#[test]
fn lxmf_core_decodes_mini_basic_wire_subset() {
    let message = MiniMessage::<TITLE, CONTENT>::new(
        [0x11; 16],
        [0x22; 16],
        signature(),
        7.0,
        b"title",
        b"content",
    )
    .expect("message");
    let mut out = [0u8; FRAME];
    let len = message.encode(&mut out).expect("encode");

    let decoded = WireMessage::unpack(&out[..len]).expect("lxmf-core decode");
    assert_eq!(decoded.destination, [0x11; 16]);
    assert_eq!(decoded.source, [0x22; 16]);
    assert_eq!(decoded.signature, Some(signature()));
    assert_eq!(decoded.payload.title.expect("title").as_ref(), b"title");
    assert_eq!(decoded.payload.content.expect("content").as_ref(), b"content");
    assert!(decoded.payload.fields.is_none());
}

#[test]
fn runtime_queues_and_flushes_when_link_is_up() {
    let mut runtime = DefaultRuntime::new(config()).expect("runtime");
    let mut transport = MockTransport::<4, 4>::new();
    let mut store = RamReplayStore::new();
    let mut scratch = [0u8; FRAME];

    let sequence =
        runtime.queue_message([0x11; 16], b"title", b"content", signature(), 1.0).expect("queue");
    runtime.tick(&mut transport, &mut store, &mut scratch).expect("tick");

    assert_eq!(sequence, 1);
    assert_eq!(runtime.status().outbound_len, 0);
    assert_eq!(runtime.status().stats.sent, 1);
    assert_eq!(transport.sent.len(), 1);
}

#[test]
fn runtime_defers_on_transport_backpressure() {
    let mut runtime = DefaultRuntime::new(config()).expect("runtime");
    let mut transport = MockTransport::<4, 4>::new();
    let mut store = RamReplayStore::new();
    let mut scratch = [0u8; FRAME];

    runtime.queue_message([0x11; 16], b"title", b"content", signature(), 1.0).expect("queue");
    transport.fail_send = Some(MiniError::Backpressure);
    runtime.tick(&mut transport, &mut store, &mut scratch).expect("tick");

    assert_eq!(runtime.status().outbound_len, 1);
    assert_eq!(runtime.status().stats.deferred, 1);
}

#[test]
fn runtime_rejects_replayed_inbound_message() {
    let mut inbound = MiniMessage::<TITLE, CONTENT>::new(
        [0x22; 16],
        [0x33; 16],
        signature(),
        9.0,
        b"title",
        b"content",
    )
    .expect("message");
    let mut frame = [0u8; FRAME];
    let len = inbound.encode(&mut frame).expect("encode");

    let mut runtime = DefaultRuntime::new(config()).expect("runtime");
    let mut transport = MockTransport::<4, 4>::new();
    transport.enqueue_inbound(&frame[..len]);
    inbound.timestamp = 9.0f64.to_bits();
    let duplicate_len = inbound.encode(&mut frame).expect("encode duplicate");
    transport.enqueue_inbound(&frame[..duplicate_len]);
    let mut store = RamReplayStore::new();
    let mut scratch = [0u8; FRAME];

    runtime.tick(&mut transport, &mut store, &mut scratch).expect("tick");

    assert_eq!(runtime.status().stats.received, 1);
    assert_eq!(runtime.status().stats.replay_rejected, 1);
    assert_eq!(store.load_replay_floor(&[0x22; 16]).expect("floor"), 9.0f64.to_bits());
}

#[test]
fn runtime_accepts_out_of_order_messages_from_different_peers() {
    let first = MiniMessage::<TITLE, CONTENT>::new(
        [0x22; 16],
        [0x33; 16],
        signature(),
        100.0,
        b"title",
        b"content-a",
    )
    .expect("first message");
    let second = MiniMessage::<TITLE, CONTENT>::new(
        [0x22; 16],
        [0x44; 16],
        signature(),
        90.0,
        b"title",
        b"content-b",
    )
    .expect("second message");

    let mut first_frame = [0u8; FRAME];
    let first_len = first.encode(&mut first_frame).expect("encode first");
    let mut second_frame = [0u8; FRAME];
    let second_len = second.encode(&mut second_frame).expect("encode second");

    let mut runtime = DefaultRuntime::new(config()).expect("runtime");
    let mut transport = MockTransport::<4, 4>::new();
    transport.enqueue_inbound(&first_frame[..first_len]);
    transport.enqueue_inbound(&second_frame[..second_len]);
    let mut store = RamReplayStore::new();
    let mut scratch = [0u8; FRAME];

    runtime.tick(&mut transport, &mut store, &mut scratch).expect("tick");

    assert_eq!(runtime.status().stats.received, 2);
    assert_eq!(runtime.status().stats.replay_rejected, 0);
}

#[test]
fn runtime_reports_event_overflow() {
    let mut runtime = TinyEventRuntime::new(config()).expect("runtime");
    let mut transport = MockTransport::<4, 4>::new();
    let mut store = RamReplayStore::new();
    let mut scratch = [0u8; FRAME];

    runtime.tick(&mut transport, &mut store, &mut scratch).expect("bootstrap event fits");
    let err = runtime
        .queue_message([0x11; 16], b"title", b"content", signature(), 1.0)
        .expect_err("event overflow");

    assert_eq!(err, MiniError::EventOverflow);
    assert_eq!(runtime.status().stats.event_overflows, 1);
    assert_eq!(runtime.status().outbound_len, 0);
    assert_eq!(runtime.status().stats.queued, 0);

    runtime.poll_event().expect("drain bootstrap event");
    let sequence =
        runtime.queue_message([0x11; 16], b"title", b"content", signature(), 1.0).expect("queue");

    assert_eq!(sequence, 1);
    assert_eq!(runtime.status().outbound_len, 1);
    assert_eq!(runtime.status().stats.queued, 1);
}