Skip to main content

pamoja_radios/sx1302/
bridge.rs

1//! Reaching a concentrator through the microcontroller on a USB card.
2//!
3//! A CoreCell on SPI is wired straight to the host. The USB version of the same card puts
4//! an STM32 between them: the host talks to the microcontroller over a serial port, and the
5//! microcontroller does the SPI on the host's behalf, drives the reset and supply pins, and
6//! reports its own clock and temperature. Everything the concentrator driver sends still
7//! goes out as the same SPI frames; they are carried inside a message to the bridge rather
8//! than clocked out by the host.
9//!
10//! Every message is a four byte header, an identifier the host picks, a size, and an order,
11//! then that many bytes of payload. The bridge answers with the same shape, the order moved
12//! up by [`ACK`]. One order carries many SPI transfers at once: each is an entry naming a
13//! target, a length, and the raw frame, and the answer echoes every entry with a status and
14//! the bytes the chip clocked back, which is where a read finds its value.
15//!
16//! This module builds those messages and reads the answers. It opens no port.
17
18/// The most bytes one message carries, on either side.
19pub const MAX_MESSAGE: usize = 4200;
20
21/// The most bytes of SPI entries one bulk order carries.
22pub const BULK_CHUNK: usize = 4096;
23
24/// The most SPI entries one bulk order carries.
25pub const BULK_ENTRIES: usize = 255;
26
27/// How long a header is: an identifier, a size, and an order.
28pub const HEADER_LEN: usize = 4;
29
30/// The bridge firmware this crate was written against, as the ping reports it after its
31/// first character.
32///
33/// The first character says whether the firmware is a release or a debug build, and the
34/// reference compares what follows. A different version is a warning there rather than a
35/// refusal, and it is here too.
36pub const FIRMWARE_VERSION: &str = "01.00.00";
37
38/// What the bridge adds to an order to answer it.
39pub const ACK: u8 = 0x40;
40
41/// The bridge answering that it could not read an order.
42pub const ORDER_ERROR: u8 = 0xff;
43
44/// Asks the bridge who it is: a unique identifier and a firmware version.
45pub const ORDER_PING: u8 = 0x00;
46
47/// Asks the bridge for its clock and its temperature.
48pub const ORDER_STATUS: u8 = 0x01;
49
50/// Drops the bridge into its bootloader, for a firmware update.
51pub const ORDER_BOOTLOADER: u8 = 0x02;
52
53/// Resets the card.
54pub const ORDER_RESET: u8 = 0x03;
55
56/// Drives one of the bridge's pins.
57pub const ORDER_WRITE_GPIO: u8 = 0x04;
58
59/// Carries SPI transfers to the concentrator, or the radio beside it.
60pub const ORDER_SPI: u8 = 0x05;
61
62/// The nominal speed the port is opened at.
63///
64/// A USB serial port does not really have one, so this only has to be a value the port
65/// accepts; the reference uses it and so does this crate.
66pub const BAUD: u32 = 115_200;
67
68/// The bridge pin port every card pin is on.
69pub const GPIO_PORT: u8 = 0;
70
71/// The pin that switches the concentrator's supply on.
72pub const PIN_POWER: u8 = 1;
73
74/// The pin that resets the concentrator, active high.
75pub const PIN_RESET: u8 = 2;
76
77/// The pin that resets the radio beside the concentrator, active low.
78pub const PIN_RADIO_RESET: u8 = 8;
79
80/// The concentrator, and the front ends behind it, as an SPI target.
81pub const TARGET_CONCENTRATOR: u8 = 0;
82
83/// The radio beside the concentrator that listens before it talks.
84pub const TARGET_LISTENER: u8 = 1;
85
86/// An SPI entry that clocks a frame out and the answer back.
87pub const ENTRY_TRANSFER: u8 = 0x01;
88
89/// An SPI entry that changes some bits of one register, done on the bridge.
90pub const ENTRY_MODIFY: u8 = 0x02;
91
92/// How many bytes a ping answers with.
93pub const PING_LEN: usize = 21;
94
95/// How many bytes a status answers with.
96pub const STATUS_LEN: usize = 6;
97
98/// The header on every message, either way.
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100pub struct Header {
101    /// An identifier the host picks. The bridge does not check it and the reference does
102    /// not compare it on the way back, so it is a courtesy rather than a guard.
103    pub id: u8,
104    /// How many bytes of payload follow.
105    pub size: u16,
106    /// What the message is, or, from the bridge, what it answers.
107    pub order: u8,
108}
109
110impl Header {
111    /// The bytes that go out.
112    ///
113    /// # Returns
114    ///
115    /// The identifier, the size high byte first, then the order.
116    ///
117    /// # Examples
118    ///
119    /// ```
120    /// use pamoja_radios::sx1302::bridge::{Header, ORDER_WRITE_GPIO};
121    ///
122    /// let header = Header { id: 7, size: 3, order: ORDER_WRITE_GPIO };
123    /// assert_eq!(header.to_bytes(), [7, 0, 3, 0x04]);
124    /// ```
125    #[must_use]
126    pub const fn to_bytes(self) -> [u8; HEADER_LEN] {
127        [self.id, (self.size >> 8) as u8, self.size as u8, self.order]
128    }
129
130    /// Reads a header the bridge sent.
131    ///
132    /// # Arguments
133    ///
134    /// * `bytes` - the four bytes.
135    ///
136    /// # Returns
137    ///
138    /// The header.
139    #[must_use]
140    pub const fn from_bytes(bytes: [u8; HEADER_LEN]) -> Header {
141        Header {
142            id: bytes[0],
143            size: ((bytes[1] as u16) << 8) | bytes[2] as u16,
144            order: bytes[3],
145        }
146    }
147
148    /// Whether this header answers an order, rather than carrying one.
149    ///
150    /// # Returns
151    ///
152    /// Whether the order is in the range the bridge answers with.
153    #[must_use]
154    pub const fn is_answer(&self) -> bool {
155        self.order >= ACK && self.order <= ACK + 6
156    }
157
158    /// Whether this header answers a particular order.
159    ///
160    /// # Arguments
161    ///
162    /// * `order` - the order that was sent.
163    ///
164    /// # Returns
165    ///
166    /// Whether the bridge answered that one.
167    ///
168    /// # Examples
169    ///
170    /// ```
171    /// use pamoja_radios::sx1302::bridge::{Header, ACK, ORDER_PING};
172    ///
173    /// let answer = Header::from_bytes([7, 0, 21, ORDER_PING + ACK]);
174    /// assert!(answer.answers(ORDER_PING));
175    /// assert!(!answer.answers(ORDER_PING + 1));
176    /// ```
177    #[must_use]
178    pub const fn answers(&self, order: u8) -> bool {
179        self.order == order + ACK
180    }
181}
182
183/// What the bridge refused, or answered wrongly.
184#[derive(Clone, Copy, Debug, PartialEq, Eq)]
185pub enum BridgeError {
186    /// The bridge answered a different order than the one sent.
187    WrongAnswer {
188        /// What was asked.
189        asked: u8,
190        /// What came back.
191        answered: u8,
192    },
193    /// The answer is shorter than that order's answer is.
194    Short {
195        /// How many bytes came back.
196        got: usize,
197        /// How many the answer needs.
198        needs: usize,
199    },
200    /// A pin write or a reset did not take.
201    Refused {
202        /// The status the bridge gave.
203        status: u8,
204    },
205    /// One SPI entry in a bulk order failed.
206    Transfer {
207        /// Which entry.
208        id: u8,
209        /// How, as the bridge reports it: 1 failed, 2 a wrong parameter, 3 a timeout.
210        status: u8,
211    },
212    /// An SPI entry in the answer is not one of the two kinds.
213    UnknownEntry {
214        /// Which entry.
215        id: u8,
216        /// What kind it claimed to be.
217        kind: u8,
218    },
219    /// A bulk order holds more than the bridge takes.
220    TooMany,
221}
222
223impl core::fmt::Display for BridgeError {
224    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
225        match self {
226            BridgeError::WrongAnswer { asked, answered } => write!(
227                f,
228                "the bridge answered order {answered:#04x} when {asked:#04x} was sent"
229            ),
230            BridgeError::Short { got, needs } => {
231                write!(f, "the bridge answered {got} bytes and {needs} were needed")
232            }
233            BridgeError::Refused { status } => {
234                write!(f, "the bridge refused with status {status}")
235            }
236            BridgeError::Transfer { id, status } => write!(
237                f,
238                "SPI entry {id} failed on the bridge with status {status}"
239            ),
240            BridgeError::UnknownEntry { id, kind } => {
241                write!(f, "SPI entry {id} came back as kind {kind:#04x}")
242            }
243            BridgeError::TooMany => {
244                f.write_str("a bulk order holds more entries or bytes than the bridge takes")
245            }
246        }
247    }
248}
249
250impl core::error::Error for BridgeError {}
251
252/// Who a bridge says it is.
253#[derive(Clone, Copy, Debug, PartialEq, Eq)]
254pub struct Identity {
255    /// The microcontroller's unique identifier, twelve bytes.
256    pub unique_id: [u8; 12],
257    /// The firmware version, nine characters such as `V01.00.00`.
258    pub version: [u8; 9],
259}
260
261impl Identity {
262    /// Reads a ping's answer.
263    ///
264    /// # Arguments
265    ///
266    /// * `payload` - what followed the header.
267    ///
268    /// # Returns
269    ///
270    /// The identity.
271    ///
272    /// # Errors
273    ///
274    /// [`BridgeError::Short`] when fewer than [`PING_LEN`] bytes came back.
275    ///
276    /// # Examples
277    ///
278    /// ```
279    /// use pamoja_radios::sx1302::bridge::Identity;
280    ///
281    /// let mut payload = [0u8; 21];
282    /// payload[12..].copy_from_slice(b"V01.00.00");
283    /// let identity = Identity::parse(&payload).unwrap();
284    /// assert_eq!(identity.version_str(), Some("V01.00.00"));
285    /// assert!(identity.matches_firmware());
286    /// ```
287    pub fn parse(payload: &[u8]) -> Result<Identity, BridgeError> {
288        if payload.len() < PING_LEN {
289            return Err(BridgeError::Short {
290                got: payload.len(),
291                needs: PING_LEN,
292            });
293        }
294        let mut unique_id = [0u8; 12];
295        unique_id.copy_from_slice(&payload[..12]);
296        let mut version = [0u8; 9];
297        version.copy_from_slice(&payload[12..21]);
298        Ok(Identity { unique_id, version })
299    }
300
301    /// The version as text.
302    ///
303    /// # Returns
304    ///
305    /// The nine characters, or `None` when the bridge sent something other than text.
306    #[must_use]
307    pub fn version_str(&self) -> Option<&str> {
308        core::str::from_utf8(&self.version).ok()
309    }
310
311    /// Whether the bridge runs the firmware this crate was written against.
312    ///
313    /// # Returns
314    ///
315    /// Whether the version after its first character is [`FIRMWARE_VERSION`]. The first
316    /// character only says release or debug.
317    #[must_use]
318    pub fn matches_firmware(&self) -> bool {
319        self.version_str()
320            .and_then(|text| text.get(1..))
321            .is_some_and(|rest| rest == FIRMWARE_VERSION)
322    }
323}
324
325/// How a bridge is doing.
326#[derive(Clone, Copy, Debug, PartialEq, Eq)]
327pub struct Status {
328    /// The bridge's own clock, in milliseconds since it started.
329    pub system_time_ms: u32,
330    /// The temperature on the card, in hundredths of a degree Celsius.
331    pub temperature_centi: i16,
332}
333
334impl Status {
335    /// Reads a status answer.
336    ///
337    /// # Arguments
338    ///
339    /// * `payload` - what followed the header.
340    ///
341    /// # Returns
342    ///
343    /// The status.
344    ///
345    /// # Errors
346    ///
347    /// [`BridgeError::Short`] when fewer than [`STATUS_LEN`] bytes came back.
348    ///
349    /// # Examples
350    ///
351    /// ```
352    /// use pamoja_radios::sx1302::bridge::Status;
353    ///
354    /// // 1000 ms up, 24.50 degrees.
355    /// let status = Status::parse(&[0, 0, 0x03, 0xe8, 0x09, 0x92]).unwrap();
356    /// assert_eq!(status.system_time_ms, 1000);
357    /// assert_eq!(status.temperature_centi, 2450);
358    /// ```
359    pub fn parse(payload: &[u8]) -> Result<Status, BridgeError> {
360        if payload.len() < STATUS_LEN {
361            return Err(BridgeError::Short {
362                got: payload.len(),
363                needs: STATUS_LEN,
364            });
365        }
366        Ok(Status {
367            system_time_ms: u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]),
368            temperature_centi: i16::from_be_bytes([payload[4], payload[5]]),
369        })
370    }
371
372    /// The temperature in degrees Celsius.
373    ///
374    /// # Returns
375    ///
376    /// The value, to a hundredth.
377    #[must_use]
378    pub fn celsius(&self) -> f32 {
379        f32::from(self.temperature_centi) / 100.0
380    }
381}
382
383/// The payload that drives a pin.
384///
385/// # Arguments
386///
387/// * `port` - the pin's port, which is [`GPIO_PORT`] for every pin on the card.
388/// * `pin` - which pin.
389/// * `high` - whether to drive it high.
390///
391/// # Returns
392///
393/// The three bytes.
394///
395/// # Examples
396///
397/// ```
398/// use pamoja_radios::sx1302::bridge::{write_gpio, GPIO_PORT, PIN_RESET};
399///
400/// assert_eq!(write_gpio(GPIO_PORT, PIN_RESET, true), [0, 2, 1]);
401/// ```
402#[must_use]
403pub const fn write_gpio(port: u8, pin: u8, high: bool) -> [u8; 3] {
404    [port, pin, high as u8]
405}
406
407/// The payload that resets the card.
408///
409/// # Returns
410///
411/// The one byte, naming the only reset the bridge has.
412#[must_use]
413pub const fn reset() -> [u8; 1] {
414    [0]
415}
416
417/// Reads the one byte status a pin write or a reset answers with.
418///
419/// # Arguments
420///
421/// * `payload` - what followed the header.
422///
423/// # Returns
424///
425/// Nothing, when the bridge did it.
426///
427/// # Errors
428///
429/// [`BridgeError::Short`] when nothing came back, and [`BridgeError::Refused`] with the
430/// status when the bridge could not do it.
431pub fn took(payload: &[u8]) -> Result<(), BridgeError> {
432    match payload.first() {
433        None => Err(BridgeError::Short { got: 0, needs: 1 }),
434        Some(0) => Ok(()),
435        Some(&status) => Err(BridgeError::Refused { status }),
436    }
437}
438
439/// SPI transfers gathered into one order to the bridge.
440///
441/// Each transfer is an entry: who it is for, how long it is, and the raw frame the chip
442/// sees, target byte first, exactly as [`spi`](super::spi) builds it. The bridge answers
443/// with every entry echoed and the bytes the chip clocked back in place of the frame, which
444/// is what [`Replies`] walks.
445#[derive(Clone, Debug)]
446pub struct Bulk {
447    bytes: [u8; BULK_CHUNK],
448    len: usize,
449    entries: u8,
450}
451
452impl Default for Bulk {
453    fn default() -> Bulk {
454        Bulk::new()
455    }
456}
457
458impl Bulk {
459    /// An empty order.
460    ///
461    /// # Returns
462    ///
463    /// Room for [`BULK_CHUNK`] bytes of entries.
464    #[must_use]
465    pub const fn new() -> Bulk {
466        Bulk {
467            bytes: [0; BULK_CHUNK],
468            len: 0,
469            entries: 0,
470        }
471    }
472
473    /// Adds a transfer.
474    ///
475    /// # Arguments
476    ///
477    /// * `target` - [`TARGET_CONCENTRATOR`] or [`TARGET_LISTENER`].
478    /// * `frame` - the raw frame, target byte first, as the chip sees it. For a read it
479    ///   includes the dummy byte and room for the answer, as [`spi::read`](super::spi::read)
480    ///   builds it.
481    ///
482    /// # Returns
483    ///
484    /// The entry's identifier, which is its position, so the reply can be matched.
485    ///
486    /// # Errors
487    ///
488    /// [`BridgeError::TooMany`] when the order is full, by entries or by bytes.
489    ///
490    /// # Examples
491    ///
492    /// ```
493    /// use pamoja_radios::sx1302::bridge::{Bulk, TARGET_CONCENTRATOR};
494    /// use pamoja_radios::sx1302::spi;
495    ///
496    /// let mut bulk = Bulk::new();
497    /// let frame = spi::write(spi::TARGET_CONCENTRATOR, 0x5b00, 0x01);
498    /// bulk.transfer(TARGET_CONCENTRATOR, &frame).unwrap();
499    ///
500    /// // Identifier, kind, target, the frame's length, then the frame itself.
501    /// assert_eq!(&bulk.as_bytes()[..5], &[0, 0x01, 0, 0, 4]);
502    /// assert_eq!(&bulk.as_bytes()[5..], &frame);
503    /// ```
504    pub fn transfer(&mut self, target: u8, frame: &[u8]) -> Result<u8, BridgeError> {
505        let needed = 5 + frame.len();
506        if self.entries as usize >= BULK_ENTRIES
507            || self.len + needed > BULK_CHUNK
508            || frame.len() > u16::MAX as usize
509        {
510            return Err(BridgeError::TooMany);
511        }
512        let id = self.entries;
513        let at = self.len;
514        self.bytes[at] = id;
515        self.bytes[at + 1] = ENTRY_TRANSFER;
516        self.bytes[at + 2] = target;
517        self.bytes[at + 3] = (frame.len() >> 8) as u8;
518        self.bytes[at + 4] = frame.len() as u8;
519        self.bytes[at + 5..at + needed].copy_from_slice(frame);
520        self.len += needed;
521        self.entries += 1;
522        Ok(id)
523    }
524
525    /// Adds a change to some bits of one concentrator register, done on the bridge.
526    ///
527    /// The bridge reads the register, replaces the field, and writes it back, which saves
528    /// the host a round trip on a register it shares with something else.
529    ///
530    /// # Arguments
531    ///
532    /// * `address` - the register.
533    /// * `offset` - which bit the field starts at.
534    /// * `width` - how many bits it spans.
535    /// * `value` - what to put there, right aligned.
536    ///
537    /// # Returns
538    ///
539    /// The entry's identifier.
540    ///
541    /// # Errors
542    ///
543    /// [`BridgeError::TooMany`] when the order is full.
544    ///
545    /// # Examples
546    ///
547    /// ```
548    /// use pamoja_radios::sx1302::bridge::Bulk;
549    ///
550    /// let mut bulk = Bulk::new();
551    /// bulk.modify(0x5b21, 4, 2, 0b11).unwrap();
552    ///
553    /// // A two bit field at bit four is the mask 0x30, and the value moves up to meet it.
554    /// assert_eq!(bulk.as_bytes(), &[0, 0x02, 0x5b, 0x21, 0x30, 0x30]);
555    /// ```
556    pub fn modify(
557        &mut self,
558        address: u16,
559        offset: u8,
560        width: u8,
561        value: u8,
562    ) -> Result<u8, BridgeError> {
563        if self.entries as usize >= BULK_ENTRIES || self.len + 6 > BULK_CHUNK {
564            return Err(BridgeError::TooMany);
565        }
566        let id = self.entries;
567        let at = self.len;
568        let mask = (((1u16 << width) - 1) as u8) << offset;
569        self.bytes[at] = id;
570        self.bytes[at + 1] = ENTRY_MODIFY;
571        self.bytes[at + 2] = (address >> 8) as u8;
572        self.bytes[at + 3] = address as u8;
573        self.bytes[at + 4] = mask;
574        self.bytes[at + 5] = value << offset;
575        self.len += 6;
576        self.entries += 1;
577        Ok(id)
578    }
579
580    /// The entries, as the order carries them.
581    ///
582    /// # Returns
583    ///
584    /// The bytes that follow the header.
585    #[must_use]
586    pub fn as_bytes(&self) -> &[u8] {
587        &self.bytes[..self.len]
588    }
589
590    /// How many entries are in the order.
591    ///
592    /// # Returns
593    ///
594    /// The count.
595    #[must_use]
596    pub const fn entries(&self) -> u8 {
597        self.entries
598    }
599
600    /// Whether the order holds nothing yet.
601    ///
602    /// # Returns
603    ///
604    /// Whether there are no entries.
605    #[must_use]
606    pub const fn is_empty(&self) -> bool {
607        self.entries == 0
608    }
609
610    /// The header this order goes out under.
611    ///
612    /// # Arguments
613    ///
614    /// * `id` - the message identifier to use.
615    ///
616    /// # Returns
617    ///
618    /// The header.
619    #[must_use]
620    pub const fn header(&self, id: u8) -> Header {
621        Header {
622            id,
623            size: self.len as u16,
624            order: ORDER_SPI,
625        }
626    }
627
628    /// Starts over.
629    pub fn clear(&mut self) {
630        self.len = 0;
631        self.entries = 0;
632    }
633}
634
635/// One entry of a bulk answer.
636#[derive(Clone, Copy, Debug, PartialEq, Eq)]
637pub struct Reply<'a> {
638    /// Which entry, matching what [`Bulk::transfer`] or [`Bulk::modify`] returned.
639    pub id: u8,
640    /// [`ENTRY_TRANSFER`] or [`ENTRY_MODIFY`].
641    pub kind: u8,
642    /// The bytes the chip clocked back, in place of the frame that went out. Empty for a
643    /// modify, which answers with nothing.
644    pub frame: &'a [u8],
645}
646
647/// Walks the entries of a bulk answer.
648///
649/// Every entry is checked as it is reached: a failed one is an error rather than an entry,
650/// because a read whose transfer failed holds nothing worth returning.
651pub struct Replies<'a> {
652    payload: &'a [u8],
653    at: usize,
654}
655
656impl<'a> Replies<'a> {
657    /// Walks what followed the header of a bulk answer.
658    ///
659    /// # Arguments
660    ///
661    /// * `payload` - the answer's payload.
662    ///
663    /// # Returns
664    ///
665    /// The walk.
666    #[must_use]
667    pub const fn new(payload: &'a [u8]) -> Replies<'a> {
668        Replies { payload, at: 0 }
669    }
670}
671
672impl<'a> Iterator for Replies<'a> {
673    type Item = Result<Reply<'a>, BridgeError>;
674
675    fn next(&mut self) -> Option<Self::Item> {
676        if self.at >= self.payload.len() {
677            return None;
678        }
679        let rest = &self.payload[self.at..];
680        if rest.len() < 5 {
681            self.at = self.payload.len();
682            return Some(Err(BridgeError::Short {
683                got: rest.len(),
684                needs: 5,
685            }));
686        }
687        let (id, kind, status) = (rest[0], rest[1], rest[2]);
688        if kind != ENTRY_TRANSFER && kind != ENTRY_MODIFY {
689            self.at = self.payload.len();
690            return Some(Err(BridgeError::UnknownEntry { id, kind }));
691        }
692        if status != 0 {
693            self.at = self.payload.len();
694            return Some(Err(BridgeError::Transfer { id, status }));
695        }
696        let frame_len = if kind == ENTRY_TRANSFER {
697            usize::from(u16::from_be_bytes([rest[3], rest[4]]))
698        } else {
699            0
700        };
701        if rest.len() < 5 + frame_len {
702            self.at = self.payload.len();
703            return Some(Err(BridgeError::Short {
704                got: rest.len(),
705                needs: 5 + frame_len,
706            }));
707        }
708        self.at += 5 + frame_len;
709        Some(Ok(Reply {
710            id,
711            kind,
712            frame: &rest[5..5 + frame_len],
713        }))
714    }
715}
716
717#[cfg(test)]
718mod tests {
719    use super::*;
720    use crate::sx1302::spi;
721
722    #[test]
723    fn a_header_is_the_identifier_the_size_high_byte_first_and_the_order() {
724        let header = Header {
725            id: 0x2a,
726            size: 0x0102,
727            order: ORDER_SPI,
728        };
729        assert_eq!(header.to_bytes(), [0x2a, 0x01, 0x02, 0x05]);
730        assert_eq!(Header::from_bytes(header.to_bytes()), header);
731    }
732
733    #[test]
734    fn an_answer_is_the_order_moved_up_and_nothing_else_is_one() {
735        for order in [
736            ORDER_PING,
737            ORDER_STATUS,
738            ORDER_BOOTLOADER,
739            ORDER_RESET,
740            ORDER_WRITE_GPIO,
741            ORDER_SPI,
742        ] {
743            let answer = Header::from_bytes([0, 0, 0, order + ACK]);
744            assert!(answer.is_answer());
745            assert!(answer.answers(order));
746        }
747        assert!(!Header::from_bytes([0, 0, 0, ORDER_SPI]).is_answer());
748        assert!(!Header::from_bytes([0, 0, 0, ORDER_ERROR]).is_answer());
749    }
750
751    #[test]
752    fn a_ping_carries_the_identifier_then_the_version_and_the_first_character_is_ignored() {
753        let mut payload = [0u8; PING_LEN];
754        payload[..12].copy_from_slice(&[0xde, 0xad, 0xbe, 0xef, 1, 2, 3, 4, 5, 6, 7, 8]);
755        payload[12..].copy_from_slice(b"D01.00.00");
756
757        let identity = Identity::parse(&payload).expect("a whole ping");
758        assert_eq!(&identity.unique_id[..4], &[0xde, 0xad, 0xbe, 0xef]);
759        assert!(
760            identity.matches_firmware(),
761            "a debug build of the same firmware"
762        );
763
764        payload[12..].copy_from_slice(b"V01.00.01");
765        assert!(!Identity::parse(&payload).unwrap().matches_firmware());
766        assert_eq!(
767            Identity::parse(&payload[..20]),
768            Err(BridgeError::Short { got: 20, needs: 21 })
769        );
770    }
771
772    #[test]
773    fn a_status_is_a_big_endian_clock_and_a_signed_temperature_in_hundredths() {
774        let status = Status::parse(&[0x00, 0x01, 0x86, 0xa0, 0xff, 0x38]).expect("whole");
775        assert_eq!(status.system_time_ms, 100_000);
776        assert_eq!(status.temperature_centi, -200, "two degrees below zero");
777        assert_eq!(status.celsius(), -2.0);
778    }
779
780    #[test]
781    fn a_pin_write_and_a_reset_answer_with_one_status_byte() {
782        assert_eq!(write_gpio(GPIO_PORT, PIN_POWER, true), [0, 1, 1]);
783        assert_eq!(write_gpio(GPIO_PORT, PIN_RADIO_RESET, false), [0, 8, 0]);
784        assert_eq!(reset(), [0]);
785
786        assert_eq!(took(&[0]), Ok(()));
787        assert_eq!(took(&[2]), Err(BridgeError::Refused { status: 2 }));
788        assert_eq!(took(&[]), Err(BridgeError::Short { got: 0, needs: 1 }));
789    }
790
791    #[test]
792    fn a_transfer_entry_carries_the_raw_frame_the_way_the_reference_builds_it() {
793        // The reference writes one byte to 0x5b00 on the concentrator: the entry is the
794        // request metadata, then the mux byte, the command with the write bit, the address
795        // low byte, and the data.
796        let mut bulk = Bulk::new();
797        let frame = spi::write(spi::TARGET_CONCENTRATOR, 0x5b00, 0xab);
798        let id = bulk.transfer(TARGET_CONCENTRATOR, &frame).unwrap();
799
800        assert_eq!(id, 0);
801        assert_eq!(
802            bulk.as_bytes(),
803            &[0, 0x01, 0, 0, 4, 0x00, 0x80 | 0x5b, 0x00, 0xab]
804        );
805
806        // A read costs one more byte, the dummy the chip turns the bus around on.
807        let read = spi::read(spi::TARGET_RADIO_A, 0x0123);
808        let id = bulk.transfer(TARGET_CONCENTRATOR, &read).unwrap();
809        assert_eq!(id, 1);
810        assert_eq!(&bulk.as_bytes()[9..14], &[1, 0x01, 0, 0, 5]);
811        assert_eq!(&bulk.as_bytes()[14..], &[0x01, 0x01, 0x23, 0x00, 0x00]);
812        assert_eq!(bulk.entries(), 2);
813    }
814
815    #[test]
816    fn a_modify_entry_is_an_address_a_mask_and_a_value_moved_into_place() {
817        let mut bulk = Bulk::new();
818        bulk.modify(0x5b21, 4, 2, 0b11).unwrap();
819        assert_eq!(bulk.as_bytes(), &[0, 0x02, 0x5b, 0x21, 0x30, 0x30]);
820
821        bulk.clear();
822        bulk.modify(0x0001, 0, 8, 0xff).unwrap();
823        assert_eq!(bulk.as_bytes(), &[0, 0x02, 0x00, 0x01, 0xff, 0xff]);
824    }
825
826    #[test]
827    fn an_order_refuses_more_than_the_bridge_takes() {
828        let mut bulk = Bulk::new();
829        let frame = [0u8; 1024];
830        for _ in 0..3 {
831            bulk.transfer(TARGET_CONCENTRATOR, &frame).unwrap();
832        }
833        // Three fill 3087 of 4096 bytes; a fourth of this size does not fit.
834        assert_eq!(
835            bulk.transfer(TARGET_CONCENTRATOR, &frame),
836            Err(BridgeError::TooMany)
837        );
838
839        let mut many = Bulk::new();
840        for _ in 0..BULK_ENTRIES {
841            many.modify(0, 0, 1, 0).unwrap();
842        }
843        assert_eq!(many.modify(0, 0, 1, 0), Err(BridgeError::TooMany));
844    }
845
846    #[test]
847    fn a_bulk_answer_walks_every_entry_and_hands_back_what_the_chip_clocked() {
848        // Two transfers and a modify, all fine: the second transfer carries a read's answer.
849        let payload = [
850            0, 0x01, 0, 0, 4, 0x00, 0xdb, 0x00, 0x00, // entry 0, four echoed bytes
851            1, 0x01, 0, 0, 5, 0x00, 0x00, 0x00, 0x00, 0x60, // entry 1, chip id in the last
852            2, 0x02, 0, 0, 0, // a modify answers with nothing
853        ];
854        let replies: Vec<Reply<'_>> = Replies::new(&payload).map(|r| r.unwrap()).collect();
855
856        assert_eq!(replies.len(), 3);
857        assert_eq!(replies[1].id, 1);
858        assert_eq!(spi::read_value(replies[1].frame), Some(0x60));
859        assert_eq!(replies[2].kind, ENTRY_MODIFY);
860        assert!(replies[2].frame.is_empty());
861    }
862
863    #[test]
864    fn a_failed_entry_ends_the_walk_with_why() {
865        let payload = [
866            0, 0x01, 0, 0, 1, 0xaa, // fine
867            1, 0x01, 3, 0, 0, // timed out
868            2, 0x02, 0, 0, 0, // never reached
869        ];
870        let mut replies = Replies::new(&payload);
871        assert!(replies.next().unwrap().is_ok());
872        assert_eq!(
873            replies.next().unwrap(),
874            Err(BridgeError::Transfer { id: 1, status: 3 })
875        );
876        assert!(
877            replies.next().is_none(),
878            "nothing after a failure is trusted"
879        );
880    }
881
882    #[test]
883    fn an_answer_cut_short_is_refused_rather_than_read_past_its_end() {
884        let payload = [0, 0x01, 0, 0, 9, 0xaa, 0xbb];
885        assert_eq!(
886            Replies::new(&payload).next().unwrap(),
887            Err(BridgeError::Short { got: 7, needs: 14 })
888        );
889        assert_eq!(
890            Replies::new(&[0, 0x01, 0]).next().unwrap(),
891            Err(BridgeError::Short { got: 3, needs: 5 })
892        );
893        assert_eq!(
894            Replies::new(&[0, 0x07, 0, 0, 0]).next().unwrap(),
895            Err(BridgeError::UnknownEntry { id: 0, kind: 0x07 })
896        );
897    }
898}