use std::time::Duration;
use btleplug::platform::{Adapter, PeripheralId};
use btleplug::{
api::{Central, CentralEvent, Manager as _, Peripheral, ScanFilter},
platform::Manager,
};
use flume;
use futures_util::{Stream, StreamExt};
use uuid::Uuid;
pub use super::{BtTimerState as TimerState, DeviceInfo};
const GAN_TIMER_SERVICE: &str = "0000fff0-0000-1000-8000-00805f9b34fb";
const GAN_TIMER_TIME_CHARACTERISTIC: &str = "0000fff2-0000-1000-8000-00805f9b34fb";
const GAN_TIMER_STATE_CHARACTERISTIC: &str = "0000fff5-0000-1000-8000-00805f9b34fb";
pub async fn get_adapter() -> anyhow::Result<Adapter> {
let manager = Manager::new().await?;
let adapter_list = manager.adapters().await?;
if adapter_list.is_empty() {
return Err(anyhow::anyhow!("No Bluetooth adapters found"));
}
Ok(adapter_list[0].clone())
}
pub async fn get_devices(adapter: &Adapter) -> anyhow::Result<impl Stream<Item = DeviceInfo>> {
adapter.start_scan(ScanFilter::default()).await?;
let (tx, rx) = flume::bounded(32);
let adapter = adapter.clone();
tokio::spawn(async move {
let Ok(mut events) = adapter.events().await else {
return;
};
loop {
tokio::select! {
Some(event) = events.next() => {
match event {
CentralEvent::DeviceDiscovered(id) | CentralEvent::DeviceUpdated(id) => {
if let Ok(peripheral) = adapter.peripheral(&id).await {
let props = peripheral.properties().await.unwrap_or(None);
let name = props.as_ref().and_then(|p| p.local_name.clone());
let is_gan = name
.as_ref()
.is_some_and(|n| n.to_lowercase().contains("gan"));
if is_gan {
let device = DeviceInfo {
id: id.clone(),
name,
};
if tx.send_async(device).await.is_err() {
break;
}
}
}
}
_ => {}
}
}
}
}
});
Ok(rx.into_stream())
}
pub async fn connect(
id: &PeripheralId,
adapter: &Adapter,
) -> anyhow::Result<impl Stream<Item = TimerState>> {
let timer_service_uuid =
Uuid::parse_str(GAN_TIMER_SERVICE).expect("The constant is a parseable uuid");
let state_uuid =
Uuid::parse_str(GAN_TIMER_STATE_CHARACTERISTIC).expect("The constant is a parseable uuid");
let time_uuid =
Uuid::parse_str(GAN_TIMER_TIME_CHARACTERISTIC).expect("The constant is a parseable uuid");
let peripheral = adapter.peripheral(id).await?;
peripheral.connect().await?;
adapter.stop_scan().await?;
while !peripheral.is_connected().await? {
tokio::time::sleep(Duration::from_millis(100)).await;
}
let mut retries = 5;
while let Err(err) = peripheral.discover_services().await {
if retries == 0 {
return Err(err.into());
}
retries -= 1;
tokio::time::sleep(Duration::from_millis(200)).await;
}
let mut characteristics = peripheral.characteristics();
let mut char_retries = 10;
while characteristics.is_empty() && char_retries > 0 {
tokio::time::sleep(Duration::from_millis(100)).await;
characteristics = peripheral.characteristics();
char_retries -= 1;
}
let state_characteristic = characteristics
.iter()
.find(|ch| ch.service_uuid == timer_service_uuid && ch.uuid == state_uuid)
.cloned()
.ok_or_else(|| anyhow::anyhow!("State characteristic not found"))?;
let time_characteristic = characteristics
.iter()
.find(|ch| ch.service_uuid == timer_service_uuid && ch.uuid == time_uuid)
.cloned()
.ok_or_else(|| anyhow::anyhow!("Time characteristic not found"))?;
let mut notifications = peripheral.notifications().await?;
peripheral.subscribe(&state_characteristic).await?;
let (tx, rx) = flume::bounded(32);
tokio::spawn(async move {
while let Some(event) = notifications.next().await {
let state = match event.value[3] {
1 => Some(TimerState::GetSet),
2 => Some(TimerState::HandsOff),
3 => Some(TimerState::Running),
5 => Some(TimerState::Idle),
6 => Some(TimerState::HandsOn),
7 => {
if let Ok(time) = peripheral.read(&time_characteristic).await
&& let Ok(bytes) = <[u8; 4]>::try_from(&time[0..4])
{
Some(TimerState::Finished(time_array_to_ms(bytes)))
} else {
None
}
}
_ => None,
};
if let Some(state) = state
&& tx.send_async(state).await.is_err()
{
break;
}
}
});
Ok(rx.into_stream())
}
pub async fn disconnect(id: &PeripheralId, adapter: &Adapter) -> anyhow::Result<()> {
let peripheral = adapter.peripheral(id).await?;
peripheral.disconnect().await?;
Ok(())
}
fn time_array_to_ms(t: [u8; 4]) -> u64 {
(u64::from(t[0]) * 60_000)
+ (u64::from(t[1]) * 1_000)
+ u64::from(u16::from_le_bytes([t[2], t[3]]))
}