Skip to content
This repository has been archived by the owner on May 18, 2022. It is now read-only.

Passive scanning #118

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion demos/nrf52-demo/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ const APP: () = {
if let Some(cmd) = ctx
.resources
.radio
.recv_interrupt(ble_ll.timer().now(), ble_ll)
.recv_ll_interrupt(ble_ll.timer().now(), ble_ll)
{
ctx.resources.radio.configure_receiver(cmd.radio);
ble_ll.timer().configure_interrupt(cmd.next_update);
Expand Down
8 changes: 8 additions & 0 deletions demos/nrf52-scanner/.cargo/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[build]
target = 'thumbv7em-none-eabi'

[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = 'arm-none-eabi-gdb'
rustflags = [
"-C", "link-arg=-Tlink.x",
]
35 changes: 35 additions & 0 deletions demos/nrf52-scanner/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[package]
authors = ["Danilo Bargen <[email protected]>"]
description = "Rubble BLE stack demo showcasing passive beacon scanning on the nRF52 MCUs"
categories = ["embedded", "no-std"]
keywords = ["arm", "nrf", "bluetooth", "low", "energy"]
repository = "https://github.com/jonas-schievink/rubble/"
license = "0BSD"
name = "nrf52-scanner"
version = "0.0.0"
edition = "2018"
publish = false

[dependencies]
rubble = { path = "../../rubble", default-features = false }
rubble-nrf5x = { path = "../../rubble-nrf5x" }
demo-utils = { path = "../demo-utils" }
cortex-m = "0.6.1"
cortex-m-rtfm = "0.5.1"
cortex-m-rt = "0.6.11"
panic-halt = "0.2.0"

nrf52810-hal = { version = "0.10", features = ["rt"], optional = true }
nrf52832-hal = { version = "0.10", features = ["rt"], optional = true }
nrf52840-hal = { version = "0.10", features = ["rt"], optional = true }

# Disable documentation to avoid spurious rustdoc warnings
[[bin]]
name = "nrf52-scanner"
doc = false
test = false

[features]
52810 = ["rubble-nrf5x/52810", "nrf52810-hal"]
52832 = ["rubble-nrf5x/52832", "nrf52832-hal"]
52840 = ["rubble-nrf5x/52840", "nrf52840-hal"]
10 changes: 10 additions & 0 deletions demos/nrf52-scanner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# `nrf52-scanner`

A passive beacon scanner.

It does not enable logging and is kept simple and minimal.

The demo works with the nRF52810, nRF52832, and nRF52840. To run it, one of the
target devices has to be enabled via a Cargo feature:

cargo run --feature 52810
116 changes: 116 additions & 0 deletions demos/nrf52-scanner/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#![no_std]
#![no_main]
#![warn(rust_2018_idioms)]

// We need to import this crate explicitly so we have a panic handler
use panic_halt as _;

// Import the right HAL and PAC
#[cfg(feature = "52810")]
use nrf52810_hal as hal;

#[cfg(feature = "52832")]
use nrf52832_hal as hal;

#[cfg(feature = "52840")]
use nrf52840_hal as hal;

use {
rubble::{
beacon::{BeaconScanner, ScanCallback},
link::{ad_structure::AdStructure, filter::AllowAll, DeviceAddress, MIN_PDU_BUF},
time::{Duration, Timer},
},
rubble_nrf5x::{
radio::{BleRadio, PacketBuffer},
timer::BleTimer,
},
};

pub struct BeaconScanCallback;

impl ScanCallback for BeaconScanCallback {
fn beacon<'a, I>(&mut self, _addr: DeviceAddress, _data: I)
where
I: Iterator<Item = AdStructure<'a>>,
{
// Detected an advertisement frame! Do something with it here.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think this should at least do something with the data, maybe even just an hprintln! if that makes sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could add a rprintln! call through rtt-target. It would require connecting with an RTT client though (e.g. by flashing through cargo-embed, which is what I usually do).

Might actually be nice for the demo, since it doesn't require wiring up and configuring UART (and in contrast to semihosting code still runs without a debug probe attached).

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since rtt-target has a bunch of open soundness issues and is still fairly new, I'd prefer to go with semihosting instead (we already pull that in for a panic handler). Since this is a demo, it won't run without a debugger anyways (and if it did, it wouldn't output anything regardless).

}
}

#[rtfm::app(device = crate::hal::target, peripherals = true)]
const APP: () = {
struct Resources {
#[init([0; MIN_PDU_BUF])]
ble_tx_buf: PacketBuffer,
#[init([0; MIN_PDU_BUF])]
ble_rx_buf: PacketBuffer,
radio: BleRadio,
timer: BleTimer<hal::pac::TIMER0>,
scanner: BeaconScanner<BeaconScanCallback, AllowAll>,
}

#[init(resources = [ble_tx_buf, ble_rx_buf])]
fn init(ctx: init::Context) -> init::LateResources {
// On reset, the internal high frequency clock is already used, but we
// also need to switch to the external HF oscillator. This is needed
// for Bluetooth to work.
let _clocks = hal::clocks::Clocks::new(ctx.device.CLOCK).enable_ext_hfosc();

// Initialize BLE timer
let mut timer = BleTimer::init(ctx.device.TIMER0);

// Initialize radio
let mut radio = BleRadio::new(
ctx.device.RADIO,
&ctx.device.FICR,
ctx.resources.ble_tx_buf,
ctx.resources.ble_rx_buf,
);

// Set up beacon scanner for continuous scanning. The advertisement
// channel that is being listened on (scan window) will be switched
// every 500 ms.
let mut scanner = BeaconScanner::new(BeaconScanCallback);
let scanner_cmd = scanner.configure(timer.now(), Duration::from_millis(500));

// Reconfigure radio and timer
radio.configure_receiver(scanner_cmd.radio);
timer.configure_interrupt(scanner_cmd.next_update);

init::LateResources {
radio,
scanner,
timer,
}
}

#[task(binds = RADIO, resources = [radio, scanner, timer])]
fn radio(ctx: radio::Context) {
let timer = ctx.resources.timer;
let scanner = ctx.resources.scanner;
let radio = ctx.resources.radio;

if let Some(next_update) = radio.recv_beacon_interrupt(scanner) {
timer.configure_interrupt(next_update);
}
}

#[task(binds = TIMER0, resources = [radio, timer, scanner])]
fn timer0(ctx: timer0::Context) {
let timer = ctx.resources.timer;
let scanner = ctx.resources.scanner;
let radio = ctx.resources.radio;

// Clear interrupt
if !timer.is_interrupt_pending() {
return;
}
timer.clear_interrupt();

// Update scanner (switch to next advertisement channel)
let cmd = scanner.timer_update(timer.now());
radio.configure_receiver(cmd.radio);
timer.configure_interrupt(cmd.next_update);
}
};
77 changes: 62 additions & 15 deletions rubble-nrf5x/src/radio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,12 @@ use {
},
pac::{radio::state::STATE_R, RADIO},
rubble::{
beacon::{BeaconScanner, ScanCallback},
config::Config,
link::{advertising, data, Cmd, LinkLayer, RadioCmd, Transmitter, CRC_POLY, MIN_PDU_BUF},
link::{
advertising, data, filter::AddressFilter, Cmd, LinkLayer, NextUpdate, RadioCmd,
Transmitter, CRC_POLY, MIN_PDU_BUF,
},
phy::{AdvertisingChannel, DataChannel},
time::{Duration, Instant},
},
Expand All @@ -84,6 +88,21 @@ pub struct BleRadio {
rx_buf: Option<&'static mut PacketBuffer>,
}

/// Acknowledge the DSIABLED event
macro_rules! acknowledge_disabled_event {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this be a method?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The early-return of None makes that a bit unergonomic... Or do you have an idea how to handle that?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use ? with a method returning Option<()>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoa, I totally missed that this is possible with options as well, and not just results 🤯

($radio:expr) => {
if $radio.events_disabled.read().bits() == 0 {
return None;
}

// "Subsequent reads and writes cannot be moved ahead of preceding reads."
compiler_fence(Ordering::Acquire);

// Acknowledge DISABLED event
$radio.events_disabled.reset();
};
}

impl BleRadio {
/// Initializes the radio in BLE mode and takes ownership of the RX and TX buffers.
// TODO: Use type-safe clock configuration to ensure that chip uses ext. crystal
Expand Down Expand Up @@ -303,25 +322,15 @@ impl BleRadio {
}
}

/// Call this when the `RADIO` interrupt fires.
/// Call this when the `RADIO` interrupt fires when using the [`LinkLayer`].
///
/// Automatically reconfigures the radio according to the `RadioCmd` returned by the BLE stack.
///
/// Returns when the `update` method should be called the next time.
pub fn recv_interrupt<C: Config<Transmitter = Self>>(
/// [`LinkLayer`]: ../../rubble/link/struct.LinkLayer.html
pub fn recv_ll_interrupt<C: Config<Transmitter = Self>>(
&mut self,
timestamp: Instant,
ll: &mut LinkLayer<C>,
) -> Option<Cmd> {
if self.radio.events_disabled.read().bits() == 0 {
return None;
}

// "Subsequent reads and writes cannot be moved ahead of preceding reads."
compiler_fence(Ordering::Acquire);

// Acknowledge DISABLED event:
self.radio.events_disabled.reset();
acknowledge_disabled_event!(self.radio);

let crc_ok = self.radio.crcstatus.read().crcstatus().is_crcok();

Expand Down Expand Up @@ -357,6 +366,44 @@ impl BleRadio {
Some(cmd)
}

/// Call this when the `RADIO` interrupt fires when using the [`BeaconScanner`].
///
/// The radio is automatically reconfigured to listen on the next
/// advertisement channel.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only the timer interrupt should do this, right?

///
/// Returns when the [`timer_update`] method should be called the next time.
///
/// [`BeaconScanner`]: ../../rubble/beacon/struct.BeaconScanner.html
/// [`timer_update`]: ../../rubble/beacon/struct.BeaconScanner.html#method.timer_update
pub fn recv_beacon_interrupt<C: ScanCallback, F: AddressFilter>(
&mut self,
scanner: &mut BeaconScanner<C, F>,
) -> Option<NextUpdate> {
acknowledge_disabled_event!(self.radio);

// Check CRC
let crc_ok = self.radio.crcstatus.read().crcstatus().is_crcok();

// When we get here, the radio must have transitioned to DISABLED state.
assert!(self.state().is_disabled());

// Parse header
let header = advertising::Header::parse(*self.rx_buf.as_ref().unwrap());

// Check that `payload_length` is in bounds
let rx_buf = self.rx_buf.as_ref().unwrap();
let pl_lim = cmp::min(2 + usize::from(header.payload_length()), rx_buf.len());

// Process payload
let payload = &rx_buf[2..pl_lim];
let cmd = scanner.process_adv_packet(header, payload, crc_ok);

// Reconfigure radio
self.configure_receiver(cmd.radio);

Some(cmd.next_update)
}

/// Perform preparations to receive or send on an advertising channel.
///
/// This will disable the radio, configure the packet layout, set initial values for CRC and
Expand Down