use embassy_time::{Duration,Timer}; use trouble_host::prelude::*; use esp_radio::ble::controller::BleConnector; use esp_radio::Controller as RadioController; use log::{warn,info}; use embassy_futures::join::join; use embassy_futures::select::select; use crypto_box::{ aead::Aead, SalsaBox, PublicKey, SecretKey, Nonce }; use alloc::vec::Vec; use alloc::string::String; use crate::ota::Flasher; /// Max number of connections const CONNECTIONS_MAX: usize = 1; /// Max number of L2CAP channels. // XXX why not 0? const L2CAP_CHANNELS_MAX: usize = 2; // Signal + att // GATT Server definition #[gatt_server] struct Server { wifi_provisioning_service: WifiProvisioningService, } #[gatt_service(uuid = "76be25c2-0000-44fc-a97c-bcb56c69c926")] struct WifiProvisioningService { #[characteristic(uuid = "76be25c2-0801-44fc-a97c-bcb56c69c926", read, notify, value = false)] connecting: bool, #[characteristic(uuid = "76be25c2-0802-44fc-a97c-bcb56c69c926", read, notify, value = 0)] max_network_index: u8, #[characteristic(uuid = "76be25c2-0803-44fc-a97c-bcb56c69c926", read, write, value = 0)] current_network_index: u8, // for arrays > 32 we must supply a default value : see // https://github.com/embassy-rs/trouble/issues/320 #[characteristic(uuid = "76be25c2-0804-44fc-a97c-bcb56c69c926", value = [0; 40], read, notify)] current_network: [u8; 40], #[characteristic(uuid = "76be25c2-0805-44fc-a97c-bcb56c69c926", value = [0; (24 + 16 + 64 + 1)], write)] secret: [u8; 24 + 16 + 64 + 1] } pub async fn run_ble(radio_init: &RadioController<'static>, peripheral: esp_hal::peripherals::BT<'_>, networks: Vec<[u8; 40]>, mut flasher: Flasher<'_>) { let connector = BleConnector::new(&radio_init, peripheral, Default::default()).expect("connector"); let controller: ExternalController<_, 20> = ExternalController::new(connector); // Using a fixed "random" address can be useful for testing. In real scenarios, one would // use e.g. the MAC 6 byte array as the address (how to get that varies by the platform). let address: Address = Address::random([0xff, 0x8f, 0x1a, 0x05, 0xe4, 0xff]); info!("Our address = {:?}", address); let mut resources: HostResources = HostResources::new(); let stack = trouble_host::new(controller, &mut resources).set_random_address(address); let Host { mut peripheral, runner, .. } = stack.build(); info!("Starting advertising and GATT service"); let server = Server::new_with_config(GapConfig::Peripheral(PeripheralConfig { name: "eculocate", appearance: &appearance::power_device::GENERIC_POWER_DEVICE, })) .unwrap(); let _ = join( ble_task(runner), async { loop { match advertise("Eculocate", &mut peripheral, &server).await { Ok(conn) => { // set up tasks when the connection is established to a central, so they don't run when no one is connected. let a = gatt_events_task(&server, &conn, &networks, &mut flasher); let b = custom_task(&server, &conn, &stack); // run until any task ends (usually because the connection has been closed), // then return to advertising state. select(a, b).await; } Err(e) => { panic!("[adv] error: {:?}", e); } } } }) .await; } /// This is a background task that is required to run forever alongside any other BLE tasks. /// async fn ble_task(mut runner: Runner<'_, C, P>) { loop { if let Err(e) = runner.run().await { panic!("[ble_task] error: {:?}", e); } } } const CLIENT_PUBLIC_KEY_BYTES: &[u8; 32] = include_bytes!("../../keys/client.pub"); const SECRET_KEY_BYTES : &[u8; 32] = include_bytes!("../../keys/device.key"); #[derive(Debug)] enum DecryptError { Crypto(crypto_box::aead::Error), Encoding(alloc::str::Utf8Error) } impl From for DecryptError { fn from(e: crypto_box::aead::Error) -> Self { Self::Crypto(e) } } impl From for DecryptError { fn from(e: alloc::str::Utf8Error) -> Self { Self::Encoding(e) } } fn decrypt_secret(ciphertext : &[u8]) -> Result { let client_public_key = PublicKey::from(*CLIENT_PUBLIC_KEY_BYTES); let secret_key= SecretKey::from(*SECRET_KEY_BYTES); let len : usize = ciphertext[0].into(); let ciphertext = &ciphertext[1..len+1]; let (nonce, encrypted) = ciphertext.split_at(24); let nonce = Nonce::from_slice(nonce); let sbox = SalsaBox::new(&client_public_key, &secret_key); let plain_bytes = sbox.decrypt(nonce, encrypted)?; Ok(String::from(core::str::from_utf8(&plain_bytes[..])?)) } /// Stream Events until the connection closes. /// /// This function will handle the GATT events and process them. /// This is how we interact with read and write requests. async fn gatt_events_task(server: &Server<'_>, conn: &GattConnection<'_, '_, P>, networks: &Vec<[u8; 40]>, flasher: &mut Flasher<'_>) -> Result<(), Error> { let wps = &server.wifi_provisioning_service; let max_index : u8 = (networks.len() & 0xff).try_into().unwrap(); server.set(&wps.max_network_index, &max_index); let current_index = wps.current_network_index; let reason = loop { match conn.next().await { GattConnectionEvent::Disconnected { reason } => break reason, GattConnectionEvent::Gatt { event } => { match &event { GattEvent::Write(event) => { info!("[gatt] write handle {:?} {:?}", event.handle(), event.data()); if event.handle() == current_index.handle { let i = (event.data()[0]) as usize; info!("[gatt] switch current index: {:?}", i); if i == 0 { server.set(&wps.current_network, &[0u8;40]); } else if i <= networks.len() { server.set(&wps.current_network, &(networks[i - 1])); } else { warn!("index {:?} out of range", i); } } else if event.handle() == wps.connecting.handle { let network = server.get(&wps.current_network).unwrap(); let password = decrypt_secret(&server.get(&wps.secret).unwrap()); match password { Ok(plaintext) => { write_wifi_config( flasher, &network[8..], &plaintext[..] ).await; }, Err(e) => { warn!("decdrpt error: {:?}", e); } } } }, _ => {} }; // This step is also performed at drop(), but writing // it explicitly is necessary in order to ensure reply // is sent. match event.accept() { Ok(reply) => reply.send().await, Err(e) => warn!("[gatt] error sending response: {:?}", e), }; } _ => {} // ignore other Gatt Connection Events } }; info!("[gatt] disconnected: {:?}", reason); Ok(()) } async fn write_wifi_config(flasher: &mut Flasher<'_>, ssid: &[u8], password: &str) -> Result<(),esp_nvs::error::Error> { let n_s = core::str::from_utf8(&ssid).unwrap(); let n0 = if let Some(i) = n_s.find("\0") { i } else { n_s.len() }; flasher.set_var("ssid", Some(&n_s[..n0]))?; flasher.set_var("secret", Some(password))?; info!("[gatt] deep breath then reboot"); Timer::after(Duration::from_millis(2000)).await; esp_hal::system::software_reset(); } /// Create an advertiser to use to connect to a BLE Central, and wait for it to connect. async fn advertise<'values, 'server, C: Controller>( name: &'values str, peripheral: &mut Peripheral<'values, C, DefaultPacketPool>, server: &'server Server<'values>, ) -> Result, BleHostError> { let mut advertiser_data = [0; 31]; let len = AdStructure::encode_slice( &[ AdStructure::Flags(LE_GENERAL_DISCOVERABLE | BR_EDR_NOT_SUPPORTED), AdStructure::ServiceUuids16(&[[0x0f, 0x18]]), AdStructure::CompleteLocalName(name.as_bytes()), ], &mut advertiser_data[..], )?; let advertiser = peripheral .advertise( &Default::default(), Advertisement::ConnectableScannableUndirected { adv_data: &advertiser_data[..len], scan_data: &[], }, ) .await?; info!("[adv] advertising"); let conn = advertiser.accept().await?.with_attribute_server(server)?; info!("[adv] connection established"); Ok(conn) } async fn custom_task( _server: &Server<'_>, conn: &GattConnection<'_, '_, P>, stack: &Stack<'_, C, P>, ) { loop { // read RSSI (Received Signal Strength Indicator) of the connection. if let Ok(rssi) = conn.raw().rssi(stack).await { info!("[custom_task] RSSI: {:?}", rssi); } else { info!("[custom_task] error getting RSSI"); break; }; Timer::after_secs(2).await; } }