rubble 0.0.4

An embedded BLE stack
Documentation
//! BLE beacon support, without dealing with Link-Layer stuff.

use crate::link::advertising::{Header, Pdu, PduBuf};
use crate::link::filter::{self, AddressFilter, ScanFilter};
use crate::link::{
    ad_structure::AdStructure, Cmd, DeviceAddress, NextUpdate, RadioCmd, Transmitter,
};
use crate::phy::AdvertisingChannel;
use crate::time::{Duration, Instant};
use crate::{bytes::*, Error};

/// A BLE beacon.
///
/// FIXME: This has to randomly offset the broadcast interval
pub struct Beacon {
    pdu: PduBuf,
}

impl Beacon {
    /// Creates a new beacon that will broadcast a packet on all advertisement
    /// channels.
    ///
    /// # Parameters
    ///
    /// * **`addr`**: Address of the beacon device.
    /// * **`data`**: Data to broadcast. This must fit within a single PDU.
    ///
    /// # Errors
    ///
    /// If `data` doesn't fit in a single PDU, an error will be returned.
    pub fn new(addr: DeviceAddress, data: &[AdStructure<'_>]) -> Result<Self, Error> {
        let pdu = PduBuf::beacon(addr, data)?;
        Ok(Self { pdu })
    }

    /// Broadcasts the beacon data using `tx`.
    ///
    /// This will broadcast once on every advertising channel.
    pub fn broadcast<T: Transmitter>(&self, tx: &mut T) {
        // The spec says that we have to broadcast on all 3 channels in sequence, so that the total
        // time of this broadcast ("advertising event") is <10ms.

        // FIXME: Do we have to write the payload again every time we send (ie.
        // is the transmitter allowed to overwrite it)?

        let payload = self.pdu.payload();
        let buf = tx.tx_payload_buf();
        buf[..payload.len()].copy_from_slice(payload);

        for channel in AdvertisingChannel::iter_all() {
            tx.transmit_advertising(self.pdu.header(), channel);
        }
    }
}

/// Callback for the `BeaconScanner`.
pub trait ScanCallback {
    /// Called when a beacon is received and has passed the configured device address filter.
    ///
    /// # Parameters
    ///
    /// * **`adv_addr`**: Address of the device sending the beacon.
    /// * **`adv_data`**: Advertising data structures attached to the beacon.
    fn beacon<'a, I>(&mut self, adv_addr: DeviceAddress, adv_data: I)
    where
        I: Iterator<Item = AdStructure<'a>>;
}

/// A passive scanner for non-connectable beacon advertisements.
pub struct BeaconScanner<C: ScanCallback, F: AddressFilter> {
    cb: C,
    filter: ScanFilter<F>,
    interval: Duration,
    channel: AdvertisingChannel,
}

impl<C: ScanCallback> BeaconScanner<C, filter::AllowAll> {
    /// Creates a `BeaconScanner` that will report beacons from any device.
    pub fn new(callback: C) -> Self {
        Self::with_filter(callback, filter::AllowAll)
    }
}

impl<C: ScanCallback, F: AddressFilter> BeaconScanner<C, F> {
    /// Creates a `BeaconScanner` with a custom device filter.
    pub fn with_filter(callback: C, scan_filter: F) -> Self {
        Self {
            cb: callback,
            filter: ScanFilter::new(scan_filter),
            interval: Duration::from_micros(0),
            channel: AdvertisingChannel::first(),
        }
    }

    /// Configures the `BeaconScanner` and returns a `Cmd` to apply to the radio.
    ///
    /// The `next_update` field of the returned `Cmd` specifies when to call `timer_update` the next
    /// time. The timer used for this does not have to be very accurate, it is only used to switch
    /// to the next advertising channel after `interval` elapses.
    pub fn configure(&mut self, now: Instant, interval: Duration) -> Cmd {
        self.interval = interval;
        self.channel = AdvertisingChannel::first();

        Cmd {
            // Switch channels
            next_update: NextUpdate::At(now + self.interval),

            radio: RadioCmd::ListenAdvertising {
                channel: self.channel,
            },

            queued_work: false,
        }
    }

    /// Updates the `BeaconScanner` after the configured timer has fired.
    ///
    /// This switches to the next advertising channel and will listen there.
    pub fn timer_update(&mut self, now: Instant) -> Cmd {
        self.channel = self.channel.cycle();

        Cmd {
            // Switch channels
            next_update: NextUpdate::At(now + self.interval),

            radio: RadioCmd::ListenAdvertising {
                channel: self.channel,
            },

            queued_work: false,
        }
    }

    /// Processes a received advertising channel packet.
    ///
    /// This should be called whenever the radio receives a packet on the configured advertising
    /// channel.
    pub fn process_adv_packet(&mut self, header: Header, payload: &[u8], crc_ok: bool) -> Cmd {
        if crc_ok && header.type_().is_beacon() {
            // Partially decode to get the device ID and run it through the filter
            if let Ok(pdu) = Pdu::from_header_and_payload(header, &mut ByteReader::new(payload)) {
                if self.filter.should_scan(*pdu.sender()) {
                    let ad = pdu.advertising_data().unwrap();
                    self.cb.beacon(*pdu.sender(), ad);
                }
            }
        }

        Cmd {
            next_update: NextUpdate::Keep,
            radio: RadioCmd::ListenAdvertising {
                channel: self.channel,
            },
            queued_work: false,
        }
    }
}