use std::collections::HashMap;
use std::sync::{Arc, RwLock as StdRwLock};
use objc2::rc::Retained;
use objc2::runtime::AnyObject;
use objc2::{msg_send, ClassType};
use objc2_core_bluetooth::{
CBAdvertisementDataLocalNameKey, CBAdvertisementDataServiceUUIDsKey, CBAttributePermissions,
CBCharacteristic, CBCharacteristicProperties, CBMutableCharacteristic, CBMutableService,
CBPeripheralManager, CBUUID,
};
use objc2_foundation::{NSArray, NSData, NSDictionary, NSString};
use tokio::sync::{mpsc, RwLock};
use crate::config::DiscoveryConfig;
use crate::error::{BleError, Result};
use crate::NodeId;
use crate::PEAT_SERVICE_UUID;
use super::delegates::{CentralState, PeripheralManagerEvent, RustPeripheralManagerDelegate};
pub struct PeripheralManager {
manager: Retained<CBPeripheralManager>,
delegate: Retained<RustPeripheralManagerDelegate>,
state: Arc<RwLock<CentralState>>,
event_rx: Arc<RwLock<mpsc::Receiver<PeripheralManagerEvent>>>,
advertising: Arc<RwLock<bool>>,
services: Arc<RwLock<HashMap<String, ServiceInfo>>>,
subscribers: Arc<RwLock<HashMap<String, Vec<String>>>>,
characteristics: Arc<StdRwLock<HashMap<String, Retained<CBMutableCharacteristic>>>>,
cb_services: Arc<StdRwLock<Vec<Retained<CBMutableService>>>>,
}
unsafe impl Send for PeripheralManager {}
unsafe impl Sync for PeripheralManager {}
#[derive(Debug, Clone)]
pub struct ServiceInfo {
pub uuid: String,
pub is_primary: bool,
pub characteristics: Vec<CharacteristicInfo>,
}
#[derive(Debug, Clone)]
pub struct CharacteristicInfo {
pub uuid: String,
pub properties: CharacteristicPropertiesFlags,
pub value: Vec<u8>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CharacteristicPropertiesFlags {
pub read: bool,
pub write: bool,
pub write_without_response: bool,
pub notify: bool,
pub indicate: bool,
}
impl CharacteristicPropertiesFlags {
pub fn readable() -> Self {
Self {
read: true,
..Default::default()
}
}
pub fn writable() -> Self {
Self {
write: true,
..Default::default()
}
}
pub fn notify() -> Self {
Self {
notify: true,
..Default::default()
}
}
pub fn read_write_notify() -> Self {
Self {
read: true,
write: true,
notify: true,
..Default::default()
}
}
pub fn to_cb_properties(&self) -> CBCharacteristicProperties {
let mut props = CBCharacteristicProperties::empty();
if self.read {
props |= CBCharacteristicProperties::CBCharacteristicPropertyRead;
}
if self.write {
props |= CBCharacteristicProperties::CBCharacteristicPropertyWrite;
}
if self.write_without_response {
props |= CBCharacteristicProperties::CBCharacteristicPropertyWriteWithoutResponse;
}
if self.notify {
props |= CBCharacteristicProperties::CBCharacteristicPropertyNotify;
}
if self.indicate {
props |= CBCharacteristicProperties::CBCharacteristicPropertyIndicate;
}
props
}
}
impl PeripheralManager {
pub(super) fn new() -> Result<Self> {
let (event_tx, event_rx) = mpsc::channel(100);
let delegate = RustPeripheralManagerDelegate::new(event_tx);
let manager = unsafe { CBPeripheralManager::new() };
unsafe {
manager.setDelegate(Some(delegate.as_protocol()));
}
log::info!("CBPeripheralManager initialized");
Ok(Self {
manager,
delegate,
state: Arc::new(RwLock::new(CentralState::Unknown)),
event_rx: Arc::new(RwLock::new(event_rx)),
advertising: Arc::new(RwLock::new(false)),
services: Arc::new(RwLock::new(HashMap::new())),
subscribers: Arc::new(RwLock::new(HashMap::new())),
characteristics: Arc::new(StdRwLock::new(HashMap::new())),
cb_services: Arc::new(StdRwLock::new(Vec::new())),
})
}
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(super) async fn register_peat_service(&self, node_id: NodeId) -> Result<()> {
{
let (cb_characteristics, char_entries) = self.create_peat_characteristics(node_id)?;
{
let mut char_map = self.characteristics.write().unwrap();
for (uuid, char) in char_entries {
char_map.insert(uuid, char);
}
}
let service = unsafe {
let service_uuid = {
let uuid_str = NSString::from_str(&PEAT_SERVICE_UUID.to_string());
CBUUID::UUIDWithString(&uuid_str)
};
let service = CBMutableService::initWithType_primary(
CBMutableService::alloc(),
&service_uuid,
true,
);
let char_refs: Vec<&CBCharacteristic> = cb_characteristics
.iter()
.map(|c| {
let ptr: *const CBMutableCharacteristic = &**c;
&*(ptr as *const CBCharacteristic)
})
.collect();
let char_array = NSArray::from_slice(&char_refs);
service.setCharacteristics(Some(&char_array));
self.manager.addService(&service);
service
};
{
let mut cb_services = self.cb_services.write().unwrap();
cb_services.push(service);
}
log::info!(
"Registered Peat service with node ID {:08X}",
node_id.as_u32()
);
self.delegate
.set_characteristic_value("0001", node_id.as_u32().to_le_bytes().to_vec());
}
let service_info = ServiceInfo {
uuid: PEAT_SERVICE_UUID.to_string(),
is_primary: true,
characteristics: vec![
CharacteristicInfo {
uuid: "0001".to_string(),
properties: CharacteristicPropertiesFlags::readable(),
value: node_id.as_u32().to_le_bytes().to_vec(),
},
CharacteristicInfo {
uuid: "0002".to_string(),
properties: CharacteristicPropertiesFlags::read_write_notify(),
value: Vec::new(),
},
CharacteristicInfo {
uuid: "0003".to_string(),
properties: CharacteristicPropertiesFlags::read_write_notify(),
value: Vec::new(),
},
CharacteristicInfo {
uuid: "0004".to_string(),
properties: CharacteristicPropertiesFlags::writable(),
value: Vec::new(),
},
CharacteristicInfo {
uuid: "0005".to_string(),
properties: CharacteristicPropertiesFlags {
read: true,
notify: true,
..Default::default()
},
value: Vec::new(),
},
],
};
self.services
.write()
.await
.insert(PEAT_SERVICE_UUID.to_string(), service_info);
Ok(())
}
fn create_peat_characteristics(
&self,
node_id: NodeId,
) -> Result<(
Vec<Retained<CBMutableCharacteristic>>,
Vec<(String, Retained<CBMutableCharacteristic>)>,
)> {
let mut cb_characteristics = Vec::new();
let mut char_entries = Vec::new();
let node_info_char = self.create_characteristic(
"0001",
CharacteristicPropertiesFlags::readable(),
Some(&node_id.as_u32().to_le_bytes()),
)?;
char_entries.push(("0001".to_string(), node_info_char.clone()));
cb_characteristics.push(node_info_char);
let sync_state_char = self.create_characteristic(
"0002",
CharacteristicPropertiesFlags::read_write_notify(),
None,
)?;
char_entries.push(("0002".to_string(), sync_state_char.clone()));
cb_characteristics.push(sync_state_char);
let sync_data_char = self.create_characteristic(
"0003",
CharacteristicPropertiesFlags::read_write_notify(),
None,
)?;
char_entries.push(("0003".to_string(), sync_data_char.clone()));
cb_characteristics.push(sync_data_char);
let command_char =
self.create_characteristic("0004", CharacteristicPropertiesFlags::writable(), None)?;
char_entries.push(("0004".to_string(), command_char.clone()));
cb_characteristics.push(command_char);
let status_char = self.create_characteristic(
"0005",
CharacteristicPropertiesFlags {
read: true,
notify: true,
..Default::default()
},
None,
)?;
char_entries.push(("0005".to_string(), status_char.clone()));
cb_characteristics.push(status_char);
Ok((cb_characteristics, char_entries))
}
fn create_characteristic(
&self,
uuid_str: &str,
props: CharacteristicPropertiesFlags,
value: Option<&[u8]>,
) -> Result<Retained<CBMutableCharacteristic>> {
let uuid = unsafe {
let ns_uuid = NSString::from_str(uuid_str);
CBUUID::UUIDWithString(&ns_uuid)
};
let cb_props = props.to_cb_properties();
let mut permissions = CBAttributePermissions::empty();
if props.read {
permissions |= CBAttributePermissions::Readable;
}
if props.write || props.write_without_response {
permissions |= CBAttributePermissions::Writeable;
}
let ns_value = value.map(|v| NSData::with_bytes(v));
let characteristic = unsafe {
CBMutableCharacteristic::initWithType_properties_value_permissions(
CBMutableCharacteristic::alloc(),
&uuid,
cb_props,
ns_value.as_deref(),
permissions,
)
};
Ok(characteristic)
}
pub(super) async fn unregister_all_services(&self) -> Result<()> {
unsafe {
self.manager.removeAllServices();
}
self.services.write().await.clear();
self.characteristics.write().unwrap().clear();
self.cb_services.write().unwrap().clear();
log::info!("Removed all GATT services");
Ok(())
}
pub(super) async fn start_advertising(
&self,
node_id: NodeId,
_config: &DiscoveryConfig,
) -> Result<()> {
let local_name = format!("PEAT-{:08X}", node_id.as_u32());
unsafe {
let name_str = NSString::from_str(&local_name);
let service_uuid_str = NSString::from_str(&PEAT_SERVICE_UUID.to_string());
let service_uuid = CBUUID::UUIDWithString(&service_uuid_str);
let uuid_array: Retained<NSArray<CBUUID>> = {
let uuid_ptr: *mut CBUUID = msg_send![&*service_uuid, retain];
NSArray::from_vec(vec![Retained::from_raw(uuid_ptr).unwrap()])
};
let keys: Retained<NSArray<NSString>> = {
let name_key_ptr: *mut NSString =
msg_send![CBAdvertisementDataLocalNameKey, retain];
let uuid_key_ptr: *mut NSString =
msg_send![CBAdvertisementDataServiceUUIDsKey, retain];
NSArray::from_vec(vec![
Retained::from_raw(name_key_ptr).unwrap(),
Retained::from_raw(uuid_key_ptr).unwrap(),
])
};
let values: Retained<NSArray<AnyObject>> = {
let name_ptr: *mut AnyObject = msg_send![&*name_str, retain];
let uuid_array_ptr: *mut AnyObject = msg_send![&*uuid_array, retain];
NSArray::from_vec(vec![
Retained::from_raw(name_ptr).unwrap(),
Retained::from_raw(uuid_array_ptr).unwrap(),
])
};
let dict: Retained<NSDictionary<NSString, AnyObject>> = {
let dict_ptr: *mut NSDictionary<NSString, AnyObject> = msg_send![
objc2::class!(NSDictionary),
dictionaryWithObjects: Retained::as_ptr(&values),
forKeys: Retained::as_ptr(&keys)
];
let retained_ptr: *mut NSDictionary<NSString, AnyObject> =
msg_send![dict_ptr, retain];
Retained::from_raw(retained_ptr).expect("NSDictionary should not be nil")
};
self.manager.startAdvertising(Some(&dict));
}
log::info!("Started advertising as {}", local_name);
*self.advertising.write().await = true;
Ok(())
}
pub(super) async fn stop_advertising(&self) -> Result<()> {
unsafe {
self.manager.stopAdvertising();
}
log::info!("Stopped advertising");
*self.advertising.write().await = false;
Ok(())
}
#[allow(dead_code)] pub(super) async fn is_advertising(&self) -> bool {
unsafe { self.manager.isAdvertising() }
}
#[allow(dead_code)] pub(super) fn set_characteristic_value(&self, characteristic_uuid: &str, value: Vec<u8>) {
self.delegate
.set_characteristic_value(characteristic_uuid, value);
}
#[allow(dead_code)] pub(super) fn get_characteristic_value(&self, characteristic_uuid: &str) -> Option<Vec<u8>> {
self.delegate.get_characteristic_value(characteristic_uuid)
}
#[allow(dead_code)] pub(super) async fn send_notification(
&self,
characteristic_uuid: &str,
value: &[u8],
) -> Result<bool> {
let chars = self.characteristics.read().unwrap();
let characteristic = chars.get(characteristic_uuid).ok_or_else(|| {
BleError::PlatformError(format!("Unknown characteristic: {}", characteristic_uuid))
})?;
let data = NSData::with_bytes(value);
let result = unsafe {
self.manager
.updateValue_forCharacteristic_onSubscribedCentrals(&data, characteristic, None)
};
if result {
log::trace!("Sent notification on {}", characteristic_uuid);
} else {
log::debug!("Notification queue full for {}", characteristic_uuid);
}
Ok(result)
}
#[allow(dead_code)] pub(super) async fn get_subscribers(&self, characteristic_uuid: &str) -> Vec<String> {
let subscribers = self.subscribers.read().await;
subscribers
.get(characteristic_uuid)
.cloned()
.unwrap_or_default()
}
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 {
PeripheralManagerEvent::StateChanged(state) => {
log::debug!("Peripheral manager state changed: {:?}", state);
*self.state.write().await = state;
}
PeripheralManagerEvent::ServiceAdded {
service_uuid,
error,
} => {
if let Some(e) = error {
log::error!("Failed to add service {}: {}", service_uuid, e);
} else {
log::info!("Service {} added successfully", service_uuid);
}
}
PeripheralManagerEvent::AdvertisingStarted { error } => {
if let Some(e) = error {
log::error!("Advertising failed: {}", e);
*self.advertising.write().await = false;
} else {
log::info!("Advertising started successfully");
}
}
PeripheralManagerEvent::CentralSubscribed {
central_identifier,
characteristic_uuid,
} => {
log::info!(
"Central {} subscribed to {}",
central_identifier,
characteristic_uuid
);
let mut subscribers = self.subscribers.write().await;
subscribers
.entry(characteristic_uuid)
.or_default()
.push(central_identifier);
}
PeripheralManagerEvent::CentralUnsubscribed {
central_identifier,
characteristic_uuid,
} => {
log::info!(
"Central {} unsubscribed from {}",
central_identifier,
characteristic_uuid
);
let mut subscribers = self.subscribers.write().await;
if let Some(subs) = subscribers.get_mut(&characteristic_uuid) {
subs.retain(|id| id != ¢ral_identifier);
}
}
PeripheralManagerEvent::ReadRequest {
request_id: _,
central_identifier,
characteristic_uuid,
offset,
} => {
log::debug!(
"Read request from {} for characteristic {} at offset {} (handled by delegate)",
central_identifier,
characteristic_uuid,
offset,
);
}
PeripheralManagerEvent::WriteRequest {
request_id: _,
central_identifier,
characteristic_uuid,
value,
offset,
response_needed: _,
} => {
let mut services = self.services.write().await;
for (_svc_uuid, service_info) in services.iter_mut() {
for char_info in service_info.characteristics.iter_mut() {
if char_info.uuid == characteristic_uuid {
if offset == 0 {
char_info.value = value.clone();
} else {
if offset <= char_info.value.len() {
char_info.value.truncate(offset);
char_info.value.extend_from_slice(&value);
} else {
char_info.value.resize(offset, 0);
char_info.value.extend_from_slice(&value);
}
}
log::debug!(
"Write from {} to characteristic {} ({} bytes at offset {})",
central_identifier,
characteristic_uuid,
value.len(),
offset,
);
break;
}
}
}
}
PeripheralManagerEvent::ReadyToUpdateSubscribers => {
log::trace!("Ready to send more notifications");
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_characteristic_properties() {
let props = CharacteristicPropertiesFlags::read_write_notify();
assert!(props.read);
assert!(props.write);
assert!(props.notify);
assert!(!props.indicate);
let cb_props = props.to_cb_properties();
assert!(cb_props.contains(CBCharacteristicProperties::CBCharacteristicPropertyRead));
assert!(cb_props.contains(CBCharacteristicProperties::CBCharacteristicPropertyWrite));
assert!(cb_props.contains(CBCharacteristicProperties::CBCharacteristicPropertyNotify));
}
#[test]
fn test_service_info() {
let service = ServiceInfo {
uuid: "D479".to_string(),
is_primary: true,
characteristics: vec![CharacteristicInfo {
uuid: "0001".to_string(),
properties: CharacteristicPropertiesFlags::readable(),
value: vec![0xDE, 0xAD, 0xBE, 0xEF],
}],
};
assert!(service.is_primary);
assert_eq!(service.characteristics.len(), 1);
}
}