mirror of
https://github.com/AU-COVIDSafe/mobile-ios.git
synced 2025-04-29 18:05:17 +00:00
COVIDSafe code from version 2.0
This commit is contained in:
parent
cf93ea43c0
commit
4ff6a506cf
55 changed files with 4624 additions and 1117 deletions
337
CovidSafe/Herald/Sensor/BLE/BLEDatabase.swift
Normal file
337
CovidSafe/Herald/Sensor/BLE/BLEDatabase.swift
Normal file
|
@ -0,0 +1,337 @@
|
|||
//
|
||||
// BLEDatabase.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
/// Registry for collating fragments of information from asynchronous BLE operations.
|
||||
protocol BLEDatabase {
|
||||
|
||||
/// Add delegate for handling database events
|
||||
func add(delegate: BLEDatabaseDelegate)
|
||||
|
||||
/// Get or create device for collating information from asynchronous BLE operations.
|
||||
func device(_ identifier: TargetIdentifier) -> BLEDevice
|
||||
|
||||
/// Get or create device for collating information from asynchronous BLE operations.
|
||||
func device(_ peripheral: CBPeripheral, delegate: CBPeripheralDelegate) -> BLEDevice
|
||||
|
||||
/// Get or create device for collating information from asynchronous BLE operations.
|
||||
func device(_ payload: PayloadData) -> BLEDevice
|
||||
|
||||
/// Get if a device exists
|
||||
func hasDevice(_ payload: PayloadData) -> Bool
|
||||
|
||||
/// Get all devices
|
||||
func devices() -> [BLEDevice]
|
||||
|
||||
/// Delete device from database
|
||||
func delete(_ identifier: TargetIdentifier)
|
||||
}
|
||||
|
||||
/// Delegate for receiving registry create/update/delete events
|
||||
protocol BLEDatabaseDelegate {
|
||||
|
||||
func bleDatabase(didCreate device: BLEDevice)
|
||||
|
||||
func bleDatabase(didUpdate device: BLEDevice, attribute: BLEDeviceAttribute)
|
||||
|
||||
func bleDatabase(didDelete device: BLEDevice)
|
||||
}
|
||||
|
||||
extension BLEDatabaseDelegate {
|
||||
func bleDatabase(didCreate device: BLEDevice) {}
|
||||
|
||||
func bleDatabase(didUpdate device: BLEDevice, attribute: BLEDeviceAttribute) {}
|
||||
|
||||
func bleDatabase(didDelete device: BLEDevice) {}
|
||||
}
|
||||
|
||||
class ConcreteBLEDatabase : NSObject, BLEDatabase, BLEDeviceDelegate {
|
||||
private let logger = ConcreteSensorLogger(subsystem: "Sensor", category: "BLE.ConcreteBLEDatabase")
|
||||
private var delegates: [BLEDatabaseDelegate] = []
|
||||
private var database: [TargetIdentifier : BLEDevice] = [:]
|
||||
private var queue = DispatchQueue(label: "Sensor.BLE.ConcreteBLEDatabase")
|
||||
|
||||
func add(delegate: BLEDatabaseDelegate) {
|
||||
delegates.append(delegate)
|
||||
}
|
||||
|
||||
func devices() -> [BLEDevice] {
|
||||
return database.values.map { $0 }
|
||||
}
|
||||
|
||||
func device(_ identifier: TargetIdentifier) -> BLEDevice {
|
||||
if database[identifier] == nil {
|
||||
let device = BLEDevice(identifier, delegate: self)
|
||||
database[identifier] = device
|
||||
queue.async {
|
||||
self.logger.debug("create (device=\(identifier))")
|
||||
self.delegates.forEach { $0.bleDatabase(didCreate: device) }
|
||||
}
|
||||
}
|
||||
let device = database[identifier]!
|
||||
return device
|
||||
}
|
||||
|
||||
func device(_ peripheral: CBPeripheral, delegate: CBPeripheralDelegate) -> BLEDevice {
|
||||
let identifier = TargetIdentifier(peripheral: peripheral)
|
||||
if database[identifier] == nil {
|
||||
let device = BLEDevice(identifier, delegate: self)
|
||||
database[identifier] = device
|
||||
queue.async {
|
||||
self.logger.debug("create (device=\(identifier))")
|
||||
self.delegates.forEach { $0.bleDatabase(didCreate: device) }
|
||||
}
|
||||
}
|
||||
let device = database[identifier]!
|
||||
if device.peripheral != peripheral {
|
||||
device.peripheral = peripheral
|
||||
peripheral.delegate = delegate
|
||||
}
|
||||
return device
|
||||
}
|
||||
|
||||
func device(_ payload: PayloadData) -> BLEDevice {
|
||||
if let device = database.values.filter({ $0.payloadData == payload }).first {
|
||||
return device
|
||||
}
|
||||
// Create temporary UUID, the taskRemoveDuplicatePeripherals function
|
||||
// will delete this when a direct connection to the peripheral has been
|
||||
// established
|
||||
let identifier = TargetIdentifier(UUID().uuidString)
|
||||
let placeholder = device(identifier)
|
||||
placeholder.payloadData = payload
|
||||
return placeholder
|
||||
}
|
||||
|
||||
func hasDevice(_ payload: PayloadData) -> Bool {
|
||||
if database.values.filter({ $0.payloadData == payload }).first != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func delete(_ identifier: TargetIdentifier) {
|
||||
guard let device = database[identifier] else {
|
||||
return
|
||||
}
|
||||
database[identifier] = nil
|
||||
queue.async {
|
||||
self.logger.debug("delete (device=\(identifier))")
|
||||
self.delegates.forEach { $0.bleDatabase(didDelete: device) }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK:- BLEDeviceDelegate
|
||||
|
||||
func device(_ device: BLEDevice, didUpdate attribute: BLEDeviceAttribute) {
|
||||
queue.async {
|
||||
self.logger.debug("update (device=\(device.identifier),attribute=\(attribute.rawValue))")
|
||||
self.delegates.forEach { $0.bleDatabase(didUpdate: device, attribute: attribute) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK:- BLEDatabase data
|
||||
|
||||
public class BLEDevice : NSObject {
|
||||
/// Device registratiion timestamp
|
||||
let createdAt: Date
|
||||
/// Last time anything changed, e.g. attribute update
|
||||
var lastUpdatedAt: Date
|
||||
/// Last time a wake up call was received from this device (iOS only)
|
||||
var lastNotifiedAt: Date = Date.distantPast
|
||||
/// Ephemeral device identifier, e.g. peripheral identifier UUID
|
||||
let identifier: TargetIdentifier
|
||||
/// Pseudo device address for tracking devices that change device identifier constantly like the Samsung A10, A20 and Note 8
|
||||
var pseudoDeviceAddress: BLEPseudoDeviceAddress? {
|
||||
didSet {
|
||||
lastUpdatedAt = Date()
|
||||
}}
|
||||
/// Delegate for listening to attribute updates events.
|
||||
let delegate: BLEDeviceDelegate
|
||||
/// CoreBluetooth peripheral object for interacting with this device.
|
||||
var peripheral: CBPeripheral? {
|
||||
didSet {
|
||||
lastUpdatedAt = Date()
|
||||
delegate.device(self, didUpdate: .peripheral)
|
||||
}}
|
||||
/// Service characteristic for signalling between BLE devices, e.g. to keep awake
|
||||
var signalCharacteristic: CBCharacteristic? {
|
||||
didSet {
|
||||
lastUpdatedAt = Date()
|
||||
delegate.device(self, didUpdate: .signalCharacteristic)
|
||||
}}
|
||||
/// Service characteristic for reading payload data
|
||||
var payloadCharacteristic: CBCharacteristic? {
|
||||
didSet {
|
||||
lastUpdatedAt = Date()
|
||||
delegate.device(self, didUpdate: .payloadCharacteristic)
|
||||
}}
|
||||
var legacyPayloadCharacteristic: CBCharacteristic? {
|
||||
didSet {
|
||||
lastUpdatedAt = Date()
|
||||
delegate.device(self, didUpdate: .payloadCharacteristic)
|
||||
}}
|
||||
/// Device operating system, this is necessary for selecting different interaction procedures for each platform.
|
||||
var operatingSystem: BLEDeviceOperatingSystem = .unknown {
|
||||
didSet {
|
||||
lastUpdatedAt = Date()
|
||||
delegate.device(self, didUpdate: .operatingSystem)
|
||||
}}
|
||||
/// Device is receive only, this is necessary for filtering payload sharing data
|
||||
var receiveOnly: Bool = false {
|
||||
didSet {
|
||||
lastUpdatedAt = Date()
|
||||
}}
|
||||
/// Payload data acquired from the device via payloadCharacteristic read
|
||||
var payloadData: PayloadData? {
|
||||
didSet {
|
||||
payloadDataLastUpdatedAt = Date()
|
||||
lastUpdatedAt = payloadDataLastUpdatedAt
|
||||
delegate.device(self, didUpdate: .payloadData)
|
||||
}}
|
||||
/// Payload data last update timestamp, this is used to determine what needs to be shared with peers.
|
||||
var payloadDataLastUpdatedAt: Date = Date.distantPast
|
||||
/// Payload data already shared with this peer
|
||||
var payloadSharingData: [PayloadData] = []
|
||||
/// Most recent RSSI measurement taken by readRSSI or didDiscover.
|
||||
var rssi: BLE_RSSI? {
|
||||
didSet {
|
||||
lastUpdatedAt = Date()
|
||||
rssiLastUpdatedAt = lastUpdatedAt
|
||||
delegate.device(self, didUpdate: .rssi)
|
||||
}}
|
||||
/// RSSI last update timestamp, this is used to track last advertised at without relying on didDiscover
|
||||
var rssiLastUpdatedAt: Date = Date.distantPast
|
||||
/// Transmit power data where available (only provided by Android devices)
|
||||
var txPower: BLE_TxPower? {
|
||||
didSet {
|
||||
lastUpdatedAt = Date()
|
||||
delegate.device(self, didUpdate: .txPower)
|
||||
}}
|
||||
/// Track discovered at timestamp, used by taskConnect to prioritise connection when device runs out of concurrent connection capacity
|
||||
var lastDiscoveredAt: Date = Date.distantPast
|
||||
/// Track connect request at timestamp, used by taskConnect to prioritise connection when device runs out of concurrent connection capacity
|
||||
var lastConnectRequestedAt: Date = Date.distantPast
|
||||
/// Track connected at timestamp, used by taskConnect to prioritise connection when device runs out of concurrent connection capacity
|
||||
var lastConnectedAt: Date? {
|
||||
didSet {
|
||||
// Reset lastDisconnectedAt
|
||||
lastDisconnectedAt = nil
|
||||
// Reset lastConnectionInitiationAttempt
|
||||
lastConnectionInitiationAttempt = nil
|
||||
}}
|
||||
/// Track read payload request at timestamp, used by readPayload to de-duplicate requests from asynchronous calls
|
||||
var lastReadPayloadRequestedAt: Date = Date.distantPast
|
||||
/// Track Herald initiated connection attempts - workaround for iOS peripheral caching incorrect state bug
|
||||
var lastConnectionInitiationAttempt: Date?
|
||||
/// Track disconnected at timestamp, used by taskConnect to prioritise connection when device runs out of concurrent connection capacity
|
||||
var lastDisconnectedAt: Date? {
|
||||
didSet {
|
||||
// Reset lastConnectionInitiationAttempt
|
||||
lastConnectionInitiationAttempt = nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Last advert timestamp, inferred from payloadDataLastUpdatedAt, payloadSharingDataLastUpdatedAt, rssiLastUpdatedAt
|
||||
var lastAdvertAt: Date { get {
|
||||
max(createdAt, lastDiscoveredAt, payloadDataLastUpdatedAt, rssiLastUpdatedAt)
|
||||
}}
|
||||
|
||||
/// Time interval since last payload data update, this is used to identify devices that require a payload update.
|
||||
var timeIntervalSinceLastPayloadDataUpdate: TimeInterval { get {
|
||||
Date().timeIntervalSince(payloadDataLastUpdatedAt)
|
||||
}}
|
||||
|
||||
/// Time interval since created at timestamp
|
||||
var timeIntervalSinceCreated: TimeInterval { get {
|
||||
Date().timeIntervalSince(createdAt)
|
||||
}}
|
||||
/// Time interval since last attribute value update, this is used to identify devices that may have expired and should be removed from the database.
|
||||
var timeIntervalSinceLastUpdate: TimeInterval { get {
|
||||
Date().timeIntervalSince(lastUpdatedAt)
|
||||
}}
|
||||
/// Time interval since last advert detected, this is used to detect concurrent connection quota and prioritise disconnections
|
||||
var timeIntervalSinceLastAdvert: TimeInterval { get {
|
||||
Date().timeIntervalSince(lastAdvertAt)
|
||||
}}
|
||||
/// Time interval between last connection request, this is used to priortise disconnections
|
||||
var timeIntervalSinceLastConnectRequestedAt: TimeInterval { get {
|
||||
Date().timeIntervalSince(lastConnectRequestedAt)
|
||||
}}
|
||||
/// Time interval between last connected at and last advert, this is used to estimate last period of continuous tracking, to priortise disconnections
|
||||
var timeIntervalSinceLastDisconnectedAt: TimeInterval { get {
|
||||
guard let lastDisconnectedAt = lastDisconnectedAt else {
|
||||
return Date().timeIntervalSince(createdAt)
|
||||
}
|
||||
return Date().timeIntervalSince(lastDisconnectedAt)
|
||||
}}
|
||||
/// Time interval between last connected at and last advert, this is used to estimate last period of continuous tracking, to priortise disconnections
|
||||
var timeIntervalBetweenLastConnectedAndLastAdvert: TimeInterval { get {
|
||||
guard let lastConnectedAt = lastConnectedAt, lastAdvertAt > lastConnectedAt else {
|
||||
return TimeInterval(0)
|
||||
}
|
||||
return lastAdvertAt.timeIntervalSince(lastConnectedAt)
|
||||
}}
|
||||
|
||||
public override var description: String { get {
|
||||
return "BLEDevice[id=\(identifier),os=\(operatingSystem.rawValue),payload=\(payloadData?.shortName ?? "nil"),address=\(pseudoDeviceAddress?.data.base64EncodedString() ?? "nil")]"
|
||||
}}
|
||||
|
||||
init(_ identifier: TargetIdentifier, delegate: BLEDeviceDelegate) {
|
||||
self.createdAt = Date()
|
||||
self.identifier = identifier
|
||||
self.delegate = delegate
|
||||
lastUpdatedAt = createdAt
|
||||
}
|
||||
}
|
||||
|
||||
protocol BLEDeviceDelegate {
|
||||
func device(_ device: BLEDevice, didUpdate attribute: BLEDeviceAttribute)
|
||||
}
|
||||
|
||||
enum BLEDeviceAttribute : String {
|
||||
case peripheral, signalCharacteristic, payloadCharacteristic, payloadSharingCharacteristic, operatingSystem, payloadData, rssi, txPower
|
||||
}
|
||||
|
||||
enum BLEDeviceOperatingSystem : String {
|
||||
case android, ios, restored, unknown, shared
|
||||
}
|
||||
|
||||
/// RSSI in dBm.
|
||||
typealias BLE_RSSI = Int
|
||||
|
||||
typealias BLE_TxPower = Int
|
||||
|
||||
class BLEPseudoDeviceAddress {
|
||||
let address: Int
|
||||
let data: Data
|
||||
var description: String { get {
|
||||
return "BLEPseudoDeviceAddress(address=\(address),data=\(data.base64EncodedString()))"
|
||||
}}
|
||||
|
||||
init?(fromAdvertisementData: [String: Any]) {
|
||||
guard let manufacturerData = fromAdvertisementData["kCBAdvDataManufacturerData"] as? Data else {
|
||||
return nil
|
||||
}
|
||||
guard let manufacturerId = manufacturerData.uint16(0), manufacturerId == BLESensorConfiguration.manufacturerIdForSensor else {
|
||||
return nil
|
||||
}
|
||||
guard manufacturerData.count == 8 else {
|
||||
return nil
|
||||
}
|
||||
data = Data(manufacturerData.subdata(in: 2..<8))
|
||||
var longValueData = Data(repeating: 0, count: 2)
|
||||
longValueData.append(data)
|
||||
guard let longValue = longValueData.int64(0) else {
|
||||
return nil
|
||||
}
|
||||
address = Int(longValue)
|
||||
}
|
||||
}
|
1004
CovidSafe/Herald/Sensor/BLE/BLEReceiver.swift
Normal file
1004
CovidSafe/Herald/Sensor/BLE/BLEReceiver.swift
Normal file
File diff suppressed because it is too large
Load diff
213
CovidSafe/Herald/Sensor/BLE/BLESensor.swift
Normal file
213
CovidSafe/Herald/Sensor/BLE/BLESensor.swift
Normal file
|
@ -0,0 +1,213 @@
|
|||
//
|
||||
// BLESensor.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
protocol BLESensor : Sensor {
|
||||
}
|
||||
|
||||
/// Defines BLE sensor configuration data, e.g. service and characteristic UUIDs
|
||||
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
|
||||
///Signaling characteristic for controlling connection between peripheral and central, e.g. keep each other from suspend state
|
||||
///- Characteristic UUID is randomly generated V4 UUIDs that has been tested for uniqueness by conducting web searches to ensure it returns no results.
|
||||
public static var androidSignalCharacteristicUUID = CBUUID(string: "f617b813-092e-437a-8324-e09a80821a11")
|
||||
///Signaling characteristic for controlling connection between peripheral and central, e.g. keep each other from suspend state
|
||||
///- Characteristic UUID is randomly generated V4 UUIDs that has been tested for uniqueness by conducting web searches to ensure it returns no results.
|
||||
public static var iosSignalCharacteristicUUID = CBUUID(string: "0eb0d5f2-eae4-4a9a-8af3-a4adb02d4363")
|
||||
///Primary payload characteristic (read) for distributing payload data from peripheral to central, e.g. identity data
|
||||
///- Characteristic UUID is randomly generated V4 UUIDs that has been tested for uniqueness by conducting web searches to ensure it returns no results.
|
||||
public static var payloadCharacteristicUUID = CBUUID(string: "3e98c0f8-8f05-4829-a121-43e38f8933e7")
|
||||
static let legacyCovidsafePayloadCharacteristicUUID = BluetraceConfig.BluetoothServiceID
|
||||
/// Time delay between notifications for subscribers.
|
||||
static let notificationDelay = DispatchTimeInterval.seconds(8)
|
||||
/// Time delay between advert restart
|
||||
static let advertRestartTimeInterval = TimeInterval.hour
|
||||
/// Herald internal connection expiry timeout
|
||||
static let connectionAttemptTimeout = TimeInterval(12)
|
||||
/// Expiry time for shared payloads, to ensure only recently seen payloads are shared
|
||||
/// Must be > payloadSharingTimeInterval to share pending payloads
|
||||
static let payloadSharingExpiryTimeInterval = TimeInterval.minute * 5
|
||||
/// Maximum number of concurrent BLE connections
|
||||
static let concurrentConnectionQuota = 12
|
||||
/// Manufacturer data is being used on Android to store pseudo device address
|
||||
static let manufacturerIdForSensor = UInt16(65530);
|
||||
/// Advert refresh time interval on Android devices
|
||||
static let androidAdvertRefreshTimeInterval = TimeInterval.minute * 15;
|
||||
// Filter duplicate payload data and suppress sensor(didRead:fromTarget) delegate calls
|
||||
/// - Set to .never to disable this feature
|
||||
/// - Set time interval N to filter duplicate payload data seen in last N seconds
|
||||
/// - Example : 60 means filter duplicates in last minute
|
||||
/// - Filters all occurrences of payload data from all targets
|
||||
public static var filterDuplicatePayloadData = TimeInterval(30 * 60)
|
||||
|
||||
|
||||
/// Signal characteristic action code for write payload, expect 1 byte action code followed by 2 byte little-endian Int16 integer value for payload data length, then payload data
|
||||
static let signalCharacteristicActionWritePayload = UInt8(1)
|
||||
/// Signal characteristic action code for write RSSI, expect 1 byte action code followed by 4 byte little-endian Int32 integer value for RSSI value
|
||||
static let signalCharacteristicActionWriteRSSI = UInt8(2)
|
||||
/// Signal characteristic action code for write payload, expect 1 byte action code followed by 2 byte little-endian Int16 integer value for payload sharing data length, then payload sharing data
|
||||
static let signalCharacteristicActionWritePayloadSharing = UInt8(3)
|
||||
|
||||
/// Are Location Permissions enabled in the app, and thus awake on screen on enabled
|
||||
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
|
||||
// Record payload data to enable de-duplication
|
||||
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 {
|
||||
// BLE receivers start on powerOn event, on status change the transmitter will be started.
|
||||
// This is to request permissions and turn on dialogs sequentially when registering
|
||||
receiver.addConnectionDelegate(delegate: self)
|
||||
}
|
||||
receiver.start()
|
||||
|
||||
// if permissions have been requested start transmitter immediately
|
||||
if permissionRequested {
|
||||
transmitter.start()
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
logger.debug("stop")
|
||||
transmitter.stop()
|
||||
receiver.stop()
|
||||
// BLE transmitter and receivers stops on powerOff event
|
||||
}
|
||||
|
||||
func add(delegate: SensorDelegate) {
|
||||
delegates.append(delegate)
|
||||
transmitter.add(delegate: delegate)
|
||||
receiver.add(delegate: delegate)
|
||||
}
|
||||
|
||||
// MARK:- BLEDatabaseDelegate
|
||||
|
||||
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
|
||||
}
|
||||
// De-duplicate payload in recent time
|
||||
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)
|
||||
}
|
||||
}
|
553
CovidSafe/Herald/Sensor/BLE/BLETransmitter.swift
Normal file
553
CovidSafe/Herald/Sensor/BLE/BLETransmitter.swift
Normal file
|
@ -0,0 +1,553 @@
|
|||
//
|
||||
// BLETransmitter.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
/**
|
||||
Beacon transmitter broadcasts a fixed service UUID to enable background scan by iOS. When iOS
|
||||
enters background mode, the UUID will disappear from the broadcast, so Android devices need to
|
||||
search for Apple devices and then connect and discover services to read the UUID.
|
||||
*/
|
||||
protocol BLETransmitter : Sensor {
|
||||
}
|
||||
|
||||
/**
|
||||
Transmitter offers two services:
|
||||
1. Signal characteristic for maintaining connection between iOS devices and also enable non-transmitting Android devices (receive only,
|
||||
like the Samsung J6) to make their presence known by writing their beacon code and RSSI as data to this characteristic.
|
||||
2. Payload characteristic for publishing beacon identity data.
|
||||
|
||||
Keeping the transmitter and receiver working in iOS background mode is a major challenge, in particular when both
|
||||
iOS devices are in background mode. The transmitter on iOS offers a notifying beacon characteristic that is triggered
|
||||
by writing anything to the characteristic. On characteristic write, the transmitter will call updateValue after 8 seconds
|
||||
to notify the receivers, to wake up the receivers with a didUpdateValueFor call. The process can repeat as a loop
|
||||
between the transmitter and receiver to keep both devices awake. This is unnecessary for Android-Android and also
|
||||
Android-iOS and iOS-Android detection, which can rely solely on scanForPeripherals for detection.
|
||||
|
||||
The notification based wake up method relies on an open connection which seems to be fine for iOS but may cause
|
||||
problems for Android. Experiments have found that Android devices cannot accept new connections (without explicit
|
||||
disconnect) indefinitely and the bluetooth stack ceases to function after around 500 open connections. The device
|
||||
will need to be rebooted to recover. However, if each connection is disconnected, the bluetooth stack can work
|
||||
indefinitely, but frequent connect and disconnect can still cause the same problem. The recommendation is to
|
||||
(1) always disconnect from Android as soon as the work is complete, (2) minimise the number of connections to
|
||||
an Android device, and (3) maximise time interval between connections. With all these in mind, the transmitter
|
||||
on Android does not support notify and also a connect is only performed on first contact to get the bacon code.
|
||||
*/
|
||||
class ConcreteBLETransmitter : NSObject, BLETransmitter, CBPeripheralManagerDelegate {
|
||||
private let logger = ConcreteSensorLogger(subsystem: "Sensor", category: "BLE.ConcreteBLETransmitter")
|
||||
private var delegates: [SensorDelegate] = []
|
||||
/// Dedicated sequential queue for all beacon transmitter and receiver tasks.
|
||||
private let queue: DispatchQueue
|
||||
private let delegateQueue: DispatchQueue
|
||||
private let database: BLEDatabase
|
||||
/// Beacon code generator for creating cryptographically secure public codes that can be later used for on-device matching.
|
||||
private let payloadDataSupplier: PayloadDataSupplier
|
||||
/// Peripheral manager for managing all connections, using a single manager for simplicity.
|
||||
private var peripheral: CBPeripheralManager!
|
||||
/// Beacon service and characteristics being broadcasted by the transmitter.
|
||||
private var signalCharacteristic: CBMutableCharacteristic?
|
||||
private var payloadCharacteristic: CBMutableCharacteristic?
|
||||
private var legacyCovidPayloadCharacteristic: CBMutableCharacteristic?
|
||||
private var advertisingStartedAt: Date = Date.distantPast
|
||||
/// Dummy data for writing to the receivers to trigger state restoration or resume from suspend state to background state.
|
||||
private let emptyData = Data(repeating: 0, count: 0)
|
||||
/**
|
||||
Shifting timer for triggering notify for subscribers several seconds after resume from suspend state to background state,
|
||||
but before re-entering suspend state. The time limit is under 10 seconds as desribed in Apple documentation.
|
||||
*/
|
||||
private var notifyTimer: DispatchSourceTimer?
|
||||
/// Dedicated sequential queue for the shifting timer.
|
||||
private let notifyTimerQueue = DispatchQueue(label: "Sensor.BLE.ConcreteBLETransmitter.Timer")
|
||||
|
||||
/**
|
||||
Create a transmitter that uses the same sequential dispatch queue as the receiver.
|
||||
Transmitter starts automatically when Bluetooth is enabled.
|
||||
*/
|
||||
init(queue: DispatchQueue, delegateQueue: DispatchQueue, database: BLEDatabase, payloadDataSupplier: PayloadDataSupplier) {
|
||||
self.queue = queue
|
||||
self.delegateQueue = delegateQueue
|
||||
self.database = database
|
||||
self.payloadDataSupplier = payloadDataSupplier
|
||||
super.init()
|
||||
|
||||
}
|
||||
|
||||
func add(delegate: SensorDelegate) {
|
||||
delegates.append(delegate)
|
||||
}
|
||||
|
||||
func start() {
|
||||
logger.debug("start")
|
||||
|
||||
// Create a peripheral that supports state restoration
|
||||
if peripheral == nil {
|
||||
self.peripheral = CBPeripheralManager(delegate: self, queue: queue, options: [
|
||||
CBPeripheralManagerOptionRestoreIdentifierKey : "Sensor.BLE.ConcreteBLETransmitter",
|
||||
CBPeripheralManagerOptionShowPowerAlertKey : true
|
||||
])
|
||||
}
|
||||
|
||||
guard peripheral.state == .poweredOn else {
|
||||
logger.fault("start denied, not powered on")
|
||||
return
|
||||
}
|
||||
if signalCharacteristic != nil, payloadCharacteristic != nil, legacyCovidPayloadCharacteristic != nil {
|
||||
logger.debug("starting advert with existing characteristics")
|
||||
if !peripheral.isAdvertising {
|
||||
startAdvertising(withNewCharacteristics: false)
|
||||
} else {
|
||||
queue.async {
|
||||
self.peripheral.stopAdvertising()
|
||||
self.peripheral.startAdvertising([CBAdvertisementDataServiceUUIDsKey : [BLESensorConfiguration.serviceUUID]])
|
||||
}
|
||||
}
|
||||
logger.debug("start successful, for existing characteristics")
|
||||
} else {
|
||||
startAdvertising(withNewCharacteristics: true)
|
||||
logger.debug("start successful, for new characteristics")
|
||||
}
|
||||
signalCharacteristic?.subscribedCentrals?.forEach() { central in
|
||||
// FEATURE : Symmetric connection on subscribe
|
||||
_ = database.device(central.identifier.uuidString)
|
||||
}
|
||||
notifySubscribers("start")
|
||||
}
|
||||
|
||||
func stop() {
|
||||
logger.debug("stop")
|
||||
guard peripheral != nil else {
|
||||
return
|
||||
}
|
||||
guard peripheral.isAdvertising else {
|
||||
logger.fault("stop denied, already stopped (source=%s)")
|
||||
self.peripheral = nil
|
||||
return
|
||||
}
|
||||
stopAdvertising()
|
||||
}
|
||||
|
||||
private func startAdvertising(withNewCharacteristics: Bool) {
|
||||
logger.debug("startAdvertising (withNewCharacteristics=\(withNewCharacteristics))")
|
||||
if withNewCharacteristics || signalCharacteristic == nil || payloadCharacteristic == nil || legacyCovidPayloadCharacteristic == nil {
|
||||
signalCharacteristic = CBMutableCharacteristic(type: BLESensorConfiguration.iosSignalCharacteristicUUID, properties: [.write, .notify], value: nil, permissions: [.writeable])
|
||||
payloadCharacteristic = CBMutableCharacteristic(type: BLESensorConfiguration.payloadCharacteristicUUID, properties: [.read], value: nil, permissions: [.readable])
|
||||
legacyCovidPayloadCharacteristic = CBMutableCharacteristic(type: BluetraceConfig.BluetoothServiceID, properties: [.read, .write, .writeWithoutResponse], value: nil, permissions: [.readable, .writeable])
|
||||
}
|
||||
let service = CBMutableService(type: BLESensorConfiguration.serviceUUID, primary: true)
|
||||
signalCharacteristic?.value = nil
|
||||
payloadCharacteristic?.value = nil
|
||||
legacyCovidPayloadCharacteristic?.value = nil
|
||||
service.characteristics = [signalCharacteristic!, payloadCharacteristic!, legacyCovidPayloadCharacteristic!]
|
||||
queue.async {
|
||||
self.peripheral.stopAdvertising()
|
||||
self.peripheral.removeAllServices()
|
||||
self.peripheral.add(service)
|
||||
self.peripheral.startAdvertising([CBAdvertisementDataServiceUUIDsKey : [BLESensorConfiguration.serviceUUID]])
|
||||
}
|
||||
}
|
||||
|
||||
private func stopAdvertising() {
|
||||
logger.debug("stopAdvertising()")
|
||||
queue.async {
|
||||
self.peripheral.stopAdvertising()
|
||||
self.peripheral = nil
|
||||
}
|
||||
notifyTimer?.cancel()
|
||||
notifyTimer = nil
|
||||
}
|
||||
|
||||
/// All work starts from notify subscribers loop.
|
||||
/// Generate updateValue notification after 8 seconds to notify all subscribers and keep the iOS receivers awake.
|
||||
private func notifySubscribers(_ source: String) {
|
||||
notifyTimer?.cancel()
|
||||
notifyTimer = DispatchSource.makeTimerSource(queue: notifyTimerQueue)
|
||||
notifyTimer?.schedule(deadline: DispatchTime.now() + BLESensorConfiguration.notificationDelay)
|
||||
notifyTimer?.setEventHandler { [weak self] in
|
||||
guard let s = self, let logger = self?.logger, let signalCharacteristic = self?.signalCharacteristic else {
|
||||
return
|
||||
}
|
||||
// Notify subscribers to keep them awake
|
||||
s.queue.async {
|
||||
logger.debug("notifySubscribers (source=\(source))")
|
||||
s.peripheral.updateValue(s.emptyData, for: signalCharacteristic, onSubscribedCentrals: nil)
|
||||
}
|
||||
// Restart advert if required
|
||||
let advertUpTime = Date().timeIntervalSince(s.advertisingStartedAt)
|
||||
if s.peripheral.isAdvertising, advertUpTime > BLESensorConfiguration.advertRestartTimeInterval {
|
||||
logger.debug("advertRestart (upTime=\(advertUpTime))")
|
||||
s.startAdvertising(withNewCharacteristics: true)
|
||||
}
|
||||
}
|
||||
notifyTimer?.resume()
|
||||
}
|
||||
|
||||
// MARK:- CBPeripheralManagerDelegate
|
||||
|
||||
/// Restore advert and reinstate advertised characteristics.
|
||||
func peripheralManager(_ peripheral: CBPeripheralManager, willRestoreState dict: [String : Any]) {
|
||||
logger.debug("willRestoreState")
|
||||
self.peripheral = peripheral
|
||||
peripheral.delegate = self
|
||||
if let services = dict[CBPeripheralManagerRestoredStateServicesKey] as? [CBMutableService] {
|
||||
for service in services {
|
||||
logger.debug("willRestoreState (service=\(service.uuid.uuidString))")
|
||||
if let characteristics = service.characteristics {
|
||||
for characteristic in characteristics {
|
||||
logger.debug("willRestoreState (characteristic=\(characteristic.uuid.uuidString))")
|
||||
switch characteristic.uuid {
|
||||
case BLESensorConfiguration.androidSignalCharacteristicUUID:
|
||||
if let mutableCharacteristic = characteristic as? CBMutableCharacteristic {
|
||||
signalCharacteristic = mutableCharacteristic
|
||||
logger.debug("willRestoreState (androidSignalCharacteristic=\(characteristic.uuid.uuidString))")
|
||||
} else {
|
||||
logger.fault("willRestoreState characteristic not mutable (androidSignalCharacteristic=\(characteristic.uuid.uuidString))")
|
||||
}
|
||||
case BLESensorConfiguration.iosSignalCharacteristicUUID:
|
||||
if let mutableCharacteristic = characteristic as? CBMutableCharacteristic {
|
||||
signalCharacteristic = mutableCharacteristic
|
||||
logger.debug("willRestoreState (iosSignalCharacteristic=\(characteristic.uuid.uuidString))")
|
||||
} else {
|
||||
logger.fault("willRestoreState characteristic not mutable (iosSignalCharacteristic=\(characteristic.uuid.uuidString))")
|
||||
}
|
||||
case BLESensorConfiguration.payloadCharacteristicUUID:
|
||||
if let mutableCharacteristic = characteristic as? CBMutableCharacteristic {
|
||||
payloadCharacteristic = mutableCharacteristic
|
||||
logger.debug("willRestoreState (payloadCharacteristic=\(characteristic.uuid.uuidString))")
|
||||
} else {
|
||||
logger.fault("willRestoreState characteristic not mutable (payloadCharacteristic=\(characteristic.uuid.uuidString))")
|
||||
}
|
||||
default:
|
||||
logger.debug("willRestoreState (unknownCharacteristic=\(characteristic.uuid.uuidString))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Start advertising on bluetooth power on.
|
||||
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
|
||||
// Bluetooth on -> Advertise
|
||||
if (peripheral.state == .poweredOn) {
|
||||
logger.debug("Update state (state=poweredOn)")
|
||||
start()
|
||||
} else {
|
||||
if #available(iOS 10.0, *) {
|
||||
logger.debug("Update state (state=\(peripheral.state.description))")
|
||||
} else {
|
||||
// Required to support iOS 9.3
|
||||
switch peripheral.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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
|
||||
logger.debug("peripheralManagerDidStartAdvertising (error=\(String(describing: error)))")
|
||||
if error == nil {
|
||||
advertisingStartedAt = Date()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Write request offers a mechanism for non-transmitting BLE devices (e.g. Samsung J6 can only receive) to make
|
||||
its presence known by submitting its beacon code and RSSI as data. This also offers a mechanism for iOS to
|
||||
write blank data to transmitter to keep bringing it back from suspended state to background state which increases
|
||||
its chance of background scanning over a long period without being killed off. Payload sharing is also based on
|
||||
write characteristic to enable Android peers to act as a bridge for sharing iOS device payloads, thus enabling
|
||||
iOS - iOS background detection without location permission or screen on, as background detection and tracking method.
|
||||
*/
|
||||
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
|
||||
// Write -> Notify delegates -> Write response -> Notify subscribers
|
||||
for request in requests {
|
||||
let targetIdentifier = TargetIdentifier(central: request.central)
|
||||
// FEATURE : Symmetric connection on write
|
||||
let targetDevice = database.device(targetIdentifier)
|
||||
logger.debug("didReceiveWrite (central=\(targetIdentifier))")
|
||||
if let data = request.value {
|
||||
guard request.characteristic.uuid != legacyCovidPayloadCharacteristic?.uuid else {
|
||||
logger.debug("didReceiveWrite (central=\(targetIdentifier),action=writeLegacyCovidPayload)")
|
||||
|
||||
// we don't do anything with the payload.
|
||||
// Herald relies only on reads. Therefore when legacy writes we ignore.
|
||||
// However, to maintain legacy data as expected, payload is still written after read.
|
||||
// See BLEReceiver writeLegacyPayload
|
||||
queue.async { peripheral.respond(to: request, withResult: .success) }
|
||||
continue
|
||||
}
|
||||
if data.count == 0 {
|
||||
// Receiver writes blank data on detection of transmitter to bring iOS transmitter back from suspended state
|
||||
logger.debug("didReceiveWrite (central=\(targetIdentifier),action=wakeTransmitter)")
|
||||
queue.async { peripheral.respond(to: request, withResult: .success) }
|
||||
} else if let actionCode = data.uint8(0) {
|
||||
switch actionCode {
|
||||
case BLESensorConfiguration.signalCharacteristicActionWritePayload:
|
||||
// Receive-only Android device writing its payload to make its presence known
|
||||
logger.debug("didReceiveWrite (central=\(targetIdentifier),action=writePayload)")
|
||||
// writePayload data format
|
||||
// 0-0 : actionCode
|
||||
// 1-2 : payload data count in bytes (Int16)
|
||||
// 3.. : payload data
|
||||
if let payloadDataCount = data.int16(1) {
|
||||
logger.debug("didReceiveWrite -> didDetect=\(targetIdentifier)")
|
||||
delegateQueue.async {
|
||||
self.delegates.forEach { $0.sensor(.BLE, didDetect: targetIdentifier) }
|
||||
}
|
||||
if data.count == (3 + payloadDataCount) {
|
||||
let payloadData = PayloadData(data.subdata(in: 3..<data.count))
|
||||
logger.debug("didReceiveWrite -> didRead=\(payloadData.shortName),fromTarget=\(targetIdentifier)")
|
||||
targetDevice.operatingSystem = .android
|
||||
targetDevice.receiveOnly = true
|
||||
targetDevice.payloadData = payloadData
|
||||
queue.async { peripheral.respond(to: request, withResult: .success) }
|
||||
} else {
|
||||
logger.fault("didReceiveWrite, invalid payload (central=\(targetIdentifier),action=writePayload)")
|
||||
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
|
||||
}
|
||||
} else {
|
||||
logger.fault("didReceiveWrite, invalid request (central=\(targetIdentifier),action=writePayload)")
|
||||
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
|
||||
|
||||
}
|
||||
case BLESensorConfiguration.signalCharacteristicActionWriteRSSI:
|
||||
// Receive-only Android device writing its RSSI to make its proximity known
|
||||
logger.debug("didReceiveWrite (central=\(targetIdentifier),action=writeRSSI)")
|
||||
// writeRSSI data format
|
||||
// 0-0 : actionCode
|
||||
// 1-2 : rssi value (Int16)
|
||||
if let rssi = data.int16(1) {
|
||||
let proximity = Proximity(unit: .RSSI, value: Double(rssi))
|
||||
logger.debug("didReceiveWrite -> didMeasure=\(proximity.description),fromTarget=\(targetIdentifier)")
|
||||
targetDevice.operatingSystem = .android
|
||||
targetDevice.receiveOnly = true
|
||||
targetDevice.rssi = BLE_RSSI(rssi)
|
||||
queue.async { peripheral.respond(to: request, withResult: .success) }
|
||||
} else {
|
||||
logger.fault("didReceiveWrite, invalid request (central=\(targetIdentifier),action=writeRSSI)")
|
||||
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
|
||||
}
|
||||
case BLESensorConfiguration.signalCharacteristicActionWritePayloadSharing:
|
||||
// Android device sharing detected iOS devices with this iOS device to enable background detection
|
||||
logger.debug("didReceiveWrite (central=\(targetIdentifier),action=writePayloadSharing)")
|
||||
// writePayloadSharing data format
|
||||
// 0-0 : actionCode
|
||||
// 1-2 : rssi value (Int16)
|
||||
// 3-4 : payload sharing data count in bytes (Int16)
|
||||
// 5.. : payload sharing data (to be parsed by payload data supplier)
|
||||
if let rssi = data.int16(1), let payloadDataCount = data.int16(3) {
|
||||
// skip if a payload with length 0 is sent
|
||||
if data.count == (5 + payloadDataCount) && payloadDataCount > 0 {
|
||||
let payloadSharingData = payloadDataSupplier.payload(data.subdata(in: 5..<data.count))
|
||||
logger.debug("didReceiveWrite -> didShare=\(payloadSharingData.description),fromTarget=\(targetIdentifier)")
|
||||
let proximity = Proximity(unit: .RSSI, value: Double(rssi))
|
||||
|
||||
var filteredPayloadSharingData: [PayloadData]
|
||||
if let cachedPayload = EncounterMessageManager.shared.getLastKnownAdvertisementPayload(identifier: request.central.identifier) {
|
||||
// check that the shared data is not the data sent to devices we are receiving from and it does not exist already
|
||||
filteredPayloadSharingData = payloadSharingData.filter({ (dataToCheck) -> Bool in
|
||||
return dataToCheck != cachedPayload && !self.database.hasDevice(dataToCheck)
|
||||
})
|
||||
} else {
|
||||
// check that it does not exist already
|
||||
filteredPayloadSharingData = payloadSharingData.filter({ (dataToCheck) -> Bool in
|
||||
return !self.database.hasDevice(dataToCheck)
|
||||
})
|
||||
}
|
||||
|
||||
self.logger.debug("didReceiveWrite -> filtered didShare=\(filteredPayloadSharingData.description),fromTarget=\(targetIdentifier)")
|
||||
|
||||
queue.async { peripheral.respond(to: request, withResult: .success) }
|
||||
|
||||
|
||||
targetDevice.operatingSystem = .android
|
||||
targetDevice.rssi = BLE_RSSI(rssi)
|
||||
filteredPayloadSharingData.forEach() { payloadData in
|
||||
logger.debug("didReceiveWrite, storing device with shared payload=\(payloadData.shortName)")
|
||||
let sharedDevice = self.database.device(payloadData)
|
||||
if sharedDevice.operatingSystem == .unknown {
|
||||
sharedDevice.operatingSystem = .shared
|
||||
}
|
||||
sharedDevice.rssi = BLE_RSSI(rssi)
|
||||
|
||||
self.delegateQueue.async {
|
||||
self.delegates.forEach {
|
||||
$0.sensor(.BLE, didShare: filteredPayloadSharingData, fromTarget: sharedDevice.identifier, atProximity: proximity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.fault("didReceiveWrite, invalid payload (central=\(targetIdentifier),action=writePayloadSharing)")
|
||||
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
|
||||
}
|
||||
} else {
|
||||
logger.fault("didReceiveWrite, invalid request (central=\(targetIdentifier),action=writePayloadSharing)")
|
||||
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
|
||||
}
|
||||
default:
|
||||
logger.fault("didReceiveWrite (central=\(targetIdentifier),action=unknown,actionCode=\(actionCode))")
|
||||
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
queue.async { peripheral.respond(to: request, withResult: .invalidAttributeValueLength) }
|
||||
}
|
||||
}
|
||||
notifySubscribers("didReceiveWrite")
|
||||
}
|
||||
|
||||
/// Read request from central for obtaining payload data from this peripheral.
|
||||
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
|
||||
// Read -> Notify subscribers
|
||||
let central = database.device(TargetIdentifier(request.central.identifier.uuidString))
|
||||
switch request.characteristic.uuid {
|
||||
case BLESensorConfiguration.payloadCharacteristicUUID, BLESensorConfiguration.legacyCovidsafePayloadCharacteristicUUID:
|
||||
logger.debug("Read received (central=\(central.description),characteristic=payload,offset=\(request.offset))")
|
||||
payloadDataSupplier.payload(request.central.identifier,
|
||||
offset: request.offset) { [self] (payloadData) in
|
||||
queue.async {
|
||||
guard let data = payloadData else {
|
||||
peripheral.respond(to: request, withResult: .unlikelyError)
|
||||
return
|
||||
}
|
||||
logger.debug("Read received (central=\(central.description),characteristic=\(request.characteristic.uuid),payload=\(data.shortName))")
|
||||
guard request.offset < data.count else {
|
||||
logger.fault("Read, invalid offset (central=\(central.description),characteristic=payload,offset=\(request.offset),data=\(data.count))")
|
||||
peripheral.respond(to: request, withResult: .invalidOffset)
|
||||
return
|
||||
}
|
||||
guard request.offset != data.count else {
|
||||
// the receiver already read all the data in its last read request
|
||||
peripheral.respond(to: request, withResult: .success)
|
||||
return
|
||||
}
|
||||
request.value = (request.offset == 0 ? data : data.subdata(in: request.offset..<data.count))
|
||||
peripheral.respond(to: request, withResult: .success)
|
||||
}
|
||||
}
|
||||
default:
|
||||
logger.fault("Read (central=\(central.description),characteristic=unknown)")
|
||||
queue.async { peripheral.respond(to: request, withResult: .requestNotSupported) }
|
||||
}
|
||||
notifySubscribers("didReceiveRead")
|
||||
}
|
||||
|
||||
/// Another iOS central has subscribed to this iOS peripheral, implying the central is also a peripheral for this device to connect to.
|
||||
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
|
||||
// Subscribe -> Notify subscribers
|
||||
// iOS receiver subscribes to the signal characteristic on first contact. This ensures the first call keeps
|
||||
// the transmitter and receiver awake. Future loops will rely on didReceiveWrite as the trigger.
|
||||
logger.debug("Subscribe (central=\(central.identifier.uuidString))")
|
||||
// FEATURE : Symmetric connection on subscribe
|
||||
_ = database.device(central.identifier.uuidString)
|
||||
notifySubscribers("didSubscribeTo")
|
||||
}
|
||||
|
||||
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
|
||||
// Unsubscribe -> Notify subscribers
|
||||
logger.debug("Unsubscribe (central=\(central.identifier.uuidString))")
|
||||
// FEATURE : Symmetric connection on unsubscribe
|
||||
_ = database.device(central.identifier.uuidString)
|
||||
notifySubscribers("didUnsubscribeFrom")
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
/// Get Int8 from byte array (little-endian).
|
||||
func int8(_ index: Int) -> Int8? {
|
||||
guard let value = uint8(index) else {
|
||||
return nil
|
||||
}
|
||||
return Int8(bitPattern: value)
|
||||
}
|
||||
|
||||
/// Get UInt8 from byte array (little-endian).
|
||||
func uint8(_ index: Int) -> UInt8? {
|
||||
let bytes = [UInt8](self)
|
||||
guard index < bytes.count else {
|
||||
return nil
|
||||
}
|
||||
return bytes[index]
|
||||
}
|
||||
|
||||
/// Get Int16 from byte array (little-endian).
|
||||
func int16(_ index: Int) -> Int16? {
|
||||
guard let value = uint16(index) else {
|
||||
return nil
|
||||
}
|
||||
return Int16(bitPattern: value)
|
||||
}
|
||||
|
||||
/// Get UInt16 from byte array (little-endian).
|
||||
func uint16(_ index: Int) -> UInt16? {
|
||||
let bytes = [UInt8](self)
|
||||
guard index < (bytes.count - 1) else {
|
||||
return nil
|
||||
}
|
||||
return UInt16(bytes[index]) |
|
||||
UInt16(bytes[index + 1]) << 8
|
||||
}
|
||||
|
||||
/// Get Int32 from byte array (little-endian).
|
||||
func int32(_ index: Int) -> Int32? {
|
||||
guard let value = uint32(index) else {
|
||||
return nil
|
||||
}
|
||||
return Int32(bitPattern: value)
|
||||
}
|
||||
|
||||
/// Get UInt32 from byte array (little-endian).
|
||||
func uint32(_ index: Int) -> UInt32? {
|
||||
let bytes = [UInt8](self)
|
||||
guard index < (bytes.count - 3) else {
|
||||
return nil
|
||||
}
|
||||
return UInt32(bytes[index]) |
|
||||
UInt32(bytes[index + 1]) << 8 |
|
||||
UInt32(bytes[index + 2]) << 16 |
|
||||
UInt32(bytes[index + 3]) << 24
|
||||
}
|
||||
|
||||
/// Get Int64 from byte array (little-endian).
|
||||
func int64(_ index: Int) -> Int64? {
|
||||
guard let value = uint64(index) else {
|
||||
return nil
|
||||
}
|
||||
return Int64(bitPattern: value)
|
||||
}
|
||||
|
||||
/// Get UInt64 from byte array (little-endian).
|
||||
func uint64(_ index: Int) -> UInt64? {
|
||||
let bytes = [UInt8](self)
|
||||
guard index < (bytes.count - 7) else {
|
||||
return nil
|
||||
}
|
||||
return UInt64(bytes[index]) |
|
||||
UInt64(bytes[index + 1]) << 8 |
|
||||
UInt64(bytes[index + 2]) << 16 |
|
||||
UInt64(bytes[index + 3]) << 24 |
|
||||
UInt64(bytes[index + 4]) << 32 |
|
||||
UInt64(bytes[index + 5]) << 40 |
|
||||
UInt64(bytes[index + 6]) << 48 |
|
||||
UInt64(bytes[index + 7]) << 56
|
||||
}
|
||||
}
|
195
CovidSafe/Herald/Sensor/BLE/BLEUtilities.swift
Normal file
195
CovidSafe/Herald/Sensor/BLE/BLEUtilities.swift
Normal file
|
@ -0,0 +1,195 @@
|
|||
//
|
||||
// BLEUtilities.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreBluetooth
|
||||
|
||||
/**
|
||||
Extension to make the state human readable in logs.
|
||||
*/
|
||||
@available(iOS 10.0, *)
|
||||
extension CBManagerState: CustomStringConvertible {
|
||||
/**
|
||||
Get plain text description of state.
|
||||
*/
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .poweredOff: return ".poweredOff"
|
||||
case .poweredOn: return ".poweredOn"
|
||||
case .resetting: return ".resetting"
|
||||
case .unauthorized: return ".unauthorized"
|
||||
case .unknown: return ".unknown"
|
||||
case .unsupported: return ".unsupported"
|
||||
@unknown default: return "undefined"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CBPeripheralManagerState : CustomStringConvertible {
|
||||
/**
|
||||
Get plain text description of state.
|
||||
*/
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .poweredOff: return ".poweredOff"
|
||||
case .poweredOn: return ".poweredOn"
|
||||
case .resetting: return ".resetting"
|
||||
case .unauthorized: return ".unauthorized"
|
||||
case .unknown: return ".unknown"
|
||||
case .unsupported: return ".unsupported"
|
||||
@unknown default: return "undefined"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CBCentralManagerState : CustomStringConvertible {
|
||||
/**
|
||||
Get plain text description of state.
|
||||
*/
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .poweredOff: return ".poweredOff"
|
||||
case .poweredOn: return ".poweredOn"
|
||||
case .resetting: return ".resetting"
|
||||
case .unauthorized: return ".unauthorized"
|
||||
case .unknown: return ".unknown"
|
||||
case .unsupported: return ".unsupported"
|
||||
@unknown default: return "undefined"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Extension to make the state human readable in logs.
|
||||
*/
|
||||
extension CBPeripheralState: CustomStringConvertible {
|
||||
/**
|
||||
Get plain text description fo state.
|
||||
*/
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .connected: return ".connected"
|
||||
case .connecting: return ".connecting"
|
||||
case .disconnected: return ".disconnected"
|
||||
case .disconnecting: return ".disconnecting"
|
||||
@unknown default: return "undefined"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Extension to make the time intervals more human readable in code.
|
||||
*/
|
||||
extension TimeInterval {
|
||||
static var day: TimeInterval { get { TimeInterval(86400) } }
|
||||
static var hour: TimeInterval { get { TimeInterval(3600) } }
|
||||
static var minute: TimeInterval { get { TimeInterval(60) } }
|
||||
static var never: TimeInterval { get { TimeInterval(Int.max) } }
|
||||
}
|
||||
|
||||
/**
|
||||
Sample statistics.
|
||||
*/
|
||||
class Sample {
|
||||
private var n:Int64 = 0
|
||||
private var m1:Double = 0.0
|
||||
private var m2:Double = 0.0
|
||||
private var m3:Double = 0.0
|
||||
private var m4:Double = 0.0
|
||||
|
||||
/**
|
||||
Minimum sample value.
|
||||
*/
|
||||
var min:Double? = nil
|
||||
/**
|
||||
Maximum sample value.
|
||||
*/
|
||||
var max:Double? = nil
|
||||
/**
|
||||
Sample size.
|
||||
*/
|
||||
var count:Int64 { get { n } }
|
||||
/**
|
||||
Mean sample value.
|
||||
*/
|
||||
var mean:Double? { get { n > 0 ? m1 : nil } }
|
||||
/**
|
||||
Sample variance.
|
||||
*/
|
||||
var variance:Double? { get { n > 1 ? m2 / Double(n - 1) : nil } }
|
||||
/**
|
||||
Sample standard deviation.
|
||||
*/
|
||||
var standardDeviation:Double? { get { n > 1 ? sqrt(m2 / Double(n - 1)) : nil } }
|
||||
/**
|
||||
String representation of mean, standard deviation, min and max
|
||||
*/
|
||||
var description: String { get {
|
||||
let sCount = n.description
|
||||
let sMean = (mean == nil ? "-" : mean!.description)
|
||||
let sStandardDeviation = (standardDeviation == nil ? "-" : standardDeviation!.description)
|
||||
let sMin = (min == nil ? "-" : min!.description)
|
||||
let sMax = (max == nil ? "-" : max!.description)
|
||||
return "count=" + sCount + ",mean=" + sMean + ",sd=" + sStandardDeviation + ",min=" + sMin + ",max=" + sMax
|
||||
} }
|
||||
|
||||
/**
|
||||
Add sample value.
|
||||
*/
|
||||
func add(_ x:Double) {
|
||||
// Sample value accumulation algorithm avoids reiterating sample to compute variance.
|
||||
let n1 = n
|
||||
n += 1
|
||||
let d = x - m1
|
||||
let d_n = d / Double(n)
|
||||
let d_n2 = d_n * d_n;
|
||||
let t = d * d_n * Double(n1);
|
||||
m1 += d_n;
|
||||
m4 += t * d_n2 * Double(n * n - 3 * n + 3) + 6 * d_n2 * m2 - 4 * d_n * m3;
|
||||
m3 += t * d_n * Double(n - 2) - 3 * d_n * m2;
|
||||
m2 += t;
|
||||
if min == nil || x < min! {
|
||||
min = x;
|
||||
}
|
||||
if max == nil || x > max! {
|
||||
max = x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Time interval samples for collecting elapsed time statistics.
|
||||
*/
|
||||
class TimeIntervalSample : Sample {
|
||||
private var startTime: Date?
|
||||
private var timestamp: Date?
|
||||
var period: TimeInterval? { get {
|
||||
(startTime == nil ? nil : timestamp?.timeIntervalSince(startTime!))
|
||||
}}
|
||||
|
||||
override var description: String { get {
|
||||
let sPeriod = (period == nil ? "-" : period!.description)
|
||||
return super.description + ",period=" + sPeriod
|
||||
}}
|
||||
|
||||
/**
|
||||
Add elapsed time since last call to add() as sample.
|
||||
*/
|
||||
func add() {
|
||||
guard timestamp != nil else {
|
||||
timestamp = Date()
|
||||
startTime = timestamp
|
||||
return
|
||||
}
|
||||
let now = Date()
|
||||
if let timestamp = timestamp {
|
||||
add(now.timeIntervalSince(timestamp))
|
||||
}
|
||||
timestamp = now
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue