Skip to content

Commit 84d17a6

Browse files
Adds a new send_recv method to the dk crate.
This puts the given data into a packet, along with a random device address, and then waits for a reply containing a matching device address. This allows the function to reject packets that were intended for another device, and makes it somewhat robust when multiple devices are running on the same channel at the same time.
1 parent afe7263 commit 84d17a6

16 files changed

+302
-234
lines changed

exercise-book/src/nrf52-radio-alt-containers.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
## Modify-in-place
44

5-
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.
5+
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.
66

77
## Using `liballoc::BTreeMap`
88

9-
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].
9+
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].
1010

11-
[btreemap]: https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html
11+
[vec]: https://doc.rust-lang.org/alloc/vec/struct.Vec.html
12+
[btreemap]: https://doc.rust-lang.org/alloc/collections/struct.BTreeMap.html
1213
[embedded-alloc]: https://github.com/rust-embedded/embedded-alloc
1314
[slice]: https://doc.rust-lang.org/std/primitive.slice.html#methods

exercise-book/src/nrf52-radio-in.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ The Dongle will respond as soon as it receives a packet. If you insert a delay b
1616

1717
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.
1818

19-
> 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!
19+
> 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!

exercise-book/src/nrf52-radio-puzzle-help.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Something you will likely run into while solving this exercise are *character* l
4040

4141
*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.
4242

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

exercise-book/src/nrf52-radio-puzzle.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,18 @@ Like in the previous sections the Dongle will listen for radio packets -- this t
1919

2020
✅ 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.
2121

22-
This will send a zero sized packet `let msg = b""` to the dongle.
22+
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:
23+
24+
1. Determine a unique address for your nRF52840 (Nordic helpfully bake a different random address into every nRF52 chip they make)
25+
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
26+
3. Use the `Radio::send()` method to wait for the channel to be clear (using a *Clear Channel Assessment*) before actually sending the packet
27+
4. Use the `Radio::recv_timeout()` method to wait for a reply, up to the given number of microseconds specified
28+
5. Check that the first six bytes in the reply match our six byte address
29+
a. If so, the remainder of the reply is returned as the `Ok` variant
30+
b. Otherwise, increment a retry counter and, if we have run out of retry attempts, we return the `Err` variant
31+
c. Otherwise, we go back to step 2 and try again.
32+
33+
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.
2334

2435
❗ 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.
2536

@@ -33,13 +44,13 @@ What happens?
3344
<details>
3445
<summary>Answer</summary>
3546

36-
The Dongle will respond differently depending on the length of the incoming packet:
47+
The Dongle will respond differently depending on the length of the payload in the incoming packet:
3748

