Skip to content

Commit 3cee238

Browse files
committed
Introduce the BleHandler struct
It handles the meshtastic BLE protocol, including the 3 characteristics used to read from and write to the radio.
1 parent e77d168 commit 3cee238

File tree

3 files changed

+186
-0
lines changed

3 files changed

+186
-0
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@ serde_json = { version = "1.0", optional = true }
4848
thiserror = "1.0.48"
4949
uuid = "1.6.1"
5050
btleplug = "0.11.5"
51+
futures = "0.3.30"

src/connections/ble_handler.rs

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
use btleplug::api::{
2+
Central, Characteristic, Manager as _, Peripheral as _, ScanFilter, ValueNotification,
3+
WriteType,
4+
};
5+
use btleplug::platform::{Adapter, Manager, Peripheral};
6+
use futures_util::Stream;
7+
use log::error;
8+
use std::pin::Pin;
9+
use uuid::Uuid;
10+
11+
use crate::errors_internal::{BleConnectionError, Error, InternalStreamError};
12+
13+
const MSH_SERVICE: Uuid = Uuid::from_u128(0x6ba1b218_15a8_461f_9fa8_5dcae273eafd);
14+
const FROMRADIO: Uuid = Uuid::from_u128(0x2c55e69e_4993_11ed_b878_0242ac120002);
15+
const TORADIO: Uuid = Uuid::from_u128(0xf75c76d2_129e_4dad_a1dd_7866124401e7);
16+
const FROMNUM: Uuid = Uuid::from_u128(0xed9da18c_a800_4f66_a670_aa7547e34453);
17+
18+
pub struct BleHandler {
19+
radio: Peripheral,
20+
toradio_char: Characteristic,
21+
fromradio_char: Characteristic,
22+
fromnum_char: Characteristic,
23+
}
24+
25+
#[allow(dead_code)]
26+
impl BleHandler {
27+
pub async fn new(name: String) -> Result<Self, Error> {
28+
let radio = Self::find_ble_radio(&name).await?;
29+
radio.connect().await.map_err(|e| Error::StreamBuildError {
30+
source: Box::new(e),
31+
description: format!("Failed to connect to the device {name}"),
32+
})?;
33+
let [toradio_char, fromnum_char, fromradio_char] =
34+
Self::find_characteristics(&radio).await?;
35+
Ok(BleHandler {
36+
radio,
37+
toradio_char,
38+
fromradio_char,
39+
fromnum_char,
40+
})
41+
}
42+
43+
async fn scan_peripherals(adapter: &Adapter) -> Result<Vec<Peripheral>, btleplug::Error> {
44+
adapter
45+
.start_scan(ScanFilter {
46+
services: vec![MSH_SERVICE],
47+
})
48+
.await?;
49+
adapter.peripherals().await
50+
}
51+
52+
/// Finds a BLE radio matching a given name and running meshtastic.
53+
/// It searches for the 'MSH_SERVICE' running on the device.
54+
async fn find_ble_radio(name: &str) -> Result<Peripheral, Error> {
55+
//TODO: support searching both by a name and by a MAC address
56+
let scan_error_fn = |e: btleplug::Error| Error::StreamBuildError {
57+
source: Box::new(e),
58+
description: "Failed to scan for BLE devices".to_owned(),
59+
};
60+
let manager = Manager::new().await.map_err(scan_error_fn)?;
61+
let adapters = manager.adapters().await.map_err(scan_error_fn)?;
62+
63+
for adapter in &adapters {
64+
let peripherals = Self::scan_peripherals(&adapter).await;
65+
match peripherals {
66+
Err(e) => {
67+
error!("Error while scanning for meshtastic peripherals: {e:?}");
68+
// We continue, as there can be another adapter that can work
69+
continue;
70+
}
71+
Ok(peripherals) => {
72+
let needle = Some(name.to_owned());
73+
for peripheral in peripherals {
74+
if let Ok(Some(peripheral_properties)) = peripheral.properties().await {
75+
if peripheral_properties.local_name == needle {
76+
return Ok(peripheral);
77+
}
78+
}
79+
}
80+
}
81+
}
82+
}
83+
Err(Error::StreamBuildError {
84+
source: Box::new(BleConnectionError()),
85+
description: format!(
86+
"Failed to find {name}, or meshtastic is not running on the device"
87+
) + ", or it's already connected.",
88+
})
89+
}
90+
91+
/// Finds the 3 meshtastic characteristics: toradio, fromnum and fromradio. It returns them in this
92+
/// order.
93+
async fn find_characteristics(radio: &Peripheral) -> Result<[Characteristic; 3], Error> {
94+
radio
95+
.discover_services()
96+
.await
97+
.map_err(|e| Error::StreamBuildError {
98+
source: Box::new(e),
99+
description: "Failed to discover services".to_owned(),
100+
})?;
101+
let characteristics = radio.characteristics();
102+
let find_characteristic = |uuid| {
103+
characteristics
104+
.iter()
105+
.find(|c| c.uuid == uuid)
106+
.ok_or(Error::StreamBuildError {
107+
source: Box::new(BleConnectionError()), // TODO
108+
description: format!("Failed to find characteristic {uuid}"),
109+
})
110+
};
111+
112+
Ok([
113+
find_characteristic(TORADIO)?.clone(),
114+
find_characteristic(FROMNUM)?.clone(),
115+
find_characteristic(FROMRADIO)?.clone(),
116+
])
117+
}
118+
119+
pub async fn write_to_radio(&self, buffer: &[u8]) -> Result<(), Error> {
120+
self.radio
121+
// TODO: remove the skipping of the first 4 bytes
122+
.write(&self.toradio_char, &buffer[4..], WriteType::WithResponse)
123+
.await
124+
.map_err(|e: btleplug::Error| {
125+
Error::InternalStreamError(InternalStreamError::StreamWriteError {
126+
source: Box::new(e),
127+
})
128+
})
129+
}
130+
131+
fn ble_read_error_fn(e: btleplug::Error) -> Error {
132+
Error::InternalStreamError(InternalStreamError::StreamReadError {
133+
source: Box::new(e),
134+
})
135+
}
136+
137+
pub async fn read_from_radio(&self) -> Result<Vec<u8>, Error> {
138+
self.radio
139+
.read(&self.fromradio_char)
140+
.await
141+
.map_err(Self::ble_read_error_fn)
142+
}
143+
144+
fn parse_u32(data: Vec<u8>) -> Result<u32, Error> {
145+
let parsed_value = u32::from_le_bytes(data.as_slice().try_into().map_err(|e| {
146+
Error::InternalStreamError(InternalStreamError::StreamReadError {
147+
source: Box::new(e),
148+
})
149+
})?);
150+
Ok(parsed_value)
151+
}
152+
153+
pub async fn read_fromnum(&self) -> Result<u32, Error> {
154+
let data = self
155+
.radio
156+
.read(&self.fromnum_char)
157+
.await
158+
.map_err(Self::ble_read_error_fn)?;
159+
Self::parse_u32(data)
160+
}
161+
162+
pub async fn notifications(
163+
&self,
164+
) -> Result<Pin<Box<dyn Stream<Item = ValueNotification> + Send>>, Error> {
165+
self.radio
166+
.subscribe(&self.fromnum_char)
167+
.await
168+
.map_err(Self::ble_read_error_fn)?;
169+
self.radio
170+
.notifications()
171+
.await
172+
.map_err(Self::ble_read_error_fn)
173+
}
174+
175+
pub async fn filter_map(notification: ValueNotification) -> Option<u32> {
176+
match notification {
177+
ValueNotification {
178+
uuid: FROMNUM,
179+
value,
180+
} => Some(Self::parse_u32(value).unwrap()),
181+
_ => None,
182+
}
183+
}
184+
}

src/connections/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::protobufs;
44

55
use self::wrappers::NodeId;
66

7+
pub mod ble_handler;
78
pub mod handlers;
89
pub mod stream_api;
910
pub mod stream_buffer;

0 commit comments

Comments
 (0)