Skip to content
Closed
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
3 changes: 2 additions & 1 deletion examples/basic_ble.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.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?;
Expand Down
108 changes: 82 additions & 26 deletions src/connections/ble_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}

Expand All @@ -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<BleId, Error> {
Expand All @@ -70,6 +71,12 @@ impl BleId {
}
}

impl From<BDAddr> 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 {
Expand All @@ -79,7 +86,6 @@ impl Display for BleId {
}
}

#[allow(dead_code)]
impl BleHandler {
pub async fn new(ble_id: &BleId, scan_duration: Duration) -> Result<Self, Error> {
let (radio, adapter) = Self::find_ble_radio(ble_id, scan_duration).await?;
Expand Down Expand Up @@ -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<Vec<(Peripheral, Adapter)>, 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<Vec<(Option<String>, 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!(
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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<RadioMessage, Error> {
self.radio
.read(&self.fromradio_char)
Expand All @@ -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<u32, Error> {
let data = self
.radio
Expand All @@ -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<BoxStream<'_, u32>, Error> {
self.radio
.subscribe(&self.fromnum_char)
Expand All @@ -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<BoxStream<'_, AdapterEvent>, Error> {
let stream = self
.adapter
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
42 changes: 40 additions & 2 deletions src/utils_internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Vec<(Option<String>, BDAddr)>, Error> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrewdavidmackenzie @matthewCmatt please test this new API.

  • name doesn't have to be always available, so we return Option<String>
  • you can do let ble_id = BleId::from(mac_addr); where mac_addr is BDaddr. And then use ble_id in build_ble_stream.

I decided to keep btleplug::BDaddr as part of the API but I've also considered using a different type. String is the next obvious choice but it would have to only have try_from() instead of from(), which is not so convenient . The other choice is implementing own MAC address type, which I find too tedious.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that it would be nicer to have a struct with two fields instead of (Option<String>, BDaddr) and I intend to do that. Please confirm first that this approach works for you.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good to me

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer the struct, and if possible that it implements PartialEq, Eq - so that I can have a Vec of them, and use contains() to determine if an instance is in the Vec.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this PR was auto-closed, I've created #72 with the BleDevice struct instead of the tuple.

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.
Expand All @@ -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
///
Expand All @@ -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;
/// ```
///
Expand Down
Loading