| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577 |
- //
- // Transmitter.swift
- // xDripG5
- //
- // Created by Nathan Racklyeft on 11/22/15.
- // Copyright © 2015 Nathan Racklyeft. All rights reserved.
- //
- import Foundation
- import CoreBluetooth
- import HealthKit
- import os.log
- public protocol TransmitterDelegate: AnyObject {
- func transmitterDidConnect(_ transmitter: Transmitter)
- func transmitter(_ transmitter: Transmitter, didError error: Error)
- func transmitter(_ transmitter: Transmitter, didRead glucose: Glucose)
- func transmitter(_ transmitter: Transmitter, didReadBackfill glucose: [Glucose])
- func transmitter(_ transmitter: Transmitter, didReadUnknownData data: Data)
- }
- /// These methods are called on a private background queue. It is the responsibility of the client to ensure thread-safety.
- public protocol TransmitterCommandSource: AnyObject {
- func dequeuePendingCommand(for transmitter: Transmitter) -> Command?
- func transmitter(_ transmitter: Transmitter, didFail command: Command, with error: Error)
- func transmitter(_ transmitter: Transmitter, didComplete command: Command)
- }
- public enum TransmitterError: Error {
- case authenticationError(String)
- case controlError(String)
- case observationError(String)
- }
- extension TransmitterError: CustomStringConvertible {
- public var description: String {
- switch self {
- case .authenticationError(let description):
- return description
- case .controlError(let description):
- return description
- case .observationError(let description):
- return description
- }
- }
- }
- public final class Transmitter: BluetoothManagerDelegate {
- /// The ID of the transmitter to connect to
- public var ID: String {
- return id.id
- }
- private var id: TransmitterID
- public var passiveModeEnabled: Bool
- public weak var delegate: TransmitterDelegate?
- public weak var commandSource: TransmitterCommandSource?
- // MARK: - Passive observation state, confined to `bluetoothManager.managerQueue`
- /// The initial activation date of the transmitter
- private var activationDate: Date?
- /// The last-observed time message
- private var lastTimeMessage: TransmitterTimeRxMessage? {
- didSet {
- if let time = lastTimeMessage {
- activationDate = Date(timeIntervalSinceNow: -TimeInterval(time.currentTime))
- } else {
- activationDate = nil
- }
- }
- }
- /// The last-observed calibration message
- private var lastCalibrationMessage: CalibrationDataRxMessage?
- /// The backfill data buffer
- private var backfillBuffer: GlucoseBackfillFrameBuffer?
- // MARK: -
- private let log = OSLog(category: "Transmitter")
- private let bluetoothManager = BluetoothManager()
- private let delegateQueue = DispatchQueue(label: "com.loudnate.CGMBLEKit.delegateQueue", qos: .unspecified)
- public init(id: String, peripheralIdentifier: UUID? = nil, passiveModeEnabled: Bool = false) {
- self.id = TransmitterID(id: id)
- self.passiveModeEnabled = passiveModeEnabled
- bluetoothManager.peripheralIdentifier = peripheralIdentifier
- bluetoothManager.delegate = self
- }
- public func resumeScanning() {
- if stayConnected {
- bluetoothManager.scanForPeripheral()
- }
- }
- public func stopScanning() {
- bluetoothManager.disconnect()
- }
- public var isScanning: Bool {
- return bluetoothManager.isScanning
- }
- public var peripheralIdentifier: UUID? {
- get {
- return bluetoothManager.peripheralIdentifier
- }
- set {
- bluetoothManager.peripheralIdentifier = newValue
- }
- }
- public var stayConnected: Bool {
- get {
- return bluetoothManager.stayConnected
- }
- set {
- bluetoothManager.stayConnected = newValue
- if newValue {
- bluetoothManager.scanForPeripheral()
- }
- }
- }
- // MARK: - BluetoothManagerDelegate
- func bluetoothManager(_ manager: BluetoothManager, peripheralManager: PeripheralManager, isReadyWithError error: Error?) {
- if let error = error {
- delegateQueue.async {
- self.delegate?.transmitter(self, didError: error)
- }
- return
- }
- delegateQueue.async {
- self.delegate?.transmitterDidConnect(self)
- }
- peripheralManager.perform { (peripheral) in
- if self.passiveModeEnabled {
- self.log.debug("Listening for authentication responses in passive mode")
- do {
- try peripheral.listenToCharacteristic(.authentication)
- } catch let error {
- self.delegateQueue.async {
- self.delegate?.transmitter(self, didError: error)
- }
- }
- } else {
- do {
- self.log.debug("Authenticating with transmitter")
- let status = try peripheral.authenticate(id: self.id)
- if !status.isBonded {
- self.log.debug("Requesting bond")
- try peripheral.requestBond()
- self.log.debug("Bonding request sent. Waiting user to respond.")
- }
- try peripheral.enableNotify(shouldWaitForBond: !status.isBonded)
- defer {
- self.log.debug("Initiating a disconnect")
- peripheral.disconnect()
- }
- self.log.debug("Reading time")
- let timeMessage = try peripheral.readTimeMessage()
- let activationDate = Date(timeIntervalSinceNow: -TimeInterval(timeMessage.currentTime))
- self.log.debug("Determined activation date: %@", String(describing: activationDate))
- while let command = self.commandSource?.dequeuePendingCommand(for: self) {
- self.log.debug("Sending command: %@", String(describing: command))
- do {
- _ = try peripheral.sendCommand(command, activationDate: activationDate)
- self.commandSource?.transmitter(self, didComplete: command)
- } catch let error {
- self.commandSource?.transmitter(self, didFail: command, with: error)
- }
- }
- self.log.debug("Reading glucose")
- let glucoseMessage = try peripheral.readGlucose()
- self.log.debug("Reading calibration data")
- let calibrationMessage = try? peripheral.readCalibrationData()
- let glucose = Glucose(
- transmitterID: self.id.id,
- glucoseMessage: glucoseMessage,
- timeMessage: timeMessage,
- calibrationMessage: calibrationMessage,
- activationDate: activationDate
- )
- self.delegateQueue.async {
- self.delegate?.transmitter(self, didRead: glucose)
- }
- } catch let error {
- self.delegateQueue.async {
- self.delegate?.transmitter(self, didError: error)
- }
- }
- }
- }
- }
- func bluetoothManager(_ manager: BluetoothManager, shouldConnectPeripheral peripheral: CBPeripheral) -> Bool {
- /// The Dexcom G5 advertises a peripheral name of "DexcomXX"
- /// where "XX" is the last-two characters of the transmitter ID.
- if let name = peripheral.name, name.suffix(2) == id.id.suffix(2) {
- return true
- } else {
- self.log.info("Not connecting to peripheral: %{public}@", peripheral.name ?? String(describing: peripheral))
- return false
- }
- }
- func bluetoothManager(_ manager: BluetoothManager, peripheralManager: PeripheralManager, didReceiveControlResponse response: Data) {
- guard passiveModeEnabled else { return }
- guard response.count > 0 else { return }
- switch Opcode(rawValue: response[0]) {
- case .glucoseRx?, .glucoseG6Rx?:
- if let glucoseMessage = GlucoseRxMessage(data: response),
- let timeMessage = lastTimeMessage,
- let activationDate = activationDate
- {
- delegateQueue.async {
- self.delegate?.transmitter(self, didRead: Glucose(transmitterID: self.id.id, glucoseMessage: glucoseMessage, timeMessage: timeMessage, calibrationMessage: self.lastCalibrationMessage, activationDate: activationDate))
- }
- } else {
- delegateQueue.async {
- self.delegate?.transmitter(self, didError: TransmitterError.observationError("Unable to handle glucose control response"))
- }
- }
- peripheralManager.perform { (peripheral) in
- // Subscribe to backfill updates
- do {
- try peripheral.listenToCharacteristic(.backfill)
- } catch let error {
- self.log.error("Error trying to enable notifications on backfill characteristic: %{public}@", String(describing: error))
- self.delegateQueue.async {
- self.delegate?.transmitter(self, didError: error)
- }
- }
- }
- case .transmitterTimeRx?:
- if let timeMessage = TransmitterTimeRxMessage(data: response) {
- self.lastTimeMessage = timeMessage
- }
- case .glucoseBackfillRx?:
- guard let backfillMessage = GlucoseBackfillRxMessage(data: response) else {
- break
- }
- guard let backfillBuffer = backfillBuffer else {
- log.error("Received GlucoseBackfillRxMessage %{public}@ but backfillBuffer is nil", String(describing: backfillMessage))
- self.delegateQueue.async {
- self.delegate?.transmitter(self, didError: TransmitterError.observationError("Received GlucoseBackfillRxMessage but backfillBuffer is nil"))
- }
- break
- }
- guard let timeMessage = lastTimeMessage, let activationDate = activationDate else {
- log.error("Received GlucoseBackfillRxMessage %{public}@ but activationDate is unknown", String(describing: backfillMessage))
- self.delegateQueue.async {
- self.delegate?.transmitter(self, didError: TransmitterError.observationError("Received GlucoseBackfillRxMessage but activationDate is unknown"))
- }
- break
- }
- guard backfillMessage.bufferLength == backfillBuffer.count else {
- log.error("GlucoseBackfillRxMessage expected buffer length %d, but was %d", backfillMessage.bufferLength, backfillBuffer.count)
- self.delegateQueue.async {
- self.delegate?.transmitter(self, didError: TransmitterError.observationError("GlucoseBackfillRxMessage expected buffer length \(backfillMessage.bufferLength), but was \(backfillBuffer.count): \(response.hexadecimalString) "))
- }
- break
- }
- guard backfillMessage.bufferCRC == backfillBuffer.crc16 else {
- log.error("GlucoseBackfillRxMessage expected CRC %04x, but was %04x", backfillMessage.bufferCRC, backfillBuffer.crc16)
- self.delegateQueue.async {
- self.delegate?.transmitter(self, didError: TransmitterError.observationError("GlucoseBackfillRxMessage expected CRC \(backfillMessage.bufferCRC), but was \(backfillBuffer.crc16)"))
- }
- break
- }
- let glucose = backfillBuffer.glucose.map {
- Glucose(transmitterID: id.id, status: backfillMessage.status, glucoseMessage: $0, timeMessage: timeMessage, activationDate: activationDate)
- }
- guard glucose.count > 0 else {
- break
- }
- guard glucose.first!.glucoseMessage.timestamp == backfillMessage.startTime,
- glucose.last!.glucoseMessage.timestamp == backfillMessage.endTime,
- glucose.first!.glucoseMessage.timestamp <= glucose.last!.glucoseMessage.timestamp
- else {
- log.error("GlucoseBackfillRxMessage time interval not reflected in glucose: %{public}@, buffer: %{public}@", response.hexadecimalString, String(reflecting: backfillBuffer))
- self.delegateQueue.async {
- self.delegate?.transmitter(self, didError: TransmitterError.observationError("GlucoseBackfillRxMessage time interval not reflected in glucose: \(backfillMessage.startTime) - \(backfillMessage.endTime), buffer: \(glucose.first!.glucoseMessage.timestamp) - \(glucose.last!.glucoseMessage.timestamp)"))
- }
- break
- }
- delegateQueue.async {
- self.delegate?.transmitter(self, didReadBackfill: glucose)
- }
- case .calibrationDataRx?:
- guard let calibrationDataMessage = CalibrationDataRxMessage(data: response) else {
- break
- }
- lastCalibrationMessage = calibrationDataMessage
- case .none:
- delegateQueue.async {
- self.delegate?.transmitter(self, didReadUnknownData: response)
- }
- default:
- // We ignore all other known opcodes
- break
- }
- }
- func bluetoothManager(_ manager: BluetoothManager, didReceiveBackfillResponse response: Data) {
- guard response.count > 2 else {
- return
- }
- if response[0] == 1 {
- log.info("Starting new backfill buffer with ID %d", response[1])
- self.backfillBuffer = GlucoseBackfillFrameBuffer(identifier: response[1])
- }
-
- log.info("appending to backfillBuffer: %@", response.hexadecimalString)
- self.backfillBuffer?.append(response)
- }
- func bluetoothManager(_ manager: BluetoothManager, peripheralManager: PeripheralManager, didReceiveAuthenticationResponse response: Data) {
- if let message = AuthChallengeRxMessage(data: response), message.isBonded, message.isAuthenticated {
- self.log.debug("Observed authenticated session. enabling notifications for control characteristic.")
- peripheralManager.perform { (peripheral) in
- // Stopping updates from authentication simultaneously with Dexcom's app causes CoreBluetooth to get into a weird state.
- /*
- do {
- try peripheral.stopListeningToCharacteristic(.authentication)
- } catch let error {
- self.log.error("Error trying to disable notifications on authentication characteristic: %{public}@", String(describing: error))
- }
- */
- do {
- try peripheral.listenToCharacteristic(.control)
- } catch let error {
- self.log.error("Error trying to enable notifications on control characteristic: %{public}@", String(describing: error))
- self.delegateQueue.async {
- self.delegate?.transmitter(self, didError: error)
- }
- }
- }
- } else {
- self.log.debug("Ignoring authentication response: %{public}@", response.hexadecimalString)
- }
- }
- }
- extension Transmitter: CustomDebugStringConvertible {
- public var debugDescription: String {
- return [
- "## Transmitter",
- String(reflecting: bluetoothManager),
- ].joined(separator: "\n")
- }
- }
- struct TransmitterID {
- let id: String
- init(id: String) {
- self.id = id
- }
- private var cryptKey: Data? {
- return "00\(id)00\(id)".data(using: .utf8)
- }
- func computeHash(of data: Data) -> Data? {
- guard data.count == 8, let key = cryptKey else {
- return nil
- }
- var doubleData = Data(capacity: data.count * 2)
- doubleData.append(data)
- doubleData.append(data)
- guard let outData = try? AESCrypt.encryptData(doubleData, usingKey: key) else {
- return nil
- }
- return outData[0..<8]
- }
- }
- // MARK: - Helpers
- fileprivate extension PeripheralManager {
- func authenticate(id: TransmitterID) throws -> AuthChallengeRxMessage {
- let authMessage = AuthRequestTxMessage()
- do {
- try writeMessage(authMessage, for: .authentication)
- } catch let error {
- throw TransmitterError.authenticationError("Error writing transmitter challenge: \(error)")
- }
- let authResponse: AuthRequestRxMessage
- do {
- authResponse = try readMessage(for: .authentication)
- } catch let error {
- throw TransmitterError.authenticationError("Unable to parse auth challenge: \(error)")
- }
- guard authResponse.tokenHash == id.computeHash(of: authMessage.singleUseToken) else {
- throw TransmitterError.authenticationError("Transmitter failed auth challenge")
- }
- guard let challengeHash = id.computeHash(of: authResponse.challenge) else {
- throw TransmitterError.authenticationError("Failed to compute challenge hash for transmitter ID")
- }
- do {
- try writeMessage(AuthChallengeTxMessage(challengeHash: challengeHash), for: .authentication)
- } catch let error {
- throw TransmitterError.authenticationError("Error writing challenge response: \(error)")
- }
- let challengeResponse: AuthChallengeRxMessage
- do {
- challengeResponse = try readMessage(for: .authentication)
- } catch let error {
- throw TransmitterError.authenticationError("Unable to parse auth status: \(error)")
- }
- guard challengeResponse.isAuthenticated else {
- throw TransmitterError.authenticationError("Transmitter rejected auth challenge")
- }
- return challengeResponse
- }
- func requestBond() throws {
- do {
- try writeMessage(KeepAliveTxMessage(time: 25), for: .authentication)
- } catch let error {
- throw TransmitterError.authenticationError("Error writing keep-alive for bond: \(error)")
- }
- do {
- try writeMessage(BondRequestTxMessage(), for: .authentication)
- } catch let error {
- throw TransmitterError.authenticationError("Error writing bond request: \(error)")
- }
- }
- func enableNotify(shouldWaitForBond: Bool = false) throws {
- do {
- if shouldWaitForBond {
- try setNotifyValue(true, for: .control, timeout: 15)
- } else {
- try setNotifyValue(true, for: .control)
- }
- } catch let error {
- throw TransmitterError.controlError("Error enabling notification: \(error)")
- }
- }
- func readTimeMessage() throws -> TransmitterTimeRxMessage {
- do {
- return try writeMessage(TransmitterTimeTxMessage(), for: .control)
- } catch let error {
- throw TransmitterError.controlError("Error getting time: \(error)")
- }
- }
- /// - Throws: TransmitterError.controlError
- func sendCommand(_ command: Command, activationDate: Date) throws -> TransmitterRxMessage {
- do {
- switch command {
- case .startSensor(let date):
- let startTime = UInt32(date.timeIntervalSince(activationDate))
- let secondsSince1970 = UInt32(date.timeIntervalSince1970)
- return try writeMessage(SessionStartTxMessage(startTime: startTime, secondsSince1970: secondsSince1970), for: .control)
- case .stopSensor(let date):
- let stopTime = UInt32(date.timeIntervalSince(activationDate))
- return try writeMessage(SessionStopTxMessage(stopTime: stopTime), for: .control)
- case .calibrateSensor(let glucose, let date):
- let glucoseValue = UInt16(glucose.doubleValue(for: .milligramsPerDeciliter).rounded())
- let time = UInt32(date.timeIntervalSince(activationDate))
- return try writeMessage(CalibrateGlucoseTxMessage(time: time, glucose: glucoseValue), for: .control)
- case .resetTransmitter:
- return try writeMessage(ResetTxMessage(), for: .control)
- }
- } catch let error {
- throw TransmitterError.controlError("Error during \(command): \(error)")
- }
- }
- func readGlucose() throws -> GlucoseRxMessage {
- do {
- return try writeMessage(GlucoseTxMessage(), for: .control)
- } catch let error {
- throw TransmitterError.controlError("Error getting glucose: \(error)")
- }
- }
- func readCalibrationData() throws -> CalibrationDataRxMessage {
- do {
- return try writeMessage(CalibrationDataTxMessage(), for: .control)
- } catch let error {
- throw TransmitterError.controlError("Error getting calibration data: \(error)")
- }
- }
- func disconnect() {
- do {
- try setNotifyValue(false, for: .control)
- try writeMessage(DisconnectTxMessage(), for: .control)
- } catch {
- }
- }
- func listenToCharacteristic(_ characteristic: CGMServiceCharacteristicUUID) throws {
- do {
- try setNotifyValue(true, for: characteristic)
- } catch let error {
- throw TransmitterError.controlError("Error enabling notification for \(characteristic): \(error)")
- }
- }
- func stopListeningToCharacteristic(_ characteristic: CGMServiceCharacteristicUUID) throws {
- do {
- try setNotifyValue(false, for: characteristic)
- } catch let error {
- throw TransmitterError.controlError("Error disabling notification for \(characteristic): \(error)")
- }
- }
- }
|