Files
GemBluetoothEmbeddedDevice/gem-remotes-esp32/src/ble_server.rs

296 lines
12 KiB
Rust

use log::*; //{trace, debug, info, warn, error}
use esp32_nimble::{enums::*, uuid128, BLEAdvertisementData, BLEDevice, BLEServer, NimbleProperties, OnWriteArgs};
use esp32_nimble::utilities::BleUuid;
use anyhow::Result;
use closure::closure;
use bitflags::bitflags;
use std::str::FromStr;
use std::sync::Arc;
use crate::dispatch::{Dispatch, RecvQ, SendQ};
use crate::commands::{Button, Commands};
// TODO HARDWARE: test these values to see if they are suitable
const BLE_MIN_INTERVAL: u16 = 24; // x 1.25ms
const BLE_MAX_INTERVAL: u16 = 48; // x 1.25ms
const BLE_LATENCY: u16 = 0; // Number of packets that can be missed, extending interval
const BLE_TIMEOUT: u16 = 500; // x10ms
// Bit flags in the custom service byte.
bitflags! {
#[derive(Debug, Clone, Copy)]
struct SvcFlags: u8 {
const PAIRING_MODE = 1 << 0;
}
}
// Names appear to truncate at 31 characters.
const DEVICE_NAME: &str = "Gem Remotes";
// The [u8] version is, for whatever reason, little-endian. This should equate to the UUID "9966ad5a-f13c-4b61-ba66-0861e08d09b4".
const UUID_SERVICE_PAIR: BleUuid = BleUuid::from_uuid128([0xB4, 0x09, 0x8D, 0xE0, 0x61, 0x08, 0x66, 0xBA, 0x61, 0x4B, 0x3C, 0xF1, 0x5A, 0xAD, 0x66, 0x99]);
const UUID_SERVICE_LIFT: BleUuid = uuid128!("c1400000-8dda-45a3-959b-d23a0f8f53d7");
const UUID_BUTTON_UP: BleUuid = uuid128!("c1401121-8dda-45a3-959b-d23a0f8f53d7");
const UUID_BUTTON_DOWN: BleUuid = uuid128!("c1401122-8dda-45a3-959b-d23a0f8f53d7");
const UUID_BUTTON_STOP: BleUuid = uuid128!("c1401123-8dda-45a3-959b-d23a0f8f53d7");
const UUID_BLUETOOTH_NAME: BleUuid = uuid128!("c1401224-8dda-45a3-959b-d23a0f8f53d7");
const BLE_BUTTON_RELEASE: u8 = 0;
const BLE_BUTTON_PRESS: u8 = 1;
pub struct BleServer {
send_q: SendQ,
recv_q: RecvQ,
svc_flags: SvcFlags,
dev_name: String,
}
impl BleServer {
pub fn new(dp: &mut Dispatch) -> Self {
let cmds = vec![
Commands::NotifyMotorDown { data: Button::Released },
Commands::NotifyMotorStop { data: Button::Released },
Commands::NotifyMotorUp { data: Button::Released },
Commands::PairTimerExpired,
Commands::AllowPairing,
Commands::EraseBleBonds,
Commands::BluetoothName { data: Arc::new(String::new())},
];
let r = dp.get_callback_channel(&cmds);
let s = dp.get_cmd_channel();
let dev_name = DEVICE_NAME; //TODO: read this from NVS if it is present.
BleServer {
send_q: s.clone(),
recv_q: r.clone(),
svc_flags: SvcFlags::empty(),
dev_name: String::from_str(dev_name).unwrap(), //Unwrap should be fine here because we control this string
}
}
pub async fn run(&mut self) -> Result<()> {
match self.do_run().await {
Ok(_) => {error!("Exited bluetooth server wait loop with no error.");panic!();}
Err(e) => {error!("Bluetooth task encountered error {}", e);panic!();}
}
}
pub async fn do_run(&mut self) -> Result<()> {
trace!("Entering BLE Run");
let ble_device = BLEDevice::take();
set_device_security(ble_device);
let server = ble_device.get_server();
set_server_callbacks(server, self.send_q.clone());
let lift_service = server.create_service(UUID_SERVICE_LIFT);
trace!("Setting up GATT");
//TODO: require authentication (bonding counts?) for these!
let sender = self.send_q.clone();
// --- Button Up Bluetooth GATT ----------------------------------------------------------
let button_up = lift_service.lock().create_characteristic(
UUID_BUTTON_UP,
NimbleProperties::READ | NimbleProperties::WRITE_NO_RSP | NimbleProperties::NOTIFY,
);
button_up.lock().set_value(&[0])
.on_write(closure!(clone sender, |args: &mut OnWriteArgs| {
on_bluetooth_cmd(&sender, args, Commands::BluetoothUp {data: Button::Released})
}));
// --- Button Down Bluetooth GATT --------------------------------------------------------
let button_down = lift_service.lock().create_characteristic(
UUID_BUTTON_DOWN,
NimbleProperties::READ | NimbleProperties::WRITE_NO_RSP | NimbleProperties::NOTIFY,
);
button_down.lock().set_value(&[0])
.on_write(closure!(clone sender, |args: &mut OnWriteArgs| {
on_bluetooth_cmd(&sender, args, Commands::BluetoothDown {data: Button::Released})
}));
// --- Button Stop Bluetooth GATT --------------------------------------------------------
let button_stop = lift_service.lock().create_characteristic(
UUID_BUTTON_STOP,
NimbleProperties::READ | NimbleProperties::WRITE_NO_RSP | NimbleProperties::NOTIFY,
);
button_stop.lock().set_value(&[1])
.on_write(closure!(clone sender, |args: &mut OnWriteArgs| {
on_bluetooth_cmd(&sender, args, Commands::BluetoothStop {data: Button::Released})
}));
// --- Device Name Bluetooth GATT --------------------------------------------------------
let device_name = lift_service.lock().create_characteristic(
UUID_BLUETOOTH_NAME,
NimbleProperties::READ | NimbleProperties::WRITE,
);
device_name.lock().set_value(self.dev_name.as_bytes())
.on_write(closure!(clone sender, |args: &mut OnWriteArgs| {
on_bluetooth_cmd(&sender, args, Commands::BluetoothName { data: Arc::new(String::new())}.clone())
}));
// Default to not pairable
self.advertise_unpairable()?;
loop {
debug!("Waiting for updates that should be notified via bluetooth");
let cmd =self.recv_q.recv().await.expect("Bluetooth notification queue unexpectedly closed");
trace!("Received update to bluetooth variable {:?}", cmd);
match cmd {
// TODO DISCUSS: This logic (if one button is pressed others are released) could be done in app instead.
Commands::NotifyMotorUp{data} => {
button_up.lock().set_value(&button_to_ble_button(data)).notify();
}
Commands::NotifyMotorDown{data} => {
button_down.lock().set_value(&button_to_ble_button(data)).notify();
}
Commands::NotifyMotorStop{data} => {
button_up.lock().set_value(&button_to_ble_button(data)).notify();
}
Commands::PairTimerExpired => {
self.advertise_unpairable()?;
self.send_q.send(Commands::PairTimerClear).await?;
}
Commands::AllowPairing => {
self.advertise_pairable()?;
debug!("pairing mode / non-pairing mode not currently supported");
}
Commands::EraseBleBonds => {
ble_device.delete_all_bonds().expect("Failed trying to erase bluetooth bonding information");
}
Commands::BluetoothName { data } => {
self.update_name(data.as_str())?; // TODO: consider not failing over trivial name change problem
}
_ => {
error!("Invalid command received by bluetooth handler {:?}", cmd);
// No need to reboot as state is recoverable.
}
}
}
}
fn update_name(&mut self, new_name: &str) -> Result<()> {
self.dev_name.clear();
let mut name = new_name;
if name.len() > 31 {name = &new_name[..31];}
self.dev_name.push_str(name);
self.advertise()?;
//TODO: save name to nvs
Ok(())
}
fn advertise(&self) -> Result<()> {
let ble_dev = BLEDevice::take();
let ble_adv = ble_dev.get_advertising();
let mut adv = ble_adv.lock();
if adv.is_advertising() {
adv.stop()?;
}
let mut beacon_data = BLEAdvertisementData::new();
beacon_data.name(self.dev_name.as_str());
beacon_data.service_data(UUID_SERVICE_PAIR, &[self.svc_flags.bits()]);
adv
.advertisement_type(ConnMode::Und)
.set_data(&mut beacon_data)?;
info!("Staring Bluetooth Server");
adv.start()?;
Ok(())
}
fn advertise_pairable(&mut self) -> Result<()> {
self.svc_flags = self.svc_flags | SvcFlags::PAIRING_MODE;
self.advertise()
}
fn advertise_unpairable(&mut self) -> Result<()> {
self.svc_flags = self.svc_flags - SvcFlags::PAIRING_MODE;
self.advertise()
}
}
fn on_bluetooth_cmd(sender: &SendQ, args: &mut OnWriteArgs, cmd: Commands) {
let v = args.recv_data();
// receiving incorrect data isn't fatal, but being unable to send events is.
let attempt = match cmd {
Commands::BluetoothUp { data: _ } => {
sender.send_blocking(Commands::BluetoothUp {data: ble_to_button(v)} )
}
Commands::BluetoothDown { data: _ } => {
sender.send_blocking(Commands::BluetoothDown {data: ble_to_button(v)} )
}
Commands::BluetoothStop { data: _ } => {
sender.send_blocking(Commands::BluetoothStop {data: ble_to_button(v)} )
}
Commands::BluetoothName { data: _ } => {
if v.len() > 0 {
let name = String::from_utf8_lossy(v).into_owned();
sender.send_blocking(Commands::BluetoothName { data: Arc::new(name) })
} else {
// If the user clears the name, revert to the default.
// Unwrap is safe here because we control the string.
sender.send_blocking(Commands::BluetoothName { data: Arc::new(String::from_str(DEVICE_NAME).unwrap()) })
}
}
//TODO when we get name changes, truncate to 31 chars, because that's what we have anyway.
_ => {error!("Tried to handle an unknown bluetooth command: {:?}",cmd);Ok(())}
};
match attempt {
Ok(_) => {}
Err(_) => {panic!("Unable to send notifications of bluetooth events")}
}
}
fn ble_to_button(val: &[u8]) -> Button {
if val.len() > 0 {
match val[0] {
BLE_BUTTON_PRESS => {Button::Pressed}
BLE_BUTTON_RELEASE => {Button::Released}
_ => {
error!("Received invalid bluetooth data {:?}", val);
Button::Released
}
}
} else {
error!("Received zero-length bluetooth data when expecting a button press");
Button::Released
}
}
fn set_device_security(dev: &mut BLEDevice) {
dev.security()
// Enable all security protections (including bond, so that bond info is saved)
.set_auth(AuthReq::all())
// Options we support for putting in pairing info.
// "NoInputOutput" means that we will have "just works" pairing
.set_io_cap(SecurityIOCap::NoInputNoOutput)
//Handle IOS's bluetooth address randomization
.resolve_rpa();
}
fn set_server_callbacks(server: &mut BLEServer, sender: SendQ) {
server.on_connect(move |server, clntdesc| {
// Print connected client data
info!("client connected: {:?}", clntdesc);
// Update connection parameters
server
.update_conn_params(
clntdesc.conn_handle(),
BLE_MIN_INTERVAL,
BLE_MAX_INTERVAL,
BLE_LATENCY,
BLE_TIMEOUT,
).unwrap();
sender.send_blocking(Commands::PairTimerClear).unwrap();
// TODO: Cancel pairing mode timeout
});
server.on_disconnect(|_desc, _reason| {
info!("Disconnected, back to advertising");
});
}
fn button_to_ble_button(but: Button) -> [u8; 1] {
match but {
Button::Released => {[BLE_BUTTON_RELEASE]}
Button::Pressed => {[BLE_BUTTON_PRESS]}
}
}
//TODO set maximum pairs to remember?
//TODO after disconnect, it returns to scanning - will it return to directed scanning? Find out when directed is working.