Skip to content

Commit

Permalink
Merge pull request #87 from ferrous-systems/add-radio-retries
Browse files Browse the repository at this point in the history
Adds a new `send_recv` method to the dk crate.
  • Loading branch information
jonathanpallant authored Apr 30, 2024
2 parents afe7263 + 9e90bb1 commit 7a43f85
Show file tree
Hide file tree
Showing 43 changed files with 303 additions and 12,091 deletions.
7 changes: 4 additions & 3 deletions exercise-book/src/nrf52-radio-alt-containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

## Modify-in-place

If you solved the puzzle using a `Vec` buffer you can try solving it without the buffer as a stretch goal. You may find the [slice methods][slice] that let you mutate its data useful. A solution that does not use a `heapless:Vec` buffer can be found in the `src/bin/radio-puzzle-solution-2.rs` file.
If you solved the puzzle using a `Vec` buffer you can try solving it without the buffer as a stretch goal. You may find the [slice methods][slice] that let you mutate a `Packet`'s data useful, but remember that the first six bytes of your `Packet` will be the random device address - you can't decrypt those! A solution that does not use a `heapless:Vec` buffer can be found in the `src/bin/radio-puzzle-solution-2.rs` file.

## Using `liballoc::BTreeMap`

If you get all that working and still need something else to try, you could look at the [`BTreeMap`][btreemap] contained within `liballoc`. This will require you to set up a global memory allocator, like [`embedded-alloc`][embedded-alloc].
If you solved the puzzle using a `heapless::Vec` buffer and a `heapless::LinearMap` and you still need something else to try, you could look at the [`Vec`][vec] and [`BTreeMap`][btreemap] types contained within `liballoc`. This will require you to set up a global memory allocator, like [`embedded-alloc`][embedded-alloc].

[btreemap]: https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html
[vec]: https://doc.rust-lang.org/alloc/vec/struct.Vec.html
[btreemap]: https://doc.rust-lang.org/alloc/collections/struct.BTreeMap.html
[embedded-alloc]: https://github.com/rust-embedded/embedded-alloc
[slice]: https://doc.rust-lang.org/std/primitive.slice.html#methods
2 changes: 1 addition & 1 deletion exercise-book/src/nrf52-radio-in.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ The Dongle will respond as soon as it receives a packet. If you insert a delay b

Having log statements between `send` and `recv_timeout` can also cause packets to be missed so try to keep those two calls as close to each other as possible and with as little code in between as possible.

> NOTE Packet loss can always occur in wireless networks, even if the radios are close to each other. The `Radio` API we are using will not detect lost packets because it does not implement IEEE 802.15.4 Acknowledgement Requests. If you are having trouble with lost packets, consider adding a retry loop, but remember you are sharing the airwaves with other users, so make sure not to spam incessantly by accident!
> NOTE Packet loss can always occur in wireless networks, even if the radios are close to each other. The `Radio` API we are using will not detect lost packets because it does not implement IEEE 802.15.4 Acknowledgement Requests. For the next step in the workshop, we will use a new function to handle this for us. For the sake of other radio users, please do ensure you never call `send()` in a tight loop!
2 changes: 1 addition & 1 deletion exercise-book/src/nrf52-radio-puzzle-help.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Something you will likely run into while solving this exercise are *character* l

*IMPORTANT* you do not need to use the `str` or `char` API to solve this problem, other than for printing purposes. Work directly with slices of bytes (`[u8]`) and bytes (`u8`); and only convert those to `str` or `char` when you are about to print them.

> Note: The plaintext string is *not* stored in `puzzle-fw` so running `strings` on it will not give you the answer. Nice try.
> Note: The plaintext secret string is *not* stored in `puzzle-fw` so running `strings` on it will not give you the answer. Nice try.
## Make sure not to flood the log buffer

Expand Down
23 changes: 17 additions & 6 deletions exercise-book/src/nrf52-radio-puzzle.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,18 @@ Like in the previous sections the Dongle will listen for radio packets -- this t

✅ Open the [`nrf52-code/radio-app`](../../nrf52-code/radio-app) folder in VS Code; then open the `src/bin/radio-puzzle.rs` file. Run the program.

This will send a zero sized packet `let msg = b""` to the dongle.
This will send a zero sized packet `let msg = b""` to the dongle. It does this using a special function called `dk::send_recv`. This function will:

1. Determine a unique address for your nRF52840 (Nordic helpfully bake a different random address into every nRF52 chip they make)
2. Construct a packet where the first six bytes are the unique address, and the remainder are the ones you passed to the `send_recv()` function
3. Use the `Radio::send()` method to wait for the channel to be clear (using a *Clear Channel Assessment*) before actually sending the packet
4. Use the `Radio::recv_timeout()` method to wait for a reply, up to the given number of microseconds specified
5. Check that the first six bytes in the reply match our six byte address
a. If so, the remainder of the reply is returned as the `Ok` variant
b. Otherwise, increment a retry counter and, if we have run out of retry attempts, we return the `Err` variant
c. Otherwise, we go back to step 2 and try again.