38-
- On zero-sized packets it will respond with the encrypted string.
39-
- 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).
40-
- 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.
49+
- On zero-sized payloads (i.e. packets that only contain the device address and nothing else) it will respond with the encrypted string.
50+
- 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).
51+
- 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`.
4152

42-
The Dongle will always respond with packets that are valid UTF-8 so you can use `str::from_utf8` on the response packets.
53+
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.
4354

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

exercise-book/src/nrf52-radio-setup.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ received 5 bytes (CRC=Ok(0xdad9), LQI=53)
1212

1313
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.
1414

15-
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).
15+
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*.

nrf52-code/boards/dk-solution/src/lib.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,94 @@ impl ops::DerefMut for Timer {
221221
}
222222
}
223223

224+
#[cfg(feature = "radio")]
225+
mod radio_retry {
226+
use super::ieee802154::Packet;
227+
228+
const RETRY_COUNT: u32 = 10;
229+
const ADDR_LEN: usize = 6;
230+
231+
fn get_id() -> [u8; ADDR_LEN] {
232+
let ficr = unsafe { &*hal::pac::FICR::ptr() };
233+
let id = ficr.deviceaddr[0].read().bits();
234+
let id2 = ficr.deviceaddr[1].read().bits();
235+
let id = u64::from(id) << 32 | u64::from(id2);
236+
defmt::trace!("Device ID: {:#08x}", id);
237+
let id_bytes = id.to_be_bytes();
238+
[
239+
id_bytes[0],
240+
id_bytes[1],
241+
id_bytes[2],
242+
id_bytes[3],
243+
id_bytes[4],
244+
id_bytes[5],
245+
]
246+
}
247+
248+
/// Send a packet, containing the device address and the given data, and
249+
/// wait for a response.
250+
///
251+
/// If we get a response containing the same device address, it returns a
252+
/// slice of the remaining payload (i.e. not including the device address).
253+
///
254+
/// If we don't get a response, or we get a bad response (with the wrong
255+
/// address in it), we try again.
256+
///
257+
/// If we try too many times, we give up.
258+
pub fn send_recv<'packet, I>(
259+
packet: &'packet mut Packet,
260+
data_to_send: &[u8],
261+
radio: &mut hal::ieee802154::Radio,
262+
timer: &mut hal::timer::Timer<I>,
263+
microseconds: u32,
264+
) -> Result<&'packet [u8], hal::ieee802154::Error>
265+
where
266+
I: hal::timer::Instance,
267+
{
268+
assert!(data_to_send.len() + ADDR_LEN < usize::from(Packet::CAPACITY));
269+
270+
let id_bytes = get_id();
271+
// Short delay before sending, so we don't get into a tight loop and steal all the bandwidth
272+
timer.delay(5000);
273+
for i in 0..RETRY_COUNT {
274+
packet.set_len(ADDR_LEN as u8 + data_to_send.len() as u8);
275+
let source_iter = id_bytes.iter().chain(data_to_send.iter());
276+
let dest_iter = packet.iter_mut();
277+
for (source, dest) in source_iter.zip(dest_iter) {
278+
*dest = *source;
279+
}
280+
defmt::debug!("TX: {=[u8]:02x}", &packet[..]);
281+
radio.send(packet);
282+
match radio.recv_timeout(packet, timer, microseconds) {
283+
Ok(_crc) => {
284+
defmt::debug!("RX: {=[u8]:02x}", packet[..]);
285+
// packet is long enough
286+
if packet[0..ADDR_LEN] == id_bytes {
287+
// and it has the right bytes at the start
288+
defmt::debug!("OK: {=[u8]:02x}", packet[ADDR_LEN..]);
289+
return Ok(&packet[ADDR_LEN..]);
290+
} else {
291+
defmt::warn!("RX Wrong Address try {}", i);
292+
timer.delay(10000);
293+
}
294+
}
295+
Err(hal::ieee802154::Error::Timeout) => {
296+
defmt::warn!("RX Timeout try {}", i);
297+
timer.delay(10000);
298+
}
299+
Err(hal::ieee802154::Error::Crc(_)) => {
300+
defmt::warn!("RX CRC Error try {}", i);
301+
timer.delay(10000);
302+
}
303+
}
304+
}
305+
Err(hal::ieee802154::Error::Timeout)
306+
}
307+
}
308+
309+
#[cfg(feature = "radio")]
310+
pub use radio_retry::send_recv;
311+
224312
/// The ways that initialisation can fail
225313
#[derive(Debug, Copy, Clone, defmt::Format)]
226314
pub enum Error {

nrf52-code/boards/dk/src/lib.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,94 @@ impl ops::DerefMut for Timer {
195195
}
196196
}
197197

198+
#[cfg(feature = "radio")]
199+
mod radio_retry {
200+
use super::ieee802154::Packet;
201+
202+
const RETRY_COUNT: u32 = 10;
203+
const ADDR_LEN: usize = 6;
204+
205+
fn get_id() -> [u8; ADDR_LEN] {
206+
let ficr = unsafe { &*hal::pac::FICR::ptr() };
207+
let id = ficr.deviceaddr[0].read().bits();
208+
let id2 = ficr.deviceaddr[1].read().bits();
209+
let id = u64::from(id) << 32 | u64::from(id2);
210+
defmt::trace!("Device ID: {:#08x}", id);
211+
let id_bytes = id.to_be_bytes();
212+
[
213+
id_bytes[0],
214+
id_bytes[1],
215+
id_bytes[2],
216+
id_bytes[3],
217+
id_bytes[4],
218+
id_bytes[5],
219+
]
220+
}
221+
222+
/// Send a packet, containing the device address and the given data, and
223+
/// wait for a response.
224+
///
225+
/// If we get a response containing the same device address, it returns a
226+
/// slice of the remaining payload (i.e. not including the device address).
227+
///
228+
/// If we don't get a response, or we get a bad response (with the wrong
229+
/// address in it), we try again.
230+
///
231+
/// If we try too many times, we give up.
232+
pub fn send_recv<'packet, I>(
233+
packet: &'packet mut Packet,
234+
data_to_send: &[u8],
235+
radio: &mut hal::ieee802154::Radio,
236+
timer: &mut hal::timer::Timer<I>,
237+
microseconds: u32,
238+
) -> Result<&'packet [u8], hal::ieee802154::Error>
239+
where
240+
I: hal::timer::Instance,
241+
{
242+
assert!(data_to_send.len() + ADDR_LEN < usize::from(Packet::CAPACITY));
243+
244+
let id_bytes = get_id();
245+
// Short delay before sending, so we don't get into a tight loop and steal all the bandwidth
246+
timer.delay(5000);
247+
for i in 0..RETRY_COUNT {
248+
packet.set_len(ADDR_LEN as u8 + data_to_send.len() as u8);
249+
let source_iter = id_bytes.iter().chain(data_to_send.iter());
250+
let dest_iter = packet.iter_mut();
251+
for (source, dest) in source_iter.zip(dest_iter) {
252+
*dest = *source;
253+
}
254+
defmt::debug!("TX: {=[u8]:02x}", &packet[..]);
255+
radio.send(packet);
256+
match radio.recv_timeout(packet, timer, microseconds) {
257+
Ok(_crc) => {
258+
defmt::debug!("RX: {=[u8]:02x}", packet[..]);
259+
// packet is long enough
260+
if packet[0..ADDR_LEN] == id_bytes {
261+
// and it has the right bytes at the start
262+
defmt::debug!("OK: {=[u8]:02x}", packet[ADDR_LEN..]);
263+
return Ok(&packet[ADDR_LEN..]);
264+
} else {
265+
defmt::warn!("RX Wrong Address try {}", i);
266+
timer.delay(10000);
267+
}
268+
}
269+
Err(hal::ieee802154::Error::Timeout) => {
270+
defmt::warn!("RX Timeout try {}", i);
271+
timer.delay(10000);
272+
}
273+
Err(hal::ieee802154::Error::Crc(_)) => {
274+
defmt::warn!("RX CRC Error try {}", i);
275+
timer.delay(10000);
276+
}
277+
}
278+
}
279+
Err(hal::ieee802154::Error::Timeout)
280+
}
281+
}
282+
283+
#[cfg(feature = "radio")]
284+
pub use radio_retry::send_recv;
285+
198286
/// The ways that initialisation can fail
199287
#[derive(Debug, Copy, Clone, defmt::Format)]
200288
pub enum Error {

0 commit comments

Comments
 (0)