COVIDSafe code from version 2.0

This commit is contained in:
covidsafe-support 2020-12-19 16:11:23 +11:00
parent cf93ea43c0
commit 4ff6a506cf
55 changed files with 4624 additions and 1117 deletions

View 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)
}
}

File diff suppressed because it is too large Load diff

View 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)
}
}

View 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
}
}

View 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
}
}

View 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()
}
}

View 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))
}
}
}

View 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()
}
}
}
}
}

View 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)
}
}

View 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)
}
}
}

View 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 + "\""
}
}

View 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) })
}
}
}

View 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

View 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()
}

View 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()
}
}

View 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
}

18
CovidSafe/Herald/herald.h Normal file
View file

@ -0,0 +1,18 @@
//
// Herald.h
//
// Copyright 2020 VMware, Inc.
// SPDX-License-Identifier: MIT
//
#import <Foundation/Foundation.h>
//! Project version number for Herald.
FOUNDATION_EXPORT double HeraldVersionNumber;
//! Project version string for Herald.
FOUNDATION_EXPORT const unsigned char HeraldVersionString[];
// In this header, you should import all the public headers of your framework using statements like #import <Herald/PublicHeader.h>