This function allows communication with the USB dongle to be relatively robust, even in the presence of other devices on the same channel. However, it's not perfect and sometimes you will run out of retry attempts and your program will need to be restarted.

❗ The Dongle responds to the DK's requests wirelessly (i.e. by sending back radio packets) as well. You'll see the dongle responses printed by the DK. This means you don't have to worry if serial-term doesn't work on your machine.

Expand All @@ -33,13 +44,13 @@ What happens?
<details>
<summary>Answer</summary>

The Dongle will respond differently depending on the length of the incoming packet:
The Dongle will respond differently depending on the length of the payload in the incoming packet:

- On zero-sized packets it will respond with the encrypted string.
- On one-byte sized packets it will respond with the *direct* mapping from a *plaintext* letter (single `u8` value) -- the letter contained in the packet -- to the *ciphertext* letter (`u8` value).
- On packets of any other length the Dongle will respond with the string `correct` if it received the decrypted string, otherwise it will respond with the `incorrect` string.
- On zero-sized payloads (i.e. packets that only contain the device address and nothing else) it will respond with the encrypted string.
- On one-byte sized payloads it will respond with the *direct* mapping from the given *plaintext* letter (single `u8` value) to the corresponding *ciphertext* letter (another `u8` value).
- On payloads of any other length the Dongle will respond with the string `correct` if it received the correct secret string, otherwise it will respond with the string `incorrect`.

The Dongle will always respond with packets that are valid UTF-8 so you can use `str::from_utf8` on the response packets.
The Dongle will always respond with payloads that are valid UTF-8 so you can use `str::from_utf8` on the response packets. However, do not attempt to look inside the raw packet, as it will contain six random address bytes at the start, and they will not be valid UTF-8. Only look at the `&[u8]` that the `send_recv()` function returns, and treat the `Packet` as just a storage area that you don't look inside.

This step is illustrated in `src/bin/radio-puzzle-1.rs`

