2020-12-19 05:13:44 +00:00
//
// B L E S e n s o 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
protocol BLESensor : Sensor {
}
// / D e f i n e s B L E s e n s o r c o n f i g u r a t i o n d a t a , e . g . s e r v i c e a n d c h a r a c t e r i s t i c U U I D s
struct BLESensorConfiguration {
#if DEBUG
static let logLevel : SensorLoggerLevel = . debug ;
#else
static let logLevel : SensorLoggerLevel = . fault ;
#endif
/* *
Service UUID for beacon service . This is a fixed UUID to enable iOS devices to find each other even
in background mode . Android devices will need to find Apple devices first using the manufacturer code
then discover services to identify actual beacons .
*/
static let serviceUUID = BluetraceConfig . BluetoothServiceID
// / S i g n a l i n g c h a r a c t e r i s t i c f o r c o n t r o l l i n g c o n n e c t i o n b e t w e e n p e r i p h e r a l a n d c e n t r a l , e . g . k e e p e a c h o t h e r f r o m s u s p e n d s t a t e
// / - C h a r a c t e r i s t i c U U I D i s r a n d o m l y g e n e r a t e d V 4 U U I D s t h a t h a s b e e n t e s t e d f o r u n i q u e n e s s b y c o n d u c t i n g w e b s e a r c h e s t o e n s u r e i t r e t u r n s n o r e s u l t s .
public static var androidSignalCharacteristicUUID = CBUUID ( string : " f617b813-092e-437a-8324-e09a80821a11 " )
// / S i g n a l i n g c h a r a c t e r i s t i c f o r c o n t r o l l i n g c o n n e c t i o n b e t w e e n p e r i p h e r a l a n d c e n t r a l , e . g . k e e p e a c h o t h e r f r o m s u s p e n d s t a t e
// / - C h a r a c t e r i s t i c U U I D i s r a n d o m l y g e n e r a t e d V 4 U U I D s t h a t h a s b e e n t e s t e d f o r u n i q u e n e s s b y c o n d u c t i n g w e b s e a r c h e s t o e n s u r e i t r e t u r n s n o r e s u l t s .
public static var iosSignalCharacteristicUUID = CBUUID ( string : " 0eb0d5f2-eae4-4a9a-8af3-a4adb02d4363 " )
// / P r i m a r y p a y l o a d c h a r a c t e r i s t i c ( r e a d ) f o r d i s t r i b u t i n g p a y l o a d d a t a f r o m p e r i p h e r a l t o c e n t r a l , e . g . i d e n t i t y d a t a
// / - C h a r a c t e r i s t i c U U I D i s r a n d o m l y g e n e r a t e d V 4 U U I D s t h a t h a s b e e n t e s t e d f o r u n i q u e n e s s b y c o n d u c t i n g w e b s e a r c h e s t o e n s u r e i t r e t u r n s n o r e s u l t s .
public static var payloadCharacteristicUUID = CBUUID ( string : " 3e98c0f8-8f05-4829-a121-43e38f8933e7 " )
static let legacyCovidsafePayloadCharacteristicUUID = BluetraceConfig . BluetoothServiceID
// / T i m e d e l a y b e t w e e n n o t i f i c a t i o n s f o r s u b s c r i b e r s .
static let notificationDelay = DispatchTimeInterval . seconds ( 8 )
// / T i m e d e l a y b e t w e e n a d v e r t r e s t a r t
static let advertRestartTimeInterval = TimeInterval . hour
// / H e r a l d i n t e r n a l c o n n e c t i o n e x p i r y t i m e o u t
static let connectionAttemptTimeout = TimeInterval ( 12 )
// / E x p i r y t i m e f o r s h a r e d p a y l o a d s , t o e n s u r e o n l y r e c e n t l y s e e n p a y l o a d s a r e s h a r e d
// / M u s t b e > p a y l o a d S h a r i n g T i m e I n t e r v a l t o s h a r e p e n d i n g p a y l o a d s
static let payloadSharingExpiryTimeInterval = TimeInterval . minute * 5
// / M a x i m u m 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
static let concurrentConnectionQuota = 12
// / M a n u f a c t u r e r d a t a i s b e i n g u s e d o n A n d r o i d t o s t o r e p s e u d o d e v i c e a d d r e s s
static let manufacturerIdForSensor = UInt16 ( 65530 ) ;
// / A d v e r t r e f r e s h t i m e i n t e r v a l o n A n d r o i d d e v i c e s
static let androidAdvertRefreshTimeInterval = TimeInterval . minute * 15 ;
// F i l t e r d u p l i c a t e p a y l o a d d a t a a n d s u p p r e s s s e n s o r ( d i d R e a d : f r o m T a r g e t ) d e l e g a t e c a l l s
// / - S e t t o . n e v e r t o d i s a b l e t h i s f e a t u r e
// / - S e t t i m e i n t e r v a l N t o f i l t e r d u p l i c a t e p a y l o a d d a t a s e e n i n l a s t N s e c o n d s
// / - E x a m p l e : 6 0 m e a n s f i l t e r d u p l i c a t e s i n l a s t m i n u t e
// / - F i l t e r s a l l o c c u r r e n c e s o f p a y l o a d d a t a f r o m a l l t a r g e t s
public static var filterDuplicatePayloadData = TimeInterval ( 30 * 60 )
// / S i g n a l c h a r a c t e r i s t i c a c t i o n c o d e f o r w r i t e p a y l o a d , e x p e c t 1 b y t e a c t i o n c o d e f o l l o w e d b y 2 b y t e l i t t l e - e n d i a n I n t 1 6 i n t e g e r v a l u e f o r p a y l o a d d a t a l e n g t h , t h e n p a y l o a d d a t a
static let signalCharacteristicActionWritePayload = UInt8 ( 1 )
// / S i g n a l c h a r a c t e r i s t i c a c t i o n c o d e f o r w r i t e R S S I , e x p e c t 1 b y t e a c t i o n c o d e f o l l o w e d b y 4 b y t e l i t t l e - e n d i a n I n t 3 2 i n t e g e r v a l u e f o r R S S I v a l u e
static let signalCharacteristicActionWriteRSSI = UInt8 ( 2 )
// / S i g n a l c h a r a c t e r i s t i c a c t i o n c o d e f o r w r i t e p a y l o a d , e x p e c t 1 b y t e a c t i o n c o d e f o l l o w e d b y 2 b y t e l i t t l e - e n d i a n I n t 1 6 i n t e g e r v a l u e f o r p a y l o a d s h a r i n g d a t a l e n g t h , t h e n p a y l o a d s h a r i n g d a t a
static let signalCharacteristicActionWritePayloadSharing = UInt8 ( 3 )
// / A r e L o c a t i o n P e r m i s s i o n s e n a b l e d i n t h e a p p , a n d t h u s a w a k e o n s c r e e n o n e n a b l e d
public static var awakeOnLocationEnabled : Bool = true
}
/* *
BLE sensor based on CoreBluetooth
Requires : Signing & Capabilities : BackgroundModes : Uses Bluetooth LE accessories = YES
Requires : Signing & Capabilities : BackgroundModes : Acts as a Bluetooth LE accessory = YES
Requires : Info . plist : Privacy - Bluetooth Always Usage Description
Requires : Info . plist : Privacy - Bluetooth Peripheral Usage Description
*/
class ConcreteBLESensor : NSObject , BLESensor , BLEDatabaseDelegate {
private let logger = ConcreteSensorLogger ( subsystem : " Sensor " , category : " BLE.ConcreteBLESensor " )
private let sensorQueue = DispatchQueue ( label : " Sensor.BLE.ConcreteBLESensor.SensorQueue " )
private let delegateQueue = DispatchQueue ( label : " Sensor.BLE.ConcreteBLESensor.DelegateQueue " )
private var delegates : [ SensorDelegate ] = [ ]
private let database : BLEDatabase
private let transmitter : BLETransmitter
private let receiver : BLEReceiver
// R e c o r d p a y l o a d d a t a t o e n a b l e d e - d u p l i c a t i o n
private var didReadPayloadData : [ PayloadData : Date ] = [ : ]
init ( _ payloadDataSupplier : PayloadDataSupplier ) {
database = ConcreteBLEDatabase ( )
transmitter = ConcreteBLETransmitter ( queue : sensorQueue , delegateQueue : delegateQueue , database : database , payloadDataSupplier : payloadDataSupplier )
receiver = ConcreteBLEReceiver ( queue : sensorQueue , delegateQueue : delegateQueue , database : database , payloadDataSupplier : payloadDataSupplier )
super . init ( )
database . add ( delegate : self )
}
func start ( ) {
logger . debug ( " start " )
var permissionRequested = false
if #available ( iOS 13.1 , * ) {
permissionRequested = ( CBManager . authorization != . notDetermined )
} else {
permissionRequested = CBPeripheralManager . authorizationStatus ( ) != . notDetermined
}
if let receiver = receiver as ? ConcreteBLEReceiver , ! permissionRequested {
// B L E r e c e i v e r s s t a r t o n p o w e r O n e v e n t , o n s t a t u s c h a n g e t h e t r a n s m i t t e r w i l l b e s t a r t e d .
// T h i s i s t o r e q u e s t p e r m i s s i o n s a n d t u r n o n d i a l o g s s e q u e n t i a l l y w h e n r e g i s t e r i n g
receiver . addConnectionDelegate ( delegate : self )
}
receiver . start ( )
// i f p e r m i s s i o n s h a v e b e e n r e q u e s t e d s t a r t t r a n s m i t t e r i m m e d i a t e l y
if permissionRequested {
transmitter . start ( )
}
}
func stop ( ) {
logger . debug ( " stop " )
transmitter . stop ( )
receiver . stop ( )
// B L E t r a n s m i t t e r a n d r e c e i v e r s s t o p s o n p o w e r O f f e v e n t
2021-02-26 03:41:20 +00:00
delegates . forEach ( { $0 . sensor ( . BLE , didUpdateState : . off ) } )
2020-12-19 05:13:44 +00:00
}
func add ( delegate : SensorDelegate ) {
delegates . append ( delegate )
transmitter . add ( delegate : delegate )
receiver . add ( delegate : delegate )
}
// 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 ) {
logger . debug ( " didDetect (device= \( device . identifier ) ,payloadData= \( device . payloadData ? . shortName ? ? " nil " ) ) " )
delegateQueue . async {
self . delegates . forEach { $0 . sensor ( . BLE , didDetect : device . identifier ) }
}
}
func bleDatabase ( didUpdate device : BLEDevice , attribute : BLEDeviceAttribute ) {
switch attribute {
case . rssi :
guard let rssi = device . rssi else {
return
}
let proximity = Proximity ( unit : . RSSI , value : Double ( rssi ) )
logger . debug ( " didMeasure (device= \( device . identifier ) ,payloadData= \( device . payloadData ? . shortName ? ? " nil " ) ,proximity= \( proximity . description ) ) " )
delegateQueue . async {
self . delegates . forEach { $0 . sensor ( . BLE , didMeasure : proximity , fromTarget : device . identifier ) }
}
guard let payloadData = device . payloadData else {
return
}
delegateQueue . async {
self . delegates . forEach { $0 . sensor ( . BLE , didMeasure : proximity , fromTarget : device . identifier , withPayload : payloadData , forDevice : device ) }
}
case . payloadData :
guard let payloadData = device . payloadData else {
return
}
guard device . lastReadPayloadRequestedAt != Date . distantPast else {
logger . debug ( " didRead payload. lastReadPayloadRequestedAt is not set and payload has been updated. This is an android data share/copy and is ignored. " )
return
}
logger . debug ( " didRead (device= \( device . identifier ) ,payloadData= \( payloadData . shortName ) ) " )
guard let rssi = device . rssi else {
logger . debug ( " didRead rssi is nil, not proceeding " )
return
}
// D e - d u p l i c a t e p a y l o a d i n r e c e n t t i m e
if BLESensorConfiguration . filterDuplicatePayloadData != . never {
let removePayloadDataBefore = Date ( ) - BLESensorConfiguration . filterDuplicatePayloadData
let recentDidReadPayloadData = didReadPayloadData . filter ( { $0 . value >= removePayloadDataBefore } )
didReadPayloadData = recentDidReadPayloadData
if let lastReportedAt = didReadPayloadData [ payloadData ] {
logger . debug ( " didRead, filtered duplicate (device= \( device . identifier ) ,payloadData= \( payloadData . shortName ) ,lastReportedAt= \( lastReportedAt . description ) ) " )
return
}
didReadPayloadData [ payloadData ] = Date ( )
}
let proximity = Proximity ( unit : . RSSI , value : Double ( rssi ) )
delegateQueue . async {
self . delegates . forEach { $0 . sensor ( . BLE , didRead : payloadData , fromTarget : device . identifier , atProximity : proximity , withTxPower : device . txPower ) }
}
default :
return
}
}
}
extension ConcreteBLESensor : SensorDelegate {
func sensor ( _ sensor : SensorType , didUpdateState : SensorState ) {
guard let receiver = receiver as ? ConcreteBLEReceiver else {
return
}
receiver . removeConnectionDelegate ( )
transmitter . start ( )
}
}
extension TargetIdentifier {
init ( peripheral : CBPeripheral ) {
self . init ( peripheral . identifier . uuidString )
}
init ( central : CBCentral ) {
self . init ( central . identifier . uuidString )
}
}