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
|
||||
}
|
||||
}
|
50
CovidSafe/Herald/Sensor/Data/BatteryLog.swift
Normal file
50
CovidSafe/Herald/Sensor/Data/BatteryLog.swift
Normal file
|
@ -0,0 +1,50 @@
|
|||
//
|
||||
// BatteryLog.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import NotificationCenter
|
||||
import os
|
||||
|
||||
/// Battery log for monitoring battery level over time
|
||||
class BatteryLog {
|
||||
private let logger = ConcreteSensorLogger(subsystem: "Sensor", category: "BatteryLog")
|
||||
private let textFile: TextFile
|
||||
private let dateFormatter = DateFormatter()
|
||||
private let updateInterval = TimeInterval(30)
|
||||
|
||||
init(filename: String) {
|
||||
textFile = TextFile(filename: filename)
|
||||
if textFile.empty() {
|
||||
textFile.write("time,source,level")
|
||||
}
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
UIDevice.current.isBatteryMonitoringEnabled = true
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(batteryLevelDidChange), name: UIDevice.batteryLevelDidChangeNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(batteryStateDidChange), name: UIDevice.batteryStateDidChangeNotification, object: nil)
|
||||
let _ = Timer.scheduledTimer(timeInterval: updateInterval, target: self, selector: #selector(update), userInfo: nil, repeats: true)
|
||||
}
|
||||
|
||||
private func timestamp() -> String {
|
||||
let timestamp = dateFormatter.string(from: Date())
|
||||
return timestamp
|
||||
}
|
||||
|
||||
@objc func update() {
|
||||
let powerSource = (UIDevice.current.batteryState == .unplugged ? "battery" : "external")
|
||||
let batteryLevel = Float(UIDevice.current.batteryLevel * 100).description
|
||||
textFile.write(timestamp() + "," + powerSource + "," + batteryLevel)
|
||||
logger.debug("update (powerSource=\(powerSource),batteryLevel=\(batteryLevel))");
|
||||
}
|
||||
|
||||
@objc func batteryLevelDidChange(_ sender: NotificationCenter) {
|
||||
update()
|
||||
}
|
||||
|
||||
@objc func batteryStateDidChange(_ sender: NotificationCenter) {
|
||||
update()
|
||||
}
|
||||
}
|
57
CovidSafe/Herald/Sensor/Data/ContactLog.swift
Normal file
57
CovidSafe/Herald/Sensor/Data/ContactLog.swift
Normal file
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// ContactLog.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// CSV contact log for post event analysis and visualisation
|
||||
class ContactLog: NSObject, SensorDelegate {
|
||||
private let textFile: TextFile
|
||||
private let dateFormatter = DateFormatter()
|
||||
|
||||
init(filename: String) {
|
||||
textFile = TextFile(filename: filename)
|
||||
if textFile.empty() {
|
||||
textFile.write("time,sensor,id,detect,read,measure,share,visit,data")
|
||||
}
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
}
|
||||
|
||||
private func timestamp() -> String {
|
||||
let timestamp = dateFormatter.string(from: Date())
|
||||
return timestamp
|
||||
}
|
||||
|
||||
private func csv(_ value: String) -> String {
|
||||
return TextFile.csv(value)
|
||||
}
|
||||
|
||||
// MARK:- SensorDelegate
|
||||
|
||||
func sensor(_ sensor: SensorType, didDetect: TargetIdentifier) {
|
||||
textFile.write(timestamp() + "," + sensor.rawValue + "," + csv(didDetect) + ",1,,,,,")
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier) {
|
||||
textFile.write(timestamp() + "," + sensor.rawValue + "," + csv(fromTarget) + ",,2,,,," + csv(didRead.shortName))
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier, atProximity: Proximity, withTxPower: Int?) {
|
||||
textFile.write(timestamp() + "," + sensor.rawValue + "," + csv(fromTarget) + ",,2,,,," + csv(didRead.shortName))
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier) {
|
||||
textFile.write(timestamp() + "," + sensor.rawValue + "," + csv(fromTarget) + ",,,3,,," + csv(didMeasure.description))
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didShare: [PayloadData], fromTarget: TargetIdentifier, atProximity: Proximity) {
|
||||
let prefix = timestamp() + "," + sensor.rawValue + "," + csv(fromTarget)
|
||||
didShare.forEach() { payloadData in
|
||||
textFile.write(prefix + ",,,,4,," + csv(payloadData.shortName))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
88
CovidSafe/Herald/Sensor/Data/DetectionLog.swift
Normal file
88
CovidSafe/Herald/Sensor/Data/DetectionLog.swift
Normal file
|
@ -0,0 +1,88 @@
|
|||
//
|
||||
// DetectionLog.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
/// CSV contact log for post event analysis and visualisation
|
||||
class DetectionLog: NSObject, SensorDelegate {
|
||||
private let logger = ConcreteSensorLogger(subsystem: "Sensor", category: "Data.DetectionLog")
|
||||
private let textFile: TextFile
|
||||
private let payloadData: PayloadData
|
||||
private let deviceName = UIDevice.current.name
|
||||
private let deviceOS = UIDevice.current.systemVersion
|
||||
private var payloads: Set<String> = []
|
||||
private let queue = DispatchQueue(label: "Sensor.Data.DetectionLog.Queue")
|
||||
|
||||
init(filename: String, payloadData: PayloadData) {
|
||||
textFile = TextFile(filename: filename)
|
||||
self.payloadData = payloadData
|
||||
super.init()
|
||||
write()
|
||||
}
|
||||
|
||||
private func csv(_ value: String) -> String {
|
||||
return TextFile.csv(value)
|
||||
}
|
||||
|
||||
private func write() {
|
||||
var content = "\(csv(deviceName)),iOS,\(csv(deviceOS)),\(csv(payloadData.shortName))"
|
||||
var payloadList: [String] = []
|
||||
payloads.forEach() { payload in
|
||||
guard payload != payloadData.shortName else {
|
||||
return
|
||||
}
|
||||
payloadList.append(payload)
|
||||
}
|
||||
payloadList.sort()
|
||||
payloadList.forEach() { payload in
|
||||
content.append(",")
|
||||
content.append(csv(payload))
|
||||
}
|
||||
logger.debug("write (content=\(content))")
|
||||
content.append("\n")
|
||||
textFile.overwrite(content)
|
||||
}
|
||||
|
||||
// MARK:- SensorDelegate
|
||||
|
||||
func sensor(_ sensor: SensorType, didDetect: TargetIdentifier) {
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier) {
|
||||
queue.async {
|
||||
if self.payloads.insert(didRead.shortName).inserted {
|
||||
self.logger.debug("didRead (payload=\(didRead.shortName))")
|
||||
self.write()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier, atProximity: Proximity, withTxPower: Int?) {
|
||||
queue.async {
|
||||
if self.payloads.insert(didRead.shortName).inserted {
|
||||
self.logger.debug("didRead (payload=\(didRead.shortName))")
|
||||
self.write()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier) {
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didShare: [PayloadData], fromTarget: TargetIdentifier, atProximity: Proximity) {
|
||||
didShare.forEach() { payloadData in
|
||||
queue.async {
|
||||
if self.payloads.insert(payloadData.shortName).inserted {
|
||||
self.logger.debug("didShare (payload=\(payloadData.shortName))")
|
||||
self.write()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
96
CovidSafe/Herald/Sensor/Data/SensorLogger.swift
Normal file
96
CovidSafe/Herald/Sensor/Data/SensorLogger.swift
Normal file
|
@ -0,0 +1,96 @@
|
|||
//
|
||||
// Logger.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import os
|
||||
|
||||
protocol SensorLogger {
|
||||
init(subsystem: String, category: String)
|
||||
|
||||
func log(_ level: SensorLoggerLevel, _ message: String)
|
||||
|
||||
func debug(_ message: String)
|
||||
|
||||
func info(_ message: String)
|
||||
|
||||
func fault(_ message: String)
|
||||
}
|
||||
|
||||
enum SensorLoggerLevel: String {
|
||||
case debug, info, fault
|
||||
}
|
||||
|
||||
class ConcreteSensorLogger: NSObject, SensorLogger {
|
||||
private let subsystem: String
|
||||
private let category: String
|
||||
private let dateFormatter = DateFormatter()
|
||||
private let log: OSLog?
|
||||
private static let logFile = TextFile(filename: "log.txt")
|
||||
|
||||
required init(subsystem: String, category: String) {
|
||||
self.subsystem = subsystem
|
||||
self.category = category
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||
if #available(iOS 10.0, *) {
|
||||
log = OSLog(subsystem: subsystem, category: category)
|
||||
} else {
|
||||
log = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func suppress(_ level: SensorLoggerLevel) -> Bool {
|
||||
switch level {
|
||||
case .debug:
|
||||
return (BLESensorConfiguration.logLevel == .info || BLESensorConfiguration.logLevel == .fault);
|
||||
case .info:
|
||||
return (BLESensorConfiguration.logLevel == .fault);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
func log(_ level: SensorLoggerLevel, _ message: String) {
|
||||
guard !suppress(level) else {
|
||||
return
|
||||
}
|
||||
// Write to unified os log if available, else print to console
|
||||
let timestamp = dateFormatter.string(from: Date())
|
||||
let csvMessage = message.replacingOccurrences(of: "\"", with: "'")
|
||||
let quotedMessage = (message.contains(",") ? "\"" + csvMessage + "\"" : csvMessage)
|
||||
let entry = timestamp + "," + level.rawValue + "," + subsystem + "," + category + "," + quotedMessage
|
||||
ConcreteSensorLogger.logFile.write(entry)
|
||||
guard let log = log else {
|
||||
print(entry)
|
||||
return
|
||||
}
|
||||
if #available(iOS 10.0, *) {
|
||||
switch (level) {
|
||||
case .debug:
|
||||
os_log("%s", log: log, type: .debug, message)
|
||||
case .info:
|
||||
os_log("%s", log: log, type: .info, message)
|
||||
case .fault:
|
||||
os_log("%s", log: log, type: .fault, message)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func debug(_ message: String) {
|
||||
log(.debug, message)
|
||||
}
|
||||
|
||||
func info(_ message: String) {
|
||||
log(.debug, message)
|
||||
}
|
||||
|
||||
func fault(_ message: String) {
|
||||
log(.debug, message)
|
||||
}
|
||||
|
||||
}
|
95
CovidSafe/Herald/Sensor/Data/StatisticsLog.swift
Normal file
95
CovidSafe/Herald/Sensor/Data/StatisticsLog.swift
Normal file
|
@ -0,0 +1,95 @@
|
|||
//
|
||||
// StatisticsLog.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// CSV contact log for post event analysis and visualisation
|
||||
class StatisticsLog: NSObject, SensorDelegate {
|
||||
private let textFile: TextFile
|
||||
private let payloadData: PayloadData
|
||||
private var identifierToPayload: [TargetIdentifier:String] = [:]
|
||||
private var payloadToTime: [String:Date] = [:]
|
||||
private var payloadToSample: [String:Sample] = [:]
|
||||
|
||||
init(filename: String, payloadData: PayloadData) {
|
||||
textFile = TextFile(filename: filename)
|
||||
self.payloadData = payloadData
|
||||
}
|
||||
|
||||
private func csv(_ value: String) -> String {
|
||||
return TextFile.csv(value)
|
||||
}
|
||||
|
||||
private func add(identifier: TargetIdentifier) {
|
||||
guard let payload = identifierToPayload[identifier] else {
|
||||
return
|
||||
}
|
||||
add(payload: payload)
|
||||
}
|
||||
|
||||
private func add(payload: String) {
|
||||
guard let time = payloadToTime[payload], let sample = payloadToSample[payload] else {
|
||||
payloadToTime[payload] = Date()
|
||||
payloadToSample[payload] = Sample()
|
||||
return
|
||||
}
|
||||
let now = Date()
|
||||
payloadToTime[payload] = now
|
||||
sample.add(Double(now.timeIntervalSince(time)))
|
||||
write()
|
||||
}
|
||||
|
||||
private func write() {
|
||||
var content = "payload,count,mean,sd,min,max\n"
|
||||
var payloadList: [String] = []
|
||||
payloadToSample.keys.forEach() { payload in
|
||||
guard payload != payloadData.shortName else {
|
||||
return
|
||||
}
|
||||
payloadList.append(payload)
|
||||
}
|
||||
payloadList.sort()
|
||||
payloadList.forEach() { payload in
|
||||
guard let sample = payloadToSample[payload] else {
|
||||
return
|
||||
}
|
||||
guard let mean = sample.mean, let sd = sample.standardDeviation, let min = sample.min, let max = sample.max else {
|
||||
return
|
||||
}
|
||||
content.append("\(csv(payload)),\(sample.count),\(mean),\(sd),\(min),\(max)\n")
|
||||
}
|
||||
textFile.overwrite(content)
|
||||
}
|
||||
|
||||
|
||||
// MARK:- SensorDelegate
|
||||
|
||||
func sensor(_ sensor: SensorType, didDetect: TargetIdentifier) {
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier) {
|
||||
identifierToPayload[fromTarget] = didRead.shortName
|
||||
add(identifier: fromTarget)
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier, atProximity: Proximity, withTxPower: Int?) {
|
||||
identifierToPayload[fromTarget] = didRead.shortName
|
||||
add(identifier: fromTarget)
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier) {
|
||||
add(identifier: fromTarget)
|
||||
}
|
||||
|
||||
func sensor(_ sensor: SensorType, didShare: [PayloadData], fromTarget: TargetIdentifier, atProximity: Proximity) {
|
||||
didShare.forEach() { payloadData in
|
||||
add(payload: payloadData.shortName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
71
CovidSafe/Herald/Sensor/Data/TextFile.swift
Normal file
71
CovidSafe/Herald/Sensor/Data/TextFile.swift
Normal file
|
@ -0,0 +1,71 @@
|
|||
//
|
||||
// TextFile.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class TextFile {
|
||||
private let logger = ConcreteSensorLogger(subsystem: "Sensor", category: "Data.TextFile")
|
||||
private var file: URL?
|
||||
private let queue: DispatchQueue
|
||||
|
||||
init(filename: String) {
|
||||
file = try? FileManager.default
|
||||
.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||
.appendingPathComponent(filename)
|
||||
queue = DispatchQueue(label: "Sensor.Data.TextFile(\(filename))")
|
||||
}
|
||||
|
||||
func empty() -> Bool {
|
||||
guard let file = file else {
|
||||
return true
|
||||
}
|
||||
return !FileManager.default.fileExists(atPath: file.path)
|
||||
}
|
||||
|
||||
/// Append line to new or existing file
|
||||
func write(_ line: String) {
|
||||
queue.sync {
|
||||
guard let file = file else {
|
||||
return
|
||||
}
|
||||
guard let data = (line+"\n").data(using: .utf8) else {
|
||||
return
|
||||
}
|
||||
if FileManager.default.fileExists(atPath: file.path) {
|
||||
if let fileHandle = try? FileHandle(forWritingTo: file) {
|
||||
fileHandle.seekToEndOfFile()
|
||||
fileHandle.write(data)
|
||||
fileHandle.closeFile()
|
||||
}
|
||||
} else {
|
||||
try? data.write(to: file, options: .atomicWrite)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Overwrite file content
|
||||
func overwrite(_ content: String) {
|
||||
queue.sync {
|
||||
guard let file = file else {
|
||||
return
|
||||
}
|
||||
guard let data = content.data(using: .utf8) else {
|
||||
return
|
||||
}
|
||||
try? data.write(to: file, options: .atomicWrite)
|
||||
}
|
||||
}
|
||||
|
||||
/// Quote value for CSV output if required.
|
||||
static func csv(_ value: String) -> String {
|
||||
guard value.contains(",") || value.contains("\"") || value.contains("'") || value.contains("’") else {
|
||||
return value
|
||||
}
|
||||
return "\"" + value + "\""
|
||||
|
||||
}
|
||||
}
|
122
CovidSafe/Herald/Sensor/Location/AwakeSensor.swift
Normal file
122
CovidSafe/Herald/Sensor/Location/AwakeSensor.swift
Normal file
|
@ -0,0 +1,122 @@
|
|||
//
|
||||
// AwakeSensor.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreLocation
|
||||
|
||||
protocol AwakeSensor : Sensor {
|
||||
}
|
||||
|
||||
/**
|
||||
Screen awake sensor based on CoreLocation. Does NOT make use of the GPS position
|
||||
Requires : Signing & Capabilities : BackgroundModes : LocationUpdates = YES
|
||||
Requires : Info.plist : Privacy - Location When In Use Usage Description
|
||||
Requires : Info.plist : Privacy - Location Always and When In Use Usage Description
|
||||
*/
|
||||
class ConcreteAwakeSensor : NSObject, AwakeSensor, CLLocationManagerDelegate {
|
||||
private let logger = ConcreteSensorLogger(subsystem: "Sensor", category: "ConcreteAwakeSensor")
|
||||
private var delegates: [SensorDelegate] = []
|
||||
private let locationManager = CLLocationManager()
|
||||
private let rangeForBeacon: UUID?
|
||||
|
||||
init(desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyThreeKilometers, distanceFilter: CLLocationDistance = CLLocationDistanceMax, rangeForBeacon: UUID? = nil) {
|
||||
logger.debug("init(desiredAccuracy=\(desiredAccuracy == kCLLocationAccuracyThreeKilometers ? "3km" : desiredAccuracy.description),distanceFilter=\(distanceFilter == CLLocationDistanceMax ? "max" : distanceFilter.description),rangeForBeacon=\(rangeForBeacon == nil ? "disabled" : rangeForBeacon!.description))")
|
||||
self.rangeForBeacon = rangeForBeacon
|
||||
super.init()
|
||||
locationManager.delegate = self
|
||||
locationManager.requestAlwaysAuthorization()
|
||||
locationManager.pausesLocationUpdatesAutomatically = false
|
||||
locationManager.desiredAccuracy = desiredAccuracy
|
||||
locationManager.distanceFilter = distanceFilter
|
||||
locationManager.allowsBackgroundLocationUpdates = true
|
||||
if #available(iOS 11.0, *) {
|
||||
logger.debug("init(ios>=11.0)")
|
||||
locationManager.showsBackgroundLocationIndicator = false
|
||||
} else {
|
||||
logger.debug("init(ios<11.0)")
|
||||
}
|
||||
}
|
||||
|
||||
func add(delegate: SensorDelegate) {
|
||||
delegates.append(delegate)
|
||||
}
|
||||
|
||||
func start() {
|
||||
logger.debug("start")
|
||||
locationManager.startUpdatingLocation()
|
||||
logger.debug("startUpdatingLocation")
|
||||
|
||||
// Start beacon ranging
|
||||
guard let beaconUUID = rangeForBeacon else {
|
||||
return
|
||||
}
|
||||
if #available(iOS 13.0, *) {
|
||||
locationManager.startRangingBeacons(satisfying: CLBeaconIdentityConstraint(uuid: beaconUUID))
|
||||
logger.debug("startRangingBeacons(ios>=13.0,beaconUUID=\(beaconUUID.description))")
|
||||
} else {
|
||||
let beaconRegion = CLBeaconRegion(proximityUUID: beaconUUID, identifier: beaconUUID.uuidString)
|
||||
locationManager.startRangingBeacons(in: beaconRegion)
|
||||
logger.debug("startRangingBeacons(ios<13.0,beaconUUID=\(beaconUUID.uuidString)))")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
func stop() {
|
||||
logger.debug("stop")
|
||||
locationManager.stopUpdatingLocation()
|
||||
logger.debug("stopUpdatingLocation")
|
||||
// Start beacon ranging
|
||||
guard let beaconUUID = rangeForBeacon else {
|
||||
return
|
||||
}
|
||||
if #available(iOS 13.0, *) {
|
||||
locationManager.stopRangingBeacons(satisfying: CLBeaconIdentityConstraint(uuid: beaconUUID))
|
||||
logger.debug("stopRangingBeacons(ios>=13.0,beaconUUID=\(beaconUUID.description))")
|
||||
} else {
|
||||
let beaconRegion = CLBeaconRegion(proximityUUID: beaconUUID, identifier: beaconUUID.uuidString)
|
||||
locationManager.stopRangingBeacons(in: beaconRegion)
|
||||
logger.debug("stopRangingBeacons(ios<13.0,beaconUUID=\(beaconUUID.description))")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK:- CLLocationManagerDelegate
|
||||
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
|
||||
var state = SensorState.off
|
||||
|
||||
if status == CLAuthorizationStatus.authorizedWhenInUse ||
|
||||
status == CLAuthorizationStatus.authorizedAlways {
|
||||
state = .on
|
||||
}
|
||||
if status == CLAuthorizationStatus.notDetermined {
|
||||
locationManager.requestAlwaysAuthorization()
|
||||
locationManager.stopUpdatingLocation()
|
||||
locationManager.startUpdatingLocation()
|
||||
}
|
||||
if status != CLAuthorizationStatus.notDetermined {
|
||||
delegates.forEach({ $0.sensor(.AWAKE, didUpdateState: state) })
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||
var state = SensorState.off
|
||||
if manager.authorizationStatus == CLAuthorizationStatus.authorizedWhenInUse ||
|
||||
manager.authorizationStatus == CLAuthorizationStatus.authorizedAlways {
|
||||
state = .on
|
||||
}
|
||||
if manager.authorizationStatus == CLAuthorizationStatus.notDetermined {
|
||||
locationManager.requestAlwaysAuthorization()
|
||||
locationManager.stopUpdatingLocation()
|
||||
locationManager.startUpdatingLocation()
|
||||
}
|
||||
if manager.authorizationStatus != CLAuthorizationStatus.notDetermined {
|
||||
delegates.forEach({ $0.sensor(.AWAKE, didUpdateState: state) })
|
||||
}
|
||||
}
|
||||
|
||||
}
|
53
CovidSafe/Herald/Sensor/PayloadDataSupplier.swift
Normal file
53
CovidSafe/Herald/Sensor/PayloadDataSupplier.swift
Normal file
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// PayloadDataSupplier.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Payload data supplier for generating payload data that is shared with other devices to provide device identity information while maintaining privacy and security.
|
||||
/// Implement this to integration your solution with this transport.
|
||||
public protocol PayloadDataSupplier {
|
||||
/// Get payload for given timestamp. Use this for integration with any payload generator.
|
||||
func payload(_ timestamp: PayloadTimestamp) -> PayloadData
|
||||
|
||||
/// Get payload for given identifier. Use this for integration with any payload generator.
|
||||
func payload(_ identifier: UUID, offset: Int, onComplete: @escaping (PayloadData?) -> Void) -> Void
|
||||
|
||||
/// Parse raw data into payloads. This is used to split concatenated payloads that are transmitted via share payload. The default implementation assumes payload data is fixed length.
|
||||
func payload(_ data: Data) -> [PayloadData]
|
||||
}
|
||||
|
||||
/// Implements payload splitting function, assuming fixed length payloads.
|
||||
public extension PayloadDataSupplier {
|
||||
/// Default implementation assumes fixed length payload data.
|
||||
func payload(_ data: Data) -> [PayloadData] {
|
||||
// Get example payload to determine length
|
||||
let fixedLengthPayload = payload(PayloadTimestamp())
|
||||
let payloadLength = fixedLengthPayload.count
|
||||
// Split data into payloads based on fixed length
|
||||
var payloads: [PayloadData] = []
|
||||
var indexStart = 0, indexEnd = payloadLength
|
||||
while indexEnd <= data.count {
|
||||
let payload = PayloadData(data.subdata(in: indexStart..<indexEnd))
|
||||
payloads.append(payload)
|
||||
indexStart += payloadLength
|
||||
indexEnd += payloadLength
|
||||
}
|
||||
return payloads
|
||||
}
|
||||
|
||||
/// Default Implementation returns payload(timestamp:)
|
||||
func payload(_ identifier: UUID, offset: Int, onComplete: @escaping (PayloadData?) -> Void) -> Void {
|
||||
onComplete(payload(PayloadTimestamp()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Payload timestamp, should normally be Date, but it may change to UInt64 in the future to use server synchronised relative timestamp.
|
||||
public typealias PayloadTimestamp = Date
|
||||
|
||||
/// Encrypted payload data received from target. This is likely to be an encrypted datagram of the target's actual permanent identifier.
|
||||
public typealias PayloadData = Data
|
||||
|
21
CovidSafe/Herald/Sensor/Sensor.swift
Normal file
21
CovidSafe/Herald/Sensor/Sensor.swift
Normal file
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// Sensor.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Sensor for detecting and tracking various kinds of disease transmission vectors, e.g. contact with people, time at location.
|
||||
public protocol Sensor {
|
||||
/// Add delegate for responding to sensor events.
|
||||
func add(delegate: SensorDelegate)
|
||||
|
||||
/// Start sensing.
|
||||
func start()
|
||||
|
||||
/// Stop sensing.
|
||||
func stop()
|
||||
}
|
||||
|
59
CovidSafe/Herald/Sensor/SensorArray.swift
Normal file
59
CovidSafe/Herald/Sensor/SensorArray.swift
Normal file
|
@ -0,0 +1,59 @@
|
|||
//
|
||||
// SensorArray.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
/// Sensor array for combining multiple detection and tracking methods.
|
||||
public class SensorArray : NSObject, Sensor {
|
||||
private let logger = ConcreteSensorLogger(subsystem: "Sensor", category: "SensorArray")
|
||||
private var sensorArray: [Sensor] = []
|
||||
private var sensorDelegates: [SensorDelegate] = []
|
||||
public let payloadData: PayloadData
|
||||
public static let deviceDescription = "\(UIDevice.current.name) (iOS \(UIDevice.current.systemVersion))"
|
||||
|
||||
public init(_ payloadDataSupplier: PayloadDataSupplier) {
|
||||
logger.debug("init")
|
||||
// BLE sensor for detecting and tracking proximity
|
||||
sensorArray.append(ConcreteBLESensor(payloadDataSupplier))
|
||||
// Payload data at initiation time for identifying this device in the logs
|
||||
payloadData = payloadDataSupplier.payload(PayloadTimestamp())
|
||||
super.init()
|
||||
|
||||
// Loggers
|
||||
#if DEBUG
|
||||
add(delegate: ContactLog(filename: "contacts.csv"))
|
||||
add(delegate: StatisticsLog(filename: "statistics.csv", payloadData: payloadData))
|
||||
add(delegate: DetectionLog(filename: "detection.csv", payloadData: payloadData))
|
||||
_ = BatteryLog(filename: "battery.csv")
|
||||
#endif
|
||||
logger.info("DEVICE (payloadPrefix=\(payloadData.shortName),description=\(SensorArray.deviceDescription))")
|
||||
}
|
||||
|
||||
public func add(delegate: SensorDelegate) {
|
||||
sensorDelegates.append(delegate)
|
||||
sensorArray.forEach { $0.add(delegate: delegate) }
|
||||
}
|
||||
|
||||
public func start() {
|
||||
logger.debug("start")
|
||||
sensorArray.forEach { $0.start() }
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
logger.debug("stop")
|
||||
sensorArray.forEach { $0.stop() }
|
||||
}
|
||||
|
||||
public func startAwakeSensor() {
|
||||
// Location sensor is necessary for enabling background BLE advert detection
|
||||
let awakeSensor = ConcreteAwakeSensor(rangeForBeacon: UUID(uuidString: BLESensorConfiguration.serviceUUID.uuidString))
|
||||
sensorDelegates.forEach { awakeSensor.add(delegate: $0) }
|
||||
sensorArray.append(awakeSensor)
|
||||
awakeSensor.start()
|
||||
}
|
||||
}
|
109
CovidSafe/Herald/Sensor/SensorDelegate.swift
Normal file
109
CovidSafe/Herald/Sensor/SensorDelegate.swift
Normal file
|
@ -0,0 +1,109 @@
|
|||
//
|
||||
// SensorDelegate.swift
|
||||
//
|
||||
// Copyright 2020 VMware, Inc.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Sensor delegate for receiving sensor events.
|
||||
public protocol SensorDelegate {
|
||||
/// Detection of a target with an ephemeral identifier, e.g. BLE central detecting a BLE peripheral.
|
||||
func sensor(_ sensor: SensorType, didDetect: TargetIdentifier)
|
||||
|
||||
/// Read payload data from target, e.g. encrypted device identifier from BLE peripheral after successful connection.
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier)
|
||||
|
||||
/// Read payload data of other targets recently acquired by a target, e.g. Android peripheral sharing payload data acquired from nearby iOS peripherals.
|
||||
func sensor(_ sensor: SensorType, didShare: [PayloadData], fromTarget: TargetIdentifier, atProximity: Proximity)
|
||||
|
||||
/// Measure proximity to target, e.g. a sample of RSSI values from BLE peripheral.
|
||||
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier)
|
||||
|
||||
/// Measure proximity to target with payload data. Combines didMeasure and didRead into a single convenient delegate method
|
||||
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier, withPayload: PayloadData)
|
||||
|
||||
/// Measure proximity to target with payload data. Combines didMeasure and didRead into a single convenient delegate method
|
||||
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier, withPayload: PayloadData, forDevice: BLEDevice)
|
||||
|
||||
/// Measure proximity to target with payload data. Combines didMeasure and didRead into a single convenient delegate method
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier, atProximity: Proximity, withTxPower: Int?)
|
||||
|
||||
/// Sensor state update
|
||||
func sensor(_ sensor: SensorType, didUpdateState: SensorState)
|
||||
|
||||
/// Check if backwards compatibility legacy payload should be written to given device
|
||||
func shouldWriteToLegacyDevice(_ device: BLEDevice) -> Bool
|
||||
|
||||
/// Did write backwards compatibility legacy payload to given device
|
||||
func didWriteToLegacyDevice(_ device: BLEDevice)
|
||||
}
|
||||
|
||||
/// Sensor delegate functions are all optional.
|
||||
public extension SensorDelegate {
|
||||
func sensor(_ sensor: SensorType, didDetect: TargetIdentifier) {}
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier) {}
|
||||
func sensor(_ sensor: SensorType, didShare: [PayloadData], fromTarget: TargetIdentifier, atProximity: Proximity) {}
|
||||
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier) {}
|
||||
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier, withPayload: PayloadData) {}
|
||||
func sensor(_ sensor: SensorType, didMeasure: Proximity, fromTarget: TargetIdentifier, withPayload: PayloadData, forDevice: BLEDevice) {}
|
||||
func sensor(_ sensor: SensorType, didUpdateState: SensorState) {}
|
||||
func sensor(_ sensor: SensorType, didRead: PayloadData, fromTarget: TargetIdentifier, atProximity: Proximity, withTxPower: Int?) {}
|
||||
|
||||
func shouldWriteToLegacyDevice(_ device: BLEDevice) -> Bool { return false }
|
||||
func didWriteToLegacyDevice(_ device: BLEDevice) {}
|
||||
}
|
||||
|
||||
// MARK:- SensorDelegate data
|
||||
|
||||
/// Sensor type as qualifier for target identifier.
|
||||
public enum SensorType : String {
|
||||
/// Bluetooth Low Energy (BLE)
|
||||
case BLE
|
||||
/// Awake location sensor - uses Location API to be alerted to screen on events
|
||||
case AWAKE
|
||||
/// GPS location sensor - not used by default in Herald
|
||||
case GPS
|
||||
/// Physical beacon, e.g. iBeacon
|
||||
case BEACON
|
||||
/// Ultrasound audio beacon.
|
||||
case ULTRASOUND
|
||||
}
|
||||
|
||||
/// Sensor state
|
||||
public enum SensorState : String {
|
||||
/// Sensor is powered on, active and operational
|
||||
case on
|
||||
/// Sensor is powered off, inactive and not operational
|
||||
case off
|
||||
/// Sensor is not available
|
||||
case unavailable
|
||||
}
|
||||
|
||||
/// Ephemeral identifier for detected target (e.g. smartphone, beacon, place). This is likely to be an UUID but using String for variable identifier length.
|
||||
public typealias TargetIdentifier = String
|
||||
|
||||
// MARK:- Proximity data
|
||||
|
||||
/// Raw data for estimating proximity between sensor and target, e.g. RSSI for BLE.
|
||||
public struct Proximity {
|
||||
/// Unit of measurement, e.g. RSSI
|
||||
let unit: ProximityMeasurementUnit
|
||||
/// Measured value, e.g. raw RSSI value.
|
||||
let value: Double
|
||||
/// Get plain text description of proximity data
|
||||
public var description: String { get {
|
||||
unit.rawValue + ":" + value.description
|
||||
}}
|
||||
}
|
||||
|
||||
/// Measurement unit for interpreting the proximity data values.
|
||||
public enum ProximityMeasurementUnit : String {
|
||||
/// Received signal strength indicator, e.g. BLE signal strength as proximity estimator.
|
||||
case RSSI
|
||||
/// Roundtrip time, e.g. Audio signal echo time duration as proximity estimator.
|
||||
case RTT
|
||||
}
|
||||
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue