Просмотр исходного кода

Heartbeat in combination with xDrip4iOS

- xDrip4iOS stores the CGM address in shared user defaults
- FreeAPS X reads this address and connects to the CGM
   - it does not process any data
   - it is only used to guarantee a regular wake up, which is each time the CGM connects, disconnects or sends data
   - when this wake up happens, FreeAPS X triggers by itself a call to fetchLastBGs()
- There is no need to initiate a scanning, it will automatically detect the CGM address in the shared defaults and connect to it
- To remove the connection, one must either
   - disconnect the CGM in xDrip4iOS (this will force the value in shared user defaults to nil)
   - remove xDrip4iOS in Free APS X

- I also had to make a change in ConcreteGlucoseDisplayable, enum GlucoseTrend, to add a description var (seems rather messy usage of glucose trend definitions in the code)

Still to do
- maybe add a field in the xDrip4iOS UI to show the name of the CGM transmitter to which FeeAPS X connects

(cherry picked from commit 2686a9795f987df0de05e62e68e4034f1f29b7f9)
Johan Degraeve 3 лет назад
Родитель
Сommit
d336fe2fef

+ 20 - 0
Dependencies/LibreTransmitter/Sources/LibreTransmitter/ConcreteGlucoseDisplayable.swift

@@ -95,6 +95,26 @@ public enum GlucoseTrend: Int, CaseIterable {
             return LocalizedString("Falling very fast", comment: "Glucose trend down-down-down")
         }
     }
+    
+    public var direction: String {
+        switch self {
+        case .upUpUp:
+            return "DoubleUp"
+        case .upUp:
+            return "SingleUp"
+        case .up:
+            return "FortyFiveUp"
+        case .flat:
+            return "Flat"
+        case .down:
+            return "FortyFiveDown"
+        case .downDown:
+            return "SingleDown"
+        case .downDownDown:
+            return "DoubleDown"
+        }
+    }
+
 }
 
 public protocol GlucoseDisplayable {

+ 8 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -318,6 +318,8 @@
 		E974172296125A5AE99E634C /* PumpConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AD22C985B79A2F0D2EA3D9D /* PumpConfigRootView.swift */; };
 		F5CA3DB1F9DC8B05792BBFAA /* CGMDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B5C0607505A38F256BF99A /* CGMDataFlow.swift */; };
 		F5F7E6C1B7F098F59EB67EC5 /* TargetsEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */; };
+		F816825E28DB441200054060 /* HeartBeatManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F816825D28DB441200054060 /* HeartBeatManager.swift */; };
+		F816826028DB441800054060 /* BluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F816825F28DB441800054060 /* BluetoothTransmitter.swift */; };
 		F90692AA274B7AAE0037068D /* HealthKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F90692A9274B7AAE0037068D /* HealthKitManager.swift */; };
 		F90692CF274B999A0037068D /* HealthKitDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F90692CE274B999A0037068D /* HealthKitDataFlow.swift */; };
 		F90692D1274B99B60037068D /* HealthKitProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F90692D0274B99B60037068D /* HealthKitProvider.swift */; };
@@ -734,6 +736,8 @@
 		E625985B47742D498CB1681A /* NotificationsConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationsConfigProvider.swift; sourceTree = "<group>"; };
 		E68CDC1E5C438D1BEAD4CF24 /* LibreConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigStateModel.swift; sourceTree = "<group>"; };
 		E9AAB83FB6C3B41EFD1846A0 /* AddTempTargetRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddTempTargetRootView.swift; sourceTree = "<group>"; };
+		F816825D28DB441200054060 /* HeartBeatManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HeartBeatManager.swift; sourceTree = "<group>"; };
+		F816825F28DB441800054060 /* BluetoothTransmitter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BluetoothTransmitter.swift; sourceTree = "<group>"; };
 		F90692A9274B7AAE0037068D /* HealthKitManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitManager.swift; sourceTree = "<group>"; };
 		F90692CE274B999A0037068D /* HealthKitDataFlow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitDataFlow.swift; sourceTree = "<group>"; };
 		F90692D0274B99B60037068D /* HealthKitProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitProvider.swift; sourceTree = "<group>"; };
@@ -1183,6 +1187,8 @@
 		3856933F270B57A00002C50D /* CGM */ = {
 			isa = PBXGroup;
 			children = (
+				F816825F28DB441800054060 /* BluetoothTransmitter.swift */,
+				F816825D28DB441200054060 /* HeartBeatManager.swift */,
 				38569346270B5DFB0002C50D /* AppGroupSource.swift */,
 				38569344270B5DFA0002C50D /* CGMType.swift */,
 				386A124E271707F000DDC61C /* DexcomSource.swift */,
@@ -2273,8 +2279,10 @@
 				6632A0DC746872439A858B44 /* ISFEditorDataFlow.swift in Sources */,
 				DBA5254DBB2586C98F61220C /* ISFEditorProvider.swift in Sources */,
 				1BBB001DAD60F3B8CEA4B1C7 /* ISFEditorStateModel.swift in Sources */,
+				F816826028DB441800054060 /* BluetoothTransmitter.swift in Sources */,
 				38192E0D261BAF980094D973 /* ConvenienceExtensions.swift in Sources */,
 				88AB39B23C9552BD6E0C9461 /* ISFEditorRootView.swift in Sources */,
+				F816825E28DB441200054060 /* HeartBeatManager.swift in Sources */,
 				38FEF413273B317A00574A46 /* HKUnit.swift in Sources */,
 				A33352ED40476125EBAC6EE0 /* CREditorDataFlow.swift in Sources */,
 				17A9D0899046B45E87834820 /* CREditorProvider.swift in Sources */,

+ 19 - 1
FreeAPS/Sources/APS/CGM/AppGroupSource.swift

@@ -1,5 +1,6 @@
 import Combine
 import Foundation
+import LibreTransmitter
 
 struct AppGroupSource: GlucoseSource {
     let from: String
@@ -19,20 +20,37 @@ struct AppGroupSource: GlucoseSource {
             return []
         }
 
+        HeartBeatManager.shared.checkCGMBluetoothTransmitter(sharedUserDefaults: sharedDefaults)
+
         let decoded = try? JSONSerialization.jsonObject(with: sharedData, options: [])
         guard let sgvs = decoded as? [AnyObject] else {
             return []
         }
 
         var results: [BloodGlucose] = []
+
         for sgv in sgvs.prefix(count) {
             guard
                 let glucose = sgv["Value"] as? Int,
-                let direction = sgv["direction"] as? String,
                 let timestamp = sgv["DT"] as? String,
                 let date = parseDate(timestamp)
             else { continue }
 
+            var direction: String?
+
+            // Dexcom changed the format of trend in 2021 so we accept both String/Int types
+            if let directionString = sgv["direction"] as? String {
+                direction = directionString
+            } else if let intTrend = sgv["trend"] as? Int {
+                direction = GlucoseTrend(rawValue: intTrend)?.direction
+            } else if let intTrend = sgv["Trend"] as? Int {
+                direction = GlucoseTrend(rawValue: intTrend)?.direction
+            } else if let stringTrend = sgv["trend"] as? String, let intTrend = Int(stringTrend) {
+                direction = GlucoseTrend(rawValue: intTrend)?.direction
+            }
+
+            guard let direction = direction else { continue }
+
             if let from = sgv["from"] as? String {
                 guard from == self.from else { continue }
             }

+ 373 - 0
FreeAPS/Sources/APS/CGM/BluetoothTransmitter.swift

@@ -0,0 +1,373 @@
+import CoreBluetooth
+import Foundation
+import os
+
+/// Generic bluetoothtransmitter class that handles scanning, connect, discover services, discover characteristics, subscribe to receive characteristic, reconnect.
+///
+/// - the connection will be set up and a subscribe to a characteristic will be done
+/// - a heartbeat function is called each time there's a disconnect (needed for Dexcom) or if there's data received on the receive characteristic
+/// - the class does nothing with the data itself
+class BluetoothTransmitter: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {
+    // MARK: - private properties
+
+    /// the address of the transmitter.
+    private let deviceAddress: String
+
+    /// services to be discovered
+    private let servicesCBUUIDs: [CBUUID]
+
+    /// receive characteristic to which we should subcribe in order to awake the app when the tarnsmitter sends data
+    private let CBUUID_ReceiveCharacteristic: String
+
+    /// centralManager
+    private var centralManager: CBCentralManager?
+
+    /// the receive Characteristic
+    private var receiveCharacteristic: CBCharacteristic?
+
+    /// peripheral, gets value during connect
+    private(set) var peripheral: CBPeripheral?
+
+    /// to be called when data is received or if there's a disconnect, this is the actual heartbeat.
+    private let heartbeat: () -> Void
+
+    // MARK: - Initialization
+
+    /// - parameters:
+    ///     - deviceAddress : the bluetooth Mac address
+    ///     - one serviceCBUUID: as string, this is the service to be discovered
+    ///     - CBUUID_Receive: receive characteristic uuid as string, to which subscribe should be done
+    ///     - heartbeat  : function to call when data is received on the receive characteristic or when there's a disconnect
+    init(deviceAddress: String, servicesCBUUID: String, CBUUID_Receive: String, heartbeat: @escaping () -> Void) {
+        servicesCBUUIDs = [CBUUID(string: servicesCBUUID)]
+
+        CBUUID_ReceiveCharacteristic = CBUUID_Receive
+
+        self.deviceAddress = deviceAddress
+
+        self.heartbeat = heartbeat
+
+        let cBCentralManagerOptionRestoreIdentifierKeyToUse = "Loop-" + deviceAddress
+
+        super.init()
+
+        debug(.deviceManager, "in initialize, creating centralManager for peripheral with address \(deviceAddress)")
+
+        centralManager = CBCentralManager(
+            delegate: self,
+            queue: nil,
+            options: [
+                CBCentralManagerOptionShowPowerAlertKey: true,
+                CBCentralManagerOptionRestoreIdentifierKey: cBCentralManagerOptionRestoreIdentifierKeyToUse
+            ]
+        )
+
+        // connect to the device
+        connect()
+    }
+
+    // MARK: - De-initialization
+
+    deinit {
+        debug(.deviceManager, "deinit called")
+
+        // disconnect the device
+        disconnect()
+    }
+
+    // MARK: - public functions
+
+    /// will try to connect to the device, first by calling retrievePeripherals, if peripheral not known, then by calling startScanning
+    func connect() {
+        if !retrievePeripherals(centralManager!) {
+            startScanning()
+        }
+    }
+
+    /// disconnect the device
+    func disconnect() {
+        if let peripheral = peripheral {
+            var name = "unknown"
+            if let peripheralName = peripheral.name {
+                name = peripheralName
+            }
+
+            debug(.deviceManager, "disconnecting from peripheral with name \(name)")
+
+            centralManager!.cancelPeripheralConnection(peripheral)
+        }
+    }
+
+    /// stops scanning
+    func stopScanning() {
+        debug(.deviceManager, "in stopScanning")
+
+        centralManager!.stopScan()
+    }
+
+    /// calls setNotifyValue for characteristic with value enabled
+    func setNotifyValue(_ enabled: Bool, for characteristic: CBCharacteristic) {
+        if let peripheral = peripheral {
+            debug(
+                .deviceManager,
+                "setNotifyValue, for peripheral with name \(peripheral.name ?? "'unknown'"), setting notify for characteristic \(characteristic.uuid.uuidString), to \(enabled.description)"
+            )
+            peripheral.setNotifyValue(enabled, for: characteristic)
+
+        } else {
+            debug(
+                .deviceManager,
+                "setNotifyValue, for peripheral with name \(peripheral?.name ?? "'unknown'"), failed to set notify for characteristic \(characteristic.uuid.uuidString), to \(enabled.description)"
+            )
+        }
+    }
+
+    // MARK: - fileprivate functions
+
+    /// start bluetooth scanning for device
+    fileprivate func startScanning() {
+        if centralManager!.state == .poweredOn {
+            debug(.deviceManager, "in startScanning")
+
+            centralManager!.scanForPeripherals(withServices: nil, options: nil)
+
+        } else {
+            debug(.deviceManager, "in startScanning. Not started, state is not poweredOn")
+        }
+    }
+
+    /// stops scanning and connect. To be called after diddiscover
+    fileprivate func stopScanAndconnect(to peripheral: CBPeripheral) {
+        centralManager!.stopScan()
+
+        self.peripheral = peripheral
+
+        peripheral.delegate = self
+
+        if peripheral.state == .disconnected {
+            debug(.deviceManager, "    trying to connect")
+
+            centralManager!.connect(peripheral, options: nil)
+
+        } else {
+            debug(.deviceManager, "    calling centralManager(newCentralManager, didConnect: peripheral")
+
+            centralManager(centralManager!, didConnect: peripheral)
+        }
+    }
+
+    /// try to connect to peripheral to which connection was successfully done previously, and that has a uuid that matches the stored deviceAddress. If such peripheral exists, then try to connect, it's not necessary to start scanning. iOS will connect as soon as the peripheral comes in range, or bluetooth status is switched on, whatever is necessary
+    ///
+    /// the result of the attempt to try to find such device, is returned
+    fileprivate func retrievePeripherals(_ central: CBCentralManager) -> Bool {
+        debug(.deviceManager, "in retrievePeripherals, deviceaddress is \(deviceAddress)")
+
+        if let uuid = UUID(uuidString: deviceAddress) {
+            debug(.deviceManager, "    uuid is not nil")
+
+            let peripheralArr = central.retrievePeripherals(withIdentifiers: [uuid])
+
+            if !peripheralArr.isEmpty {
+                peripheral = peripheralArr[0]
+
+                if let peripheral = peripheral {
+                    debug(.deviceManager, "    trying to connect")
+
+                    peripheral.delegate = self
+
+                    central.connect(peripheral, options: nil)
+
+                    return true
+
+                } else {
+                    debug(.deviceManager, "     peripheral is nil")
+                }
+            } else {
+                debug(.deviceManager, "    uuid is not nil, but central.retrievePeripherals returns 0 peripherals")
+            }
+
+        } else {
+            debug(.deviceManager, "    uuid is nil")
+        }
+
+        return false
+    }
+
+    // MARK: - methods from protocols CBCentralManagerDelegate, CBPeripheralDelegate
+
+    func centralManager(
+        _: CBCentralManager,
+        didDiscover peripheral: CBPeripheral,
+        advertisementData _: [String: Any],
+        rssi _: NSNumber
+    ) {
+        // devicename needed unwrapped for logging
+        var deviceName = "unknown"
+        if let temp = peripheral.name {
+            deviceName = temp
+        }
+
+        debug(.deviceManager, "Did discover peripheral with name: \(deviceName)")
+
+        // check if stored address not nil, in which case we already connected before and we expect a full match with the already known device name
+        if peripheral.identifier.uuidString == deviceAddress {
+            debug(.deviceManager, "    stored address matches peripheral address, will try to connect")
+
+            stopScanAndconnect(to: peripheral)
+
+        } else {
+            debug(.deviceManager, "    stored address does not match peripheral address, ignoring this device")
+        }
+    }
+
+    func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) {
+        debug(.deviceManager, "connected to peripheral with name \(peripheral.name ?? "'unknown'")")
+
+        peripheral.discoverServices(servicesCBUUIDs)
+    }
+
+    func centralManager(_: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
+        if let error = error {
+            debug(
+                .deviceManager,
+                "failed to connect, for peripheral with name \(peripheral.name ?? "'unknown'"), with error: \(error.localizedDescription), will try again"
+            )
+
+        } else {
+            debug(.deviceManager, "failed to connect, for peripheral with name \(peripheral.name ?? "'unknown'"), will try again")
+        }
+
+        centralManager!.connect(peripheral, options: nil)
+    }
+
+    func centralManagerDidUpdateState(_ central: CBCentralManager) {
+        debug(
+            .deviceManager,
+            "in centralManagerDidUpdateState, for peripheral with name \(peripheral?.name ?? "'unknown'"), new state is \(central.state.rawValue)"
+        )
+
+        /// in case status changed to powered on and if device address known then try to retrieveperipherals
+        if central.state == .poweredOn {
+            /// try to connect to device to which connection was successfully done previously, this attempt is done by callling retrievePeripherals(central)
+            _ = retrievePeripherals(central)
+        }
+    }
+
+    func centralManager(_: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
+        debug(.deviceManager, "    didDisconnect peripheral with name \(peripheral.name ?? "'unknown'")")
+
+        // call heartbeat, useful for Dexcom transmitters, after a disconnect, then there's probably a new reading available
+        heartbeat()
+
+        if let error = error {
+            debug(.deviceManager, "    error: \(error.localizedDescription)")
+        }
+
+        // if self.peripheral == nil, then a manual disconnect or something like that has occured, no need to reconnect
+        // otherwise disconnect occurred because of other (like out of range), so let's try to reconnect
+        if let ownPeripheral = self.peripheral {
+            debug(.deviceManager, "    Will try to reconnect")
+
+            centralManager!.connect(ownPeripheral, options: nil)
+
+        } else {
+            debug(.deviceManager, "    peripheral is nil, will not try to reconnect")
+        }
+    }
+
+    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
+        debug(.deviceManager, "didDiscoverServices for peripheral with name \(peripheral.name ?? "'unknown'")")
+
+        if let error = error {
+            debug(.deviceManager, "    didDiscoverServices error: \(error.localizedDescription)")
+        }
+
+        if let services = peripheral.services {
+            for service in services {
+                debug(
+                    .deviceManager,
+                    "    Call discovercharacteristics for service with uuid \(String(describing: service.uuid))"
+                )
+                peripheral.discoverCharacteristics(nil, for: service)
+            }
+        } else {
+            disconnect()
+        }
+    }
+
+    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
+        debug(
+            .deviceManager,
+            "didDiscoverCharacteristicsFor for peripheral with name \(peripheral.name ?? "'unknown'"), for service with uuid \(String(describing: service.uuid))"
+        )
+
+        if let error = error {
+            debug(.deviceManager, "    didDiscoverCharacteristicsFor error: \(error.localizedDescription)")
+        }
+
+        if let characteristics = service.characteristics {
+            for characteristic in characteristics {
+                debug(.deviceManager, "    characteristic: \(String(describing: characteristic.uuid))")
+
+                if characteristic.uuid == CBUUID(string: CBUUID_ReceiveCharacteristic) {
+                    debug(.deviceManager, "    found receiveCharacteristic")
+
+                    receiveCharacteristic = characteristic
+
+                    peripheral.setNotifyValue(true, for: characteristic)
+                }
+            }
+
+        } else {
+            debug(.deviceManager, "    Did discover characteristics, but no characteristics listed. There must be some error.")
+        }
+    }
+
+    func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
+        if let error = error {
+            debug(
+                .deviceManager,
+                "didUpdateNotificationStateFor for peripheral with name \(peripheral.name ?? "'unkonwn'"), characteristic \(String(describing: characteristic.uuid)), error =  \(error.localizedDescription)"
+            )
+        }
+    }
+
+    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor _: CBCharacteristic, error _: Error?) {
+        debug(.deviceManager, "didUpdateValueFor for peripheral with name \(peripheral.name ?? "'unknown'")")
+
+        // call heartbeat
+        heartbeat()
+    }
+
+    func centralManager(
+        _: CBCentralManager,
+        willRestoreState _: [String: Any]
+    ) {
+        // willRestoreState must be defined, otherwise the app would crash (because the centralManager was created with a CBCentralManagerOptionRestoreIdentifierKey)
+        // even if it's an empty function
+        // trace is called here because it allows us to see in the issue reports if there was a restart after app crash or removed from memory - in all other cases (force closed by user) this function is not called
+
+        debug(.deviceManager, "in willRestoreState")
+    }
+}
+
+// MARK: - UserDefaults
+
+extension UserDefaults {
+    public enum BTKey: String {
+        /// used as local copy of cgmTransmitterDeviceAddress, will be compared regularly against value in shared UserDefaults
+        ///
+        /// this is the local stored (ie not shared with xDrip4iOS) copy of the cgm (bluetooth) device address
+        case cgmTransmitterDeviceAddress = "com.loopkit.Loop.cgmTransmitterDeviceAddress"
+    }
+
+    /// used as local copy of cgmTransmitterDeviceAddress, will be compared regularly against value in shared UserDefaults
+    var cgmTransmitterDeviceAddress: String? {
+        get {
+            string(forKey: BTKey.cgmTransmitterDeviceAddress.rawValue)
+        }
+        set {
+            set(newValue, forKey: BTKey.cgmTransmitterDeviceAddress.rawValue)
+        }
+    }
+}

