diff --git a/examples/basic_ble.rs b/examples/basic_ble.rs index 0d0b25e..9e9ac59 100644 --- a/examples/basic_ble.rs +++ b/examples/basic_ble.rs @@ -22,7 +22,8 @@ async fn main() -> Result<(), Box> { .expect("Failed to find next line") .expect("Could not read next line"); - // You can also use `BleId::from_mac_address(..)` instead of `BleId::from_name(..)`. + // You can also use `BleId::from_mac_address(..)` instead of `BleId::from_name(..)` to + // search for a MAC address. let ble_stream = utils::stream::build_ble_stream(&BleId::from_name(&entered_name), Duration::from_secs(5)) .await?; diff --git a/src/connections/ble_handler.rs b/src/connections/ble_handler.rs index 8b7c453..0b91d01 100644 --- a/src/connections/ble_handler.rs +++ b/src/connections/ble_handler.rs @@ -40,10 +40,11 @@ pub enum RadioMessage { } /// Bluetooth Low Energy ID, used to filter available devices. +#[derive(Debug, Clone)] pub enum BleId { - /// ID constructed from a name + /// A Meshtastic device identified by its broadcast name. Name(String), - /// ID represented from a MAC address + /// A Meshtastic device identified by its MAC address. MacAddress(BDAddr), } @@ -58,7 +59,7 @@ impl BleId { BleId::Name(name.to_owned()) } - /// Constructs a BLE ID from a MAC address. + /// Constructs a BLE ID from a string MAC address. /// /// Both `aa:bb:cc:dd:ee:ff` and `aabbccddeeff` formats are acceptable. pub fn from_mac_address(mac: &str) -> Result { @@ -70,6 +71,12 @@ impl BleId { } } +impl From for BleId { + fn from(mac: BDAddr) -> Self { + BleId::MacAddress(mac) + } +} + impl Display for BleId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -79,7 +86,6 @@ impl Display for BleId { } } -#[allow(dead_code)] impl BleHandler { pub async fn new(ble_id: &BleId, scan_duration: Duration) -> Result { let (radio, adapter) = Self::find_ble_radio(ble_id, scan_duration).await?; @@ -111,47 +117,79 @@ impl BleHandler { adapter.peripherals().await } - /// Finds a BLE radio matching a given name and running meshtastic. - /// It searches for the 'MSH_SERVICE' running on the device. + /// Scans for nearby Meshtastic devices and returns a list of peripherals that expose the + /// Meshtastic service. /// - /// It also returns the associated adapter that can reach this radio. - async fn find_ble_radio( - ble_id: &BleId, + /// This function searches for BLE devices that have the `MSH_SERVICE` UUID, which identifies + /// them as Meshtastic devices. For each device found, it returns a tuple containing the + /// `Peripheral` and the `Adapter` that can be used to connect to it. + async fn available_peripherals( scan_duration: Duration, - ) -> Result<(Peripheral, Adapter), Error> { - //TODO: support searching both by a name and by a MAC address + ) -> Result, Error> { let scan_error_fn = |e: btleplug::Error| Error::StreamBuildError { source: Box::new(e), description: "Failed to scan for BLE devices".to_owned(), }; let manager = Manager::new().await.map_err(scan_error_fn)?; let adapters = manager.adapters().await.map_err(scan_error_fn)?; - + let mut available_peripherals = Vec::new(); for adapter in &adapters { let peripherals = Self::scan_peripherals(adapter, scan_duration).await; match peripherals { Err(e) => { error!("Error while scanning for meshtastic peripherals: {e:?}"); - // We continue, as there can be another adapter that can work + // We continue, as there can be another adapter that works continue; } Ok(peripherals) => { for peripheral in peripherals { - if let Ok(Some(peripheral_properties)) = peripheral.properties().await { - let matches = match ble_id { - BleId::Name(name) => { - peripheral_properties.local_name.as_ref() == Some(name) - } - BleId::MacAddress(mac) => peripheral_properties.address == *mac, - }; - if matches { - return Ok((peripheral, adapter.clone())); - } - } + available_peripherals.push((peripheral, adapter.clone())); } } } } + + Ok(available_peripherals) + } + + /// Returns a list of all available Meshtastic BLE devices. + /// + /// This function scans for devices that expose the Meshtastic service UUID + /// (`6ba1b218-15a8-461f-9fa8-5dcae273eafd`) and returns a list of (name, MAC address) tuples + /// that can be used to connect to them. + pub async fn available_ble_devices( + scan_duration: Duration, + ) -> Result, BDAddr)>, Error> { + let peripherals = Self::available_peripherals(scan_duration).await?; + let mut devices = Vec::new(); + for (p, _) in &peripherals { + if let Ok(Some(properties)) = p.properties().await { + devices.push((properties.local_name, properties.address)); + } + } + Ok(devices) + } + + /// Finds a specific Meshtastic BLE radio matching the provided `BleId`. + /// + /// This function scans for available Meshtastic devices and attempts to find one that matches + /// the given `BleId`. If a matching device is found, it returns a tuple containing the + /// `Peripheral` and the `Adapter` required for connection. + async fn find_ble_radio( + ble_id: &BleId, + scan_duration: Duration, + ) -> Result<(Peripheral, Adapter), Error> { + for (peripheral, adapter) in Self::available_peripherals(scan_duration).await? { + if let Ok(Some(peripheral_properties)) = peripheral.properties().await { + let matches = match ble_id { + BleId::Name(name) => peripheral_properties.local_name.as_ref() == Some(name), + BleId::MacAddress(mac) => peripheral_properties.address == *mac, + }; + if matches { + return Ok((peripheral, adapter.clone())); + } + } + } Err(Error::StreamBuildError { source: Box::new(BleConnectionError()), description: format!( @@ -160,8 +198,8 @@ impl BleHandler { }) } - /// Finds the 3 meshtastic characteristics: toradio, fromnum and fromradio. It returns them in this - /// order. + /// Finds the 3 meshtastic characteristics: toradio, fromnum and fromradio. It returns them in + /// this order. async fn find_characteristics(radio: &Peripheral) -> Result<[Characteristic; 3], Error> { radio .discover_services() @@ -188,6 +226,9 @@ impl BleHandler { ]) } + /// Writes a data buffer to the radio, skipping the first 4 bytes. + /// + /// The first 4 bytes of the buffer are ignored because they are not used in BLE communication. pub async fn write_to_radio(&self, buffer: &[u8]) -> Result<(), Error> { self.radio // TODO: remove the skipping of the first 4 bytes @@ -206,6 +247,11 @@ impl BleHandler { }) } + /// Reads the next message from the radio. + /// + /// This function reads data from the `fromradio` characteristic and returns it as a + /// `RadioMessage`. A `RadioMessage` can be either a `Packet` containing the data or an `Eof` + /// marker to indicate the end of the stream. pub async fn read_from_radio(&self) -> Result { self.radio .read(&self.fromradio_char) @@ -229,6 +275,10 @@ impl BleHandler { Ok(u32::from_le_bytes(data)) } + /// Reads a `u32` value from the `fromnum` characteristic. + /// + /// This characteristic indicates the number of packets available to be read from the + /// `fromradio` characteristic. pub async fn read_fromnum(&self) -> Result { let data = self .radio @@ -241,6 +291,9 @@ impl BleHandler { Self::parse_u32(data) } + /// Returns an asynchronous stream of notifications from the `fromnum` characteristic. + /// + /// The stream contains `u32` values that indicate the number of packets available to be read. pub async fn notifications(&self) -> Result, Error> { self.radio .subscribe(&self.fromnum_char) @@ -263,6 +316,9 @@ impl BleHandler { ))) } + /// Returns a stream of `AdapterEvent`s. + /// + /// Currently, the only supported event is `Disconnected`. pub async fn adapter_events(&self) -> Result, Error> { let stream = self .adapter diff --git a/src/lib.rs b/src/lib.rs index ff6fa8c..96e87b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -141,6 +141,8 @@ pub mod utils { pub mod stream { #[cfg(feature = "bluetooth-le")] pub use crate::connections::ble_handler::BleId; + #[cfg(feature = "bluetooth-le")] + pub use crate::utils_internal::available_ble_devices; pub use crate::utils_internal::available_serial_ports; #[cfg(feature = "bluetooth-le")] pub use crate::utils_internal::build_ble_stream; diff --git a/src/utils_internal.rs b/src/utils_internal.rs index bf97633..581b076 100644 --- a/src/utils_internal.rs +++ b/src/utils_internal.rs @@ -2,6 +2,8 @@ use crate::connections::ble_handler::BleHandler; use crate::errors_internal::Error; #[cfg(feature = "bluetooth-le")] +use btleplug::api::BDAddr; +#[cfg(feature = "bluetooth-le")] use futures::stream::StreamExt; use std::time::Duration; use std::time::UNIX_EPOCH; @@ -201,6 +203,37 @@ pub async fn build_tcp_stream( Ok(StreamHandle::from_stream(stream)) } +/// A helper method to list the names of all reachable Meshtastic Bluetooth radios. +/// +/// This method is intended to be used to select a valid Bluetooth radio, then to pass that device +/// MAC address to the `build_ble_stream` method. +/// +/// # Arguments +/// +/// `scan_duration` - Duration of a Bluetooth LE scan for devices +/// +/// # Returns +/// +/// A vector of Bluetooth devices identified by a MAC address and optionally also a name. +/// +/// # Examples +/// +/// ``` +/// let ble_devices = utils::available_ble_devices().await.unwrap(); +/// println!("Available Meshtastic BLE devices: {:?}", ble_devices); +/// let stream = build_ble_stream(&ble_devices[0].1.into(), Duration::from_secs(10)).await; +/// ``` +/// +/// # Errors +/// +/// Fails if the Blueetooth scan fails. +#[cfg(feature = "bluetooth-le")] +pub async fn available_ble_devices( + scan_duration: Duration, +) -> Result, BDAddr)>, Error> { + BleHandler::available_ble_devices(scan_duration).await +} + /// A helper method that uses the `btleplug` and `tokio` crates to build a BLE stream /// that is compatible with the `StreamApi` API. This requires that the stream /// implements `AsyncReadExt + AsyncWriteExt` traits. @@ -210,7 +243,8 @@ pub async fn build_tcp_stream( /// /// # Arguments /// -/// * `ble_id` - Name or MAC address of a BLE device +/// * `ble_id` - Name or MAC address of a BLE device to connect to. +/// * `scan_duration` - The duration of the BLE scan. /// /// # Returns /// @@ -221,7 +255,11 @@ pub async fn build_tcp_stream( /// /// ``` /// // Connect to a radio, identified by its MAC address -/// let duplex_stream = utils::build_ble_stream(BleId::from_mac_address("E3:44:4E:18:F7:A4").await?; +/// let duplex_stream = utils::build_ble_stream( +/// &BleId::from_mac_address("E3:44:4E:18:F7:A4").unwrap(), +/// Duration::from_secs(5), +/// ) +/// .await?; /// let decoded_listener = stream_api.connect(duplex_stream).await; /// ``` ///