#[cfg(feature = "ble")]
mod inner {
use std::sync::{Arc, Mutex};
use std::time::Duration;
use btleplug::api::{
Central, CentralEvent, Characteristic, Manager as _, Peripheral as _, ScanFilter,
WriteType,
};
use btleplug::platform::{Adapter, Manager, Peripheral};
use tokio::sync::mpsc;
use uuid::Uuid;
use crate::error::UnicornError;
use crate::protocol::{self, UnicornPayload, PAYLOAD_LENGTH};
pub const SERVICE_UUID: Uuid = Uuid::from_fields(
0x39a76676, 0x2788, 0x46c9,
&[0xaf, 0xa0, 0xf0, 0xc0, 0xc3, 0x1e, 0x6f, 0xd9],
);
pub const CHAR_DATA_NOTIFY: Uuid = Uuid::from_fields(
0xB5211405, 0x449F, 0x4BA0,
&[0xA6, 0x51, 0xB8, 0x87, 0x92, 0x4B, 0x81, 0xE8],
);
pub const CHAR_CONTROL_WRITE: Uuid = Uuid::from_fields(
0xB5211406, 0x449F, 0x4BA0,
&[0xA6, 0x51, 0xB8, 0x87, 0x92, 0x4B, 0x81, 0xE8],
);
pub const CHAR_PAYLOAD_CONFIG: Uuid = Uuid::from_fields(
0xB5211408, 0x449F, 0x4BA0,
&[0xA6, 0x51, 0xB8, 0x87, 0x92, 0x4B, 0x81, 0xE8],
);
pub const CHAR_CHANNEL_CONFIG: Uuid = Uuid::from_fields(
0xB5211409, 0x449F, 0x4BA0,
&[0xA6, 0x51, 0xB8, 0x87, 0x92, 0x4B, 0x81, 0xE8],
);
pub const CHAR_FILTER_CONFIG: Uuid = Uuid::from_fields(
0xB521140A, 0x449F, 0x4BA0,
&[0xA6, 0x51, 0xB8, 0x87, 0x92, 0x4B, 0x81, 0xE8],
);
pub const CHAR_BATTERY_LEVEL: Uuid = Uuid::from_u16(0x2A19);
#[derive(Debug, Clone)]
pub struct DiscoveredDevice {
pub name: String,
pub address: String,
pub rssi: Option<i16>,
peripheral: Peripheral,
}
#[derive(Debug, Clone)]
pub struct BleDeviceInfo {
pub model: String,
pub serial: String,
pub firmware: String,
pub hardware: String,
pub manufacturer: String,
}
pub struct BleDevice {
peripheral: Peripheral,
data_char: Option<Characteristic>,
control_char: Option<Characteristic>,
}
impl BleDevice {
pub async fn scan(timeout: Duration) -> Result<Vec<DiscoveredDevice>, UnicornError> {
let manager = Manager::new().await.map_err(|e| {
UnicornError::LibraryNotAvailable {
reason: format!("BLE manager init failed: {}", e),
}
})?;
let adapters = manager.adapters().await.map_err(|e| {
UnicornError::LibraryNotAvailable {
reason: format!("No BLE adapter: {}", e),
}
})?;
let adapter = adapters.into_iter().next().ok_or(UnicornError::LibraryNotAvailable {
reason: "No BLE adapter found".into(),
})?;
adapter
.start_scan(ScanFilter {
services: vec![SERVICE_UUID],
})
.await
.map_err(|e| UnicornError::SdkError {
code: -1,
message: format!("Scan failed: {}", e),
})?;
tokio::time::sleep(timeout).await;
adapter.stop_scan().await.ok();
let peripherals = adapter.peripherals().await.map_err(|e| {
UnicornError::SdkError {
code: -1,
message: format!("Failed to list peripherals: {}", e),
}
})?;
let mut devices = Vec::new();
for p in peripherals {
if let Some(props) = p.properties().await.ok().flatten() {
if props.services.contains(&SERVICE_UUID) {
devices.push(DiscoveredDevice {
name: props.local_name.unwrap_or_else(|| "Unknown".into()),
address: props.address.to_string(),
rssi: props.rssi,
peripheral: p,
});
}
}
}
Ok(devices)
}
pub async fn connect(discovered: &DiscoveredDevice) -> Result<Self, UnicornError> {
let p = &discovered.peripheral;
p.connect().await.map_err(|e| UnicornError::SdkError {
code: -1,
message: format!("Connect failed: {}", e),
})?;
p.discover_services().await.map_err(|e| UnicornError::SdkError {
code: -1,
message: format!("Service discovery failed: {}", e),
})?;
let chars = p.characteristics();
let data_char = chars.iter().find(|c| c.uuid == CHAR_DATA_NOTIFY).cloned();
let control_char = chars.iter().find(|c| c.uuid == CHAR_CONTROL_WRITE).cloned();
Ok(BleDevice {
peripheral: discovered.peripheral.clone(),
data_char,
control_char,
})
}
pub async fn device_info(&self) -> Result<BleDeviceInfo, UnicornError> {
let chars = self.peripheral.characteristics();
let read_char = |uuid: Uuid| -> String {
chars
.iter()
.find(|c| c.uuid == uuid)
.map(|_| "".to_string())
.unwrap_or_default()
};
let model = self.read_characteristic_string(Uuid::from_u16(0x2A24)).await;
let serial = self.read_characteristic_string(Uuid::from_u16(0x2A25)).await;
let firmware = self.read_characteristic_string(Uuid::from_u16(0x2A26)).await;
let hardware = self.read_characteristic_string(Uuid::from_u16(0x2A27)).await;
let manufacturer = self.read_characteristic_string(Uuid::from_u16(0x2A29)).await;
Ok(BleDeviceInfo { model, serial, firmware, hardware, manufacturer })
}
async fn read_characteristic_string(&self, uuid: Uuid) -> String {
let chars = self.peripheral.characteristics();
if let Some(c) = chars.iter().find(|c| c.uuid == uuid) {
if let Ok(data) = self.peripheral.read(c).await {
return String::from_utf8_lossy(&data).trim_end_matches('\0').to_string();
}
}
String::new()
}
pub async fn battery_level(&self) -> Result<u8, UnicornError> {
let chars = self.peripheral.characteristics();
let c = chars.iter().find(|c| c.uuid == CHAR_BATTERY_LEVEL).ok_or(
UnicornError::NotSupported("Battery characteristic not found".into()),
)?;
let data = self.peripheral.read(c).await.map_err(|e| UnicornError::SdkError {
code: -1,
message: format!("Read battery failed: {}", e),
})?;
Ok(*data.first().unwrap_or(&0))
}
pub async fn start_streaming<F>(
&self,
mut callback: F,
) -> Result<(), UnicornError>
where
F: FnMut(UnicornPayload) + Send + 'static,
{
let data_char = self.data_char.as_ref().ok_or(
UnicornError::NotSupported("Data characteristic not found".into()),
)?;
self.peripheral.subscribe(data_char).await.map_err(|e| {
UnicornError::SdkError {
code: -1,
message: format!("Subscribe failed: {}", e),
}
})?;
if let Some(ref ctrl) = self.control_char {
self.peripheral
.write(ctrl, &protocol::CMD_START_ACQUISITION, WriteType::WithResponse)
.await
.map_err(|e| UnicornError::SdkError {
code: -1,
message: format!("Start command failed: {}", e),
})?;
}
let mut notifications = self.peripheral.notifications().await.map_err(|e| {
UnicornError::SdkError {
code: -1,
message: format!("Notification stream failed: {}", e),
}
})?;
tokio::spawn(async move {
use tokio_stream::StreamExt;
while let Some(notification) = notifications.next().await {
if notification.uuid == CHAR_DATA_NOTIFY
&& notification.value.len() == PAYLOAD_LENGTH
{
let mut buf = [0u8; PAYLOAD_LENGTH];
buf.copy_from_slice(¬ification.value);
if let Some(payload) = protocol::decode_payload(&buf) {
callback(payload);
}
}
}
});
Ok(())
}
pub async fn stop_streaming(&self) -> Result<(), UnicornError> {
if let Some(ref ctrl) = self.control_char {
self.peripheral
.write(ctrl, &protocol::CMD_STOP_ACQUISITION, WriteType::WithResponse)
.await
.ok(); }
if let Some(ref data) = self.data_char {
self.peripheral.unsubscribe(data).await.ok();
}
Ok(())
}
pub async fn channel_config(&self) -> Result<Vec<u8>, UnicornError> {
self.read_config_char(CHAR_CHANNEL_CONFIG).await
}
pub async fn payload_config(&self) -> Result<Vec<u8>, UnicornError> {
self.read_config_char(CHAR_PAYLOAD_CONFIG).await
}
pub async fn filter_config(&self) -> Result<Vec<u8>, UnicornError> {
self.read_config_char(CHAR_FILTER_CONFIG).await
}
async fn read_config_char(&self, uuid: Uuid) -> Result<Vec<u8>, UnicornError> {
let chars = self.peripheral.characteristics();
let c = chars.iter().find(|c| c.uuid == uuid).ok_or(
UnicornError::NotSupported(format!("Characteristic {} not found", uuid)),
)?;
self.peripheral.read(c).await.map_err(|e| UnicornError::SdkError {
code: -1,
message: format!("Read failed: {}", e),
})
}
pub async fn disconnect(&self) -> Result<(), UnicornError> {
self.peripheral.disconnect().await.map_err(|e| UnicornError::SdkError {
code: -1,
message: format!("Disconnect failed: {}", e),
})
}
}
}
#[cfg(feature = "ble")]
pub use inner::*;