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}