Skip to main content

tf_embedded_hal/
adapters.rs

1//! In-memory adapters for unit tests and host-side simulators. None
2//! of these touch real hardware; they exist so the trait surface is
3//! exercisable end-to-end without bring-up of a board.
4
5use core::convert::TryInto;
6
7use ed25519_compact::{KeyPair, PublicKey, Seed, Signature};
8use heapless::spsc::Queue;
9
10use crate::{BleAdvertiser, Entropy, LoraRadio, NfcReader, SecureElement};
11
12/// Errors from the mock adapters.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum MockError {
15    /// Radio inbox / outbox / NFC buffer was empty.
16    Empty,
17    /// Provided buffer too small for the next frame.
18    BufferTooSmall,
19    /// Outbox at capacity.
20    OutboxFull,
21    /// SecureElement seed could not be parsed.
22    BadSeed,
23    /// Internal RNG state failure (never returned by the mock — kept
24    /// for symmetry with real Entropy implementations).
25    Rng,
26}
27
28/// Maximum frame length carried by `MockLoraRadio`.
29pub const MOCK_FRAME_CAP: usize = 1024;
30/// Maximum number of frames queued in the radio outbox / inbox.
31pub const MOCK_QUEUE_CAP: usize = 16;
32
33/// In-memory LoRa radio. Implements both send and recv against a pair
34/// of FIFO buffers. Tests typically pre-load `inbox` and inspect
35/// `outbox` after the unit under test runs.
36#[derive(Debug)]
37pub struct MockLoraRadio {
38    pub inbox: heapless::Vec<heapless::Vec<u8, MOCK_FRAME_CAP>, MOCK_QUEUE_CAP>,
39    pub outbox: heapless::Vec<heapless::Vec<u8, MOCK_FRAME_CAP>, MOCK_QUEUE_CAP>,
40}
41
42impl Default for MockLoraRadio {
43    fn default() -> Self {
44        Self::new()
45    }
46}
47
48impl MockLoraRadio {
49    pub fn new() -> Self {
50        MockLoraRadio {
51            inbox: heapless::Vec::new(),
52            outbox: heapless::Vec::new(),
53        }
54    }
55
56    /// Push a frame into the inbox so a subsequent `recv` returns it.
57    pub fn enqueue_inbox(&mut self, frame: &[u8]) -> Result<(), MockError> {
58        let mut v: heapless::Vec<u8, MOCK_FRAME_CAP> = heapless::Vec::new();
59        v.extend_from_slice(frame)
60            .map_err(|_| MockError::BufferTooSmall)?;
61        self.inbox.push(v).map_err(|_| MockError::OutboxFull)?;
62        Ok(())
63    }
64}
65
66impl LoraRadio for MockLoraRadio {
67    type Error = MockError;
68
69    fn send(&mut self, payload: &[u8]) -> Result<(), Self::Error> {
70        let mut v: heapless::Vec<u8, MOCK_FRAME_CAP> = heapless::Vec::new();
71        v.extend_from_slice(payload)
72            .map_err(|_| MockError::BufferTooSmall)?;
73        self.outbox.push(v).map_err(|_| MockError::OutboxFull)?;
74        Ok(())
75    }
76
77    fn recv(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
78        if self.inbox.is_empty() {
79            return Err(MockError::Empty);
80        }
81        // Pop front — heapless::Vec doesn't have pop_front so we
82        // shift. Frame counts are bounded by MOCK_QUEUE_CAP (≤16).
83        let frame = self.inbox.remove(0);
84        if frame.len() > buf.len() {
85            return Err(MockError::BufferTooSmall);
86        }
87        buf[..frame.len()].copy_from_slice(&frame);
88        Ok(frame.len())
89    }
90}
91
92/// In-memory BLE advertiser; captures advertised payloads in `last`.
93#[derive(Debug)]
94pub struct MockBleAdvertiser {
95    pub last: Option<heapless::Vec<u8, MOCK_FRAME_CAP>>,
96    pub advertise_count: u32,
97}
98
99impl Default for MockBleAdvertiser {
100    fn default() -> Self {
101        Self::new()
102    }
103}
104
105impl MockBleAdvertiser {
106    pub fn new() -> Self {
107        MockBleAdvertiser {
108            last: None,
109            advertise_count: 0,
110        }
111    }
112}
113
114impl BleAdvertiser for MockBleAdvertiser {
115    type Error = MockError;
116    fn advertise(&mut self, payload: &[u8]) -> Result<(), Self::Error> {
117        let mut v: heapless::Vec<u8, MOCK_FRAME_CAP> = heapless::Vec::new();
118        v.extend_from_slice(payload)
119            .map_err(|_| MockError::BufferTooSmall)?;
120        self.last = Some(v);
121        self.advertise_count += 1;
122        Ok(())
123    }
124}
125
126/// In-memory NFC reader — `read` returns frames previously pushed via
127/// [`MockNfcReader::enqueue`].
128#[derive(Debug)]
129pub struct MockNfcReader {
130    pub queue: heapless::Vec<heapless::Vec<u8, MOCK_FRAME_CAP>, MOCK_QUEUE_CAP>,
131}
132
133impl Default for MockNfcReader {
134    fn default() -> Self {
135        Self::new()
136    }
137}
138
139impl MockNfcReader {
140    pub fn new() -> Self {
141        MockNfcReader {
142            queue: heapless::Vec::new(),
143        }
144    }
145    pub fn enqueue(&mut self, frame: &[u8]) -> Result<(), MockError> {
146        let mut v: heapless::Vec<u8, MOCK_FRAME_CAP> = heapless::Vec::new();
147        v.extend_from_slice(frame)
148            .map_err(|_| MockError::BufferTooSmall)?;
149        self.queue.push(v).map_err(|_| MockError::OutboxFull)?;
150        Ok(())
151    }
152}
153
154impl NfcReader for MockNfcReader {
155    type Error = MockError;
156    fn read(&mut self, buf: &mut [u8]) -> Result<usize, Self::Error> {
157        if self.queue.is_empty() {
158            return Err(MockError::Empty);
159        }
160        let frame = self.queue.remove(0);
161        if frame.len() > buf.len() {
162            return Err(MockError::BufferTooSmall);
163        }
164        buf[..frame.len()].copy_from_slice(&frame);
165        Ok(frame.len())
166    }
167}
168
169/// SecureElement backed by a fixed seed. Useful for tests and host
170/// simulators; should never be used in a production build because it
171/// keeps the seed in plain RAM.
172#[derive(Debug)]
173pub struct MockSecureElement {
174    keypair: KeyPair,
175    pub_bytes: [u8; 32],
176}
177
178impl MockSecureElement {
179    /// Create from a 32-byte ed25519 seed.
180    pub fn from_seed(seed_bytes: [u8; 32]) -> Result<Self, MockError> {
181        let seed = Seed::from_slice(&seed_bytes).map_err(|_| MockError::BadSeed)?;
182        let keypair = KeyPair::from_seed(seed);
183        let pub_bytes: [u8; 32] = keypair
184            .pk
185            .as_ref()
186            .try_into()
187            .map_err(|_| MockError::BadSeed)?;
188        Ok(MockSecureElement { keypair, pub_bytes })
189    }
190
191    /// Verify a signature against this element's public key. Useful in
192    /// round-trip tests.
193    pub fn verify(&self, msg: &[u8], sig: &[u8; 64]) -> bool {
194        let sig = match Signature::from_slice(sig) {
195            Ok(s) => s,
196            Err(_) => return false,
197        };
198        let pk = match PublicKey::from_slice(&self.pub_bytes) {
199            Ok(p) => p,
200            Err(_) => return false,
201        };
202        pk.verify(msg, &sig).is_ok()
203    }
204}
205
206impl SecureElement for MockSecureElement {
207    type Error = MockError;
208
209    fn sign(&mut self, msg: &[u8]) -> Result<[u8; 64], Self::Error> {
210        let sig = self.keypair.sk.sign(msg, None);
211        let bytes: [u8; 64] = sig.as_ref().try_into().expect("ed25519 sig is 64 bytes");
212        Ok(bytes)
213    }
214
215    fn pubkey(&self) -> [u8; 32] {
216        self.pub_bytes
217    }
218}
219
220/// Simple deterministic RNG for tests. Uses xorshift64* seeded from a
221/// constructor argument. Not cryptographically secure — production
222/// drivers must implement `Entropy` against a real HW-RNG.
223#[derive(Debug)]
224pub struct MockEntropy {
225    state: u64,
226}
227
228impl MockEntropy {
229    pub fn new(seed: u64) -> Self {
230        // xorshift64* requires non-zero state.
231        let s = if seed == 0 {
232            0xdead_beef_dead_beef
233        } else {
234            seed
235        };
236        MockEntropy { state: s }
237    }
238}
239
240impl Entropy for MockEntropy {
241    type Error = MockError;
242    fn fill(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> {
243        for chunk in buf.chunks_mut(8) {
244            self.state ^= self.state << 13;
245            self.state ^= self.state >> 7;
246            self.state ^= self.state << 17;
247            let v = self.state.wrapping_mul(0x2545_F491_4F6C_DD1Du64);
248            let bytes = v.to_le_bytes();
249            chunk.copy_from_slice(&bytes[..chunk.len()]);
250        }
251        Ok(())
252    }
253}
254
255// `Queue` is not used by the mocks today but the import keeps the
256// crate's heapless feature surface visible to downstream callers.
257#[doc(hidden)]
258pub type _UnusedQueueImport<T, const N: usize> = Queue<T, N>;