diff --git a/gem-remotes-esp32/Cargo.toml b/gem-remotes-esp32/Cargo.toml index 4f354be..60492c8 100644 --- a/gem-remotes-esp32/Cargo.toml +++ b/gem-remotes-esp32/Cargo.toml @@ -41,7 +41,7 @@ async-executor = "1.13.0" async-channel = "2.3.1" strum = "0.26.3" strum_macros = "0.26.4" -async-io = "2.3.4" +closure = "0.3.0" [build-dependencies] embuild = "0.32.0" diff --git a/gem-remotes-esp32/src/ble_server.rs b/gem-remotes-esp32/src/ble_server.rs index 30f7ee9..bf61064 100644 --- a/gem-remotes-esp32/src/ble_server.rs +++ b/gem-remotes-esp32/src/ble_server.rs @@ -1,81 +1,196 @@ +use log::*; //{trace, debug, info, warn, error} +use esp32_nimble::{enums::*, uuid128, BLEAdvertisementData, BLEAdvertising, BLEDevice, BLEServer, NimbleProperties, OnWriteArgs}; +use esp32_nimble::utilities::{mutex::Mutex, BleUuid}; +use anyhow::Result; +use closure::closure; + +use crate::dispatch::{Dispatch, RecvQ, SendQ}; +use crate::commands::Commands; + +// TODO: 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 + +const DEVICE_NAME: &str = "Gem Remotes"; + +const UUID_SERVICE_PAIR: BleUuid = uuid128!("9966ad5a-f13c-4b61-ba66-0861e08d09b4"); +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"); + +pub struct BleServer { + send_q: SendQ, + recv_q: RecvQ, +} + +impl BleServer { + pub fn new(dp: &mut Dispatch) -> Self { + let cmds = vec![ + Commands::BluetoothUp { data: 0 }, + Commands::BluetoothDown { data: 0 }, + Commands::BluetoothStop { data: 0 }, + ]; + let r = dp.get_callback_channel(&cmds); + let s = dp.get_cmd_channel(); + BleServer { + send_q: s.clone(), + recv_q: r.clone(), + } + } + + pub async fn run(&mut self) -> Result<()> { + match self.do_run().await { + Ok(_) => {} + Err(e) => {error!("Bluetooth task encountered error {}", e);} + } + Ok(()) //TODO this is not ok; reboot the chip! + //TODO: we need a structure like this at each spawn point;; apparently functions don't pass errors up tasks. + } + + 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); + let _pairing_service = server.create_service(UUID_SERVICE_PAIR); + 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(); + //let mut cmd = Commands::BluetoothUp { data: 0 }; + 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_single_byte(&sender, args, Commands::BluetoothUp {data: 0}) + })); + + 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_single_byte(&sender, args, Commands::BluetoothDown {data: 0}) + })); + + 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_single_byte(&sender, args, Commands::BluetoothStop {data: 0}) + })); + -// Example for reference; not ready to use at all. + let ble_advertiser = ble_device.get_advertising(); + + // TODO: we will need to enable / disable the ability to pair! + advertise_pairing(ble_advertiser)?; + + loop { + debug!("Waiting for updates"); + let cmd = self.recv_q.recv().await?; + trace!("Received update to bluetooth variable {:?}", cmd); + match cmd { + Commands::BluetoothUp { data } => { + //TODO: this sends a notify even if the command initially came from phone. Is this correct? + trace!("Updating BluetoothUp with {:?}", data); + button_up.lock().set_value(&[data]).notify(); + } + Commands::BluetoothDown { data } => { + trace!("Updating BluetoothDown with {:?}", data); + button_down.lock().set_value(&[data]).notify(); + } + Commands::BluetoothStop { data} => { + trace!("Updating BluetoothStop with {:?}", data); + button_stop.lock().set_value(&[data]).notify(); -use esp32_nimble::{enums::*, uuid128, BLEAdvertisementData, BLEDevice, NimbleProperties}; -use esp_idf_svc::timer::EspTaskTimerService; -use core::time::Duration; + } + _ => { + error!("Invalid command received by bluetooth handler {:?}", cmd); + } + } + } + } -fn run_ble_server() { - // Take ownership of device - let ble_device = BLEDevice::take(); +} - // Obtain handle for peripheral advertiser - let ble_advertiser = ble_device.get_advertising(); +fn on_single_byte(sender: &SendQ, args: &mut OnWriteArgs, cmd: Commands) { + let v = args.recv_data(); + //TODO: add "update" versions of these commands, instead of the "got changes from bluetooth" versions, and handle those instead. + match cmd { + Commands::BluetoothUp { data: _ } => { + if v.len() > 0 { + sender.send_blocking(Commands::BluetoothUp {data: v[0]} ).ok(); + } else {error!("Received zero-byte bluetooth characteristic update {:?}", cmd)} + } + Commands::BluetoothDown { data: _ } => { + if v.len() > 0 { + sender.send_blocking(Commands::BluetoothDown {data: v[0]} ).ok(); + } else {error!("Received zero-byte bluetooth characteristic update {:?}", cmd)} + } + Commands::BluetoothStop { data: _ } => { + if v.len() > 0 { + sender.send_blocking(Commands::BluetoothStop {data: v[0]} ).ok(); + } else {error!("Received zero-byte bluetooth characteristic update {:?}", cmd)} + } + _ => {error!("Tried to handle an unknown bluetooth command: {:?}",cmd);} + } +} - // Configure Device Security - ble_device - .security() +fn set_device_security(dev: &mut BLEDevice) { + dev.security() + // Enable all security protections .set_auth(AuthReq::all()) - .set_passkey(123456) - .set_io_cap(SecurityIOCap::DisplayOnly) + // 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(); +} - // Obtain handle for server - let server = ble_device.get_server(); - - // Define server connect behaviour +fn set_server_callbacks(server: &mut BLEServer) { server.on_connect(|server, clntdesc| { // Print connected client data - println!("{:?}", clntdesc); + info!("client connected: {:?}", clntdesc); // Update connection parameters server - .update_conn_params(clntdesc.conn_handle(), 24, 48, 0, 60) - .unwrap(); + .update_conn_params( + clntdesc.conn_handle(), + BLE_MIN_INTERVAL, + BLE_MAX_INTERVAL, + BLE_LATENCY, + BLE_TIMEOUT, + ).unwrap(); }); - - // Define server disconnect behaviour server.on_disconnect(|_desc, _reason| { - println!("Disconnected, back to advertising"); + info!("Disconnected, back to advertising"); }); +} - // Create a service with custom UUID - let my_service = server.create_service(uuid128!("9b574847-f706-436c-bed7-fc01eb0965c1")); - - // Create a characteristic to associate with created service - let my_service_characteristic = my_service.lock().create_characteristic( - uuid128!("681285a6-247f-48c6-80ad-68c3dce18585"), - NimbleProperties::READ | NimbleProperties::READ_ENC, - ); - - // Modify characteristic value - my_service_characteristic.lock().set_value(b"Start Value"); - - // Configure Advertiser Data - ble_advertiser +fn advertise_pairing(advertiser: &Mutex) -> Result<()> { + trace!("Setting up advertiser"); + advertiser .lock() .set_data( BLEAdvertisementData::new() - .name("ESP32 Server") - .add_service_uuid(uuid128!("9b574847-f706-436c-bed7-fc01eb0965c1")), - ) - .unwrap(); - - // Start Advertising - ble_advertiser.lock().start().unwrap(); - - // (Optional) Print dump of local GATT table - // server.ble_gatts_show_local(); - - // Init a value to pass to characteristic - let mut val = 0; - let timer_service = EspTaskTimerService::new()?; - - let mut async_timer = timer_service.timer_async()?; - - loop { - async_timer.after(Duration::from_secs(1)).await?; - my_service_characteristic.lock().set_value(&[val]).notify(); - val = val.wrapping_add(1); - } + .name(DEVICE_NAME) + .add_service_uuid(UUID_SERVICE_PAIR) + )?; + // TODO: this appears to run in its own thread; verify. + // TODO: isn't there a restart? We'll need to switch between pairing and not. + info!("Staring Bluetooth Server"); + advertiser.lock().start()?; + Ok(()) } diff --git a/gem-remotes-esp32/src/commands.rs b/gem-remotes-esp32/src/commands.rs index ce4b1c8..42fbe80 100644 --- a/gem-remotes-esp32/src/commands.rs +++ b/gem-remotes-esp32/src/commands.rs @@ -14,7 +14,7 @@ pub enum Commands { // Inputs from bluetooth BluetoothUp {data: u8}, BluetoothDown {data: u8}, - BluetoothStop {_data: u8}, // There is no state where releasing the stop button induces a change. + BluetoothStop {data: u8}, // There is no state where releasing the stop button induces a change. // Internal messages StopTimerExpired, // Sent when the 2 second stop sequence is complete diff --git a/gem-remotes-esp32/src/main.rs b/gem-remotes-esp32/src/main.rs index 2b8a598..735277e 100644 --- a/gem-remotes-esp32/src/main.rs +++ b/gem-remotes-esp32/src/main.rs @@ -22,6 +22,7 @@ mod dispatch; mod motor_controller; mod motor_driver; mod message_timer; +mod ble_server; use crate::message_timer::MessageTimer; use crate::commands::Commands; @@ -33,7 +34,8 @@ fn main() { // Do basic initialization esp_idf_svc::sys::link_patches(); esp_idf_svc::log::EspLogger::initialize_default(); - log::set_max_level(log::LevelFilter::Info); + log::set_max_level(log::LevelFilter::Trace); + //log::set_max_level(log::LevelFilter::Info); match future::block_on(main_loop()) { Ok(_) => {error!("Exited main loop normally, but this should be impossible.")} @@ -78,6 +80,7 @@ async fn main_loop() -> Result<()> { Commands::StopTimerExpired, STOP_SAFETY_TIME_MS, &mut dp); + let mut ble_server = ble_server::BleServer::new(&mut dp); let executor = Executor::new(); let mut tasks:Vec<_> = Vec::new(); @@ -93,6 +96,7 @@ async fn main_loop() -> Result<()> { tasks.push(executor.spawn(motor_control.run())); tasks.push(executor.spawn(button_timer.run())); tasks.push(executor.spawn(stopping_timer.run())); + tasks.push(executor.spawn(ble_server.run())); tasks.push(executor.spawn(dp.cmd_loop())); //Once we have all our tasks, await on them all to run them in parallel. diff --git a/gem-remotes-esp32/src/motor_controller.rs b/gem-remotes-esp32/src/motor_controller.rs index 0646a71..5b42643 100644 --- a/gem-remotes-esp32/src/motor_controller.rs +++ b/gem-remotes-esp32/src/motor_controller.rs @@ -44,7 +44,7 @@ impl Controller { Commands::PicRecvStop, Commands::BluetoothUp{data: 0}, Commands::BluetoothDown{data: 0}, - Commands::BluetoothStop{_data: 0}, + Commands::BluetoothStop{data: 0}, Commands::StopTimerExpired, Commands::ButtonTimerExpired, ]; @@ -83,6 +83,7 @@ impl Controller { async fn exit_state(&mut self, old_s: &ControllerStates) -> Result <()> { match old_s { + //TODO: We need to notify the BLE controller! ControllerStates::Stopped => {} ControllerStates::Stopping => { self.send.send(Commands::StopTimerClear).await?; diff --git a/gem-remotes-esp32/src/test_console.rs b/gem-remotes-esp32/src/test_console.rs index f77e0f7..6d11340 100644 --- a/gem-remotes-esp32/src/test_console.rs +++ b/gem-remotes-esp32/src/test_console.rs @@ -111,7 +111,7 @@ pub fn process_menu( Menu::BluetoothStop { data } => { cli.writer() .write_str("SendingBluetoothStop")?; - let _ = dispatch.send_blocking(Commands::BluetoothStop { _data: data }); + let _ = dispatch.send_blocking(Commands::BluetoothStop { data: data }); } Menu::BluetoothLearn { data } => { cli.writer()