+ 73 - 0
FreeAPS/Sources/APS/CGM/HeartBeatManager.swift

@@ -0,0 +1,73 @@
+import Foundation
+
+class HeartBeatManager {
+    private let keyForcgmTransmitterDeviceAddress = "cgmTransmitterDeviceAddress"
+
+    private let keyForcgmTransmitter_CBUUID_Service = "cgmTransmitter_CBUUID_Service"
+
+    private let keycgmTransmitter_CBUUID_Receive = "cgmTransmitter_CBUUID_Receive"
+
+    /// to be used as singleton, no instanstation from outside allowed - class to be accessed via shared
+    static let shared = HeartBeatManager()
+
+    /// - instance of bluetoothTransmitter that will connect to the CGM, with goal to achieve heartbeat mechanism,  nothing else
+    /// - if nil then there's no heartbeat generated
+    private var bluetoothTransmitter: BluetoothTransmitter?
+
+    private var initialSetupDone = false
+
+    /// to be used as singleton, no instanstation from outside allowed
+    private init() {}
+
+    /// verifies if local copy of cgmTransmitterDeviceAddress  is different than the one stored in shared User Defaults
+    /// - parameters:
+    ///     - sharedData : shared User Defaults
+    public func checkCGMBluetoothTransmitter(sharedUserDefaults: UserDefaults) {
+        if !initialSetupDone {
+            initialSetupDone = true
+
+            // set to nil, this will force recreation of bluetooth transmitter at app startup
+            UserDefaults.standard.cgmTransmitterDeviceAddress = nil
+        }
+
+        if UserDefaults.standard.cgmTransmitterDeviceAddress != sharedUserDefaults
+            .string(forKey: keyForcgmTransmitterDeviceAddress)
+        {
+
+            // assign local copy of cgmTransmitterDeviceAddress to the value stored in sharedUserDefaults (possibly nil value)
+            UserDefaults.standard.cgmTransmitterDeviceAddress = sharedUserDefaults
+                .string(forKey: keyForcgmTransmitterDeviceAddress)
+
+            // assign new bluetoothTransmitter. If return value is nil, and if it was not nil before, and if it was currently connected then it will disconnect automatically, because there's no other reference to it, hence deinit will be called
+            bluetoothTransmitter = setupBluetoothTransmitter(sharedData: sharedUserDefaults)
+        }
+    }
+
+    private func setupBluetoothTransmitter(sharedData: UserDefaults) -> BluetoothTransmitter? {
+        // if sharedUserDefaults.cgmTransmitterDeviceAddress is not nil then, create a new bluetoothTranmsitter instance
+        if let cgmTransmitterDeviceAddress = sharedData.string(forKey: keyForcgmTransmitterDeviceAddress) {
+            // unwrap cgmTransmitter_CBUUID_Service and cgmTransmitter_CBUUID_Receive
+            if let cgmTransmitter_CBUUID_Service = sharedData.string(forKey: keyForcgmTransmitter_CBUUID_Service),
+               let cgmTransmitter_CBUUID_Receive = sharedData.string(forKey: keycgmTransmitter_CBUUID_Receive)
+            {
+                // a new cgm transmitter has been setup in xDrip4iOS
+                // we will connect to the same transmitter here so it can be used as heartbeat
+                let newBluetoothTransmitter = BluetoothTransmitter(
+                    deviceAddress: cgmTransmitterDeviceAddress,
+                    servicesCBUUID: cgmTransmitter_CBUUID_Service,
+                    CBUUID_Receive: cgmTransmitter_CBUUID_Receive,
+                    heartbeat: {}
+                )
+
+                return newBluetoothTransmitter
+
+            } else {
+                // looks like a coding error, xdrip4iOS did set a value for cgmTransmitterDeviceAddress in sharedUserDefaults but did not set a value for cgmTransmitter_CBUUID_Service or cgmTransmitter_CBUUID_Receive
+
+                return nil
+            }
+        }
+
+        return nil
+    }
+}