use std::collections::HashMap;
use std::sync::Mutex;
use objc2::rc::Retained;
use objc2::runtime::ProtocolObject;
use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass};
use objc2_core_bluetooth::{
CBATTError, CBATTRequest, CBAdvertisementDataServiceUUIDsKey, CBCentralManager,
CBCentralManagerDelegate, CBCharacteristic, CBPeripheral, CBPeripheralDelegate,
CBPeripheralManager, CBPeripheralManagerDelegate, CBService, CBUUID,
};
use objc2_foundation::{
NSArray, NSData, NSDictionary, NSError, NSNumber, NSObject, NSObjectProtocol, NSString,
};
use tokio::sync::mpsc;
use crate::{NodeId, PEAT_SERVICE_UUID};
#[derive(Debug, Clone)]
pub enum CentralEvent {
StateChanged(CentralState),
DiscoveredPeripheral {
identifier: String,
name: Option<String>,
rssi: i8,
advertisement_data: Vec<u8>,
service_uuids: Vec<String>,
is_peat_node: bool,
node_id: Option<NodeId>,
},
Connected {
identifier: String,
},
Disconnected {
identifier: String,
error: Option<String>,
},
ConnectionFailed {
identifier: String,
error: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CentralState {
Unknown,
Resetting,
Unsupported,
Unauthorized,
PoweredOff,
PoweredOn,
}
impl CentralState {
pub(super) fn from_raw(value: isize) -> Self {
match value {
0 => CentralState::Unknown,
1 => CentralState::Resetting,
2 => CentralState::Unsupported,
3 => CentralState::Unauthorized,
4 => CentralState::PoweredOff,
5 => CentralState::PoweredOn,
_ => CentralState::Unknown,
}
}
#[cfg(test)]
pub(super) fn is_ready(&self) -> bool {
matches!(self, CentralState::PoweredOn)
}
}
#[derive(Debug, Clone)]
pub enum PeripheralEvent {
ServicesDiscovered {
identifier: String,
error: Option<String>,
},
CharacteristicsDiscovered {
identifier: String,
service_uuid: String,
error: Option<String>,
},
CharacteristicValueUpdated {
identifier: String,
characteristic_uuid: String,
value: Vec<u8>,
error: Option<String>,
},
CharacteristicWritten {
identifier: String,
characteristic_uuid: String,
error: Option<String>,
},
NotificationStateChanged {
identifier: String,
characteristic_uuid: String,
enabled: bool,
error: Option<String>,
},
RssiRead {
identifier: String,
rssi: i8,
error: Option<String>,
},
MtuUpdated { identifier: String, mtu: u16 },
}
#[derive(Debug, Clone)]
pub enum PeripheralManagerEvent {
StateChanged(CentralState),
ServiceAdded {
service_uuid: String,
error: Option<String>,
},
AdvertisingStarted { error: Option<String> },
CentralSubscribed {
central_identifier: String,
characteristic_uuid: String,
},
CentralUnsubscribed {
central_identifier: String,
characteristic_uuid: String,
},
ReadRequest {
request_id: u64,
central_identifier: String,
characteristic_uuid: String,
offset: usize,
},
WriteRequest {
request_id: u64,
central_identifier: String,
characteristic_uuid: String,
value: Vec<u8>,
offset: usize,
response_needed: bool,
},
ReadyToUpdateSubscribers,
}
pub struct CentralDelegateIvars {
event_tx: Mutex<Option<mpsc::Sender<CentralEvent>>>,
peripherals: Mutex<HashMap<String, Retained<CBPeripheral>>>,
}
impl Default for CentralDelegateIvars {
fn default() -> Self {
Self {
event_tx: Mutex::new(None),
peripherals: Mutex::new(HashMap::new()),
}
}
}
declare_class!(
pub struct RustCentralManagerDelegate;
unsafe impl ClassType for RustCentralManagerDelegate {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
const NAME: &'static str = "RustCentralManagerDelegate";
}
impl DeclaredClass for RustCentralManagerDelegate {
type Ivars = CentralDelegateIvars;
}
unsafe impl NSObjectProtocol for RustCentralManagerDelegate {}
unsafe impl CBCentralManagerDelegate for RustCentralManagerDelegate {
#[method(centralManagerDidUpdateState:)]
fn central_manager_did_update_state(&self, central: &CBCentralManager) {
let state_raw = unsafe { central.state() };
let state = CentralState::from_raw(state_raw.0);
log::debug!("Central manager state changed: {:?}", state);
if let Ok(guard) = self.ivars().event_tx.lock() {
if let Some(tx) = guard.as_ref() {
let _ = tx.try_send(CentralEvent::StateChanged(state));
}
}
}
#[method(centralManager:didDiscoverPeripheral:advertisementData:RSSI:)]
fn central_manager_did_discover_peripheral(
&self,
_central: &CBCentralManager,
peripheral: &CBPeripheral,
advertisement_data: &NSDictionary<NSString, objc2::runtime::AnyObject>,
rssi: &NSNumber,
) {
let identifier = unsafe {
let uuid = peripheral.identifier();
uuid.UUIDString().to_string()
};
let name = unsafe { peripheral.name().map(|s| s.to_string()) };
let rssi_val = rssi.as_i8();
let service_uuids = unsafe {
Self::parse_service_uuids(advertisement_data)
};
let peat_uuid_full = PEAT_SERVICE_UUID.to_string().to_uppercase();
let peat_uuid_16bit_expanded = "0000F47A-0000-1000-8000-00805F9B34FB";
let peat_uuid_16bit_bare = "F47A";
let has_peat_service = service_uuids.iter().any(|uuid| {
let upper = uuid.to_uppercase();
upper == peat_uuid_full || upper == peat_uuid_16bit_expanded || upper == peat_uuid_16bit_bare
});
let name_indicates_peat = name.as_ref().map(|n| {
n.starts_with("PEAT_") || n.starts_with("PEAT-")
}).unwrap_or(false);
let is_peat_node = has_peat_service || name_indicates_peat;
let node_id = name.as_ref().and_then(|n| {
crate::config::MeshConfig::parse_device_name(n)
.map(|(_, node_id)| node_id)
});
if !service_uuids.is_empty() || name_indicates_peat {
log::debug!(
"Discovered peripheral: {} ({:?}) RSSI: {} Peat: {} (service_uuid: {}, name: {}, uuids: {:?})",
identifier, name, rssi_val, is_peat_node, has_peat_service, name_indicates_peat, service_uuids
);
} else {
log::trace!(
"Discovered peripheral: {} ({:?}) RSSI: {}",
identifier, name, rssi_val
);
}
if let Ok(mut guard) = self.ivars().peripherals.lock() {
guard.insert(identifier.clone(), peripheral.retain());
}
if let Ok(guard) = self.ivars().event_tx.lock() {
if let Some(tx) = guard.as_ref() {
let _ = tx.try_send(CentralEvent::DiscoveredPeripheral {
identifier,
name,
rssi: rssi_val,
advertisement_data: Vec::new(),
service_uuids,
is_peat_node,
node_id,
});
}
}
}
#[method(centralManager:didConnectPeripheral:)]
fn central_manager_did_connect_peripheral(
&self,
_central: &CBCentralManager,
peripheral: &CBPeripheral,
) {
let identifier = unsafe {
peripheral.identifier().UUIDString().to_string()
};
log::info!("Connected to peripheral: {}", identifier);
if let Ok(guard) = self.ivars().event_tx.lock() {
if let Some(tx) = guard.as_ref() {
let _ = tx.try_send(CentralEvent::Connected { identifier });
}
}
}
#[method(centralManager:didDisconnectPeripheral:error:)]
fn central_manager_did_disconnect_peripheral(
&self,
_central: &CBCentralManager,
peripheral: &CBPeripheral,
error: Option<&NSError>,
) {
let identifier = unsafe {
peripheral.identifier().UUIDString().to_string()
};
let error_str = error.map(|e| e.localizedDescription().to_string());
log::info!("Disconnected from peripheral: {} (error: {:?})", identifier, error_str);
if let Ok(guard) = self.ivars().event_tx.lock() {
if let Some(tx) = guard.as_ref() {
let _ = tx.try_send(CentralEvent::Disconnected {
identifier,
error: error_str,
});
}
}
}
#[method(centralManager:didFailToConnectPeripheral:error:)]
fn central_manager_did_fail_to_connect_peripheral(
&self,
_central: &CBCentralManager,
peripheral: &CBPeripheral,
error: Option<&NSError>,
) {
let identifier = unsafe {
peripheral.identifier().UUIDString().to_string()
};
let error_str = error
.map(|e| e.localizedDescription().to_string())
.unwrap_or_else(|| "Unknown error".to_string());
log::warn!("Failed to connect to peripheral: {} ({})", identifier, error_str);
if let Ok(guard) = self.ivars().event_tx.lock() {
if let Some(tx) = guard.as_ref() {
let _ = tx.try_send(CentralEvent::ConnectionFailed {
identifier,
error: error_str,
});
}
}
}
}
);
impl RustCentralManagerDelegate {
pub fn new(event_tx: mpsc::Sender<CentralEvent>) -> Retained<Self> {
let this = Self::alloc();
let this = this.set_ivars(CentralDelegateIvars {
event_tx: Mutex::new(Some(event_tx)),
peripherals: Mutex::new(HashMap::new()),
});
unsafe { msg_send_id![super(this), init] }
}
pub(super) fn as_protocol(&self) -> &ProtocolObject<dyn CBCentralManagerDelegate> {
ProtocolObject::from_ref(self)
}
pub(super) fn get_peripheral(&self, identifier: &str) -> Option<Retained<CBPeripheral>> {
self.ivars()
.peripherals
.lock()
.ok()
.and_then(|guard| guard.get(identifier).cloned())
}
#[allow(dead_code)] pub(super) fn remove_peripheral(&self, identifier: &str) -> Option<Retained<CBPeripheral>> {
self.ivars()
.peripherals
.lock()
.ok()
.and_then(|mut guard| guard.remove(identifier))
}
unsafe fn parse_service_uuids(
advertisement_data: &NSDictionary<NSString, objc2::runtime::AnyObject>,
) -> Vec<String> {
let mut service_uuids = Vec::new();
let uuids_obj = advertisement_data.objectForKey(CBAdvertisementDataServiceUUIDsKey);
if let Some(uuids_any) = uuids_obj {
let uuids_ptr: *const objc2::runtime::AnyObject = Retained::as_ptr(&uuids_any);
let uuids_array: &NSArray<CBUUID> = &*(uuids_ptr as *const NSArray<CBUUID>);
for i in 0..uuids_array.len() {
let uuid = &uuids_array[i];
let uuid_string = uuid.UUIDString().to_string();
service_uuids.push(uuid_string);
}
}
service_uuids
}
}
pub struct PeripheralDelegateIvars {
event_tx: Mutex<Option<mpsc::Sender<PeripheralEvent>>>,
}
impl Default for PeripheralDelegateIvars {
fn default() -> Self {
Self {
event_tx: Mutex::new(None),
}
}
}
declare_class!(
pub struct RustPeripheralDelegate;
unsafe impl ClassType for RustPeripheralDelegate {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
const NAME: &'static str = "RustPeripheralDelegate";
}
impl DeclaredClass for RustPeripheralDelegate {
type Ivars = PeripheralDelegateIvars;
}
unsafe impl NSObjectProtocol for RustPeripheralDelegate {}
unsafe impl CBPeripheralDelegate for RustPeripheralDelegate {
#[method(peripheral:didDiscoverServices:)]
fn peripheral_did_discover_services(
&self,
peripheral: &CBPeripheral,
error: Option<&NSError>,
) {
let identifier = unsafe {
peripheral.identifier().UUIDString().to_string()
};
let error_str = error.map(|e| e.localizedDescription().to_string());
log::debug!("Services discovered for {}: error={:?}", identifier, error_str);
if let Ok(guard) = self.ivars().event_tx.lock() {
if let Some(tx) = guard.as_ref() {
let _ = tx.try_send(PeripheralEvent::ServicesDiscovered {
identifier,
error: error_str,
});
}
}
}
#[method(peripheral:didDiscoverCharacteristicsForService:error:)]
fn peripheral_did_discover_characteristics_for_service(
&self,
peripheral: &CBPeripheral,
service: &CBService,
error: Option<&NSError>,
) {
let identifier = unsafe {
peripheral.identifier().UUIDString().to_string()
};
let service_uuid = unsafe { service.UUID().UUIDString().to_string() };
let error_str = error.map(|e| e.localizedDescription().to_string());
if let Ok(guard) = self.ivars().event_tx.lock() {
if let Some(tx) = guard.as_ref() {
let _ = tx.try_send(PeripheralEvent::CharacteristicsDiscovered {
identifier,
service_uuid,
error: error_str,
});
}
}
}
#[method(peripheral:didUpdateValueForCharacteristic:error:)]
fn peripheral_did_update_value_for_characteristic(
&self,
peripheral: &CBPeripheral,
characteristic: &CBCharacteristic,
error: Option<&NSError>,
) {
let identifier = unsafe {
peripheral.identifier().UUIDString().to_string()
};
let characteristic_uuid = unsafe { characteristic.UUID().UUIDString().to_string() };
let value = unsafe {
characteristic.value()
.map(|d| d.bytes().to_vec())
.unwrap_or_default()
};
let error_str = error.map(|e| e.localizedDescription().to_string());
if let Ok(guard) = self.ivars().event_tx.lock() {
if let Some(tx) = guard.as_ref() {
let _ = tx.try_send(PeripheralEvent::CharacteristicValueUpdated {
identifier,
characteristic_uuid,
value,
error: error_str,
});
}
}
}
#[method(peripheral:didWriteValueForCharacteristic:error:)]
fn peripheral_did_write_value_for_characteristic(
&self,
peripheral: &CBPeripheral,
characteristic: &CBCharacteristic,
error: Option<&NSError>,
) {
let identifier = unsafe {
peripheral.identifier().UUIDString().to_string()
};
let characteristic_uuid = unsafe { characteristic.UUID().UUIDString().to_string() };
let error_str = error.map(|e| e.localizedDescription().to_string());
if let Ok(guard) = self.ivars().event_tx.lock() {
if let Some(tx) = guard.as_ref() {
let _ = tx.try_send(PeripheralEvent::CharacteristicWritten {
identifier,
characteristic_uuid,
error: error_str,
});
}
}
}
#[method(peripheral:didUpdateNotificationStateForCharacteristic:error:)]
fn peripheral_did_update_notification_state_for_characteristic(
&self,
peripheral: &CBPeripheral,
characteristic: &CBCharacteristic,
error: Option<&NSError>,
) {
let identifier = unsafe {
peripheral.identifier().UUIDString().to_string()
};
let characteristic_uuid = unsafe { characteristic.UUID().UUIDString().to_string() };
let enabled = unsafe { characteristic.isNotifying() };
let error_str = error.map(|e| e.localizedDescription().to_string());
if let Ok(guard) = self.ivars().event_tx.lock() {
if let Some(tx) = guard.as_ref() {
let _ = tx.try_send(PeripheralEvent::NotificationStateChanged {
identifier,
characteristic_uuid,
enabled,
error: error_str,
});
}
}
}
#[method(peripheral:didReadRSSI:error:)]
fn peripheral_did_read_rssi(
&self,
peripheral: &CBPeripheral,
rssi: &NSNumber,
error: Option<&NSError>,
) {
let identifier = unsafe {
peripheral.identifier().UUIDString().to_string()
};
let rssi_val = rssi.as_i8();
let error_str = error.map(|e| e.localizedDescription().to_string());
if let Ok(guard) = self.ivars().event_tx.lock() {
if let Some(tx) = guard.as_ref() {
let _ = tx.try_send(PeripheralEvent::RssiRead {
identifier,
rssi: rssi_val,
error: error_str,
});
}
}
}
}
);
impl RustPeripheralDelegate {
pub fn new(event_tx: mpsc::Sender<PeripheralEvent>) -> Retained<Self> {
let this = Self::alloc();
let this = this.set_ivars(PeripheralDelegateIvars {
event_tx: Mutex::new(Some(event_tx)),
});
unsafe { msg_send_id![super(this), init] }
}
pub fn as_protocol(&self) -> &ProtocolObject<dyn CBPeripheralDelegate> {
ProtocolObject::from_ref(self)
}
}
pub struct PeripheralManagerDelegateIvars {
event_tx: Mutex<Option<mpsc::Sender<PeripheralManagerEvent>>>,
characteristic_values: Mutex<HashMap<String, Vec<u8>>>,
}
impl Default for PeripheralManagerDelegateIvars {
fn default() -> Self {
Self {
event_tx: Mutex::new(None),
characteristic_values: Mutex::new(HashMap::new()),
}
}
}
declare_class!(
pub struct RustPeripheralManagerDelegate;
unsafe impl ClassType for RustPeripheralManagerDelegate {
type Super = NSObject;
type Mutability = mutability::InteriorMutable;
const NAME: &'static str = "RustPeripheralManagerDelegate";
}
impl DeclaredClass for RustPeripheralManagerDelegate {
type Ivars = PeripheralManagerDelegateIvars;
}
unsafe impl NSObjectProtocol for RustPeripheralManagerDelegate {}
unsafe impl CBPeripheralManagerDelegate for RustPeripheralManagerDelegate {
#[method(peripheralManagerDidUpdateState:)]
fn peripheral_manager_did_update_state(&self, peripheral: &CBPeripheralManager) {
let state_raw = unsafe { peripheral.state() };
let state = CentralState::from_raw(state_raw.0);
log::debug!("Peripheral manager state changed: {:?}", state);
if let Ok(guard) = self.ivars().event_tx.lock() {
if let Some(tx) = guard.as_ref() {
let _ = tx.try_send(PeripheralManagerEvent::StateChanged(state));
}
}
}
#[method(peripheralManager:didAddService:error:)]
fn peripheral_manager_did_add_service(
&self,
_peripheral: &CBPeripheralManager,
service: &CBService,
error: Option<&NSError>,
) {
let service_uuid = unsafe { service.UUID().UUIDString().to_string() };
let error_str = error.map(|e| e.localizedDescription().to_string());
log::debug!("Service {} added: error={:?}", service_uuid, error_str);
if let Ok(guard) = self.ivars().event_tx.lock() {
if let Some(tx) = guard.as_ref() {
let _ = tx.try_send(PeripheralManagerEvent::ServiceAdded {
service_uuid,
error: error_str,
});
}
}
}
#[method(peripheralManagerDidStartAdvertising:error:)]
fn peripheral_manager_did_start_advertising(
&self,
_peripheral: &CBPeripheralManager,
error: Option<&NSError>,
) {
let error_str = error.map(|e| e.localizedDescription().to_string());
if let Some(ref e) = error_str {
log::warn!("Advertising failed to start: {}", e);
} else {
log::info!("Advertising started successfully");
}
if let Ok(guard) = self.ivars().event_tx.lock() {
if let Some(tx) = guard.as_ref() {
let _ = tx.try_send(PeripheralManagerEvent::AdvertisingStarted { error: error_str });
}
}
}
#[method(peripheralManagerIsReadyToUpdateSubscribers:)]
fn peripheral_manager_is_ready_to_update_subscribers(
&self,
_peripheral: &CBPeripheralManager,
) {
log::trace!("Ready to update subscribers");
if let Ok(guard) = self.ivars().event_tx.lock() {
if let Some(tx) = guard.as_ref() {
let _ = tx.try_send(PeripheralManagerEvent::ReadyToUpdateSubscribers);
}
}
}
#[method(peripheralManager:didReceiveReadRequest:)]
fn peripheral_manager_did_receive_read_request(
&self,
peripheral: &CBPeripheralManager,
request: &CBATTRequest,
) {
let characteristic_uuid = unsafe {
request.characteristic().UUID().UUIDString().to_string()
};
let offset = unsafe { request.offset() } as usize;
let central_id = unsafe {
request.central().identifier().UUIDString().to_string()
};
log::debug!(
"Read request from {} for characteristic {} at offset {}",
central_id,
characteristic_uuid,
offset
);
let response_result = if let Ok(values) = self.ivars().characteristic_values.lock() {
if let Some(value) = values.get(&characteristic_uuid) {
if offset <= value.len() {
let response_data = &value[offset..];
unsafe {
let ns_data = NSData::with_bytes(response_data);
request.setValue(Some(&ns_data));
}
CBATTError::Success
} else {
CBATTError::InvalidOffset
}
} else {
log::warn!("No value stored for characteristic {}", characteristic_uuid);
CBATTError::AttributeNotFound
}
} else {
CBATTError::UnlikelyError
};
unsafe {
peripheral.respondToRequest_withResult(request, response_result);
}
if let Ok(guard) = self.ivars().event_tx.lock() {
if let Some(tx) = guard.as_ref() {
let _ = tx.try_send(PeripheralManagerEvent::ReadRequest {
request_id: 0, central_identifier: central_id,
characteristic_uuid,
offset,
});
}
}
}
#[method(peripheralManager:didReceiveWriteRequests:)]
fn peripheral_manager_did_receive_write_requests(
&self,
peripheral: &CBPeripheralManager,
requests: &NSArray<CBATTRequest>,
) {
log::debug!("Received {} write request(s)", requests.len());
for i in 0..requests.len() {
let request = &requests[i];
let characteristic_uuid = unsafe {
request.characteristic().UUID().UUIDString().to_string()
};
let offset = unsafe { request.offset() } as usize;
let central_id = unsafe {
request.central().identifier().UUIDString().to_string()
};
let value = unsafe {
request.value()
.map(|d| d.bytes().to_vec())
.unwrap_or_default()
};
log::debug!(
"Write request from {} for {} at offset {}: {} bytes",
central_id,
characteristic_uuid,
offset,
value.len()
);
if let Ok(mut values) = self.ivars().characteristic_values.lock() {
if offset == 0 {
values.insert(characteristic_uuid.clone(), value.clone());
} else {
let entry = values.entry(characteristic_uuid.clone()).or_default();
if offset <= entry.len() {
entry.truncate(offset);
entry.extend_from_slice(&value);
} else {
entry.resize(offset, 0);
entry.extend_from_slice(&value);
}
}
}
if let Ok(guard) = self.ivars().event_tx.lock() {
if let Some(tx) = guard.as_ref() {
let _ = tx.try_send(PeripheralManagerEvent::WriteRequest {
request_id: i as u64,
central_identifier: central_id,
characteristic_uuid,
value,
offset,
response_needed: true, });
}
}
}
if !requests.is_empty() {
unsafe {
peripheral.respondToRequest_withResult(&requests[0], CBATTError::Success);
}
}
}
}
);
impl RustPeripheralManagerDelegate {
pub fn new(event_tx: mpsc::Sender<PeripheralManagerEvent>) -> Retained<Self> {
let this = Self::alloc();
let this = this.set_ivars(PeripheralManagerDelegateIvars {
event_tx: Mutex::new(Some(event_tx)),
characteristic_values: Mutex::new(HashMap::new()),
});
unsafe { msg_send_id![super(this), init] }
}
pub fn as_protocol(&self) -> &ProtocolObject<dyn CBPeripheralManagerDelegate> {
ProtocolObject::from_ref(self)
}
pub fn set_characteristic_value(&self, characteristic_uuid: &str, value: Vec<u8>) {
if let Ok(mut values) = self.ivars().characteristic_values.lock() {
values.insert(characteristic_uuid.to_string(), value);
log::debug!(
"Set characteristic {} value ({} bytes)",
characteristic_uuid,
values
.get(characteristic_uuid)
.map(|v| v.len())
.unwrap_or(0)
);
}
}
pub fn get_characteristic_value(&self, characteristic_uuid: &str) -> Option<Vec<u8>> {
if let Ok(values) = self.ivars().characteristic_values.lock() {
values.get(characteristic_uuid).cloned()
} else {
None
}
}
}
pub struct CentralDelegate {
pub event_tx: mpsc::Sender<CentralEvent>,
}
impl CentralDelegate {
pub fn new(event_tx: mpsc::Sender<CentralEvent>) -> Self {
Self { event_tx }
}
}
pub struct PeripheralDelegate {
pub event_tx: mpsc::Sender<PeripheralEvent>,
}
impl PeripheralDelegate {
pub fn new(event_tx: mpsc::Sender<PeripheralEvent>) -> Self {
Self { event_tx }
}
}
pub struct PeripheralManagerDelegate {
pub event_tx: mpsc::Sender<PeripheralManagerEvent>,
}
impl PeripheralManagerDelegate {
pub fn new(event_tx: mpsc::Sender<PeripheralManagerEvent>) -> Self {
Self { event_tx }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_central_state_from_raw() {
assert_eq!(CentralState::from_raw(0), CentralState::Unknown);
assert_eq!(CentralState::from_raw(4), CentralState::PoweredOff);
assert_eq!(CentralState::from_raw(5), CentralState::PoweredOn);
assert_eq!(CentralState::from_raw(99), CentralState::Unknown);
}
#[test]
fn test_central_state_is_ready() {
assert!(!CentralState::Unknown.is_ready());
assert!(!CentralState::PoweredOff.is_ready());
assert!(CentralState::PoweredOn.is_ready());
}
}