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

Basic writeable connection attributes + attribute permissions #171

Merged
Merged
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: 2 additions & 0 deletions demos/nrf52-demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

This is a demo application we're using when hacking on Rubble. It runs on any
chip in the nRF52 family.
It exposes a dummy read-only battery service attribute, as well as a read/write attribute that
toggles an on-board LED (pin 17 by default).

The demo allows establishing a connection and provides a GATT server. A *lot*
of things are logged over the UART, so it's recommended to hook that up.
213 changes: 213 additions & 0 deletions demos/nrf52-demo/src/attrs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
//! Defines a custom `AttributeValueProvider`.
#[cfg(feature = "52810")]
use nrf52810_hal as hal;
#[cfg(feature = "52832")]
use nrf52832_hal as hal;
#[cfg(feature = "52833")]
use nrf52833_hal as hal;
#[cfg(feature = "52840")]
use nrf52840_hal as hal;

use hal::{
gpio::{Output, Pin, PushPull},
prelude::OutputPin,
};
use rubble::{
att::{AttUuid, Attribute, AttributeAccessPermissions, AttributeProvider, Handle, HandleRange},
uuid::{Uuid128, Uuid16},
Error,
};

pub struct DemoAttrs {
// Attributes exposed to clients that don't change.
// This includes the "primary service" and "characteristic" attributes.
// Some attributes are copied from the declaration of `BatteryServiceAttrs` in the gatt module.
static_attributes: [Attribute<&'static [u8]>; 6],
// State and resources to be modified/queried when packets are received.
// The `AttributeValueProvider` interface allows attributes to be generated lazily; those
// attributes should use these fields.
led_pin: Pin<Output<PushPull>>,
led_buf: [u8; 1],
}

const PRIMARY_SERVICE_UUID16: Uuid16 = Uuid16(0x2800);
const CHARACTERISTIC_UUID16: Uuid16 = Uuid16(0x2803);
const GENERIC_ATTRIBUTE_UUID16: Uuid16 = Uuid16(0x1801);
const BATTERY_LEVEL_UUID16: Uuid16 = Uuid16(0x2A19);

// Randomly generated
// a86a62f0-5d26-4538-b364-5654961515c9
const LED_UUID128: [u8; 16] = [
0xC9, 0x15, 0x15, 0x96, 0x54, 0x56, 0x64, 0xB3, 0x38, 0x45, 0x26, 0x5D, 0xF0, 0x62, 0x6A, 0xA8,
];
// Replace bytes 12/13 (0x62F0) of the 128-bit UUID with 62F1
const LED_STATE_CHAR_UUID128: [u8; 16] = [
0xC9, 0x15, 0x15, 0x96, 0x54, 0x56, 0x64, 0xB3, 0x38, 0x45, 0x26, 0x5D, 0xF1, 0x62, 0x6A, 0xA8,
];

const LED_CHAR_DECL_VALUE: [u8; 19] = [
0x02 | 0x08, // 0x02 = read, 0x08 = write with response
// 2 byte handle pointing to characteristic value
0x03,
0x00,
// 128-bit UUID of characteristic value (copied from above constant)
0xC9,
0x15,
0x15,
0x96,
0x54,
0x56,
0x64,
0xB3,
0x38,
0x45,
0x26,
0x5D,
0xF1,
0x62,
0x6A,
0xA8,
];

impl DemoAttrs {
pub fn new(mut led_pin: Pin<Output<PushPull>>) -> Self {
// Turn off by default (active low)
led_pin.set_high().unwrap();
Self {
static_attributes: [
Attribute::new(
PRIMARY_SERVICE_UUID16.into(),
Handle::from_raw(0x0001),
&LED_UUID128,
),
Attribute::new(
CHARACTERISTIC_UUID16.into(),
Handle::from_raw(0x0002),
&LED_CHAR_DECL_VALUE,
),
// 0x0003 is skipped because it's lazily generated
// Dummy ending attribute
// This needs to come after our lazily generated data attribute because group_end()
// needs to return a reference
Attribute::new(
GENERIC_ATTRIBUTE_UUID16.into(),
Handle::from_raw(0x0004),
&[],
),
// Below is copied from `gatt::BatteryServiceAttrs`
Attribute::new(
PRIMARY_SERVICE_UUID16.into(),
Handle::from_raw(0x0005),
&[0x0F, 0x18], // "Battery Service" = 0x180F
),
Attribute::new(
CHARACTERISTIC_UUID16.into(),
Handle::from_raw(0x0006),
&[
0x02, // 1 byte properties: READ = 0x02
0x07, 0x00, // 2 bytes handle = 0x0007
0x19, 0x2A, // 2 bytes UUID = 0x2A19 (Battery Level)
],
),
// Characteristic value (Battery Level)
Attribute::new(
BATTERY_LEVEL_UUID16.into(),
Handle::from_raw(0x0007),
&[48u8],
),
],
led_pin,
led_buf: [0u8],
}
}
}

impl DemoAttrs {
// Lazily produces an attribute to be read/written, representing the LED state.
fn led_data_attr(&self) -> Attribute<[u8; 1]> {
Attribute::new(
Uuid128::from_bytes(LED_STATE_CHAR_UUID128).into(),
Handle::from_raw(0x0003),
self.led_buf,
)
}
}

impl AttributeProvider for DemoAttrs {
/// Retrieves the permissions for attribute with the given handle.
fn attr_access_permissions(&self, handle: Handle) -> AttributeAccessPermissions {
match handle.as_u16() {
0x0003 => AttributeAccessPermissions::ReadableAndWriteable,
_ => AttributeAccessPermissions::Readable,
}
}

/// Attempts to write data to the attribute with the given handle.
/// If any of your attributes are writeable, this function must be implemented.
fn write_attr(&mut self, handle: Handle, data: &[u8]) -> Result<(), Error> {
match handle.as_u16() {
0x0003 => {
if data.is_empty() {
return Err(Error::InvalidLength);
}
// If we receive a 1, activate the LED; otherwise deactivate it
// Assumes LED is active low
if data[0] == 1 {
self.led_pin.set_low().unwrap();
} else {
self.led_pin.set_high().unwrap();
}
// Copy written value into buffer to display back for reading
self.led_buf.copy_from_slice(data);
Ok(())
}
_ => panic!("Attempted to write an unwriteable attribute"),
}
}

fn is_grouping_attr(&self, uuid: AttUuid) -> bool {
uuid == PRIMARY_SERVICE_UUID16 || uuid == CHARACTERISTIC_UUID16
}

fn group_end(&self, handle: Handle) -> Option<&Attribute<dyn AsRef<[u8]>>> {
match handle.as_u16() {
// Handles for the LED primary service and characteristic
// The group end is a dummy attribute; strictly speaking it's not required
// but we can't use the lazily generated attribute because this funtion requires
// returning a reference
0x0001 | 0x0002 => Some(&self.static_attributes[2]),
// Handles for Battery Service
0x0005 | 0x0006 => Some(&self.static_attributes[5]),
_ => None,
}
}

/// Applies a function to all attributes with handles within the specified range
fn for_attrs_in_range(
&mut self,
range: HandleRange,
mut f: impl FnMut(&Self, &Attribute<dyn AsRef<[u8]>>) -> Result<(), Error>,
) -> Result<(), Error> {
// Handles start at 1, not 0, but we're not directly indexing
let start = range.start().as_u16();
let end = range.end().as_u16();
let range_u16 = start..=end;
// Can't just iterate from start to end because of the presence of lazy attributes
// Ranges are empty if start >= end
for attr in &self.static_attributes {
if range_u16.contains(&attr.handle.as_u16()) {
f(self, attr)?;
}
}
// Check lazy attributes
// Note that with this implementation, if a static attribute has handle greater than a
// lazy attribute, the order in which f() is applied is not preserved.
// This may matter for the purposes of short-circuiting an operation if it cannot be applied
// to a particular attribute
if range_u16.contains(&0x0003) {
f(self, &self.led_data_attr())?;
};
Ok(())
}
}
45 changes: 32 additions & 13 deletions demos/nrf52-demo/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ macro_rules! config {

#[macro_use]
mod config;
mod attrs;
mod logger;

// Import the right HAL/PAC crate, depending on the target chip
Expand All @@ -48,24 +49,38 @@ use nrf52833_hal as hal;
use nrf52840_hal as hal;

use bbqueue::Consumer;
use core::fmt::Write;
use core::sync::atomic::{compiler_fence, Ordering};
use hal::uarte::{Baudrate, Parity, Uarte};
use hal::{gpio::Level, pac::UARTE0};
use rubble::l2cap::{BleChannelMap, L2CAPState};
use rubble::link::queue::{PacketQueue, SimpleQueue};
use rubble::link::{ad_structure::AdStructure, LinkLayer, Responder, MIN_PDU_BUF};
use rubble::time::{Duration, Timer};
use rubble::{config::Config, gatt::BatteryServiceAttrs, security::NoSecurity};
use rubble_nrf5x::radio::{BleRadio, PacketBuffer};
use rubble_nrf5x::{timer::BleTimer, utils::get_device_address};
use core::{
fmt::Write,
sync::atomic::{compiler_fence, Ordering},
};
use hal::{
gpio::Level,
pac::UARTE0,
uarte::{Baudrate, Parity, Uarte},
};
use rubble::{
config::Config,
l2cap::{BleChannelMap, L2CAPState},
link::{
ad_structure::AdStructure,
queue::{PacketQueue, SimpleQueue},
LinkLayer, Responder, MIN_PDU_BUF,
},
security::NoSecurity,
time::{Duration, Timer},
};
use rubble_nrf5x::{
radio::{BleRadio, PacketBuffer},
timer::BleTimer,
utils::get_device_address,
};

pub enum AppConfig {}

impl Config for AppConfig {
type Timer = BleTimer<hal::pac::TIMER0>;
type Transmitter = BleRadio;
type ChannelMapper = BleChannelMap<BatteryServiceAttrs, NoSecurity>;
type ChannelMapper = BleChannelMap<attrs::DemoAttrs, NoSecurity>;
type PacketQueue = &'static mut SimpleQueue;
}

Expand Down Expand Up @@ -121,10 +136,14 @@ const APP: () = {
// Create the actual BLE stack objects
let mut ble_ll = LinkLayer::<AppConfig>::new(device_address, ble_timer);

// Assumes pin 17 corresponds to an LED.
// On the NRF52DK board, this is LED 1.
let ble_r = Responder::new(
tx,
rx,
L2CAPState::new(BleChannelMap::with_attributes(BatteryServiceAttrs::new())),
L2CAPState::new(BleChannelMap::with_attributes(attrs::DemoAttrs::new(
p0.p0_17.into_push_pull_output(Level::High).degrade(),
))),
);

// Send advertisement and set up regular interrupt
Expand Down
Loading