use std::collections::HashMap;
use std::sync::Arc;
use objc2::msg_send;
use objc2::rc::Retained;
use objc2_core_bluetooth::{CBCentralManager, CBPeripheral, CBUUID};
use objc2_foundation::{NSArray, NSString};
use tokio::sync::{mpsc, RwLock};
use crate::config::DiscoveryConfig;
use crate::error::{BleError, Result};
use crate::NodeId;
use super::delegates::{
CentralEvent, CentralState, PeripheralEvent, RustCentralManagerDelegate, RustPeripheralDelegate,
};
pub struct CentralManager {
manager: Retained<CBCentralManager>,
delegate: Retained<RustCentralManagerDelegate>,
state: Arc<RwLock<CentralState>>,
event_rx: Arc<RwLock<mpsc::Receiver<CentralEvent>>>,
peripherals: Arc<RwLock<HashMap<String, PeripheralInfo>>>,
scanning: Arc<RwLock<bool>>,
peripheral_delegates: Arc<RwLock<HashMap<String, Retained<RustPeripheralDelegate>>>>,
peripheral_event_tx: mpsc::Sender<PeripheralEvent>,
peripheral_event_rx: Arc<RwLock<mpsc::Receiver<PeripheralEvent>>>,
}
#[derive(Debug, Clone)]
pub struct PeripheralInfo {
pub identifier: String,
pub name: Option<String>,
pub rssi: i8,
pub is_peat_node: bool,
pub node_id: Option<NodeId>,
pub connected: bool,
}
unsafe impl Send for CentralManager {}
unsafe impl Sync for CentralManager {}
impl CentralManager {
pub fn new() -> Result<Self> {
let (event_tx, event_rx) = mpsc::channel(100);
let (peripheral_event_tx, peripheral_event_rx) = mpsc::channel(100);
let delegate = RustCentralManagerDelegate::new(event_tx);
let manager = unsafe { CBCentralManager::new() };
unsafe {
manager.setDelegate(Some(delegate.as_protocol()));
}
log::info!("CBCentralManager initialized");
Ok(Self {
manager,
delegate,
state: Arc::new(RwLock::new(CentralState::Unknown)),
event_rx: Arc::new(RwLock::new(event_rx)),
peripherals: Arc::new(RwLock::new(HashMap::new())),
scanning: Arc::new(RwLock::new(false)),
peripheral_delegates: Arc::new(RwLock::new(HashMap::new())),
peripheral_event_tx,
peripheral_event_rx: Arc::new(RwLock::new(peripheral_event_rx)),
})
}
pub(super) async fn state(&self) -> CentralState {
*self.state.read().await
}
#[allow(dead_code)] pub(super) async fn wait_ready(&self) -> Result<()> {
loop {
self.process_events().await?;
let state = self.state().await;
match state {
CentralState::PoweredOn => return Ok(()),
CentralState::Unsupported => {
return Err(BleError::NotSupported(
"Bluetooth not supported".to_string(),
))
}
CentralState::Unauthorized => {
return Err(BleError::PlatformError(
"Bluetooth not authorized".to_string(),
))
}
CentralState::PoweredOff => {
return Err(BleError::PlatformError(
"Bluetooth is powered off".to_string(),
))
}
CentralState::Unknown | CentralState::Resetting => {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
}
}
}
pub async fn start_scan(
&self,
_config: &DiscoveryConfig,
service_uuids: Option<Vec<String>>,
) -> Result<()> {
let filter_count = service_uuids.as_ref().map(|v| v.len());
{
let uuid_filter: Option<Retained<NSArray<CBUUID>>> =
service_uuids.map(|uuids| unsafe {
let cb_uuids: Vec<Retained<CBUUID>> = uuids
.iter()
.map(|uuid_str| {
let ns_str = NSString::from_str(uuid_str);
CBUUID::UUIDWithString(&ns_str)
})
.collect();
let uuid_refs: Vec<Retained<CBUUID>> = cb_uuids
.into_iter()
.map(|uuid| {
let ptr: *mut CBUUID = msg_send![Retained::as_ptr(&uuid), retain];
Retained::from_raw(ptr).unwrap()
})
.collect();
NSArray::from_vec(uuid_refs)
});
unsafe {
self.manager
.scanForPeripheralsWithServices_options(uuid_filter.as_deref(), None);
}
}
let filter_desc = filter_count
.map(|count| format!("filtering by {} service UUID(s)", count))
.unwrap_or_else(|| "no filter".to_string());
log::info!("Started BLE scanning ({})", filter_desc);
*self.scanning.write().await = true;
Ok(())
}
pub async fn stop_scan(&self) -> Result<()> {
unsafe {
self.manager.stopScan();
}
log::info!("Stopped BLE scanning");
*self.scanning.write().await = false;
Ok(())
}
#[allow(dead_code)] pub(super) async fn is_scanning(&self) -> bool {
*self.scanning.read().await
}
pub(super) async fn connect(&self, identifier: &str) -> Result<()> {
let peripheral = self.delegate.get_peripheral(identifier).ok_or_else(|| {
BleError::ConnectionFailed(format!("Unknown peripheral: {}", identifier))
})?;
unsafe {
self.manager.connectPeripheral_options(&peripheral, None);
}
log::info!("Connecting to peripheral: {}", identifier);
Ok(())
}
pub(super) async fn disconnect(&self, identifier: &str) -> Result<()> {
if let Some(peripheral) = self.delegate.get_peripheral(identifier) {
unsafe {
self.manager.cancelPeripheralConnection(&peripheral);
}
log::info!("Disconnecting from peripheral: {}", identifier);
}
Ok(())
}
#[allow(dead_code)] pub(super) fn get_cb_peripheral(&self, identifier: &str) -> Option<Retained<CBPeripheral>> {
self.delegate.get_peripheral(identifier)
}
pub(super) async fn discover_services(
&self,
identifier: &str,
service_uuids: Option<&[&str]>,
) -> Result<()> {
let peripheral = self.delegate.get_peripheral(identifier).ok_or_else(|| {
BleError::ConnectionFailed(format!("Unknown peripheral: {}", identifier))
})?;
unsafe {
let uuids: Option<Retained<NSArray<CBUUID>>> = service_uuids.map(|uuids| {
let uuid_objects: Vec<_> = uuids
.iter()
.map(|uuid_str| CBUUID::UUIDWithString(&NSString::from_str(uuid_str)))
.collect();
NSArray::from_vec(uuid_objects)
});
peripheral.discoverServices(uuids.as_deref());
}
log::info!("Discovering services on peripheral: {}", identifier);
Ok(())
}
pub(super) async fn discover_characteristics(
&self,
identifier: &str,
service_uuid: &str,
) -> Result<()> {
let peripheral = self.delegate.get_peripheral(identifier).ok_or_else(|| {
BleError::ConnectionFailed(format!("Unknown peripheral: {}", identifier))
})?;
let target_uuid_upper = service_uuid.to_uppercase();
unsafe {
let services = peripheral.services();
if let Some(services) = services {
for i in 0..services.len() {
let service = &services[i];
let service_uuid_str = service.UUID().UUIDString().to_string().to_uppercase();
if service_uuid_str == target_uuid_upper {
peripheral.discoverCharacteristics_forService(None, service);
log::info!(
"Discovering characteristics for service {} on {}",
service_uuid,
identifier
);
return Ok(());
}
}
}
}
Err(BleError::ConnectionFailed(format!(
"Service {} not found on {}",
service_uuid, identifier
)))
}
pub(super) async fn read_characteristic(
&self,
identifier: &str,
service_uuid: &str,
characteristic_uuid: &str,
) -> Result<()> {
let peripheral = self.delegate.get_peripheral(identifier).ok_or_else(|| {
BleError::ConnectionFailed(format!("Unknown peripheral: {}", identifier))
})?;
let target_service_upper = service_uuid.to_uppercase();
let target_char_upper = characteristic_uuid.to_uppercase();
unsafe {
if let Some(services) = peripheral.services() {
for i in 0..services.len() {
let service = &services[i];
let svc_uuid = service.UUID().UUIDString().to_string().to_uppercase();
if svc_uuid == target_service_upper || svc_uuid.contains(&target_service_upper)
{
if let Some(characteristics) = service.characteristics() {
let char_list: Vec<String> = (0..characteristics.len())
.map(|j| characteristics[j].UUID().UUIDString().to_string())
.collect();
log::debug!(
"Available characteristics on {}: {:?}",
identifier,
char_list
);
for j in 0..characteristics.len() {
let characteristic = &characteristics[j];
let char_uuid = characteristic
.UUID()
.UUIDString()
.to_string()
.to_uppercase();
if char_uuid == target_char_upper
|| char_uuid.contains(&target_char_upper)
{
peripheral.readValueForCharacteristic(characteristic);
log::debug!(
"Reading characteristic {} (matched {}) from {}",
char_uuid,
characteristic_uuid,
identifier
);
return Ok(());
}
}
}
}
}
}
}
Err(BleError::ConnectionFailed(format!(
"Characteristic {} not found",
characteristic_uuid
)))
}
pub(super) async fn write_characteristic(
&self,
identifier: &str,
service_uuid: &str,
characteristic_uuid: &str,
data: &[u8],
with_response: bool,
) -> Result<()> {
use objc2_foundation::NSData;
let peripheral = self.delegate.get_peripheral(identifier).ok_or_else(|| {
BleError::ConnectionFailed(format!("Unknown peripheral: {}", identifier))
})?;
let target_service_upper = service_uuid.to_uppercase();
let target_char_upper = characteristic_uuid.to_uppercase();
unsafe {
if let Some(services) = peripheral.services() {
for i in 0..services.len() {
let service = &services[i];
let svc_uuid = service.UUID().UUIDString().to_string().to_uppercase();
if svc_uuid == target_service_upper {
if let Some(characteristics) = service.characteristics() {
for j in 0..characteristics.len() {
let characteristic = &characteristics[j];
let char_uuid = characteristic
.UUID()
.UUIDString()
.to_string()
.to_uppercase();
if char_uuid == target_char_upper {
let ns_data = NSData::with_bytes(data);
let write_type: isize = if with_response { 0 } else { 1 };
let _: () = msg_send![&*peripheral, writeValue: &*ns_data forCharacteristic: &**characteristic type: write_type];
log::debug!(
"Writing {} bytes to characteristic {} on {}",
data.len(),
characteristic_uuid,
identifier
);
return Ok(());
}
}
}
}
}
}
}
Err(BleError::ConnectionFailed(format!(
"Characteristic {} not found",
characteristic_uuid
)))
}
pub(super) async fn try_recv_peripheral_event(&self) -> Option<PeripheralEvent> {
let mut rx = self.peripheral_event_rx.write().await;
rx.try_recv().ok()
}
pub(super) fn setup_peripheral_delegate(&self, identifier: &str) -> Result<()> {
let peripheral = self.delegate.get_peripheral(identifier).ok_or_else(|| {
BleError::ConnectionFailed(format!("Unknown peripheral: {}", identifier))
})?;
let delegate = RustPeripheralDelegate::new(self.peripheral_event_tx.clone());
unsafe {
peripheral.setDelegate(Some(delegate.as_protocol()));
}
if let Ok(mut delegates) = self.peripheral_delegates.try_write() {
delegates.insert(identifier.to_string(), delegate);
log::debug!("Set up peripheral delegate for {}", identifier);
Ok(())
} else {
log::warn!(
"Could not store peripheral delegate for {} (lock contention)",
identifier
);
Ok(())
}
}
#[allow(dead_code)] pub(super) async fn get_peripheral(&self, identifier: &str) -> Option<PeripheralInfo> {
let peripherals = self.peripherals.read().await;
peripherals.get(identifier).cloned()
}
#[allow(dead_code)] pub(super) async fn get_discovered_peripherals(&self) -> Vec<PeripheralInfo> {
let peripherals = self.peripherals.read().await;
peripherals.values().cloned().collect()
}
pub(super) async fn get_peat_peripherals(&self) -> Vec<PeripheralInfo> {
let peripherals = self.peripherals.read().await;
peripherals
.values()
.filter(|p| p.is_peat_node)
.cloned()
.collect()
}
pub(super) async fn process_events(&self) -> Result<()> {
unsafe {
use objc2_foundation::NSRunLoop;
let run_loop = NSRunLoop::mainRunLoop();
let mode = objc2_foundation::NSDefaultRunLoopMode;
let date = objc2_foundation::NSDate::dateWithTimeIntervalSinceNow(0.001);
run_loop.runMode_beforeDate(mode, &date);
}
let mut event_rx = self.event_rx.write().await;
while let Ok(event) = event_rx.try_recv() {
match event {
CentralEvent::StateChanged(state) => {
log::debug!("Central state changed: {:?}", state);
*self.state.write().await = state;
}
CentralEvent::DiscoveredPeripheral {
identifier,
name,
rssi,
is_peat_node,
node_id,
..
} => {
let mut peripherals = self.peripherals.write().await;
peripherals.insert(
identifier.clone(),
PeripheralInfo {
identifier,
name,
rssi,
is_peat_node,
node_id,
connected: false,
},
);
}
CentralEvent::Connected { identifier } => {
log::info!("Connected to peripheral: {}", identifier);
if let Err(e) = self.setup_peripheral_delegate(&identifier) {
log::warn!("Failed to set up peripheral delegate: {}", e);
}
let mut peripherals = self.peripherals.write().await;
if let Some(peripheral) = peripherals.get_mut(&identifier) {
peripheral.connected = true;
}
}
CentralEvent::Disconnected { identifier, .. } => {
log::info!("Disconnected from peripheral: {}", identifier);
let mut peripherals = self.peripherals.write().await;
if let Some(peripheral) = peripherals.get_mut(&identifier) {
peripheral.connected = false;
}
}
CentralEvent::ConnectionFailed { identifier, error } => {
log::warn!("Connection to {} failed: {}", identifier, error);
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_peripheral_info() {
let info = PeripheralInfo {
identifier: "12345678-1234-1234-1234-123456789ABC".to_string(),
name: Some("PEAT-DEADBEEF".to_string()),
rssi: -65,
is_peat_node: true,
node_id: Some(NodeId::new(0xDEADBEEF)),
connected: false,
};
assert!(info.is_peat_node);
assert!(!info.connected);
assert_eq!(info.rssi, -65);
}
}