Expand Down
4 changes: 2 additions & 2 deletions exercise-book/src/nrf52-radio-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ deviceid=588c06af0877c8f2 channel=20 TxPower=+8dBm app=loopback-fw
received 5 bytes (CRC=Ok(0xdad9), LQI=53)
```

The program broadcasts a radio packet that contains the 5-byte string `Hello` over channel 20 (which has a center frequency of 2450 MHz). The `loopback` program running on the Dongle is listening to all packets sent over channel 20; every time it receives a new packet it reports its length and the Link Quality Indicator (LQI) metric of the transmission over the USB/serial interface. As the name implies the LQI metric indicates how good the connection between the sender and the receiver is.
The program broadcasts a radio packet that contains the 5-byte string `Hello` over channel 20 (which has a center frequency of 2450 MHz). The `loopback` program running on the Dongle is listening to all packets sent over channel 20; every time it receives a new packet it reports its length and the Link Quality Indicator (LQI) metric of the transmission over the USB/serial interface. As the name implies the LQI metric indicates how good the connection between the sender and the receiver is (a higher number means better quality).

To repeatedly send a packet without re-flashing your DK, try pressing the BOOT/RESET button (next to the nRF USB connector that we aren't using yet).
Because of how our firmware generates a *semihosting exception* to tell our flashing tool (`probe-run`) when the firmware has finished running, if you load the `radio-send` firmware and then power-cycle the nRF52840-DK, the firmware will enter a reboot loop and repeatedly send a packet. This is because nothing catches the *semihosting exception* and so the CPU reboots, sends a packet, and then tries another *semihosting exception*.
88 changes: 88 additions & 0 deletions nrf52-code/boards/dk-solution/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,94 @@ impl ops::DerefMut for Timer {
}
}

#[cfg(feature = "radio")]
mod radio_retry {
use super::ieee802154::Packet;

const RETRY_COUNT: u32 = 10;
const ADDR_LEN: usize = 6;

fn get_id() -> [u8; ADDR_LEN] {
let ficr = unsafe { &*hal::pac::FICR::ptr() };
let id = ficr.deviceaddr[0].read().bits();
let id2 = ficr.deviceaddr[1].read().bits();
let id = u64::from(id) << 32 | u64::from(id2);
defmt::trace!("Device ID: {:#08x}", id);
let id_bytes = id.to_be_bytes();
[
id_bytes[0],
id_bytes[1],
id_bytes[2],
id_bytes[3],
id_bytes[4],
id_bytes[5],
]
}

/// Send a packet, containing the device address and the given data, and
/// wait for a response.
///
/// If we get a response containing the same device address, it returns a
/// slice of the remaining payload (i.e. not including the device address).
///
/// If we don't get a response, or we get a bad response (with the wrong
/// address in it), we try again.
///
/// If we try too many times, we give up.
pub fn send_recv<'packet, I>(
packet: &'packet mut Packet,
data_to_send: &[u8],
radio: &mut hal::ieee802154::Radio,
timer: &mut hal::timer::Timer<I>,
microseconds: u32,
) -> Result<&'packet [u8], hal::ieee802154::Error>
where
I: hal::timer::Instance,
{
assert!(data_to_send.len() + ADDR_LEN < usize::from(Packet::CAPACITY));

let id_bytes = get_id();
// Short delay before sending, so we don't get into a tight loop and steal all the bandwidth
timer.delay(5000);
for i in 0..RETRY_COUNT {
packet.set_len(ADDR_LEN as u8 + data_to_send.len() as u8);
let source_iter = id_bytes.iter().chain(data_to_send.iter());
let dest_iter = packet.iter_mut();
for (source, dest) in source_iter.zip(dest_iter) {
*dest = *source;
}
defmt::debug!("TX: {=[u8]:02x}", &packet[..]);
radio.send(packet);
match radio.recv_timeout(packet, timer, microseconds) {
Ok(_crc) => {
defmt::debug!("RX: {=[u8]:02x}", packet[..]);
// packet is long enough
if packet[0..ADDR_LEN] == id_bytes {
// and it has the right bytes at the start
defmt::debug!("OK: {=[u8]:02x}", packet[ADDR_LEN..]);
return Ok(&packet[ADDR_LEN..]);
} else {
defmt::warn!("RX Wrong Address try {}", i);
timer.delay(10000);
}
}
Err(hal::ieee802154::Error::Timeout) => {
defmt::warn!("RX Timeout try {}", i);
timer.delay(10000);
}
Err(hal::ieee802154::Error::Crc(_)) => {
defmt::warn!("RX CRC Error try {}", i);
timer.delay(10000);
}
}
}
Err(hal::ieee802154::Error::Timeout)
}
}

#[cfg(feature = "radio")]
pub use radio_retry::send_recv;

/// The ways that initialisation can fail
#[derive(Debug, Copy, Clone, defmt::Format)]
pub enum Error {
Expand Down
88 changes: 88 additions & 0 deletions nrf52-code/boards/dk/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,94 @@ impl ops::DerefMut for Timer {
}
}

#[cfg(feature = "radio")]
mod radio_retry {
use super::ieee802154::Packet;

const RETRY_COUNT: u32 = 10;
const ADDR_LEN: usize = 6;

fn get_id() -> [u8; ADDR_LEN] {
let ficr = unsafe { &*hal::pac::FICR::ptr() };
let id = ficr.deviceaddr[0].read().bits();
let id2 = ficr.deviceaddr[1].read().bits();
let id = u64::from(id) << 32 | u64::from(id2);
defmt::trace!("Device ID: {:#08x}", id);
let id_bytes = id.to_be_bytes();
[
id_bytes[0],
id_bytes[1],
id_bytes[2],
id_bytes[3],
id_bytes[4],
id_bytes[5],
]
}

/// Send a packet, containing the device address and the given data, and
/// wait for a response.
///
/// If we get a response containing the same device address, it returns a
/// slice of the remaining payload (i.e. not including the device address).
///
/// If we don't get a response, or we get a bad response (with the wrong
/// address in it), we try again.
///
/// If we try too many times, we give up.
pub fn send_recv<'packet, I>(
packet: &'packet mut Packet,
data_to_send: &[u8],
radio: &mut hal::ieee802154::Radio,
timer: &mut hal::timer::Timer<I>,
microseconds: u32,
) -> Result<&'packet [u8], hal::ieee802154::Error>
where
I: hal::timer::Instance,
{
assert!(data_to_send.len() + ADDR_LEN < usize::from(Packet::CAPACITY));

let id_bytes = get_id();
// Short delay before sending, so we don't get into a tight loop and steal all the bandwidth
timer.delay(5000);
for i in 0..RETRY_COUNT {
packet.set_len(ADDR_LEN as u8 + data_to_send.len() as u8);
let source_iter = id_bytes.iter().chain(data_to_send.iter());
let dest_iter = packet.iter_mut();
for (source, dest) in source_iter.zip(dest_iter) {
*dest = *source;
}
defmt::debug!("TX: {=[u8]:02x}", &packet[..]);
radio.send(packet);
match radio.recv_timeout(packet, timer, microseconds) {
Ok(_crc) => {
defmt::debug!("RX: {=[u8]:02x}", packet[..]);
// packet is long enough
if packet[0..ADDR_LEN] == id_bytes {
// and it has the right bytes at the start
defmt::debug!("OK: {=[u8]:02x}", packet[ADDR_LEN..]);
return Ok(&packet[ADDR_LEN..]);
} else {
defmt::warn!("RX Wrong Address try {}", i);
timer.delay(10000);
}
}
Err(hal::ieee802154::Error::Timeout) => {
defmt::warn!("RX Timeout try {}", i);
timer.delay(10000);
}
Err(hal::ieee802154::Error::Crc(_)) => {
defmt::warn!("RX CRC Error try {}", i);
timer.delay(10000);
}
}
}
Err(hal::ieee802154::Error::Timeout)
}
}

#[cfg(feature = "radio")]
pub use radio_retry::send_recv;

/// The ways that initialisation can fail
#[derive(Debug, Copy, Clone, defmt::Format)]
pub enum Error {
Expand Down
2 changes: 0 additions & 2 deletions nrf52-code/boards/dongle-fw/old/.cargo/config.toml

This file was deleted.

Loading

0 comments on commit 7a43f85

Please sign in to comment.