2020-12-19 05:13:44 +00:00
//
// B L E R e c e i v e r . s w i f t
//
// C o p y r i g h t 2 0 2 0 V M w a r e , I n c .
// S P D X - L i c e n s e - I d e n t i f i e r : M I T
//
import Foundation
import CoreBluetooth
import os
/* *
Beacon receiver scans for peripherals with fixed service UUID .
*/
protocol BLEReceiver : Sensor {
}
/* *
Beacon receiver scans for peripherals with fixed service UUID in foreground and background modes . Background scan
for Android is trivial as scanForPeripherals will always return all Android devices on every call . Background scan for iOS
devices that are transmitting in background mode is more complex , requiring an open connection to subscribe to a
notifying characteristic that is used as trigger for keeping both iOS devices in background state ( rather than suspended
or killed ) . For iOS - iOS devices , on detection , the receiver will ( 1 ) write blank data to the transmitter , which triggers the
transmitter to send a characteristic data update after 8 seconds , which in turns ( 2 ) triggers the receiver to receive a value
update notification , to ( 3 ) create the opportunity for a read RSSI call and repeat of this looped process that keeps both
devices awake .
Please note , the iOS - iOS process is unreliable if ( 1 ) the user switches off bluetooth via Airplane mode settings , ( 2 ) the
device reboots , and ( 3 ) it will fail completely if the app has been killed by the user . These are conditions that cannot be
handled reliably by CoreBluetooth state restoration .
*/
class ConcreteBLEReceiver : NSObject , BLEReceiver , BLEDatabaseDelegate , CBCentralManagerDelegate , CBPeripheralDelegate {
private let logger = ConcreteSensorLogger ( subsystem : " Sensor " , category : " BLE.ConcreteBLEReceiver " )
private var delegates : [ SensorDelegate ] = [ ]
private var connectionDelegate : SensorDelegate ?
// / D e d i c a t e d s e q u e n t i a l q u e u e f o r a l l b e a c o n t r a n s m i t t e r a n d r e c e i v e r t a s k s .
private let queue : DispatchQueue !
private let delegateQueue : DispatchQueue
// / D a t a b a s e o f p e r i p h e r a l s
private let database : BLEDatabase
// / P a y l o a d d a t a s u p p l i e r f o r p a r s i n g s h a r e d p a y l o a d s
private let payloadDataSupplier : PayloadDataSupplier
// / C e n t r a l m a n a g e r f o r m a n a g i n g a l l c o n n e c t i o n s , u s i n g a s i n g l e m a n a g e r f o r s i m p l i c i t y .
2021-02-02 00:04:43 +00:00
private var central : CBCentralManager ?
2020-12-19 05:13:44 +00:00
// / D u m m y d a t a f o r w r i t i n g t o t h e t r a n s m i t t e r t o t r i g g e r s t a t e r e s t o r a t i o n o r r e s u m e f r o m s u s p e n d s t a t e t o b a c k g r o u n d s t a t e .
private let emptyData = Data ( repeating : 0 , count : 0 )
/* *
Shifting timer for triggering peripheral scan just before the app switches from background to suspend state following a
call to CoreBluetooth delegate methods . Apple documentation suggests the time limit is about 10 seconds .
*/
private var scanTimer : DispatchSourceTimer ?
// / D e d i c a t e d s e q u e n t i a l q u e u e f o r t h e s h i f t i n g t i m e r .
private let scanTimerQueue = DispatchQueue ( label : " Sensor.BLE.ConcreteBLEReceiver.ScanTimer " )
// / D e d i c a t e d s e q u e n t i a l q u e u e f o r t h e a c t u a l s c a n c a l l .
private let scheduleScanQueue = DispatchQueue ( label : " Sensor.BLE.ConcreteBLEReceiver.ScheduleScan " )
// / T r a c k s c a n i n t e r v a l a n d u p t i m e s t a t i s t i c s f o r t h e r e c e i v e r , f o r d e b u g p u r p o s e s .
private let statistics = TimeIntervalSample ( )
// / S c a n r e s u l t q u e u e f o r r e c o r d i n g d i s c o v e r e d d e v i c e s w i t h n o i m m e d i a t e p e n d i n g a c t i o n .
private var scanResults : [ BLEDevice ] = [ ]
// / C r e a t e a B L E r e c e i v e r t h a t s h a r e s t h e s a m e s e q u e n t i a l d i s p a t c h q u e u e a s t h e t r a n s m i t t e r b e c a u s e c o n c u r r e n t t r a n s m i t a n d r e c e i v e
// / o p e r a t i o n s i m p a c t s C o r e B l u e t o o t h s t a b i l i t y . T h e r e c e i v e r a n d t r a n s m i t t e r s h a r e a c o m m o n d a t a b a s e o f d e v i c e s t o e n a b l e t h e t r a n s m i t t e r
// / t o r e g i s t e r c e n t r a l s f o r r e s o l u t i o n b y t h e r e c e i v e r a s p e r i p h e r a l s t o c r e a t e s y m m e t r i c c o n n e c t i o n s . T h e p a y l o a d d a t a s u p p l i e r p r o v i d e s
// / t h e a c t u a l p a y l o a d d a t a t o b e t r a n s m i t t e d a n d r e c e i v e d v i a B L E .
required init ( queue : DispatchQueue , delegateQueue : DispatchQueue , database : BLEDatabase , payloadDataSupplier : PayloadDataSupplier ) {
self . queue = queue
self . delegateQueue = delegateQueue
self . database = database
self . payloadDataSupplier = payloadDataSupplier
super . init ( )
database . add ( delegate : self )
}
func add ( delegate : SensorDelegate ) {
delegates . append ( delegate )
}
func addConnectionDelegate ( delegate : SensorDelegate ) {
connectionDelegate = delegate
}
func removeConnectionDelegate ( ) {
connectionDelegate = nil
}
func start ( ) {
logger . debug ( " start " )
if central = = nil {
self . central = CBCentralManager ( delegate : self , queue : queue , options : [
CBCentralManagerOptionRestoreIdentifierKey : " Sensor.BLE.ConcreteBLEReceiver " ,
// S e t t h i s t o f a l s e t o s t o p i O S f r o m d i s p l a y i n g a n a l e r t i f t h e a p p i s o p e n e d w h i l e b l u e t o o t h i s o f f .
CBCentralManagerOptionShowPowerAlertKey : false ] )
}
// S t a r t s c a n n i n g
2021-02-02 00:04:43 +00:00
if central ? . state = = . poweredOn {
2020-12-19 05:13:44 +00:00
scan ( " start " )
}
}
func stop ( ) {
logger . debug ( " stop " )
2021-02-02 00:04:43 +00:00
guard let central = central else {
2020-12-19 05:13:44 +00:00
return
}
guard central . isScanning else {
logger . fault ( " stop denied, already stopped " )
2021-02-02 00:04:43 +00:00
self . central = nil
2020-12-19 05:13:44 +00:00
return
}
// S t o p s c a n n i n g
scanTimer ? . cancel ( )
scanTimer = nil
queue . async {
2021-02-02 00:04:43 +00:00
central . stopScan ( )
2020-12-19 05:13:44 +00:00
self . central = nil
}
// C a n c e l a l l c o n n e c t i o n s , t h e r e s u l t i n g d i d D i s c o n n e c t a n d d i d F a i l T o C o n n e c t
database . devices ( ) . forEach ( ) { device in
if let peripheral = device . peripheral , peripheral . state != . disconnected {
disconnect ( " stop " , peripheral )
}
}
}
// MARK: - S c a n f o r p e r i p h e r a l s a n d i n i t i a t e c o n n e c t i o n i f r e q u i r e d
// / A l l w o r k s t a r t s f r o m s c a n l o o p .
func scan ( _ source : String ) {
statistics . add ( )
logger . debug ( " scan (source= \( source ) ,statistics={ \( statistics . description ) }) " )
2021-02-02 00:04:43 +00:00
guard central ? . state = = . poweredOn else {
2020-12-19 05:13:44 +00:00
logger . fault ( " scan failed, bluetooth is not powered on " )
return
}
// S c a n f o r p e r i p e r a l s a d v e r t i s i n g t h e s e n s o r s e r v i c e .
// T h i s w i l l f i n d a l l A n d r o i d a n d i O S f o r e g r o u n d a d v e r t s
// b u t i t w i l l m i s s t h e i O S b a c k g r o u n d a d v e r t s u n l e s s
// l o c a t i o n h a s b e e n e n a b l e d a n d s c r e e n i s o n f o r a m o m e n t .
queue . async { self . taskScanForPeripherals ( ) }
// R e g i s t e r c o n n e c t e d p e r i p h e r a l s t h a t a r e a d v e r t i s i n g t h e
// s e n s o r s e r v i c e . T h i s c a t c h e s t h e o r p h a n p e r i p h e r a l s t h a t
// m a y h a v e b e e n m i s s e d b y C o r e B l u e t o o t h d u r i n g s t a t e
// r e s t o r a t i o n o r i n t e r n a l e r r o r s .
queue . async { self . taskRegisterConnectedPeripherals ( ) }
// R e s o l v e p e r i p h e r a l s b y d e v i c e i d e n t i f i e r o b t a i n e d v i a
// t h e t r a n s m i t t e r . W h e n a n i O S c e n t r a l c o n n e c t s t o t h i s
// p e r i p h e r a l , t h e t r a n s m i t t e r c o d e r e g i s t e r s t h e c e n t r a l ' s
// a d d r e s s a s a n e w d e v i c e p e n d i n g r e s o l u t i o n h e r e t o
// e s t a b l i s h a s y m m e t r i c c o n n e c t i o n . T h i s e n a b l e s e i t h e r
// d e v i c e t o d e t e c t t h e o t h e r ( e . g . w i t h s c r e e n o n )
// a n d t r i g g e r i n g b o t h d e v i c e s t o d e t e c t e a c h o t h e r .
queue . async { self . taskResolveDevicePeripherals ( ) }
// R e m o v e d e v i c e s t h a t h a v e n o t b e e n s e e n f o r a w h i l e a s
// t h e i d e n t i f i e r w o u l d h a v e c h a n g e d a f t e r a b o u t 2 0 m i n s ,
// t h u s i t i s w a s t e f u l t o m a i n t a i n a r e f e r e n c e .
queue . async { self . taskRemoveExpiredDevices ( ) }
// R e m o v e d u p l i c a t e d e v i c e s w i t h t h e s a m e p a y l o a d b u t
// d i f f e r e n t i d e n t i f i e r s . T h i s h a p p e n s f r e q u e n t l y a s
// d e v i c e a d d r e s s c h a n g e s a t r e g u l a r i n t e r v a l s a s p a r t
// o f t h e B l u e t o o t h p r i v a c y f e a t u r e , t h u s i t l o o k s l i k e
// a n e w d e v i c e b u t i s a c t u a l l y a s s o c i a t e d w i t h t h e s a m e
// p a y l o a d . A l l r e f e r e n c e s t o t h e d u p l i c a t e w i l l b e
// r e m o v e d b u t t h e a c t u a l c o n n e c t i o n w i l l b e t e r m i n a t e d
// b y C o r e B l u e t o o t h , o f t e n s h o w i n g a n A P I m i s u s e w a r n i n g
// w h i c h c a n b e i g n o r e d .
queue . async { self . taskRemoveDuplicatePeripherals ( ) }
// i O S d e v i c e s a r e k e p t i n b a c k g r o u n d s t a t e i n d e f i n i t e l y
// ( i n s t e a d o f d r o p p i n g i n t o s u s p e n d e d o r t e r m i n a t e d s t a t e )
// b y a s e r i e s o f t i m e d e l a y e d B L E o p e r a t i o n s . W h i l e t h i s
// d e v i c e i s a w a k e , i t w i l l w r i t e d a t a t o o t h e r i O S d e v i c e s
// t o k e e p t h e m a w a k e , a n d v i c e v e r s a .
queue . async { self . taskWakeTransmitters ( ) }
// A l l d e v i c e s h a v e a n u p p e r l i m i t o n t h e n u m b e r o f c o n c u r r e n t
// B L E c o n n e c t i o n s i t c a n m a i n t a i n . F o r i O S , i t i s u s u a l l y 1 2
// o r a b o v e . i O S d e v i c e s m a i n t a i n a n a c t i v e c o n n e c t i o n w i t h
// o t h e r i O S d e v i c e s t o k e e p a w a k e a n d o b t a i n r e g u l a r R S S I
// m e a s u r e m e n t s , t h u s i t c a n t r a c k u p t o 1 2 i O S d e v i c e s a t a n y
// m o m e n t i n t i m e . A b o v e t h i s f i g u r e , t h i s d e v i c e w i l l n e e d
// t o r o t a t e ( d i s c o n n e c t / c o n n e c t ) c o n n e c t i o n s t o m u l t i p l e x
// b e t w e e n t h e i O S d e v i c e s f o r c o v e r a g e . T h i s i s u n n e c e s s a r y
// f o r t r a c k i n g A n d r o i d d e v i c e s a s t h e y a r e t r a c k e d b y s c a n
// o n l y . A c o n n e c t i o n t o A n d r o i d i s o n l y r e q u i r e d f o r r e a d i n g
// i t s p a y l o a d u p o n d i s c o v e r y .
queue . async { self . taskIosMultiplex ( ) }
// C o n n e c t t o d i s c o v e r e d d e v i c e s i f t h e d e v i c e h a s p e n d i n g t a s k s .
// T h e v a s t m a j o r i t y o f d e v i c e s w i l l b e c o n n e c t e d i m m e d i a t e l y u p o n
// d i s c o v e r y , i f t h e y h a v e a p e n d i n g t a s k ( e . g . t o e s t a b l i s h i t s
// o p e r a t i n g s y s t e m o r r e a d i t s p a y l o a d ) . D e v i c e s m a y b e d i s c o v e r e d
// b u t n o t h a v e a p e n d i n g t a s k i f t h e y h a v e a l r e a d y b e e n f u l l y
// r e s o l v e d ( e . g . h a s o p e r a t i n g s y s t e m , p a y l o a d a n d r e c e n t R S S I
// m e a s u r e m n e t ) , t h e s e a r e p l a c e d i n t h e s c a n r e s u l t s q u e u e f o r
// r e g u l a r c h e c k i n g b y t h i s c o n n e c t t a s k ( e . g . t o r e a d R S S I i f
// t h e e x i s t i n g v a l u e i s n o w o u t o f d a t e ) .
queue . async { self . taskConnect ( ) }
// S c h e d u l e t h i s s c a n c a l l a g a i n f o r e x e c u t i o n i n a t l e a s t 8 s e c o n d s
// t i m e t o r e p e a t t h e s c a n l o o p . T h e a c t u a l c a l l m a y b e d e l a y e d b e y o n d
// t h e 8 s e c o n d d e l a y f r o m t h i s p o i n t b e c a u s e a l l t e r m i n a t i n g o p e r a t i o n s
// ( i . e . e v e n t s t h a t w i l l e v e n t u a l l y l e a d t h e a p p t o e n t e r s u s p e n d e d
// s t a t e i f n o t h i n g e l s e h a p p e n s ) c a l l s t h i s f u n c t i o n t o k e e p t h e l o o p
// r u n n i n g i n d e f i n i t e l y . T h e 8 o r l e s s s e c o n d s d e l a y w a s c h o s e n t o
// e n s u r e t h e s c a n c a l l i s a c t i v a t e d b e f o r e t h e a p p n a t u r a l l y e n t e r s
// s u s p e n d e d s t a t e , b u t n o t s o s o o n t h e l o o p r u n s t o o o f t e n .
scheduleScan ( " scan " )
}
/* *
Schedule scan for beacons after a delay of 8 seconds to start scan again just before
state change from background to suspended . Scan is sufficient for finding Android
devices repeatedly in both foreground and background states .
*/
private func scheduleScan ( _ source : String ) {
scheduleScanQueue . sync {
scanTimer ? . cancel ( )
scanTimer = DispatchSource . makeTimerSource ( queue : scanTimerQueue )
scanTimer ? . schedule ( deadline : DispatchTime . now ( ) + BLESensorConfiguration . notificationDelay )
scanTimer ? . setEventHandler { [ weak self ] in
self ? . scan ( " scheduleScan| " + source )
}
scanTimer ? . resume ( )
}
}
/* *
Scan for peripherals advertising the beacon service .
*/
private func taskScanForPeripherals ( ) {
// S c a n f o r p e r i p h e r a l s - > d i d D i s c o v e r
2021-02-02 00:04:43 +00:00
central ? . scanForPeripherals (
2020-12-19 05:13:44 +00:00
withServices : [ BLESensorConfiguration . serviceUUID ] ,
options : [ CBCentralManagerScanOptionSolicitedServiceUUIDsKey : [ BLESensorConfiguration . serviceUUID ] ] )
}
/* *
Register all connected peripherals advertising the sensor service as a device .
*/
private func taskRegisterConnectedPeripherals ( ) {
2021-02-02 00:04:43 +00:00
central ? . retrieveConnectedPeripherals ( withServices : [ BLESensorConfiguration . serviceUUID ] ) . forEach ( ) { peripheral in
2020-12-19 05:13:44 +00:00
let targetIdentifier = TargetIdentifier ( peripheral : peripheral )
let device = database . device ( targetIdentifier )
if device . peripheral = = nil || device . peripheral != peripheral {
logger . debug ( " taskRegisterConnectedPeripherals (device= \( device ) ) " )
_ = database . device ( peripheral , delegate : self )
}
}
}
/* *
Resolve peripheral for all database devices . This enables the symmetric connection feature where connections from central to peripheral ( BLETransmitter ) registers the existence
of a potential peripheral for resolution by this central ( BLEReceiver ) .
*/
private func taskResolveDevicePeripherals ( ) {
let devicesToResolve = database . devices ( ) . filter { $0 . peripheral = = nil }
devicesToResolve . forEach ( ) { device in
guard let identifier = UUID ( uuidString : device . identifier ) else {
return
}
2021-02-02 00:04:43 +00:00
if let peripherals = central ? . retrievePeripherals ( withIdentifiers : [ identifier ] ) , let peripheral = peripherals . last {
2020-12-19 05:13:44 +00:00
logger . debug ( " taskResolveDevicePeripherals (resolved= \( device ) ) " )
_ = database . device ( peripheral , delegate : self )
}
}
}
/* *
Remove devices that have not been updated for over an hour , as the UUID is likely to have changed after being out of range for over 20 minutes , so it will require discovery .
*/
private func taskRemoveExpiredDevices ( ) {
let devicesToRemove = database . devices ( ) . filter { Date ( ) . timeIntervalSince ( $0 . lastUpdatedAt ) > BluetraceConfig . PeripheralCleanInterval }
devicesToRemove . forEach ( ) { device in
logger . debug ( " taskRemoveExpiredDevices (remove= \( device ) ) " )
database . delete ( device . identifier )
if let peripheral = device . peripheral {
disconnect ( " taskRemoveExpiredDevices " , peripheral )
}
}
}
/* *
Remove devices with the same payload data but different peripherals .
*/
private func taskRemoveDuplicatePeripherals ( ) {
var index : [ PayloadData : BLEDevice ] = [ : ]
let devices = database . devices ( )
devices . forEach ( ) { device in
guard let payloadData = device . payloadData else {
return
}
guard let duplicate = index [ payloadData ] else {
index [ payloadData ] = device
return
}
var keeping = device
if device . peripheral != nil , duplicate . peripheral = = nil {
keeping = device
} else if duplicate . peripheral != nil , device . peripheral = = nil {
keeping = duplicate
} else if device . payloadDataLastUpdatedAt > duplicate . payloadDataLastUpdatedAt {
keeping = device
} else {
keeping = duplicate
}
let discarding = ( keeping . identifier = = device . identifier ? duplicate : device )
index [ payloadData ] = keeping
database . delete ( discarding . identifier )
self . logger . debug ( " taskRemoveDuplicatePeripherals (payload= \( payloadData . shortName ) ,device= \( device . identifier ) ,duplicate= \( duplicate . identifier ) ,keeping= \( keeping . identifier ) ) " )
// C o r e B l u e t o o t h w i l l e v e n t u a l l y g i v e w a r n i n g a n d d i s c o n n e c t a c t u a l d u p l i c a t e s i l e n t l y .
// W h i l e c a l l i n g d i s c o n n e c t h e r e i s c l e a n e r b u t i t w i l l t r i g g e r d i d D i s c o v e r a n d
// r e t a i n t h e d u p l i c a t e s . E x p e c t t o s e e m e s s a g e :
// [ C o r e B l u e t o o t h ] A P I M I S U S E : F o r c i n g d i s c o n n e c t i o n o f u n u s e d p e r i p h e r a l
// < C B P e r i p h e r a l : X X X , i d e n t i f i e r = X X X , n a m e = i P h o n e , s t a t e = c o n n e c t e d > .
// D i d y o u f o r g e t t o c a n c e l t h e c o n n e c t i o n ?
}
}
/* *
Wake transmitter on all connected iOS devices
*/
private func taskWakeTransmitters ( ) {
database . devices ( ) . forEach ( ) { device in
guard device . operatingSystem = = . ios , let peripheral = device . peripheral , peripheral . state = = . connected else {
return
}
guard device . timeIntervalSinceLastUpdate < TimeInterval . minute else {
// T h r o t t l e b a c k k e e p a w a k e c a l l s w h e n o u t o f r a n g e , i s s u e p e n d i n g c o n n e c t i n s t e a d
connect ( " taskWakeTransmitters " , peripheral )
return
}
wakeTransmitter ( " taskWakeTransmitters " , device )
}
}
/* *
Connect to devices and maintain concurrent connection quota
*/
private func taskConnect ( ) {
// G e t r e c e n t l y d i s c o v e r e d d e v i c e s
let didDiscover = taskConnectScanResults ( )
// I d e n t i f y r e c e n t l y d i s c o v e r e d d e v i c e s w i t h p e n d i n g t a s k s : c o n n e c t - > n e x t T a s k
let hasPendingTask = didDiscover . filter ( { deviceHasPendingTask ( $0 ) } )
// I d e n t i f y a l l c o n n e c t e d ( i O S ) d e v i c e s t o t r i g g e r r e f r e s h : c o n n e c t - > n e x t T a s k
let toBeRefreshed = database . devices ( ) . filter ( { ! hasPendingTask . contains ( $0 ) && $0 . peripheral ? . state = = . connected } )
// I d e n t i f y a l l u n c o n n e c t e d d e v i c e s w i t h u n k n o w n o p e r a t i n g s y s t e m , t h e s e a r e
// c r e a t e d b y C o n c r e t e B L E T r a n s m i t t e r o n c h a r a c t e r i s t i c w r i t e , t o e n s u r e a l l
// c e n t r a l s t h a t c o n n e c t t o t h i s p e r i p h e r a l a r e r e c o r d e d , t o e n a b l e t h i s c e n t r a l
// t o a t t e m p t c o n n e c t i o n t o t h e p e r i p h e r a l , t h u s e s t a b l i s h i n g a b i - d i r e c t i o n a l
// c o n n e c t i o n . T h i s i s e s s e n t i a l f o r i O S - i O S b a c k g r o u n d d e t e c t i o n , w h e r e t h e
// d i s c o v e r y o f p h o n e B b y p h o n e A , a n d a c o n n e c t i o n f r o m A t o B , w i l l t r i g g e r
// B t o c o n n e c t t o A , t h u s a s s u m i n g l o c a t i o n p e r m i s s i o n h a s b e e n e n a b l e d , i t
// w i l l o n l y r e q u i r e s c r e e n O N a t e i t h e r p h o n e t o t r i g g e r b i - d i r e c t i o n a l c o n n e c t i o n .
let asymmetric = database . devices ( ) . filter ( { ! hasPendingTask . contains ( $0 )
&& $0 . operatingSystem = = . unknown
&& $0 . timeIntervalSinceLastUpdate < TimeInterval . minute
&& $0 . peripheral ? . state != . connected } )
// C o n n e c t t o r e c e n t l y d i s c o v e r e d d e v i c e s w i t h p e n d i n g t a s k s
hasPendingTask . forEach ( ) { device in
guard let peripheral = device . peripheral else {
return
}
connect ( " taskConnect|hasPending " , peripheral ) ;
}
// R e f r e s h c o n n e c t i o n t o e x i s t i n g d e v i c e s t o t r i g g e r n e x t t a s k
toBeRefreshed . forEach ( ) { device in
guard let peripheral = device . peripheral else {
return
}
connect ( " taskConnect|refresh " , peripheral ) ;
}
// C o n n e c t t o u n k n o w n d e v i c e s t h a t h a v e w r i t t e n t o t h i s p e r i p h e r a l
asymmetric . forEach ( ) { device in
guard let peripheral = device . peripheral else {
return
}
connect ( " taskConnect|asymmetric " , peripheral ) ;
}
}
// / E m p t y s c a n r e s u l t s t o p r o d u c e a l i s t o f r e c e n t l y d i s c o v e r e d d e v i c e s f o r c o n n e c t i o n a n d p r o c e s s i n g
private func taskConnectScanResults ( ) -> [ BLEDevice ] {
var set : Set < BLEDevice > = [ ]
var list : [ BLEDevice ] = [ ]
while let device = scanResults . popLast ( ) {
if set . insert ( device ) . inserted , let peripheral = device . peripheral , peripheral . state != . connected {
list . append ( device )
logger . debug ( " taskConnectScanResults, didDiscover (device= \( device ) ) " )
}
}
return list
}
// / C h e c k i f d e v i c e h a s p e n d i n g t a s k
private func deviceHasPendingTask ( _ device : BLEDevice ) -> Bool {
// R e s o l v e o p e r a t i n g s y s t e m
if device . operatingSystem = = . unknown || device . operatingSystem = = . restored {
return true
}
// R e a d p a y l o a d
if device . payloadData = = nil {
return true
}
// P a y l o a d u p d a t e
if device . timeIntervalSinceLastPayloadDataUpdate > BluetraceConfig . PeripheralPayloadExpiry {
return true
}
// i O S s h o u l d a l w a y s b e c o n n e c t e d
if device . operatingSystem = = . ios , let peripheral = device . peripheral , peripheral . state != . connected {
return true
}
return false
}
// / C h e c k i f i O S d e v i c e i s w a i t i n g f o r c o n n e c t i o n a n d f r e e c a p a c i t y i f r e q u i r e d
private func taskIosMultiplex ( ) {
// I d e n t i f y i O S d e v i c e s
let devices = database . devices ( ) . filter ( { $0 . operatingSystem = = . ios && $0 . peripheral != nil } )
// G e t a l i s t o f c o n n e c t e d d e v i c e s a n d u p t i m e
let connected = devices . filter ( { $0 . peripheral ? . state = = . connected } ) . sorted ( by : { $0 . timeIntervalBetweenLastConnectedAndLastAdvert > $1 . timeIntervalBetweenLastConnectedAndLastAdvert } )
// G e t a l i s t o f c o n n e c t i n g d e v i c e s
let pending = devices . filter ( { $0 . peripheral ? . state != . connected } ) . sorted ( by : { $0 . lastConnectRequestedAt < $1 . lastConnectRequestedAt } )
logger . debug ( " taskIosMultiplex summary (connected= \( connected . count ) ,pending= \( pending . count ) ) " )
connected . forEach ( ) { device in
logger . debug ( " taskIosMultiplex, connected (device= \( device . description ) ,upTime= \( device . timeIntervalBetweenLastConnectedAndLastAdvert ) ) " )
}
pending . forEach ( ) { device in
logger . debug ( " taskIosMultiplex, pending (device= \( device . description ) ,downTime= \( device . timeIntervalSinceLastConnectRequestedAt ) ) " )
}
// R e t r y a l l p e n d i n g c o n n e c t i o n s i f t h e r e i s s u r p l u s c a p a c i t y
if connected . count < BLESensorConfiguration . concurrentConnectionQuota {
pending . forEach ( ) { device in
guard let toBeConnected = device . peripheral else {
return
}
connect ( " taskIosMultiplex|retry " , toBeConnected ) ;
}
}
// I n i t i a t e m u l t i p l e x i n g w h e n c a p a c i t y h a s b e e n r e a c h e d
guard connected . count > BLESensorConfiguration . concurrentConnectionQuota , pending . count > 0 , let deviceToBeDisconnected = connected . first , let peripheralToBeDisconnected = deviceToBeDisconnected . peripheral , deviceToBeDisconnected . timeIntervalBetweenLastConnectedAndLastAdvert > TimeInterval . minute else {
return
}
logger . debug ( " taskIosMultiplex, multiplexing (toBeDisconnected= \( deviceToBeDisconnected . description ) ) " )
disconnect ( " taskIosMultiplex " , peripheralToBeDisconnected )
pending . forEach ( ) { device in
guard let toBeConnected = device . peripheral else {
return
}
connect ( " taskIosMultiplex|multiplex " , toBeConnected ) ;
}
}
// / I n i t i a t e n e x t a c t i o n o n p e r i p h e r a l b a s e d o n c u r r e n t s t a t e a n d i n f o r m a t i o n a v a i l a b l e
private func taskInitiateNextAction ( _ source : String , peripheral : CBPeripheral ) {
let targetIdentifier = TargetIdentifier ( peripheral : peripheral )
let device = database . device ( peripheral , delegate : self )
logger . debug ( " time since last payload= \( device . timeIntervalSinceLastPayloadDataUpdate ) " )
if device . rssi = = nil {
// 1 . R S S I
logger . debug ( " taskInitiateNextAction (goal=rssi,peripheral= \( targetIdentifier ) ) " )
readRSSI ( " taskInitiateNextAction| " + source , peripheral )
} else if ( device . signalCharacteristic = = nil || device . payloadCharacteristic = = nil ) && device . legacyPayloadCharacteristic = = nil {
// 2 . C h a r a c t e r i s t i c s
logger . debug ( " taskInitiateNextAction (goal=characteristics,peripheral= \( targetIdentifier ) ) " )
discoverServices ( " taskInitiateNextAction| " + source , peripheral )
} else if device . payloadData = = nil {
// 3 . P a y l o a d
logger . debug ( " taskInitiateNextAction (goal=payload,peripheral= \( targetIdentifier ) ) " )
readPayload ( " taskInitiateNextAction| " + source , device )
} else if device . timeIntervalSinceLastPayloadDataUpdate > BluetraceConfig . PeripheralPayloadExpiry {
// 4 . P a y l o a d u p d a t e
logger . debug ( " taskInitiateNextAction (goal=payloadUpdate,peripheral= \( targetIdentifier ) ,elapsed= \( device . timeIntervalSinceLastPayloadDataUpdate ) ) " )
readPayload ( " taskInitiateNextAction| " + source , device )
} else if let delegatesToWrite = delegatesToWriteLegacyPayload ( device : device ) {
// 5 . W r i t e l e g a c y p a y l o a d
delegatesToWrite . forEach { ( delegate ) in
if let peripheral = device . peripheral {
writeLegacyPayload ( " didReadRSSI " , peripheral : peripheral )
delegate . didWriteToLegacyDevice ( device )
}
}
} else if device . operatingSystem != . ios {
// 6 . D i s c o n n e c t A n d r o i d
logger . debug ( " taskInitiateNextAction (goal=disconnect| \( device . operatingSystem . rawValue ) ,peripheral= \( targetIdentifier ) ) " )
disconnect ( " taskInitiateNextAction| " + source , peripheral )
} else {
// 7 . S c a n
logger . debug ( " taskInitiateNextAction (goal=scan,peripheral= \( targetIdentifier ) ) " )
scheduleScan ( " taskInitiateNextAction| " + source )
}
}
/* *
Connect peripheral . Scanning is stopped temporarily , as recommended by Apple documentation , before initiating connect , otherwise
pending scan operations tend to take priority and connect takes longer to start . Scanning is scheduled to resume later , to ensure scan
resumes , even if connect fails .
*/
private func connect ( _ source : String , _ peripheral : CBPeripheral ) {
let device = database . device ( peripheral , delegate : self )
logger . debug ( " connect (source= \( source ) ,device= \( device ) ) " )
2021-02-02 00:04:43 +00:00
guard central ? . state = = . poweredOn else {
2020-12-19 05:13:44 +00:00
logger . fault ( " connect denied, central not powered on (source= \( source ) ,device= \( device ) ) " )
return
}
queue . async {
device . lastConnectRequestedAt = Date ( )
2021-02-02 00:04:43 +00:00
guard let central = self . central else {
return
}
central . retrievePeripherals ( withIdentifiers : [ peripheral . identifier ] ) . forEach {
2020-12-19 05:13:44 +00:00
if $0 . state != . connected {
// C h e c k t o s e e i f H e r a l d h a s i n i t i a t e d a c o n n e c t i o n a t t e m p t b e f o r e
if let lastAttempt = device . lastConnectionInitiationAttempt {
// H a s H e r a l d a l r e a d y i n i t i a t e d a c o n n e c t a t t e m p t ?
if ( Date ( ) > lastAttempt + BLESensorConfiguration . connectionAttemptTimeout ) {
// I f t i m e o u t r e a c h e d , f o r c e d i s c o n n e c t
self . logger . fault ( " connect, timeout forcing disconnect (source= \( source ) ,device= \( device ) ,elapsed= \( - lastAttempt . timeIntervalSinceNow ) ) " )
device . lastConnectionInitiationAttempt = nil
2021-02-02 00:04:43 +00:00
self . queue . async { central . cancelPeripheralConnection ( peripheral ) }
2020-12-19 05:13:44 +00:00
} else {
// I f n o t t i m e d o u t y e t , k e e p t r y i n g
self . logger . debug ( " connect, retrying (source= \( source ) ,device= \( device ) ,elapsed= \( - lastAttempt . timeIntervalSinceNow ) ) " )
2021-02-02 00:04:43 +00:00
central . connect ( $0 )
2020-12-19 05:13:44 +00:00
}
} else {
// I f n o t , c o n n e c t n o w
self . logger . debug ( " connect, initiation (source= \( source ) ,device= \( device ) ) " )
device . lastConnectionInitiationAttempt = Date ( )
2021-02-02 00:04:43 +00:00
central . connect ( $0 )
2020-12-19 05:13:44 +00:00
}
} else {
self . taskInitiateNextAction ( " connect| " + source , peripheral : $0 )
}
}
}
scheduleScan ( " connect " )
}
/* *
Disconnect peripheral . On didDisconnect , a connect request will be made for iOS devices to maintain an open connection ;
there is no further action for Android . On didFailedToConnect , a connect request will be made for both iOS and Android
devices as the error is likely to be transient ( as described in Apple documentation ) , except if the error is " Device in invalid "
then the peripheral is unregistered by removing it from the beacons table .
*/
private func disconnect ( _ source : String , _ peripheral : CBPeripheral ) {
let targetIdentifier = TargetIdentifier ( peripheral : peripheral )
logger . debug ( " disconnect (source= \( source ) ,peripheral= \( targetIdentifier ) ) " )
guard peripheral . state = = . connected || peripheral . state = = . connecting else {
logger . fault ( " disconnect denied, peripheral not connected or connecting (source= \( source ) ,peripheral= \( targetIdentifier ) ) " )
return
}
2021-02-02 00:04:43 +00:00
queue . async { self . central ? . cancelPeripheralConnection ( peripheral ) }
2020-12-19 05:13:44 +00:00
}
// / R e a d R S S I
private func readRSSI ( _ source : String , _ peripheral : CBPeripheral ) {
let targetIdentifier = TargetIdentifier ( peripheral : peripheral )
logger . debug ( " readRSSI (source= \( source ) ,peripheral= \( targetIdentifier ) ) " )
guard peripheral . state = = . connected else {
logger . fault ( " readRSSI denied, peripheral not connected (source= \( source ) ,peripheral= \( targetIdentifier ) ) " )
scheduleScan ( " readRSSI " )
return
}
queue . async { peripheral . readRSSI ( ) }
}
// / D i s c o v e r s e r v i c e s
private func discoverServices ( _ source : String , _ peripheral : CBPeripheral ) {
let targetIdentifier = TargetIdentifier ( peripheral : peripheral )
logger . debug ( " discoverServices (source= \( source ) ,peripheral= \( targetIdentifier ) ) " )
guard peripheral . state = = . connected else {
logger . fault ( " discoverServices denied, peripheral not connected (source= \( source ) ,peripheral= \( targetIdentifier ) ) " )
scheduleScan ( " discoverServices " )
return
}
queue . async { peripheral . discoverServices ( [ BLESensorConfiguration . serviceUUID ] ) }
}
// / R e a d p a y l o a d d a t a f r o m d e v i c e
private func readPayload ( _ source : String , _ device : BLEDevice ) {
logger . debug ( " readPayload (source= \( source ) ,peripheral= \( device . identifier ) ) " )
guard let peripheral = device . peripheral , peripheral . state = = . connected else {
logger . fault ( " readPayload denied, peripheral not connected (source= \( source ) ,peripheral= \( device . identifier ) ) " )
return
}
guard let payloadCharacteristic = device . payloadCharacteristic != nil ? device . payloadCharacteristic : device . legacyPayloadCharacteristic else {
logger . fault ( " readPayload denied, device missing payload characteristic (source= \( source ) ,peripheral= \( device . identifier ) ) " )
discoverServices ( " readPayload " , peripheral )
return
}
// D e - d u p l i c a t e r e a d p a y l o a d r e q u e s t s f r o m m u l t i p l e a s y n c h r o n o u s c a l l s
let timeIntervalSinceLastReadPayloadRequestedAt = Date ( ) . timeIntervalSince ( device . lastReadPayloadRequestedAt )
guard timeIntervalSinceLastReadPayloadRequestedAt > 2 else {
logger . fault ( " readPayload denied, duplicate request (source= \( source ) ,peripheral= \( device . identifier ) ,elapsed= \( timeIntervalSinceLastReadPayloadRequestedAt ) " )
return
}
// I n i t i a t e r e a d p a y l o a d
device . lastReadPayloadRequestedAt = Date ( )
if device . operatingSystem = = . android , let peripheral = device . peripheral {
discoverServices ( " readPayload|android " , peripheral )
} else {
queue . async { peripheral . readValue ( for : payloadCharacteristic ) }
}
}
// / R e t r i e v e d e l e g a t e s t h a t a r e r e q u i r e d t o w r i t e l e g a c y p a y l o a d t o f o r a s p e c i f i c d e v i c e
private func delegatesToWriteLegacyPayload ( device : BLEDevice ) -> [ SensorDelegate ] ? {
var delegatesToWriteLegacyPayload : [ SensorDelegate ] = [ ]
delegates . forEach { ( delegate ) in
if delegate . shouldWriteToLegacyDevice ( device ) {
delegatesToWriteLegacyPayload . append ( delegate )
}
}
return delegatesToWriteLegacyPayload . count > 0 ? delegatesToWriteLegacyPayload : nil
}
// / l e g a c y c o v i d s a f e d e v i c e , e x i s t i n g c o v i d s a f e c o d e w i l l h a v e t h e c e n t r a l \ r e c e i v e r w r i t e t o t h e p e r i p h e r a l a f t e r i t h a s r e q u e s t e d t o r e a d i t s p a y l o a d
private func writeLegacyPayload ( _ source : String , peripheral : CBPeripheral ) {
let device = database . device ( peripheral , delegate : self )
logger . debug ( " writeLegacyPayload (source= \( source ) ,peripheral= \( device . identifier ) ) " )
guard device . rssi != nil else {
logger . fault ( " writeLegacyPayload denied (source= \( source ) , rssi should be present in \( device . identifier ) before write " )
return
}
guard let characteristic = device . legacyPayloadCharacteristic else {
logger . fault ( " writeLegacyPayload denied (source= \( source ) ,peripheral= \( device . identifier ) legacyPayloadCharacteristic not present) " )
return
}
EncounterMessageManager . shared . getWritePayloadForCentral ( device : device ) { [ weak self ] ( result ) in
self ? . queue . async {
guard let payloadToWrite = result else {
self ? . logger . fault ( " writeLegacyPayload denied (source= \( source ) ,peripheral= \( device . identifier ) failed to obtain tempId) " )
return
}
self ? . logger . debug ( " writeLegacyPayload (source= \( source ) ,peripheral= \( device . identifier ) writing...) " )
peripheral . writeValue ( payloadToWrite , for : characteristic , type : . withResponse )
}
}
}
/* *
Wake transmitter by writing blank data to the beacon characteristic . This will trigger the transmitter to generate a data value update notification
in 8 seconds , which in turn will trigger this receiver to receive a didUpdateValueFor call to keep both the transmitter and receiver awake , while
maximising the time interval between bluetooth calls to minimise power usage .
*/
private func wakeTransmitter ( _ source : String , _ device : BLEDevice ) {
guard device . operatingSystem = = . ios , let peripheral = device . peripheral , let characteristic = device . signalCharacteristic else {
return
}
logger . debug ( " wakeTransmitter (source= \( source ) ,peripheral= \( device . identifier ) ,write= \( characteristic . properties . contains ( . write ) ) " )
queue . async { peripheral . writeValue ( self . emptyData , for : characteristic , type : . withResponse ) }
}
// MARK: - B L E D a t a b a s e D e l e g a t e
func bleDatabase ( didCreate device : BLEDevice ) {
// F E A T U R E : S y m m e t r i c c o n n e c t i o n o n w r i t e
// A l l C o r e B l u e t o o t h d e l e g a t e c a l l b a c k s i n B L E T r a n s m i t t e r w i l l r e g i s t e r t h e c e n t r a l i n t e r a c t i n g w i t h t h i s p e r i p h e r a l
// i n t h e d a t a b a s e a n d g e n e r a t e a d i d C r e a t e c a l l b a c k h e r e t o t r i g g e r s c a n , w h i c h i n c l u d e s a t a s k f o r r e s o l v i n g a l l
// d e v i c e i d e n t i f i e r s t o a c t u a l p e r i p h e r a l s .
scheduleScan ( " bleDatabase:didCreate (device= \( device . identifier ) ) " )
}
// MARK: - C B C e n t r a l M a n a g e r D e l e g a t e
// / R e i n s t a t e d e v i c e s f o l l o w i n g s t a t e r e s t o r a t i o n
func centralManager ( _ central : CBCentralManager , willRestoreState dict : [ String : Any ] ) {
// R e s t o r e - > P o p u l a t e d a t a b a s e
logger . debug ( " willRestoreState " )
self . central = central
central . delegate = self
if let restoredPeripherals = dict [ CBCentralManagerRestoredStatePeripheralsKey ] as ? [ CBPeripheral ] {
for peripheral in restoredPeripherals {
let targetIdentifier = TargetIdentifier ( peripheral : peripheral )
let device = database . device ( peripheral , delegate : self )
if device . operatingSystem = = . unknown {
device . operatingSystem = . restored
}
if peripheral . state = = . connected {
device . lastConnectedAt = Date ( )
}
logger . debug ( " willRestoreState (peripheral= \( targetIdentifier ) ) " )
}
}
// R e c o n n e c t i o n c h e c k p e r f o r m e d i n s c a n f o l l o w i n g c e n t r a l M a n a g e r D i d U p d a t e S t a t e : c e n t r a l . s t a t e = = . p o w e r O n
}
// / S t a r t s c a n w h e n b l u e t o o t h i s o n .
func centralManagerDidUpdateState ( _ central : CBCentralManager ) {
// B l u e t o o t h o n - > S c a n
if ( central . state = = . poweredOn ) {
logger . debug ( " Update state (state=poweredOn)) " )
delegateQueue . async {
self . delegates . forEach ( { $0 . sensor ( . BLE , didUpdateState : . on ) } )
self . connectionDelegate ? . sensor ( . BLE , didUpdateState : . on )
}
scan ( " updateState " )
} else {
if #available ( iOS 10.0 , * ) {
logger . debug ( " Update state (state= \( central . state . description ) ) " )
} else {
// R e q u i r e d f o r c o m p a t i b i l i t y w i t h i O S 9 . 3
switch central . state {
case . poweredOff :
logger . debug ( " Update state (state=poweredOff) " )
case . poweredOn :
logger . debug ( " Update state (state=poweredOn) " )
case . resetting :
logger . debug ( " Update state (state=resetting) " )
case . unauthorized :
logger . debug ( " Update state (state=unauthorized) " )
case . unknown :
logger . debug ( " Update state (state=unknown) " )
case . unsupported :
logger . debug ( " Update state (state=unsupported) " )
default :
logger . debug ( " Update state (state=undefined) " )
}
}
delegateQueue . async {
self . delegates . forEach ( { $0 . sensor ( . BLE , didUpdateState : . off ) } )
self . connectionDelegate ? . sensor ( . BLE , didUpdateState : . off )
}
}
}
// / S h a r e p a y l o a d d a t a a c r o s s d e v i c e s w i t h t h e s a m e p s e u d o d e v i c e a d d r e s s
private func shareDataAcrossDevices ( _ pseudoDeviceAddress : BLEPseudoDeviceAddress ) {
// G e t d e v i c e s w i t h t h e s a m e p s e u d o a d d r e s s c r e a t e d r e c e n t l y
let devicesWithSamePseudoAddress = database . devices ( ) . filter ( { pseudoDeviceAddress . address = = $0 . pseudoDeviceAddress ? . address && $0 . timeIntervalSinceCreated <= BLESensorConfiguration . androidAdvertRefreshTimeInterval } )
// G e t d e v i c e w i t h m o s t r e c e n t v e r s i o n o f p a y l o a d a m o n g s t t h e s e d e v i c e s
guard let mostRecentDevice = devicesWithSamePseudoAddress . filter ( { $0 . payloadData != nil } ) . sorted ( by : { $0 . payloadDataLastUpdatedAt > $1 . payloadDataLastUpdatedAt } ) . first , let payloadData = mostRecentDevice . payloadData else {
return
}
// C o p y d a t a t o a l l d e v i c e s w i t h t h e s a m e p s e u d o a d d r e s s
let payloadDataLastUpdatedAt = mostRecentDevice . payloadDataLastUpdatedAt
let devicesToCopyPayload = devicesWithSamePseudoAddress . filter ( { $0 . payloadData = = nil } )
devicesToCopyPayload . forEach ( {
$0 . signalCharacteristic = mostRecentDevice . signalCharacteristic
$0 . payloadCharacteristic = mostRecentDevice . payloadCharacteristic
$0 . legacyPayloadCharacteristic = mostRecentDevice . legacyPayloadCharacteristic
// O n l y A n d r o i d d e v i c e s h a v e a p s e u d o a d d r e s s
$0 . operatingSystem = . android
$0 . payloadData = payloadData
$0 . payloadDataLastUpdatedAt = payloadDataLastUpdatedAt
logger . debug ( " shareDataAcrossDevices, copied payload data (from= \( mostRecentDevice . description ) ,to= \( $0 . description ) ) " )
} )
// G e t d e v i c e s w i t h t h e s a m e p a y l o a d
let devicesWithSamePayload = database . devices ( ) . filter ( { payloadData = = $0 . payloadData } )
// C o p y p s e u d o a d d r e s s t o a l l d e v i c e s w i t h t h e s a m e p a y l o a d
let devicesToCopyAddress = devicesWithSamePayload . filter ( { $0 . pseudoDeviceAddress = = nil } )
devicesToCopyAddress . forEach ( {
$0 . pseudoDeviceAddress = pseudoDeviceAddress
logger . debug ( " shareDataAcrossDevices, copied pseudo address (payloadData= \( payloadData . shortName ) ,to= \( $0 . description ) ) " )
} )
}
// / D e v i c e d i s c o v e r y w i l l t r i g g e r c o n n e c t i o n t o r e s o l v e o p e r a t i n g s y s t e m a n d r e a d p a y l o a d f o r i O S a n d A n d r o i d d e v i c e s .
// / C o n n e c t i o n i s k e p t a c t i v e f o r i O S d e v i c e s f o r o n - g o i n g R S S I m e a s u r e m e n t s , a n d c l o s e d f o r A n d r o i d d e v i c e s , a s t h i s
// / i O S d e v i c e c a n r e l y o n t h i s d i s c o v e r y c a l l b a c k ( t r i g g e r e d b y r e g u l a r s c a n c a l l s ) f o r o n - g o i n g R S S I a n d T X p o w e r
// / u p d a t e s , t h u s e l i m i n a t i n g t h e n e e d t o k e e p c o n n e c t i o n s o p e n f o r A n d r o i d d e v i c e s t h a t c a n c a u s e s t a b i l i t y i s s u e s f o r
// / A n d r o i d d e v i c e s .
func centralManager ( _ central : CBCentralManager , didDiscover peripheral : CBPeripheral , advertisementData : [ String : Any ] , rssi RSSI : NSNumber ) {
// P o p u l a t e d e v i c e d a t a b a s e
let device = database . device ( peripheral , delegate : self )
device . lastDiscoveredAt = Date ( )
device . rssi = BLE_RSSI ( RSSI . intValue )
// W e s e t o p e r a t i n g s y s t e m t o e n a b l e d i s c o v e r y w i t h l e g a c y a p p s
if let manuData = advertisementData [ CBAdvertisementDataManufacturerDataKey ] as ? Data , manuData . count > 2 {
device . operatingSystem = . android
} else {
device . operatingSystem = . ios
}
if let pseudoDeviceAddress = BLEPseudoDeviceAddress ( fromAdvertisementData : advertisementData ) {
device . pseudoDeviceAddress = pseudoDeviceAddress
shareDataAcrossDevices ( pseudoDeviceAddress )
}
if let txPower = ( advertisementData [ CBAdvertisementDataTxPowerLevelKey ] as ? NSNumber ) ? . intValue {
device . txPower = BLE_TxPower ( txPower )
}
logger . debug ( " didDiscover (device= \( device ) ,rssi= \( ( String ( describing : device . rssi ) ) ) ,txPower= \( ( String ( describing : device . txPower ) ) ) ) " )
if deviceHasPendingTask ( device ) {
connect ( " didDiscover " , peripheral ) ;
} else {
scanResults . append ( device )
}
// S c h e d u l e s c a n ( a c t u a l c o n n e c t i s i n i t i a t e d f r o m s c a n v i a p r i o r i t i s a t i o n l o g i c )
scheduleScan ( " didDiscover " )
}
// / S u c c e s s f u l c o n n e c t i o n t o a d e v i c e w i l l i n i t a t e t h e n e x t p e n d i n g a c t i o n .
func centralManager ( _ central : CBCentralManager , didConnect peripheral : CBPeripheral ) {
// c o n n e c t - > r e a d R S S I - > d i s c o v e r S e r v i c e s
let device = database . device ( peripheral , delegate : self )
device . lastConnectedAt = Date ( )
logger . debug ( " didConnect (device= \( device ) ) " )
taskInitiateNextAction ( " didConnect " , peripheral : peripheral )
}
// / F a i l u r e t o c o n n e c t t o a d e v i c e w i l l r e s u l t i n d e - r e g i s t r a t i o n f o r i n v a l i d d e v i c e s o r r e c o n n e c t i o n a t t e m p t o t h e r w i s e .
func centralManager ( _ central : CBCentralManager , didFailToConnect peripheral : CBPeripheral , error : Error ? ) {
// C o n n e c t f a i l - > D e l e t e | C o n n e c t
// F a i l u r e f o r p e r i p h e r a l s a d v e r t i s i n g t h e b e a c o n s e r v i c e s h o u l d b e t r a n s i e n t , s o t r y a g a i n .
// T h i s i s a l s o w h e r e i O S r e p o r t s i n v a l i d a t e d d e v i c e s i f c o n n e c t i s c a l l e d a f t e r r e s t o r e ,
// t h u s o f f e r s a n o p p o r t u n i t y f o r h o u s e k e e p i n g .
let device = database . device ( peripheral , delegate : self )
logger . debug ( " didFailToConnect (device= \( device ) ,error= \( String ( describing : error ) ) ) " )
if String ( describing : error ) . contains ( " Device is invalid " ) {
logger . debug ( " Unregister invalid device (device= \( device ) ) " )
database . delete ( device . identifier )
} else {
connect ( " didFailToConnect " , peripheral )
}
}
// / G r a c e f u l d i s c o n n e c t i o n i s u s u a l l y c a u s e d b y d e v i c e g o i n g o u t o f r a n g e o r d e v i c e c h a n g i n g i d e n t i t y , t h u s a r e c o n n e c t i o n c a l l i s i n i t i a t e d
// / h e r e f o r i O S d e v i c e s t o r e s u m e c o n n e c t i o n w h e r e p o s s i b l e . T h i s i s u n n e c e s s a r y f o r A n d r o i d d e v i c e s a s t h e y c a n b e r e d i s c o v e r e d b y
// / t h e r e g u l a r s c a n c a l l s . P l e a s e n o t e , r e c o n n e c t i o n t o i O S d e v i c e s i s l i k e l y t o f a i l f o l l o w i n g p r o l o n g e d p e r i o d o f b e i n g o u t o f r a n g e a s
// / t h e t a r g e t d e v i c e i s l i k e l y t o h a v e c h a n g e d i d e n t i t y a f t e r a b o u t 2 0 m i n u t e s . T h i s r e q u i r e s r e d i s c o v e r y w h i c h i s i m p o s s i b l e i f t h e i O S d e v i c e
// / i s i n b a c k g r o u n d s t a t e , h e n c e t h e n e e d f o r e n a b l i n g l o c a t i o n a n d s c r e e n o n t o t r i g g e r r e d i s c o v e r y ( y e s , i t s w e i r d , b u t i t w o r k s ) .
func centralManager ( _ central : CBCentralManager , didDisconnectPeripheral peripheral : CBPeripheral , error : Error ? ) {
// D i s c o n n e c t e d - > C o n n e c t i f i O S
// K e e p c o n n e c t i o n o n l y f o r i O S , n o t n e c e s s a r y f o r A n d r o i d a s t h e y a r e a l w a y s d e t e c t a b l e
let device = database . device ( peripheral , delegate : self )
device . lastDisconnectedAt = Date ( )
logger . debug ( " didDisconnectPeripheral (device= \( device ) ,error= \( String ( describing : error ) ) ) " )
if device . operatingSystem = = . ios {
// I n v a l i d a t e c h a r a c t e r i s t i c s
device . signalCharacteristic = nil
device . payloadCharacteristic = nil
device . legacyPayloadCharacteristic = nil
// R e c o n n e c t
connect ( " didDisconnectPeripheral " , peripheral )
}
}
// MARK: - C B P e r i p h e r a l D e l e g a t e
// / R e a d R S S I f o r p r o x i m i t y e s t i m a t i o n .
func peripheral ( _ peripheral : CBPeripheral , didReadRSSI RSSI : NSNumber , error : Error ? ) {
// R e a d R S S I - > R e a d C o d e | N o t i f y d e l e g a t e s - > S c a n a g a i n
// T h i s i s t h e p r i m a r y l o o p f o r i O S a f t e r i n i t i a l c o n n e c t i o n a n d s u b s c r i p t i o n t o
// t h e n o t i f y i n g b e a c o n c h a r a c t e r i s t i c . T h e l o o p i s s c a n - > w a k e T r a n s m i t t e r - >
// d i d U p d a t e V a l u e F o r - > r e a d R S S I - > n o t i f y D e l e g a t e s - > s c h e d u l e S c a n - > s c a n
let device = database . device ( peripheral , delegate : self )
device . rssi = BLE_RSSI ( RSSI . intValue )
logger . debug ( " didReadRSSI (device= \( device ) ,rssi= \( String ( describing : device . rssi ) ) ,error= \( String ( describing : error ) ) ) " )
taskInitiateNextAction ( " didReadRSSI " , peripheral : peripheral )
}
// / S e r v i c e d i s c o v e r y t r i g g e r s c h a r a c t e r i s t i c d i s c o v e r y .
func peripheral ( _ peripheral : CBPeripheral , didDiscoverServices error : Error ? ) {
// D i s c o v e r s e r v i c e s - > D i s c o v e r c h a r a c t e r i s t i c s | D i s c o n n e c t
let device = database . device ( peripheral , delegate : self )
logger . debug ( " didDiscoverServices (device= \( device ) ,error= \( String ( describing : error ) ) ) " )
guard let services = peripheral . services else {
disconnect ( " didDiscoverServices|serviceEmpty " , peripheral )
return
}
for service in services {
if ( service . uuid = = BLESensorConfiguration . serviceUUID ) {
logger . debug ( " didDiscoverServices, found sensor service (device= \( device ) ) " )
queue . async {
peripheral . discoverCharacteristics ( [ BLESensorConfiguration . legacyCovidsafePayloadCharacteristicUUID , BLESensorConfiguration . androidSignalCharacteristicUUID , BLESensorConfiguration . payloadCharacteristicUUID , BLESensorConfiguration . iosSignalCharacteristicUUID ] , for : service )
}
return
}
}
disconnect ( " didDiscoverServices|serviceNotFound " , peripheral )
// T h e d i s c o n n e c t c a l l s h e r e s h a l l b e h a n d l e d b y d i d D i s c o n n e c t w h i c h d e t e r m i n e s w h e t h e r t o r e t r y f o r i O S o r s t o p f o r A n d r o i d
}
// / C h a r a c t e r i s t i c d i s c o v e r y p r o v i d e s d e f i n i t i v e c l a s s i f i c a t i o n a n d c o n f i r m a t i o n o f d e v i c e o p e r a t i n g s y s t e m t o i n f o r m n e x t a c t i o n s .
func peripheral ( _ peripheral : CBPeripheral , didDiscoverCharacteristicsFor service : CBService , error : Error ? ) {
// D i s c o v e r c h a r a c t e r i s t i c s - > N o t i f y d e l e g a t e s - > D i s c o n n e c t | W a k e t r a n s m i t t e r - > S c a n a g a i n
let device = database . device ( peripheral , delegate : self )
logger . debug ( " didDiscoverCharacteristicsFor (device= \( device ) ,error= \( String ( describing : error ) ) ) " )
guard let characteristics = service . characteristics else {
disconnect ( " didDiscoverCharacteristicsFor|characteristicEmpty " , peripheral )
return
}
for characteristic in characteristics {
switch characteristic . uuid {
case BLESensorConfiguration . androidSignalCharacteristicUUID :
device . operatingSystem = . android
device . signalCharacteristic = characteristic
logger . debug ( " didDiscoverCharacteristicsFor, found android signal characteristic (device= \( device ) ) " )
case BLESensorConfiguration . iosSignalCharacteristicUUID :
// M a i n t a i n c o n n e c t i o n w i t h i O S d e v i c e s f o r k e e p a w a k e
let notify = characteristic . properties . contains ( . notify )
let write = characteristic . properties . contains ( . write )
device . operatingSystem = . ios
device . signalCharacteristic = characteristic
queue . async {
peripheral . setNotifyValue ( true , for : characteristic )
}
logger . debug ( " didDiscoverCharacteristicsFor, found ios signal characteristic (device= \( device ) ,notify= \( notify ) ,write= \( write ) ) " )
case BLESensorConfiguration . payloadCharacteristicUUID :
device . payloadCharacteristic = characteristic
logger . debug ( " didDiscoverCharacteristicsFor, found payload characteristic (device= \( device ) ) " )
case BLESensorConfiguration . legacyCovidsafePayloadCharacteristicUUID :
// i f w e o n l y h a v e l e g a c y c h a r a c t e r i s t i c , u s e i t a s w i l l b e a d e v i c e w i t h o l d v e r s i o n . O t h e r w i s e i g n o r e a n d u s e n e w c h a r a c t e r i s t i c s o n l y .
if characteristics . count = = 1 {
device . legacyPayloadCharacteristic = characteristic
logger . debug ( " didDiscoverCharacteristicsFor, found covidsafe legacy payload characteristic (device= \( device ) ) " )
} else {
logger . debug ( " didDiscoverCharacteristicsFor, found covidsafe legacy payload characteristic but discarding as there are more characteristics, assuming new ble (device= \( device ) ) " )
}
default :
logger . fault ( " didDiscoverCharacteristicsFor, found unknown characteristic (device= \( device ) ,characteristic= \( characteristic . uuid ) ) " )
}
}
// A n d r o i d - > R e a d p a y l o a d
if device . operatingSystem = = . android {
let payloadCharacteristic = device . payloadCharacteristic != nil ? device . payloadCharacteristic : device . legacyPayloadCharacteristic
if device . payloadData = = nil || device . timeIntervalSinceLastPayloadDataUpdate > BluetraceConfig . PeripheralPayloadExpiry , let characteristicToRead = payloadCharacteristic {
device . lastReadPayloadRequestedAt = Date ( )
queue . async { peripheral . readValue ( for : characteristicToRead ) }
} else {
disconnect ( " didDiscoverCharacteristicsFor|android " , peripheral )
}
}
// A l w a y s - > S c a n a g a i n
// F o r i n i t i a l c o n n e c t i o n , t h e s c h e d u l e S c a n c a l l w o u l d h a v e b e e n m a d e j u s t b e f o r e c o n n e c t .
// I t i s c a l l e d a g a i n h e r e t o e x t e n d t h e t i m e i n t e r v a l b e t w e e n s c a n s .
scheduleScan ( " didDiscoverCharacteristicsFor " )
}
// / T h i s i O S d e v i c e w i l l w r i t e t o c o n n e c t e d i O S d e v i c e s t o k e e p t h e m a w a k e , a n d t h i s c a l l b a c k p r o v i d e s a b a c k u p m e c h a n i s m f o r k e e p i n g t h i s
// / d e v i c e a w a k e f o r l o n g e r i n t h e e v e n t t h a t o t h e r d e v i c e s a r e n o l o n g e r r e s p o n d i n g o r i n r a n g e .
func peripheral ( _ peripheral : CBPeripheral , didWriteValueFor characteristic : CBCharacteristic , error : Error ? ) {
// W r o t e c h a r a c t e r i s t i c - > S c a n a g a i n
let device = database . device ( peripheral , delegate : self )
logger . debug ( " didWriteValueFor (device= \( device ) ,error= \( String ( describing : error ) ) ) " )
// F o r a l l s i t u a t i o n s , s c h e d u l e S c a n w o u l d h a v e b e e n m a d e e a r l i e r i n t h e c h a i n o f a s y n c c a l l s .
// I t i s c a l l e d a g a i n h e r e t o e x t e n d t h e t i m e i n t e r v a l b e t w e e n s c a n s , a s t h i s i s u s u a l l y t h e
// l a s t c a l l m a d e i n a l l p a t h s t o w a k e t h e t r a n s m i t t e r .
scheduleScan ( " didWriteValueFor " )
}
// / O t h e r i O S d e v i c e s m a y r e f r e s h ( s t o p / r e s t a r t ) t h e i r a d v e r t s a t r e g u l a r i n t e r v a l s , t h u s t r i g g e r i n g t h i s s e r v i c e m o d i f i c a t i o n c a l l b a c k
// / t o i n v a l i d a t e e x i s t i n g c h a r a c t e r i s t i c s a n d r e c o n n e c t t o r e f r e s h t h e d e v i c e d a t a .
func peripheral ( _ peripheral : CBPeripheral , didModifyServices invalidatedServices : [ CBService ] ) {
// i O S o n l y
// M o d i f i e d s e r v i c e - > I n v a l i d a t e b e a c o n - > S c a n
let device = database . device ( peripheral , delegate : self )
let characteristics = invalidatedServices . map { $0 . characteristics } . count
logger . debug ( " didModifyServices (device= \( device ) ,characteristics= \( characteristics ) ) " )
guard characteristics = = 0 else {
return
}
device . signalCharacteristic = nil
device . payloadCharacteristic = nil
device . legacyPayloadCharacteristic = nil
if peripheral . state = = . connected {
discoverServices ( " didModifyServices " , peripheral )
} else if peripheral . state != . connecting {
connect ( " didModifyServices " , peripheral )
}
}
// / A l l r e a d c h a r a c t e r i s t i c r e q u e s t s w i l l t r i g g e r t h i s c a l l b a c k t o h a n d l e t h e r e s p o n s e .
func peripheral ( _ peripheral : CBPeripheral , didUpdateValueFor characteristic : CBCharacteristic , error : Error ? ) {
// U p d a t e d v a l u e - > R e a d R S S I | R e a d P a y l o a d
// B e a c o n c h a r a c t e r i s t i c i s w r i t a b l e , p r i m a r i l y t o e n a b l e n o n - t r a n s m i t t i n g A n d r o i d d e v i c e s t o s u b m i t t h e i r
// b e a c o n c o d e a n d R S S I a s d a t a t o t h e t r a n s m i t t e r v i a G A T T w r i t e . T h e c h a r a c t e r i s t i c i s a l s o n o t i f y i n g o n
// i O S d e v i c e s , t o o f f e r a m e c h a n i s m f o r w a k i n g r e c e i v e r s . T h e p r o c e s s w o r k s a s f o l l o w s , ( 1 ) r e c e i v e r w r i t e s
// b l a n k d a t a t o t r a n s m i t t e r , ( 2 ) t r a n s m i t t e r b r o a d c a s t s v a l u e u p d a t e n o t i f i c a t i o n a f t e r 8 s e c o n d s , ( 3 )
// r e c e i v e r i s w o k e n u p t o h a n d l e d i d U p d a t e V a l u e F o r n o t i f i c a t i o n , ( 4 ) r e c e i v e r c a l l s r e a d R S S I , ( 5 ) r e a d R S S I
// c a l l c o m p l e t e s a n d s c h e d u l e s s c a n a f t e r 8 s e c o n d s , ( 6 ) s c a n w r i t e s b l a n k d a t a t o a l l i O S t r a n s m i t t e r s .
// P r o c e s s r e p e a t s t o k e e p b o t h i O S t r a n s m i t t e r s a n d r e c e i v e r s a w a k e w h i l e m a x i m i s i n g t i m e i n t e r v a l b e t w e e n
// b l u e t o o t h c a l l s t o m i n i m i s e p o w e r u s a g e .
let device = database . device ( peripheral , delegate : self )
logger . debug ( " didUpdateValueFor (device= \( device ) ,characteristic= \( characteristic . uuid ) ,error= \( String ( describing : error ) ) ) " )
switch characteristic . uuid {
case BLESensorConfiguration . iosSignalCharacteristicUUID :
// W a k e u p c a l l f r o m t r a n s m i t t e r
logger . debug ( " didUpdateValueFor (device= \( device ) ,characteristic=iosSignalCharacteristic,error= \( String ( describing : error ) ) ) " )
device . lastNotifiedAt = Date ( )
readRSSI ( " didUpdateValueFor " , peripheral )
return
case BLESensorConfiguration . androidSignalCharacteristicUUID :
// S h o u l d n o t h a p p e n a s A n d r o i d s i g n a l i s n o t n o t i f y i n g
logger . fault ( " didUpdateValueFor (device= \( device ) ,characteristic=androidSignalCharacteristic,error= \( String ( describing : error ) ) ) " )
case BLESensorConfiguration . payloadCharacteristicUUID , BLESensorConfiguration . legacyCovidsafePayloadCharacteristicUUID :
// R e a d p a y l o a d d a t a
logger . debug ( " didUpdateValueFor (device= \( device ) ,characteristic=payloadCharacteristic,error= \( String ( describing : error ) ) ) " )
if let data = characteristic . value {
device . payloadData = PayloadData ( data )
}
if device . operatingSystem = = . android {
disconnect ( " didUpdateValueFor|payload|android " , peripheral )
}
default :
logger . fault ( " didUpdateValueFor, unknown characteristic (device= \( device ) ,characteristic= \( characteristic . uuid ) ,error= \( String ( describing : error ) ) ) " )
}
scheduleScan ( " didUpdateValueFor " )
return
}
}