diff --git a/Bluetooth/BluetoothSearch.swift b/Bluetooth/BluetoothSearch.swift index dd1c450..8fd4e46 100644 --- a/Bluetooth/BluetoothSearch.swift +++ b/Bluetooth/BluetoothSearch.swift @@ -8,21 +8,20 @@ import CoreBluetooth import Foundation -import LibreTransmitter import OSLog import UIKit import Combine -struct RSSIInfo { - let bledeviceID: String - let signalStrength: Int +public struct RSSIInfo { + public let bledeviceID: String + public let signalStrength: Int - var totalBars: Int { + public var totalBars: Int { 3 } - var signalBars: Int { + public var signalBars: Int { if signalStrength < -80 { return 1 // near } @@ -36,30 +35,35 @@ struct RSSIInfo { } -final class BluetoothSearchManager: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate { +public protocol BluetoothSearcher { + func disconnectManually() + func scanForCompatibleDevices() + func stopTimer() + + var passThroughMetaData: PassthroughSubject<(PeripheralProtocol, [String: Any]), Never> { get } + var throttledRSSI: GenericThrottler { get } +} + + +public final class BluetoothSearchManager: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate, BluetoothSearcher { var centralManager: CBCentralManager! fileprivate lazy var logger = Logger(forType: Self.self) - // fileprivate let deviceNames = SupportedDevices.allNames - // fileprivate let serviceUUIDs:[CBUUID]? = [CBUUID(string: "6E400001-B5A3-F393-E0A9-E50E24DCCA9E")] - private var discoveredDevices = [CBPeripheral]() - public let passThrough = PassthroughSubject() - public let passThroughMetaData = PassthroughSubject<(CBPeripheral, [String: Any]), Never>() + public let passThroughMetaData = PassthroughSubject<(PeripheralProtocol, [String: Any]), Never>() public let throttledRSSI = GenericThrottler(identificator: \RSSIInfo.bledeviceID, interval: 5) private var rescanTimerBag = Set() public func addDiscoveredDevice(_ device: CBPeripheral, with metadata: [String: Any], rssi: Int) { - passThrough.send(device) passThroughMetaData.send((device, metadata)) throttledRSSI.incoming.send(RSSIInfo(bledeviceID: device.identifier.uuidString, signalStrength: rssi)) } - override init() { + public override init() { super.init() // calling readrssi on a peripheral is only supported on connected peripherals // here we want the AllowDuplicatesKey to be true so that we get a continous feed of new rssi values for @@ -106,7 +110,7 @@ final class BluetoothSearchManager: NSObject, CBCentralManagerDelegate, CBPeriph self.scanForCompatibleDevices() } - func scanForCompatibleDevices() { + public func scanForCompatibleDevices() { if centralManager.state == .poweredOn && !centralManager.isScanning { logger.debug("Before scan for transmitter while central manager state \(String(describing: self.centralManager.state.rawValue))") @@ -120,19 +124,16 @@ final class BluetoothSearchManager: NSObject, CBCentralManagerDelegate, CBPeriph } } - func disconnectManually() { + public func disconnectManually() { logger.debug("did disconnect manually") - // NotificationManager.scheduleDebugNotification(message: "Timer fired in Background", wait: 3) - // _ = Timer(timeInterval: 150, repeats: false, block: {timer in NotificationManager.scheduleDebugNotification(message: "Timer fired in Background", wait: 0.5)}) - - if centralManager.isScanning { - centralManager.stopScan() - } + if centralManager.isScanning { + centralManager.stopScan() + } } // MARK: - CBCentralManagerDelegate - func centralManagerDidUpdateState(_ central: CBCentralManager) { + public func centralManagerDidUpdateState(_ central: CBCentralManager) { logger.debug("Central Manager did update state to \(String(describing: central.state.rawValue))") switch central.state { case .poweredOff, .resetting, .unauthorized, .unknown, .unsupported: @@ -140,7 +141,7 @@ final class BluetoothSearchManager: NSObject, CBCentralManagerDelegate, CBPeriph case .poweredOn: // we don't want this to start scanning right away, but rather wait until the view has appeared // this means that the view is responsible for calling scanForCompatibleDevices it self - // scanForCompatibleDevices() // power was switched on, while app is running -> reconnect. + //scanForCompatibleDevices() // power was switched on, while app is running -> reconnect. break @unknown default: @@ -148,7 +149,7 @@ final class BluetoothSearchManager: NSObject, CBCentralManagerDelegate, CBPeriph } } - func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { + public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { guard let name = peripheral.name?.lowercased() else { logger.debug("could not find name for device \(peripheral.identifier.uuidString)") return @@ -176,22 +177,22 @@ final class BluetoothSearchManager: NSObject, CBCentralManagerDelegate, CBPeriph } } - func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { + public func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { // self.lastConnectedIdentifier = peripheral.identifier.uuidString } - func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { + public func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { logger.error("did fail to connect") } - func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { + public func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { logger.debug("did didDisconnectPeripheral") } // MARK: - CBPeripheralDelegate - func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { + public func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { logger.debug("Did discover services") if let error { logger.error("Did discover services error: \(error.localizedDescription)") @@ -206,7 +207,7 @@ final class BluetoothSearchManager: NSObject, CBCentralManagerDelegate, CBPeriph } } - func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { + public func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { logger.debug("Did discover characteristics for service \(String(describing: peripheral.name))") if let error { @@ -230,22 +231,22 @@ final class BluetoothSearchManager: NSObject, CBCentralManagerDelegate, CBPeriph } } - func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) { + public func peripheral(_ peripheral: CBPeripheral, didReadRSSI RSSI: NSNumber, error: Error?) { // throttledRSSI.incoming.send(RSSIInfo(bledeviceID: peripheral.identifier.uuidString, signalStrength: RSSI.intValue)) // peripheral.readRSSI() //we keep contuing to update the rssi (only works if peripheral is already connected.... } - func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { + public func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { logger.debug("Did update notification state for characteristic: \(String(describing: characteristic.debugDescription))") } - func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { + public func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { logger.debug("Did update value for characteristic: \(String(describing: characteristic.debugDescription))") } - func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { + public func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { logger.debug("Did Write value \(String(characteristic.value.debugDescription)) for characteristic \(String(characteristic.debugDescription))") } diff --git a/Bluetooth/CBPeripheralExtensions.swift b/Bluetooth/CBPeripheralExtensions.swift index 0ae7ae9..805f33c 100644 --- a/Bluetooth/CBPeripheralExtensions.swift +++ b/Bluetooth/CBPeripheralExtensions.swift @@ -21,43 +21,8 @@ public enum Either { case Right(B) } -public typealias SomePeripheral = Either -extension SomePeripheral: PeripheralProtocol, Identifiable, Hashable, Equatable { - public static func == (lhs: Either, rhs: Either) -> Bool { - lhs.asStringIdentifier == rhs.asStringIdentifier - } - - public var id: String { - actualPeripheral.asStringIdentifier - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(actualPeripheral.asStringIdentifier) - } - - private var actualPeripheral: PeripheralProtocol { - switch self { - case let .Left(real): - return real - case let .Right(mocked): - return mocked - } - } - public var name: String? { - actualPeripheral.name - } - - public var name2: String { - actualPeripheral.name2 - } - - public var asStringIdentifier: String { - actualPeripheral.asStringIdentifier - } -} - -extension CBPeripheral: PeripheralProtocol, Identifiable { +extension CBPeripheral: PeripheralProtocol { public var name2: String { self.name ?? "" } @@ -66,19 +31,3 @@ extension CBPeripheral: PeripheralProtocol, Identifiable { self.identifier.uuidString } } - -public class MockedPeripheral: PeripheralProtocol, Identifiable { - public var name: String? - - public var name2: String { - name ?? "unknown-device" - } - - public var asStringIdentifier: String { - name2 - } - - public init(name: String) { - self.name = name - } -} diff --git a/Bluetooth/GenericThrottler.swift b/Bluetooth/GenericThrottler.swift index 7cbc4c5..0582dd7 100644 --- a/Bluetooth/GenericThrottler.swift +++ b/Bluetooth/GenericThrottler.swift @@ -9,7 +9,7 @@ import Foundation import Combine -class GenericThrottler { +public class GenericThrottler { public var throttledPublisher: AnyPublisher { throttledSubject.eraseToAnyPublisher() @@ -88,7 +88,7 @@ class GenericThrottler { .store(in: &bag) } - init(identificator: KeyPath, interval: TimeInterval) { + public init(identificator: KeyPath, interval: TimeInterval) { self.identificator = identificator self.interval = interval diff --git a/Bluetooth/LibreTransmitterMetadata.swift b/Bluetooth/LibreTransmitterMetadata.swift index 70e6ed2..d1543d1 100644 --- a/Bluetooth/LibreTransmitterMetadata.swift +++ b/Bluetooth/LibreTransmitterMetadata.swift @@ -110,7 +110,7 @@ public enum SensorType: String, CustomStringConvertible { case 0xDF, 0xA2: self = .libre1 case 0xE5, 0xE6: self = .libreUS14day case 0x70: self = .libreProH - case 0x9D: self = .libre2 + case 0xC5, 0x9D: self = .libre2 case 0x76: self = patchInfo[3] == 0x02 ? .libre2US : patchInfo[3] == 0x04 ? .libre2CA : patchInfo[2] >> 4 == 7 ? .libreSense : .unknown default: if patchInfo.count == 24 { diff --git a/Bluetooth/Transmitter/BubbleTransmitter.swift b/Bluetooth/Transmitter/BubbleTransmitter.swift index 1607512..0856c98 100644 --- a/Bluetooth/Transmitter/BubbleTransmitter.swift +++ b/Bluetooth/Transmitter/BubbleTransmitter.swift @@ -58,7 +58,7 @@ class BubbleTransmitter: MiaoMiaoTransmitter { UIImage(named: "bubble", in: Bundle.current, compatibleWith: nil) } - override static func canSupportPeripheral(_ peripheral: CBPeripheral) -> Bool { + override static func canSupportPeripheral(_ peripheral: PeripheralProtocol) -> Bool { peripheral.name?.lowercased().starts(with: "bubble") ?? false } @@ -93,7 +93,7 @@ class BubbleTransmitter: MiaoMiaoTransmitter { private static func getDeviceDetailsFromAdvertisementInternal(advertisementData: [String: Any]?) -> (String?, String?, String?) { - guard let data = advertisementData?["kCBAdvDataManufacturerData"] as? Data else { + guard let data = advertisementData?[CBAdvertisementDataManufacturerDataKey] as? Data else { return (nil, nil, nil) } var mac = "" diff --git a/Bluetooth/Transmitter/Libre2DirectTransmitter.swift b/Bluetooth/Transmitter/Libre2DirectTransmitter.swift index 674c3a4..f34eb44 100644 --- a/Bluetooth/Transmitter/Libre2DirectTransmitter.swift +++ b/Bluetooth/Transmitter/Libre2DirectTransmitter.swift @@ -45,7 +45,7 @@ class Libre2DirectTransmitter: LibreTransmitterProxyProtocol { private var sensorData: SensorData? private var metadata: LibreTransmitterMetadata? - class func canSupportPeripheral(_ peripheral: CBPeripheral) -> Bool { + class func canSupportPeripheral(_ peripheral: PeripheralProtocol) -> Bool { peripheral.name?.lowercased().starts(with: "abbott") ?? false } diff --git a/Bluetooth/Transmitter/LibreTransmitterProxyManager.swift b/Bluetooth/Transmitter/LibreTransmitterProxyManager.swift index f5d1841..833c103 100644 --- a/Bluetooth/Transmitter/LibreTransmitterProxyManager.swift +++ b/Bluetooth/Transmitter/LibreTransmitterProxyManager.swift @@ -449,28 +449,15 @@ public final class LibreTransmitterProxyManager: NSObject, CBCentralManagerDeleg var foundUUID = manufacturerData.subdata(in: 2..<8) foundUUID.append(contentsOf: [0x07, 0xe0]) - logger.debug("ManufacturerData: \(manufacturerData), found uid: \(foundUUID)") + logger.debug("ManufacturerData: \(manufacturerData.hex), found uid: \(foundUUID.hex)") guard foundUUID == selectedUid && Libre2DirectTransmitter.canSupportPeripheral(peripheral) else { return false } - - return true } - private func verifyLibre2ByName(peripheral: CBPeripheral, name: String) -> Bool { - - // This method is not as robust, and should only be used in cases where manufacturerdata is not available, such as when using the tzachi-dar simulator - guard peripheral.name?.contains(name) == true else { - return false - } - return Libre2DirectTransmitter.canSupportPeripheral(peripheral) - - - } - public func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) { dispatchPrecondition(condition: .onQueue(managerQueue)) @@ -488,22 +475,10 @@ public final class LibreTransmitterProxyManager: NSObject, CBCentralManagerDeleg logger.debug("preselected sensor is: \(String(describing:sensor))") - if let sensor = UserDefaults.standard.preSelectedSensor, let name = sensor.sensorName, sensor.initialIdentificationStrategy == .byFakeSensorName { - logger.debug("Verifiying libre2 connection using sensor name") - if !verifyLibre2ByName(peripheral: peripheral, name: name) { - logger.debug("failed Verifiying libre2 connection using sensor name") - return - } - - } else { - logger.debug("Verifiying libre2 connection using manufacturerData") - if !verifyLibre2ManufacturerData(peripheral: peripheral, selectedUid: selectedUid, advertisementData: advertisementData) { - logger.debug("failed Verifiying libre2 connection using manufacturerData") - return - } + if !verifyLibre2ManufacturerData(peripheral: peripheral, selectedUid: selectedUid, advertisementData: advertisementData) { + logger.debug("failed Verifiying libre2 connection using manufacturerData") + return } - - // next time we search via bluetooth, let's identify the sensor with its bluetooth identifier UserDefaults.standard.preSelectedUid = nil diff --git a/Bluetooth/Transmitter/LibreTransmitterProxyProtocol.swift b/Bluetooth/Transmitter/LibreTransmitterProxyProtocol.swift index fd9c26e..4bcf343 100644 --- a/Bluetooth/Transmitter/LibreTransmitterProxyProtocol.swift +++ b/Bluetooth/Transmitter/LibreTransmitterProxyProtocol.swift @@ -15,7 +15,7 @@ public protocol LibreTransmitterProxyProtocol: AnyObject { static var manufacturerer: String { get } static var requiresPhoneNFC: Bool { get } static var requiresSetup: Bool { get } - static func canSupportPeripheral(_ peripheral: CBPeripheral) -> Bool + static func canSupportPeripheral(_ peripheral: PeripheralProtocol) -> Bool static var writeCharacteristic: UUIDContainer? { get set } static var notifyCharacteristic: UUIDContainer? { get set } @@ -36,7 +36,7 @@ public protocol LibreTransmitterProxyProtocol: AnyObject { } extension LibreTransmitterProxyProtocol { - func canSupportPeripheral(_ peripheral: CBPeripheral) -> Bool { + func canSupportPeripheral(_ peripheral: PeripheralProtocol) -> Bool { Self.canSupportPeripheral(peripheral) } public var staticType: LibreTransmitterProxyProtocol.Type { @@ -78,7 +78,7 @@ public enum LibreTransmitters { getSupportedPlugins(peripheral)?.isEmpty == false } - public static func getSupportedPlugins(_ peripheral: CBPeripheral) -> [LibreTransmitterProxyProtocol.Type]? { + public static func getSupportedPlugins(_ peripheral: PeripheralProtocol) -> [LibreTransmitterProxyProtocol.Type]? { all.enumerated().compactMap { $0.element.canSupportPeripheral(peripheral) ? $0.element : nil } diff --git a/Bluetooth/Transmitter/MiaomiaoTransmitter.swift b/Bluetooth/Transmitter/MiaomiaoTransmitter.swift index 472e9cd..e78d25e 100644 --- a/Bluetooth/Transmitter/MiaomiaoTransmitter.swift +++ b/Bluetooth/Transmitter/MiaomiaoTransmitter.swift @@ -215,7 +215,7 @@ class MiaoMiaoTransmitter: LibreTransmitterProxyProtocol { private var sensorData: SensorData? private var metadata: LibreTransmitterMetadata? - class func canSupportPeripheral(_ peripheral: CBPeripheral) -> Bool { + class func canSupportPeripheral(_ peripheral: PeripheralProtocol) -> Bool { peripheral.name?.lowercased().starts(with: "miaomiao") ?? false } diff --git a/Common/HKUnit.swift b/Common/HKUnit.swift index 6e717c3..719502d 100644 --- a/Common/HKUnit.swift +++ b/Common/HKUnit.swift @@ -8,7 +8,6 @@ import HealthKit -/* not needed for loop dev/jojo ? */ extension HKUnit { static let milligramsPerDeciliter: HKUnit = { HKUnit.gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci)) diff --git a/Common/Settings/GlucoseSchedules.swift b/Common/Settings/GlucoseSchedules.swift index 5f26362..dd69bc5 100644 --- a/Common/Settings/GlucoseSchedules.swift +++ b/Common/Settings/GlucoseSchedules.swift @@ -147,9 +147,6 @@ class GlucoseSchedule: Codable, CustomStringConvertible { var highAlarm: Double? var enabled: Bool? - init() { - } - // glucose schedules are stored as standalone datecomponents (i.e. offsets) // this takes the current start of day and adds those offsets, // and returns a Dateinterval with those offsets applied diff --git a/Features.swift b/Features.swift index 713e950..e5f56f1 100644 --- a/Features.swift +++ b/Features.swift @@ -8,9 +8,7 @@ import Foundation -#if canImport(CoreNFC) import CoreNFC -#endif public final class Features { @@ -21,21 +19,7 @@ public final class Features { static public var allowsEditingFactoryCalibrationData = false - // Only to be used with this program, running on a linux amd64 system or rasberry pi - // https://github.com/tzachi-dar/gatt#this-is-a-program-for-creating-a-simulation-for-libre-2-for-xdrip - static public var supportsFakeSensor = false - static var phoneNFCAvailable: Bool { - #if canImport(CoreNFC) - if NSClassFromString("NFCNDEFReaderSession") == nil { - return false - - } - return NFCNDEFReaderSession.readingAvailable - #else - return false - #endif } - } diff --git a/Info.plist b/Info.plist new file mode 100644 index 0000000..53401cf --- /dev/null +++ b/Info.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSHumanReadableCopyright + Copyright © 2023 LoopKit Authors + NSPrincipalClass + LibreDemoPlugin.LibreDemoPlugin + com.loopkit.Loop.CGMManagerDisplayName + FreeStyle Libre Demo + com.loopkit.Loop.CGMManagerIdentifier + LibreDemoCGMManager + + diff --git a/LibreDemoPlugin/Extensions/HKUnit.swift b/LibreDemoPlugin/Extensions/HKUnit.swift new file mode 100644 index 0000000..9d0432f --- /dev/null +++ b/LibreDemoPlugin/Extensions/HKUnit.swift @@ -0,0 +1,18 @@ +// +// HKUnit.swift +// LibreDemoPlugin +// +// Created by Pete Schwamb on 6/24/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import Foundation + + +import HealthKit + +extension HKUnit { + static let milligramsPerDeciliter: HKUnit = { + HKUnit.gramUnit(with: .milli).unitDivided(by: .literUnit(with: .deci)) + }() +} diff --git a/LibreDemoPlugin/Extensions/TimeInterval.swift b/LibreDemoPlugin/Extensions/TimeInterval.swift new file mode 100644 index 0000000..2f27544 --- /dev/null +++ b/LibreDemoPlugin/Extensions/TimeInterval.swift @@ -0,0 +1,64 @@ +// +// TimeInterval.swift +// LibreDemoPlugin +// +// Created by Pete Schwamb on 6/27/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import Foundation + +extension TimeInterval { + static func hours(_ hours: Double) -> TimeInterval { + return self.init(hours: hours) + } + + static func minutes(_ minutes: Int) -> TimeInterval { + return self.init(minutes: Double(minutes)) + } + + static func minutes(_ minutes: Double) -> TimeInterval { + return self.init(minutes: minutes) + } + + static func seconds(_ seconds: Double) -> TimeInterval { + return self.init(seconds) + } + + static func milliseconds(_ milliseconds: Double) -> TimeInterval { + return self.init(milliseconds / 1000) + } + + init(minutes: Double) { + self.init(minutes * 60) + } + + init(hours: Double) { + self.init(minutes: hours * 60) + } + + init(days: Double) { + self.init(hours: days * 24) + } + + init(seconds: Double) { + self.init(seconds) + } + + init(milliseconds: Double) { + self.init(milliseconds / 1000) + } + + var milliseconds: Double { + return self * 1000 + } + + var minutes: Double { + return self / 60.0 + } + + var hours: Double { + return minutes / 60.0 + } + +} diff --git a/LibreDemoPlugin/LibreDemoCGMManager.swift b/LibreDemoPlugin/LibreDemoCGMManager.swift new file mode 100644 index 0000000..98697fa --- /dev/null +++ b/LibreDemoPlugin/LibreDemoCGMManager.swift @@ -0,0 +1,120 @@ +// +// LibreDemoCGMManager.swift +// LibreTransmitter +// +// Created by Pete Schwamb on 6/21/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import Foundation +import LibreTransmitter +import LibreTransmitterUI +import LoopKit +import LoopKitUI +import HealthKit +import os.log +import LoopTestingKit + +class LibreDemoCGMManager: LibreTransmitterManagerV3 { + var timer: Timer? + + private let log = OSLog(category: "LibreDemoCGMManager") + + + override var localizedTitle: String { "Libre Demo" } + + public override var managerIdentifier: String { + "LibreDemoCGMManager" + } + + public override var pairingService: SensorPairingProtocol { + return MockSensorPairingService() + } + + public override var bluetoothSearcher: BluetoothSearcher { + return MockBluetoothSearcher() + } + + public override func establishProxy() { + // do nothing + } + + private var sensorStartDate = Date().addingTimeInterval(TimeInterval(days: -1)) + + public required init() { + super.init() + + self.lastConnected = Date() + + + timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(5*60), repeats: true) { _ in + self.reportMockSample() + } + + // Also trigger a sample immediately, for dev convenience. + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.reportMockSample() + } + } + + private func reportMockSample() { + let date = Date() + let value = 110.0 + sin(date.timeIntervalSinceReferenceDate.truncatingRemainder(dividingBy: 3600 * 5) / (3600*5) * Double.pi * 2) * 60 + let quantity = HKQuantity(unit: .milligramsPerDeciliter, doubleValue: value) + let newSample = NewGlucoseSample( + date: date, + quantity: quantity, + condition: nil, + trend: nil, + trendRate: nil, + isDisplayOnly: false, + wasUserEntered: false, + syncIdentifier: "mock-libre + \(date)", + device: testingDevice + ) + log.debug("Reporting mock value of %{public}@", String(describing: value)) + + // must be inside this handler as setobservables "depend" on latestbackfill + let sensorData = MockSensorData( + minutesSinceStart: Int(date.timeIntervalSince(sensorStartDate).minutes), + maxMinutesWearTime: Int(TimeInterval(days: 14).minutes), + state: .ready, + serialNumber: "12345", + footerCrc: 0xabcd, + date: date) + + self.latestBackfill = LibreGlucose(unsmoothedGlucose: value, glucoseDouble: value, timestamp: date) + + self.setObservables(sensorData: sensorData, bleData: nil, metaData: nil) + + self.delegateQueue.async { + self.cgmManagerDelegate?.cgmManager(self, hasNew: CGMReadingResult.newData([newSample])) + } + } +} + +extension LibreDemoCGMManager: TestingCGMManager { + func injectGlucoseSamples(_ pastSamples: [LoopKit.NewGlucoseSample], futureSamples: [LoopKit.NewGlucoseSample]) { + // TODO: Support scenarios + } + + var testingDevice: HKDevice { + HKDevice( + name: "LibreDemoCGM", + manufacturer: "LoopKit", + model: nil, + hardwareVersion: nil, + firmwareVersion: nil, + softwareVersion: nil, + localIdentifier: nil, + udiDeviceIdentifier: nil + ) + } + + func acceptDefaultsAndSkipOnboarding() { + } + + func trigger(action: LoopTestingKit.DeviceAction) { + // TODO: Support scenario actions + } +} diff --git a/LibreDemoPlugin/LibreDemoPlugin.h b/LibreDemoPlugin/LibreDemoPlugin.h new file mode 100644 index 0000000..02f957b --- /dev/null +++ b/LibreDemoPlugin/LibreDemoPlugin.h @@ -0,0 +1,19 @@ +// +// LibreTransmitterDemo.h +// LibreTransmitterDemo +// +// Created by Pete Schwamb on 6/21/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +#import + +//! Project version number for LibreTransmitterDemo. +FOUNDATION_EXPORT double LibreTransmitterDemoVersionNumber; + +//! Project version string for LibreTransmitterDemo. +FOUNDATION_EXPORT const unsigned char LibreTransmitterDemoVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/LibreDemoPlugin/LibreDemoPlugin.swift b/LibreDemoPlugin/LibreDemoPlugin.swift new file mode 100644 index 0000000..9ff2f74 --- /dev/null +++ b/LibreDemoPlugin/LibreDemoPlugin.swift @@ -0,0 +1,42 @@ +// +// LibreDemoPlugin.swift +// LibreTransmitter +// +// Created by Pete Schwamb on 6/21/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import LoopKitUI +import LibreTransmitter +import LibreTransmitterUI + +import os.log + +class LibreDemoPlugin: NSObject, CGMManagerUIPlugin { + + private let log = OSLog(category: "LibreDemoPlugin") + + public var pumpManagerType: PumpManagerUI.Type? { + nil + } + + public var cgmManagerType: CGMManagerUI.Type? { + LibreDemoCGMManager.self + } + + override init() { + super.init() + log.default("Instantiated") + LibreTransmitter.AppMetaData.allProperties = allProperties + + } + + let prefix = "com-loopkit-libre" + let bundle = Bundle(for: LibreTransmitterPlugin.self) + + var allProperties: String { + bundle.infoDictionary?.compactMap { + $0.key.starts(with: prefix) ? "\($0.key): \($0.value)" : nil + }.joined(separator: "\n") ?? "none" + } +} diff --git a/LibreSensor/SensorContents/Measurement.swift b/LibreSensor/SensorContents/Measurement.swift index 55677da..c9a4d81 100644 --- a/LibreSensor/SensorContents/Measurement.swift +++ b/LibreSensor/SensorContents/Measurement.swift @@ -23,7 +23,6 @@ extension MeasurementProtocol { // add extraslope and extraoffset as indicated by user in the ui round(glucoseValueFromRaw(calibrationInfo: calibrationInfo)) * calibrationInfo.extraSlope + calibrationInfo.extraOffset } - } public enum MeasurementError: Int, CaseIterable, Codable { diff --git a/LibreSensor/SensorContents/PreLibre2.swift b/LibreSensor/SensorContents/PreLibre2.swift index 70f3db5..f14545e 100644 --- a/LibreSensor/SensorContents/PreLibre2.swift +++ b/LibreSensor/SensorContents/PreLibre2.swift @@ -272,12 +272,6 @@ public extension Libre2 { } } -extension UInt16 { - init(_ byte0: UInt8, _ byte1: UInt8) { - self = Data([byte1, byte0]).withUnsafeBytes { $0.load(as: UInt16.self) } - } -} - extension Libre2 { enum Example { static let sensorInfo: [UInt8] = [ diff --git a/LibreSensor/SensorContents/Sensor.swift b/LibreSensor/SensorContents/Sensor.swift index 2815dbe..e76f307 100644 --- a/LibreSensor/SensorContents/Sensor.swift +++ b/LibreSensor/SensorContents/Sensor.swift @@ -60,10 +60,6 @@ public struct CalibrationToSensorMapping: Codable { self.reverseFooterCRC = reverseFooterCRC } } -public enum Libre2IdentificationStrategy: Int, Codable { - case byUid = 0 //default - case byFakeSensorName = 1 //only when used with tzachi-dar-simulator -} public struct Sensor: Codable { public let uuid: Data @@ -81,8 +77,6 @@ public struct Sensor: Codable { public var unlockCount: Int - var initialIdentificationStrategy: Libre2IdentificationStrategy - var sensorName : String? /* @@ -116,7 +110,7 @@ public struct Sensor: Codable { } } */ - public init(uuid: Data, patchInfo: Data, maxAge: Int, unlockCount: Int = 0, initialIdentificationStrategy: Libre2IdentificationStrategy = .byUid, sensorName: String? = nil) { + public init(uuid: Data, patchInfo: Data, maxAge: Int, unlockCount: Int = 0, sensorName: String? = nil) { self.uuid = uuid self.patchInfo = patchInfo @@ -129,7 +123,6 @@ public struct Sensor: Codable { self.unlockCount = 0 self.maxAge = maxAge // self.calibrationInfo = calibrationInfo - self.initialIdentificationStrategy = initialIdentificationStrategy self.sensorName = sensorName } diff --git a/LibreSensor/SensorContents/SensorData.swift b/LibreSensor/SensorContents/SensorData.swift index 900719a..5ccd2de 100644 --- a/LibreSensor/SensorContents/SensorData.swift +++ b/LibreSensor/SensorContents/SensorData.swift @@ -8,9 +8,44 @@ import Foundation +public protocol SensorDataProtocol { + var minutesSinceStart: Int { get } + var maxMinutesWearTime: Int { get } + var state: SensorState { get } + var serialNumber: String { get } + var footerCrc: UInt16 { get } + var date: Date { get } +} + +public extension SensorDataProtocol { + var humanReadableSensorAge: String { + let days = TimeInterval(minutesSinceStart * 60).days + return String(format: "%.2f", days) + " day(s)" + } + + var humanReadableTimeLeft: String { + let days = TimeInterval(minutesLeft * 60).days + return String(format: "%.2f", days) + " day(s)" + } + + // the amount of minutes left before this sensor expires + var minutesLeft: Int { + maxMinutesWearTime - minutesSinceStart + } + + // once the sensor has ended we don't know the exact date anymore + var sensorEndTime: Date? { + if minutesLeft <= 0 { + return nil + } + + return self.date.addingTimeInterval(TimeInterval(minutes: Double(self.minutesLeft))) + } +} + /// Structure for data from Freestyle Libre sensor /// To be initialized with the bytes as read via nfc. Provides all derived data. -public struct SensorData: Codable { +public struct SensorData: Codable, SensorDataProtocol { /// Parameters for the temperature compensation algorithm // let temperatureAlgorithmParameterSet: TemperatureAlgorithmParameters? @@ -21,7 +56,7 @@ public struct SensorData: Codable { /// The uid of the sensor let uuid: Data /// The serial number of the sensor - var serialNumber: String { + public var serialNumber: String { guard let patchInfo, patchInfo.count >= 6, let family = SensorFamily(rawValue: Int(patchInfo[2] >> 4)) @@ -48,7 +83,7 @@ public struct SensorData: Codable { Array(bytes[footerRange]) } /// Date when data was read from sensor - let date: Date + public let date: Date /// Minutes (approx) since start of sensor public var minutesSinceStart: Int { Int(body[293]) << 8 + Int(body[292]) @@ -82,26 +117,12 @@ public struct SensorData: Codable { Crc.hasValidCrc16InFirstTwoBytes(footer) } /// Footer crc needed for checking integrity of SwiftLibreOOPWeb response - var footerCrc: UInt16 { + public var footerCrc: UInt16 { Crc.crc16(Array(footer.dropFirst(2)), seed: 0xffff) } - // the amount of minutes left before this sensor expires - public var minutesLeft: Int { - maxMinutesWearTime - minutesSinceStart - } - - // once the sensor has ended we don't know the exact date anymore - var sensorEndTime: Date? { - if minutesLeft <= 0 { - return nil - } - - return self.date.addingTimeInterval(TimeInterval(minutes: Double(self.minutesLeft))) - } - /// Sensor state (ready, failure, starting etc.) - var state: SensorState { + public var state: SensorState { SensorState(stateByte: header[4]) } @@ -230,18 +251,6 @@ public struct SensorData: Codable { // strictly only needed for decryption and calculating serial numbers properly public var patchInfo : Data? - fileprivate let aday = 86_400.0 // in seconds - - var humanReadableSensorAge: String { - let days = TimeInterval(minutesSinceStart * 60) / aday - return String(format: "%.2f", days) + " day(s)" - } - - var humanReadableTimeLeft: String { - let days = TimeInterval(minutesLeft * 60) / aday - return String(format: "%.2f", days) + " day(s)" - } - var toJson: String { "[" + self.bytes.map { String(format: "0x%02x", $0) }.joined(separator: ", ") + "]" } diff --git a/LibreSensor/SensorContents/SensorState.swift b/LibreSensor/SensorContents/SensorState.swift index 467c3b8..7ff36d5 100644 --- a/LibreSensor/SensorContents/SensorState.swift +++ b/LibreSensor/SensorContents/SensorState.swift @@ -17,7 +17,7 @@ import Foundation /// - expired: 0x05 sensor is expired /// - failure: 0x06 sensor has an error /// - unknown: any other state -enum SensorState { +public enum SensorState { case notYetStarted case starting case ready diff --git a/LibreTransmitterUI/SensorPairing/SensorPairing.swift b/LibreSensor/SensorPairing/SensorPairing.swift similarity index 56% rename from LibreTransmitterUI/SensorPairing/SensorPairing.swift rename to LibreSensor/SensorPairing/SensorPairing.swift index 7ee3691..a495065 100644 --- a/LibreTransmitterUI/SensorPairing/SensorPairing.swift +++ b/LibreSensor/SensorPairing/SensorPairing.swift @@ -6,34 +6,31 @@ // import Foundation import Combine -import LibreTransmitter -class SensorPairingInfo: ObservableObject, Codable { - @Published private(set) var uuid: Data - @Published private(set) var patchInfo: Data - @Published private(set) var fram: Data - @Published private(set) var streamingEnabled: Bool - @Published private(set) var initialIdentificationStrategy: Libre2IdentificationStrategy = .byUid - - @Published private(set) var sensorName : String? = nil +public class SensorPairingInfo: ObservableObject, Codable { + @Published public var uuid: Data + @Published public var patchInfo: Data + @Published public var fram: Data + @Published public var streamingEnabled: Bool + + @Published public var sensorName : String? = nil enum CodingKeys: CodingKey { - case uuid, patchInfo, fram, streamingEnabled, initialIdentificationStrategy, sensorName + case uuid, patchInfo, fram, streamingEnabled, sensorName } - func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(uuid, forKey: .uuid) try container.encode(patchInfo, forKey: .patchInfo) try container.encode(fram, forKey: .fram) try container.encode(streamingEnabled, forKey: .streamingEnabled) - try container.encode(initialIdentificationStrategy, forKey: .initialIdentificationStrategy) try container.encode(sensorName, forKey: .sensorName) } - required init(from decoder: Decoder) throws { + public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) uuid = try container.decode(Data.self, forKey: .uuid) @@ -41,32 +38,27 @@ class SensorPairingInfo: ObservableObject, Codable { fram = try container.decode(Data.self, forKey: .fram) streamingEnabled = try container.decode(Bool.self, forKey: .streamingEnabled) - initialIdentificationStrategy = try container.decode(Libre2IdentificationStrategy.self, forKey: .initialIdentificationStrategy) sensorName = try container.decode(String?.self, forKey: .sensorName) - - - } - public init(uuid: Data=Data(), patchInfo: Data=Data(), fram: Data=Data(), streamingEnabled: Bool = false, initialIdentificationStrategy: Libre2IdentificationStrategy = .byUid, sensorName: String? = nil ) { + public init(uuid: Data=Data(), patchInfo: Data=Data(), fram: Data=Data(), streamingEnabled: Bool = false, sensorName: String? = nil ) { self.uuid = uuid self.patchInfo = patchInfo self.fram = fram self.streamingEnabled = streamingEnabled - self.initialIdentificationStrategy = initialIdentificationStrategy self.sensorName = sensorName } - var sensorData: SensorData? { + public var sensorData: SensorData? { SensorData(bytes: [UInt8](self.fram)) } - var calibrationData: SensorData.CalibrationInfo? { + public var calibrationData: SensorData.CalibrationInfo? { sensorData?.calibrationData } - var description: String { + public var description: String { let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted @@ -80,6 +72,8 @@ class SensorPairingInfo: ObservableObject, Codable { } -protocol SensorPairingProtocol { - func pairSensor() -> AnyPublisher +public protocol SensorPairingProtocol: AnyObject { + var onCancel: (() -> Void)? { get set } + var publisher: AnyPublisher { get } + func pairSensor() throws } diff --git a/LibreTransmitterUI/SensorPairing/SensorPairingService.swift b/LibreSensor/SensorPairing/SensorPairingService.swift similarity index 87% rename from LibreTransmitterUI/SensorPairing/SensorPairingService.swift rename to LibreSensor/SensorPairing/SensorPairingService.swift index 1c53319..fb7f73b 100644 --- a/LibreTransmitterUI/SensorPairing/SensorPairingService.swift +++ b/LibreSensor/SensorPairing/SensorPairingService.swift @@ -5,7 +5,6 @@ // Created by Reimar Metzen on 06.07.21. // -#if canImport(CoreNFC) import Foundation import Combine import CoreNFC @@ -17,26 +16,38 @@ public enum PairingError: Error { case wrongSensorType case decryptionError case noPatchInfo - - public var errorDescription: String { + case nfcNotSupported +} + +extension PairingError: LocalizedError { + public var errorDescription: String? { switch self { case .noTagInfo: - return "Could not get tag info" + return LocalizedString("Could not get tag info", comment: "error description for PairingError.noTagInfo") case .noSensorData: - return "Could not get sensor data" + return LocalizedString("Could not get sensor data", comment: "error description for PairingError.noSensorData") case .wrongSensorType: - return "Wrong sensor type detected" + return LocalizedString("Wrong sensor type detected", comment: "error description for PairingError.wrongSensorType") case .decryptionError: - return "Could not decrypt sensor contents" + return LocalizedString("Could not decrypt sensor contents", comment: "error description for PairingError.decryptionError") case .noPatchInfo: - return "Could not get patch info" + return LocalizedString("Could not get patch info", comment: "error description for PairingError.noPatchInfo") + case .nfcNotSupported: + return LocalizedString("Phone NFC not supported!", comment: "error description for PairingError.nfcNotSupported") } + } + public var recoverySuggestion: String? { + switch self { + case .nfcNotSupported: + return LocalizedString("Your phone or app is not enabled for NFC communications, which is needed to pair to libre2 sensors", comment: "Recovery suggestion for PairingError.nfcNotSupported") + default: + return nil + } } - } -class SensorPairingService: NSObject, NFCTagReaderSessionDelegate, SensorPairingProtocol { +public class SensorPairingService: NSObject, NFCTagReaderSessionDelegate, SensorPairingProtocol { private var session: NFCTagReaderSession? private var readingsSubject = PassthroughSubject() private var errorSubject = PassthroughSubject() @@ -46,7 +57,14 @@ class SensorPairingService: NSObject, NFCTagReaderSessionDelegate, SensorPairing private let unlockCode: UInt32 = 42 // 42 - @discardableResult func pairSensor() -> AnyPublisher { + public var onCancel: (() -> Void)? + + public func pairSensor() throws { + if !Features.phoneNFCAvailable { + throw PairingError.nfcNotSupported + } + print("Asked to pair sensor! phoneNFCAvailable: \(Features.phoneNFCAvailable)") + if NFCTagReaderSession.readingAvailable { accessQueue.async { self.session = NFCTagReaderSession(pollingOption: .iso15693, delegate: self, queue: self.nfcQueue) @@ -54,8 +72,6 @@ class SensorPairingService: NSObject, NFCTagReaderSessionDelegate, SensorPairing self.session?.begin() } } - - return readingsSubject.eraseToAnyPublisher() } public var publisher: AnyPublisher { @@ -78,17 +94,19 @@ class SensorPairingService: NSObject, NFCTagReaderSessionDelegate, SensorPairing } } - internal func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) { + public func tagReaderSessionDidBecomeActive(_ session: NFCTagReaderSession) { } - internal func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) { + public func tagReaderSession(_ session: NFCTagReaderSession, didInvalidateWithError error: Error) { if let error = error as? NFCReaderError, error.code != .readerSessionInvalidationErrorUserCanceled { session.invalidate(errorMessage: "Connection failure: \(error.localizedDescription)") self.sendError(error) } + + self.onCancel?() } - internal func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { + public func tagReaderSession(_ session: NFCTagReaderSession, didDetect tags: [NFCTag]) { guard let firstTag = tags.first else { return } guard case .iso15693(let tag) = firstTag else { return } @@ -107,7 +125,7 @@ class SensorPairingService: NSObject, NFCTagReaderSessionDelegate, SensorPairing tag.getSystemInfo(requestFlags: [.address, .highDataRate]) { result in switch result { case .failure: - session.invalidate(errorMessage: PairingError.noTagInfo.errorDescription) + session.invalidate(errorMessage: PairingError.noTagInfo.localizedDescription) self.sendError(PairingError.noTagInfo) return case .success: @@ -143,7 +161,7 @@ class SensorPairingService: NSObject, NFCTagReaderSessionDelegate, SensorPairing // patchInfo should have length 6, which sometimes is not the case, as there are occuring crashes in nfcCommand and Libre2BLEUtilities.streamingUnlockPayload guard patchInfo.count >= 6 else { - session.invalidate(errorMessage: PairingError.noPatchInfo.errorDescription) + session.invalidate(errorMessage: PairingError.noPatchInfo.localizedDescription) return } @@ -166,13 +184,13 @@ class SensorPairingService: NSObject, NFCTagReaderSessionDelegate, SensorPairing guard sensorUID.count == 8 && patchInfo.count == 6 && fram.count == 344 else { // self.readingsSubject.send(completion: .failure(LibreError.noSensorData)) - session.invalidate(errorMessage: PairingError.noSensorData.errorDescription) + session.invalidate(errorMessage: PairingError.noSensorData.localizedDescription) self.sendError(PairingError.noSensorData) return } guard sensorType == .libre2 else { - session.invalidate(errorMessage: PairingError.wrongSensorType.errorDescription) + session.invalidate(errorMessage: PairingError.wrongSensorType.localizedDescription) self.sendError(PairingError.noSensorData) return } @@ -184,20 +202,13 @@ class SensorPairingService: NSObject, NFCTagReaderSessionDelegate, SensorPairing self.sendUpdate(SensorPairingInfo(uuid: sensorUID, patchInfo: patchInfo, fram: Data(decryptedBytes), streamingEnabled: streamingEnabled)) session.invalidate() - - return } catch { print("problem decrypting") - session.invalidate(errorMessage: PairingError.decryptionError.errorDescription) + session.invalidate(errorMessage: PairingError.decryptionError.localizedDescription) self.sendError(PairingError.decryptionError) - } - - - } - } } } @@ -382,4 +393,3 @@ private enum Subcommand: UInt8, CustomStringConvertible { } } } -#endif diff --git a/LibreTransmitter.xcodeproj/project.pbxproj b/LibreTransmitter.xcodeproj/project.pbxproj index 71239c0..ca5d74d 100644 --- a/LibreTransmitter.xcodeproj/project.pbxproj +++ b/LibreTransmitter.xcodeproj/project.pbxproj @@ -3,11 +3,10 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 53; objects = { /* Begin PBXBuildFile section */ - 2709A6F329E5E0D3004355A3 /* FakeSensorPairingData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2709A6F229E5E0D3004355A3 /* FakeSensorPairingData.swift */; }; 270C1429266047490063405B /* NotificationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 270C1428266047490063405B /* NotificationSettingsView.swift */; }; 271A39F52975844B0005FEDA /* NotificationHelperOverride.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271A39F42975844B0005FEDA /* NotificationHelperOverride.swift */; }; 2735A6E029637DEA00D4E868 /* LibreTransmitter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 432B0E881CDFC3C50045347B /* LibreTransmitter.framework */; }; @@ -21,8 +20,6 @@ 2746C73C26D91FC900E31BD9 /* Libre2DirectTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2746C73B26D91FC900E31BD9 /* Libre2DirectTransmitter.swift */; }; 2746C73F26DCF83700E31BD9 /* Features.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2746C73D26DCF83400E31BD9 /* Features.swift */; }; 2746C74226DD0F8800E31BD9 /* Libre2DirectSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2746C74126DD0F8800E31BD9 /* Libre2DirectSetup.swift */; }; - 2746C74526DF636900E31BD9 /* SensorPairingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2746C74426DF636900E31BD9 /* SensorPairingService.swift */; }; - 2746C74726DF63C800E31BD9 /* SensorPairing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2746C74626DF63C800E31BD9 /* SensorPairing.swift */; }; 274E71D3297ED77300FCFECD /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274E71D2297ED77300FCFECD /* AuthView.swift */; }; 274E71D52986D4A600FCFECD /* CriticalAlarmsVolumeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274E71D42986D4A600FCFECD /* CriticalAlarmsVolumeView.swift */; }; 275786AB26753CC400845D0E /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 275786AA26753CC400845D0E /* SettingsView.swift */; }; @@ -69,7 +66,6 @@ 27850D6A25672DFB0020D109 /* icons8-down-arrow-50.png in Resources */ = {isa = PBXBuildFile; fileRef = 27850D4725672DFB0020D109 /* icons8-down-arrow-50.png */; }; 27850D6C25672DFB0020D109 /* SnoozeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850D4A25672DFB0020D109 /* SnoozeView.swift */; }; 27850D6D25672DFB0020D109 /* BluetoothSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850D4B25672DFB0020D109 /* BluetoothSelection.swift */; }; - 27850D8B25672ECA0020D109 /* BluetoothSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850BC8256725E70020D109 /* BluetoothSearch.swift */; }; 27850D9125672F020020D109 /* CBPeripheralExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850BC7256725E70020D109 /* CBPeripheralExtensions.swift */; }; 27850D9225672F020020D109 /* LibreTransmitterMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850BC6256725E70020D109 /* LibreTransmitterMetadata.swift */; }; 27850D9825672F130020D109 /* BubbleTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850BCC256725E80020D109 /* BubbleTransmitter.swift */; }; @@ -102,7 +98,6 @@ 27DD8F2A269106A100649010 /* ViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27DD8F29269106A100649010 /* ViewExtensions.swift */; }; 27ED67B82698EF38003E5DAB /* AlarmStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27ED67B72698EF38003E5DAB /* AlarmStatus.swift */; }; 27ED67BA26990D6B003E5DAB /* GenericObservableObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27ED67B926990D6B003E5DAB /* GenericObservableObject.swift */; }; - 27F1593026CAF77200EBA666 /* GenericThrottler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F1592E26CAF6BE00EBA666 /* GenericThrottler.swift */; }; 27F93B2A2816B7FF00EE39A7 /* LibreTransmitterManager+Libre2EU.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F93B292816B7FF00EE39A7 /* LibreTransmitterManager+Libre2EU.swift */; }; 27F93B2C2816B93900EE39A7 /* LibreTransmitterManager+Transmitters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F93B2B2816B93900EE39A7 /* LibreTransmitterManager+Transmitters.swift */; }; 27FED6CC26C275A8001BD5E4 /* ic_bubble_mini_3-2.png in Resources */ = {isa = PBXBuildFile; fileRef = 27FED6CB26C275A8001BD5E4 /* ic_bubble_mini_3-2.png */; }; @@ -120,6 +115,25 @@ B40BF26623ABD4E700A43CEE /* LibreTransmitterPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40BF26523ABD4E700A43CEE /* LibreTransmitterPlugin.swift */; }; B40BF26823ABD55200A43CEE /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40BF26723ABD55200A43CEE /* OSLog.swift */; }; B40BF26923ABD55200A43CEE /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40BF26723ABD55200A43CEE /* OSLog.swift */; }; + C1AF1D122A4BC18400F46A26 /* MockSensorData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1AF1D112A4BC18400F46A26 /* MockSensorData.swift */; }; + C1AF1D152A4BC23D00F46A26 /* TimeInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1AF1D142A4BC23D00F46A26 /* TimeInterval.swift */; }; + C1AF65982A49C902008D0690 /* SelectionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1AF65972A49C902008D0690 /* SelectionState.swift */; }; + C1BDBAED2A4397E200A787D1 /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40BF26723ABD55200A43CEE /* OSLog.swift */; }; + C1BDBAEE2A4397E200A787D1 /* LibreTransmitterPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = B40BF26523ABD4E700A43CEE /* LibreTransmitterPlugin.swift */; }; + C1BDBAF02A4397E200A787D1 /* LibreTransmitter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 432B0E881CDFC3C50045347B /* LibreTransmitter.framework */; }; + C1BDBAF52A4397E200A787D1 /* LibreTransmitter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 432B0E881CDFC3C50045347B /* LibreTransmitter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + C1BDBB002A45051B00A787D1 /* LibreDemoPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1BDBAFC2A43985000A787D1 /* LibreDemoPlugin.swift */; }; + C1BDBB022A45064F00A787D1 /* LibreDemoCGMManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1BDBAFD2A43992A00A787D1 /* LibreDemoCGMManager.swift */; }; + C1BDBB032A45158A00A787D1 /* SensorPairingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2746C74426DF636900E31BD9 /* SensorPairingService.swift */; }; + C1BDBB042A45159100A787D1 /* SensorPairing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2746C74626DF63C800E31BD9 /* SensorPairing.swift */; }; + C1C318CD2A47468D00C6F29F /* BluetoothSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27850BC8256725E70020D109 /* BluetoothSearch.swift */; }; + C1C318CE2A4746A800C6F29F /* GenericThrottler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F1592E26CAF6BE00EBA666 /* GenericThrottler.swift */; }; + C1C318D22A475C0E00C6F29F /* LibreTransmitterUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43A8EC82210E664300A81379 /* LibreTransmitterUI.framework */; }; + C1C318D32A475C0E00C6F29F /* LibreTransmitterUI.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 43A8EC82210E664300A81379 /* LibreTransmitterUI.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + C1C318D52A47638E00C6F29F /* FakeSensorPairingData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2709A6F229E5E0D3004355A3 /* FakeSensorPairingData.swift */; }; + C1C318D62A47638E00C6F29F /* MockBluetoothSearcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1C318CF2A4748A100C6F29F /* MockBluetoothSearcher.swift */; }; + C1C318D72A47638E00C6F29F /* MockSensorPairingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1BDBAFE2A44FFD800A787D1 /* MockSensorPairingService.swift */; }; + C1C318D92A478E0000C6F29F /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1C318D82A478E0000C6F29F /* HKUnit.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -144,6 +158,20 @@ remoteGlobalIDString = 432B0E871CDFC3C50045347B; remoteInfo = LibreTransmitter; }; + C1BDBAE92A4397E200A787D1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 432B0E7F1CDFC3C50045347B /* Project object */; + proxyType = 1; + remoteGlobalIDString = 432B0E871CDFC3C50045347B; + remoteInfo = LibreTransmitter; + }; + C1BDBAEB2A4397E200A787D1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 432B0E7F1CDFC3C50045347B /* Project object */; + proxyType = 1; + remoteGlobalIDString = 43A8EC81210E664300A81379; + remoteInfo = LibreTransmitterUI; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -159,6 +187,18 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + C1BDBAF42A4397E200A787D1 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + C1BDBAF52A4397E200A787D1 /* LibreTransmitter.framework in Embed Frameworks */, + C1C318D32A475C0E00C6F29F /* LibreTransmitterUI.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -293,6 +333,17 @@ B40BF26423ABD4E600A43CEE /* LibreTransmitterPlugin-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LibreTransmitterPlugin-Bridging-Header.h"; sourceTree = ""; }; B40BF26523ABD4E700A43CEE /* LibreTransmitterPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreTransmitterPlugin.swift; sourceTree = ""; }; B40BF26723ABD55200A43CEE /* OSLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; + C1AF1D112A4BC18400F46A26 /* MockSensorData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSensorData.swift; sourceTree = ""; }; + C1AF1D142A4BC23D00F46A26 /* TimeInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeInterval.swift; sourceTree = ""; }; + C1AF65972A49C902008D0690 /* SelectionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionState.swift; sourceTree = ""; }; + C1BDBAE22A4397AA00A787D1 /* LibreDemoPlugin.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LibreDemoPlugin.h; sourceTree = ""; }; + C1BDBAFA2A4397E200A787D1 /* LibreDemoPlugin.loopplugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = LibreDemoPlugin.loopplugin; sourceTree = BUILT_PRODUCTS_DIR; }; + C1BDBAFB2A4397E200A787D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = /Users/pete/dev/LoopWorkspace/LibreTransmitter/Info.plist; sourceTree = ""; }; + C1BDBAFC2A43985000A787D1 /* LibreDemoPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreDemoPlugin.swift; sourceTree = ""; }; + C1BDBAFD2A43992A00A787D1 /* LibreDemoCGMManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreDemoCGMManager.swift; sourceTree = ""; }; + C1BDBAFE2A44FFD800A787D1 /* MockSensorPairingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSensorPairingService.swift; sourceTree = ""; }; + C1C318CF2A4748A100C6F29F /* MockBluetoothSearcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockBluetoothSearcher.swift; sourceTree = ""; }; + C1C318D82A478E0000C6F29F /* HKUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -324,6 +375,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + C1BDBAEF2A4397E200A787D1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C1BDBAF02A4397E200A787D1 /* LibreTransmitter.framework in Frameworks */, + C1C318D22A475C0E00C6F29F /* LibreTransmitterUI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -343,7 +403,6 @@ children = ( 2746C74426DF636900E31BD9 /* SensorPairingService.swift */, 2746C74626DF63C800E31BD9 /* SensorPairing.swift */, - 2709A6F229E5E0D3004355A3 /* FakeSensorPairingData.swift */, ); path = SensorPairing; sourceTree = ""; @@ -382,6 +441,7 @@ isa = PBXGroup; children = ( 27850BA7256724F00020D109 /* SensorContents */, + 2746C74326DF635300E31BD9 /* SensorPairing */, 27850BA3256724EF0020D109 /* GlucoseAlgorithm */, 27850BA0256724EF0020D109 /* LibreError.swift */, 27850BA2256724EF0020D109 /* LimitedQueue.swift */, @@ -526,6 +586,7 @@ 27DD8F25268FA75A00649010 /* SensorInfo.swift */, 27DD8F27268FA79800649010 /* GlucoseInfo.swift */, 27ED67B72698EF38003E5DAB /* AlarmStatus.swift */, + C1AF65972A49C902008D0690 /* SelectionState.swift */, ); path = Observables; sourceTree = ""; @@ -542,6 +603,7 @@ 432B0E961CDFC3C50045347B /* LibreTransmitterTests */, 43A8EC83210E664300A81379 /* LibreTransmitterUI */, B40BF25F23ABD47400A43CEE /* LibreTransmitterPlugin */, + C1BDBAE12A4397AA00A787D1 /* LibreDemoPlugin */, 432B0E891CDFC3C50045347B /* Products */, 43A8EC7A210E661300A81379 /* Frameworks */, ); @@ -553,6 +615,7 @@ 432B0E881CDFC3C50045347B /* LibreTransmitter.framework */, 43A8EC82210E664300A81379 /* LibreTransmitterUI.framework */, B40BF25E23ABD47400A43CEE /* LibreTransmitterPlugin.loopplugin */, + C1BDBAFA2A4397E200A787D1 /* LibreDemoPlugin.loopplugin */, ); name = Products; sourceTree = ""; @@ -560,6 +623,7 @@ 432B0E8A1CDFC3C50045347B /* LibreTransmitter */ = { isa = PBXGroup; children = ( + C1C318D42A47637100C6F29F /* Mocks */, 27ED67B62698EEEF003E5DAB /* Observables */, 27850D0B25672CA60020D109 /* ConcreteGlucoseDisplayable.swift */, 27850D0D25672CA60020D109 /* NotificationHelper.swift */, @@ -599,7 +663,6 @@ 43A8EC83210E664300A81379 /* LibreTransmitterUI */ = { isa = PBXGroup; children = ( - 2746C74326DF635300E31BD9 /* SensorPairing */, 27850D2125672DFB0020D109 /* Controllers */, 27850D3E25672DFB0020D109 /* Graphics */, 27850D4825672DFB0020D109 /* Views */, @@ -632,6 +695,38 @@ path = LibreTransmitterPlugin; sourceTree = ""; }; + C1AF1D132A4BC22A00F46A26 /* Extensions */ = { + isa = PBXGroup; + children = ( + C1C318D82A478E0000C6F29F /* HKUnit.swift */, + C1AF1D142A4BC23D00F46A26 /* TimeInterval.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + C1BDBAE12A4397AA00A787D1 /* LibreDemoPlugin */ = { + isa = PBXGroup; + children = ( + C1AF1D132A4BC22A00F46A26 /* Extensions */, + C1BDBAFB2A4397E200A787D1 /* Info.plist */, + C1BDBAE22A4397AA00A787D1 /* LibreDemoPlugin.h */, + C1BDBAFC2A43985000A787D1 /* LibreDemoPlugin.swift */, + C1BDBAFD2A43992A00A787D1 /* LibreDemoCGMManager.swift */, + ); + path = LibreDemoPlugin; + sourceTree = ""; + }; + C1C318D42A47637100C6F29F /* Mocks */ = { + isa = PBXGroup; + children = ( + C1BDBAFE2A44FFD800A787D1 /* MockSensorPairingService.swift */, + 2709A6F229E5E0D3004355A3 /* FakeSensorPairingData.swift */, + C1C318CF2A4748A100C6F29F /* MockBluetoothSearcher.swift */, + C1AF1D112A4BC18400F46A26 /* MockSensorData.swift */, + ); + path = Mocks; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -712,14 +807,35 @@ productReference = B40BF25E23ABD47400A43CEE /* LibreTransmitterPlugin.loopplugin */; productType = "com.apple.product-type.bundle"; }; + C1BDBAE72A4397E200A787D1 /* LibreDemoPlugin */ = { + isa = PBXNativeTarget; + buildConfigurationList = C1BDBAF72A4397E200A787D1 /* Build configuration list for PBXNativeTarget "LibreDemoPlugin" */; + buildPhases = ( + C1BDBAEC2A4397E200A787D1 /* Sources */, + C1BDBAEF2A4397E200A787D1 /* Frameworks */, + C1BDBAF22A4397E200A787D1 /* Resources */, + C1BDBAF42A4397E200A787D1 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + C1BDBAE82A4397E200A787D1 /* PBXTargetDependency */, + C1BDBAEA2A4397E200A787D1 /* PBXTargetDependency */, + ); + name = LibreDemoPlugin; + productName = LibreTransmitterPlugin; + productReference = C1BDBAFA2A4397E200A787D1 /* LibreDemoPlugin.loopplugin */; + productType = "com.apple.product-type.bundle"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 432B0E7F1CDFC3C50045347B /* Project object */ = { isa = PBXProject; attributes = { + BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 0730; - LastUpgradeCheck = 1020; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = "LoopKit Authors"; TargetAttributes = { 432B0E871CDFC3C50045347B = { @@ -771,6 +887,7 @@ 432B0E871CDFC3C50045347B /* LibreTransmitter */, 43A8EC81210E664300A81379 /* LibreTransmitterUI */, B40BF25D23ABD47400A43CEE /* LibreTransmitterPlugin */, + C1BDBAE72A4397E200A787D1 /* LibreDemoPlugin */, ); }; /* End PBXProject section */ @@ -810,6 +927,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + C1BDBAF22A4397E200A787D1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -842,31 +966,40 @@ 27850D1025672CA60020D109 /* LibreGlucose.swift in Sources */, 27850D9E25672F170020D109 /* UUIDContainer.swift in Sources */, 27850DCE25672F780020D109 /* SensorData.swift in Sources */, + C1C318D72A47638E00C6F29F /* MockSensorPairingService.swift in Sources */, 2746C73C26D91FC900E31BD9 /* Libre2DirectTransmitter.swift in Sources */, 27F93B2C2816B93900EE39A7 /* LibreTransmitterManager+Transmitters.swift in Sources */, + C1AF65982A49C902008D0690 /* SelectionState.swift in Sources */, 27850CFB25672C0C0020D109 /* GlucoseSchedules.swift in Sources */, + C1C318CD2A47468D00C6F29F /* BluetoothSearch.swift in Sources */, 27850D9125672F020020D109 /* CBPeripheralExtensions.swift in Sources */, 27850DA625672F1E0020D109 /* MiaomiaoTransmitter.swift in Sources */, B40BF26823ABD55200A43CEE /* OSLog.swift in Sources */, 43A8EC9D210E68CE00A81379 /* LibreTransmitterManagerV3.swift in Sources */, 27850DB225672F3F0020D109 /* LimitedQueue.swift in Sources */, 274557C82979EA38003B027B /* LoggerExtension.swift in Sources */, + C1C318CE2A4746A800C6F29F /* GenericThrottler.swift in Sources */, 27850D9825672F130020D109 /* BubbleTransmitter.swift in Sources */, 27850DCD25672F780020D109 /* SensorState.swift in Sources */, 27B2150526E2C4EB000C322D /* Sensor.swift in Sources */, 27850D0E25672CA60020D109 /* ConcreteGlucoseDisplayable.swift in Sources */, 27850DC525672F640020D109 /* GlucoseFromRaw.swift in Sources */, 27850CEB25672C0C0020D109 /* DoubleExtensions.swift in Sources */, + C1BDBB042A45159100A787D1 /* SensorPairing.swift in Sources */, 27850CEF25672C0C0020D109 /* CollectionExtensions.swift in Sources */, 27850DB825672F4B0020D109 /* Calibration.swift in Sources */, 27850CED25672C0C0020D109 /* DateExtensions.swift in Sources */, 27850DBE25672F530020D109 /* LibreError.swift in Sources */, 27DD8F24268FA13F00649010 /* HashableClass.swift in Sources */, + C1C318D62A47638E00C6F29F /* MockBluetoothSearcher.swift in Sources */, 27850D9F25672F190020D109 /* LibreTransmitterProxyProtocol.swift in Sources */, 27850DCC25672F770020D109 /* Measurement.swift in Sources */, + C1BDBB032A45158A00A787D1 /* SensorPairingService.swift in Sources */, 27850DCB25672F770020D109 /* SensorSerialNumber.swift in Sources */, + C1C318D52A47638E00C6F29F /* FakeSensorPairingData.swift in Sources */, 27850DA525672F1C0020D109 /* LibreTransmitterProxyManager.swift in Sources */, 27850CF725672C0C0020D109 /* UserDefaults+Bluetooth.swift in Sources */, + C1AF1D122A4BC18400F46A26 /* MockSensorData.swift in Sources */, 271A39F52975844B0005FEDA /* NotificationHelperOverride.swift in Sources */, 27850CE725672C0C0020D109 /* DataExtensions.swift in Sources */, 27850D1225672CA60020D109 /* NotificationHelper.swift in Sources */, @@ -903,7 +1036,6 @@ 275EC9AB265EDC280043210E /* GlucoseSettingsView.swift in Sources */, 274E71D3297ED77300FCFECD /* AuthView.swift in Sources */, 27850CEA25672C0C0020D109 /* TimeIntervalExtensions.swift in Sources */, - 27850D8B25672ECA0020D109 /* BluetoothSearch.swift in Sources */, 27850DAC25672F3E0020D109 /* LimitedQueue.swift in Sources */, 275EC998265AF64E0043210E /* StatusMessage.swift in Sources */, 27850D6C25672DFB0020D109 /* SnoozeView.swift in Sources */, @@ -916,18 +1048,14 @@ 27850CF625672C0C0020D109 /* UserDefaults+Alarmsettings.swift in Sources */, 27850CEE25672C0C0020D109 /* DateExtensions.swift in Sources */, 27850CF425672C0C0020D109 /* UserDefaults+GlucoseSettings.swift in Sources */, - 2746C74726DF63C800E31BD9 /* SensorPairing.swift in Sources */, 27A5912726E0E32C00A7EE36 /* ModeSelectionView.swift in Sources */, 2746C73F26DCF83700E31BD9 /* Features.swift in Sources */, 27850CEC25672C0C0020D109 /* DoubleExtensions.swift in Sources */, - 27F1593026CAF77200EBA666 /* GenericThrottler.swift in Sources */, 277773AC2639EFB800431547 /* ErrorTextFieldStyle.swift in Sources */, 277773B62639F51300431547 /* CustomDataPickerView.swift in Sources */, 43A8EC91210E676500A81379 /* LibreTransmitterManager+UI.swift in Sources */, 27850CFA25672C0C0020D109 /* UIApplication+metadata.swift in Sources */, 27850CF025672C0C0020D109 /* CollectionExtensions.swift in Sources */, - 2746C74526DF636900E31BD9 /* SensorPairingService.swift in Sources */, - 2709A6F329E5E0D3004355A3 /* FakeSensorPairingData.swift in Sources */, 277773A72639EF2B00431547 /* BlueButtonStyle.swift in Sources */, 27850CFC25672C0C0020D109 /* GlucoseSchedules.swift in Sources */, 27DD8F2A269106A100649010 /* ViewExtensions.swift in Sources */, @@ -944,6 +1072,19 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + C1BDBAEC2A4397E200A787D1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C1C318D92A478E0000C6F29F /* HKUnit.swift in Sources */, + C1AF1D152A4BC23D00F46A26 /* TimeInterval.swift in Sources */, + C1BDBAED2A4397E200A787D1 /* OSLog.swift in Sources */, + C1BDBB002A45051B00A787D1 /* LibreDemoPlugin.swift in Sources */, + C1BDBAEE2A4397E200A787D1 /* LibreTransmitterPlugin.swift in Sources */, + C1BDBB022A45064F00A787D1 /* LibreDemoCGMManager.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -962,6 +1103,16 @@ target = 432B0E871CDFC3C50045347B /* LibreTransmitter */; targetProxy = A9E521F6225E949400EDDEF2 /* PBXContainerItemProxy */; }; + C1BDBAE82A4397E200A787D1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 432B0E871CDFC3C50045347B /* LibreTransmitter */; + targetProxy = C1BDBAE92A4397E200A787D1 /* PBXContainerItemProxy */; + }; + C1BDBAEA2A4397E200A787D1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 43A8EC81210E664300A81379 /* LibreTransmitterUI */; + targetProxy = C1BDBAEB2A4397E200A787D1 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -1040,6 +1191,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -1109,6 +1261,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -1138,7 +1291,8 @@ ); MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; @@ -1153,16 +1307,23 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CODE_SIGN_IDENTITY = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = LibreTransmitter/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.1; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11"; PRODUCT_BUNDLE_IDENTIFIER = com.mddub.LibreTransmitter; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -1175,16 +1336,23 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + CODE_SIGN_IDENTITY = ""; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = LibreTransmitter/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.1; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11"; PRODUCT_BUNDLE_IDENTIFIER = com.mddub.LibreTransmitter; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -1208,12 +1376,19 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = LibreTransmitterUI/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.1; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; PRODUCT_BUNDLE_IDENTIFIER = com.loopkit.LibreTransmitterUI; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -1239,12 +1414,19 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 2; DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_MODULE_VERIFIER = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; GCC_C_LANGUAGE_STANDARD = gnu11; INFOPLIST_FILE = LibreTransmitterUI/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.1; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++"; + MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu11 gnu++14"; PRODUCT_BUNDLE_IDENTIFIER = com.loopkit.LibreTransmitterUI; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -1268,7 +1450,11 @@ INFOPLIST_FILE = LibreTransmitterPlugin/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; IPHONEOS_DEPLOYMENT_TARGET = 15.1; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MACH_O_TYPE = mh_dylib; MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; @@ -1301,7 +1487,11 @@ INFOPLIST_FILE = LibreTransmitterPlugin/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; IPHONEOS_DEPLOYMENT_TARGET = 15.1; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); MACH_O_TYPE = mh_dylib; MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_FAST_MATH = YES; @@ -1316,6 +1506,80 @@ }; name = Release; }; + C1BDBAF82A4397E200A787D1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGNING_ALLOWED = NO; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = Info.plist; + INFOPLIST_KEY_NSPrincipalClass = LibreDemoPlugin; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = mh_dylib; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.loopkit.LibreDemoPlugin; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "LibreTransmitterPlugin/LibreTransmitterPlugin-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + WRAPPER_EXTENSION = loopplugin; + }; + name = Debug; + }; + C1BDBAF92A4397E200A787D1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGNING_ALLOWED = NO; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = Info.plist; + INFOPLIST_KEY_NSPrincipalClass = LibreDemoPlugin; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Bundles"; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = mh_dylib; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.loopkit.LibreDemoPlugin; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_OBJC_BRIDGING_HEADER = "LibreTransmitterPlugin/LibreTransmitterPlugin-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + WRAPPER_EXTENSION = loopplugin; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1355,6 +1619,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + C1BDBAF72A4397E200A787D1 /* Build configuration list for PBXNativeTarget "LibreDemoPlugin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + C1BDBAF82A4397E200A787D1 /* Debug */, + C1BDBAF92A4397E200A787D1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 432B0E7F1CDFC3C50045347B /* Project object */; diff --git a/LibreTransmitter.xcodeproj/xcshareddata/xcschemes/Shared-watchOS.xcscheme b/LibreTransmitter.xcodeproj/xcshareddata/xcschemes/Shared-watchOS.xcscheme index 442faa1..4d87245 100644 --- a/LibreTransmitter.xcodeproj/xcshareddata/xcschemes/Shared-watchOS.xcscheme +++ b/LibreTransmitter.xcodeproj/xcshareddata/xcschemes/Shared-watchOS.xcscheme @@ -1,6 +1,6 @@ TimeInterval { newGlucose.startDate.timeIntervalSince(oldGlucose.startDate) } @@ -50,64 +56,6 @@ extension LibreGlucose: GlucoseValue { } } -extension LibreGlucose { - public var description: String { - guard let glucoseUnit = UserDefaults.standard.mmGlucoseUnit, let formatter = LibreGlucose.dynamicFormatter, let formatted = formatter.string(from: self.quantity, for: glucoseUnit) else { - logger.debug("glucose unit was not recognized, aborting") - return "Unknown" - } - - return formatted - } - private static var glucoseFormatterMgdl: QuantityFormatter = { - let formatter = QuantityFormatter() - formatter.setPreferredNumberFormatter(for: HKUnit.milligramsPerDeciliter) - return formatter - }() - - private static var glucoseFormatterMmol: QuantityFormatter = { - let formatter = QuantityFormatter() - formatter.setPreferredNumberFormatter(for: HKUnit.millimolesPerLiter) - return formatter - }() - - public static var dynamicFormatter: QuantityFormatter? { - guard let glucoseUnit = UserDefaults.standard.mmGlucoseUnit else { - logger.debug("glucose unit was not recognized, aborting") - return nil - } - - return (glucoseUnit == HKUnit.milligramsPerDeciliter ? glucoseFormatterMgdl : glucoseFormatterMmol) - } - - public static func glucoseDiffDesc(oldValue: Self, newValue: Self) -> String { - guard let glucoseUnit = UserDefaults.standard.mmGlucoseUnit else { - logger.debug("glucose unit was not recognized, aborting") - return "Unknown" - } - - var stringValue = [String]() - - var diff = newValue.glucoseDouble - oldValue.glucoseDouble - let sign = diff < 0 ? "-" : "+" - - if diff == 0 { - stringValue.append( "\(sign) 0") - } else { - diff = abs(diff) - let asObj = LibreGlucose( - unsmoothedGlucose: diff, - glucoseDouble: diff, - timestamp: Date()) - if let formatted = dynamicFormatter?.string(from: asObj.quantity, for: glucoseUnit) { - stringValue.append( "\(sign) \(formatted)") - } - } - - return stringValue.joined(separator: ",") - } -} - extension LibreGlucose { static func calculateSlope(current: Self, last: Self) -> Double { if current.timestamp == last.timestamp { diff --git a/LibreTransmitter/LibreTransmitterManagerV3.swift b/LibreTransmitter/LibreTransmitterManagerV3.swift index 512a6f8..de72574 100644 --- a/LibreTransmitter/LibreTransmitterManagerV3.swift +++ b/LibreTransmitter/LibreTransmitterManagerV3.swift @@ -6,7 +6,7 @@ import Foundation import LoopKit -// import LoopKitUI +import LoopKitUI import UIKit import UserNotifications import Combine @@ -15,13 +15,15 @@ import CoreBluetooth import HealthKit import os.log -public final class LibreTransmitterManagerV3: CGMManager, LibreTransmitterDelegate { +open class LibreTransmitterManagerV3: CGMManager, LibreTransmitterDelegate { public typealias GlucoseArrayWithPrediction = (trends: [LibreGlucose], historical: [LibreGlucose], prediction: [LibreGlucose]) public lazy var logger = Logger(forType: Self.self) public let isOnboarded = true // No distinction between created and onboarded + private var alertsUnitPreference = DisplayGlucosePreference(displayGlucoseUnit: .milligramsPerDeciliter) + public var hasValidSensorSession: Bool { lastConnected != nil } @@ -107,9 +109,9 @@ public final class LibreTransmitterManagerV3: CGMManager, LibreTransmitterDelega "## LibreTransmitterManager", "Testdata: foo", "lastConnected: \(String(describing: lastConnected))", - "Connection state: \(self.proxy?.connectionStateString)", - "Sensor state: \(proxy?.sensorData?.state.description)", - "transmitterbattery: \(proxy?.metadata?.batteryString)", + "Connection state: \(String(describing: self.proxy?.connectionStateString))", + "Sensor state: \(String(describing: proxy?.sensorData?.state.description))", + "transmitterbattery: \(String(describing: proxy?.metadata?.batteryString))", "SensorData: \(getPersistedSensorDataForDebug())", "providesBLEHeartbeat: \(providesBLEHeartbeat)", "Metainfo::\n\(AppMetaData.allProperties)", @@ -117,21 +119,19 @@ public final class LibreTransmitterManagerV3: CGMManager, LibreTransmitterDelega ].joined(separator: "\n") } - // public var miaomiaoService: MiaomiaoService - public func fetchNewDataIfNeeded(_ completion: @escaping (CGMReadingResult) -> Void) { logger.debug("fetchNewDataIfNeeded called but we don't continue") completion(.noData) } - internal var lastConnected: Date? + public var lastConnected: Date? public internal(set) var alarmStatus = AlarmStatus() internal var latestPrediction: LibreGlucose? - internal var latestBackfill: LibreGlucose? { + public var latestBackfill: LibreGlucose? { willSet(newValue) { guard let newValue else { return @@ -142,10 +142,11 @@ public final class LibreTransmitterManagerV3: CGMManager, LibreTransmitterDelega defer { logger.debug("sending glucose notification") - NotificationHelper.sendGlucoseNotitifcationIfNeeded(glucose: newValue, - oldValue: oldValue, - trend: trend, - battery: proxy?.metadata?.batteryString ?? "n/a") + NotificationHelper.sendGlucoseNotificationIfNeeded(glucose: newValue, + oldValue: oldValue, + trend: trend, + battery: proxy?.metadata?.batteryString ?? "n/a", + glucoseFormatter: alertsUnitPreference.formatter) // once we have a new glucose value, we can update the isalarming property if let activeAlarms = UserDefaults.standard.glucoseSchedules?.getActiveAlarms(newValue.glucoseDouble) { @@ -162,7 +163,7 @@ public final class LibreTransmitterManagerV3: CGMManager, LibreTransmitterDelega } - logger.debug("latestBackfill set, newvalue is \(newValue.description)") + logger.debug("latestBackfill set, newvalue is \(newValue.glucose)") if let oldValue { // the idea here is to use the diff between the old and the new glucose to calculate slope and direction, rather than using trend from the glucose value. @@ -182,7 +183,9 @@ public final class LibreTransmitterManagerV3: CGMManager, LibreTransmitterDelega } - public var managerIdentifier = "LibreTransmitterManagerV3" + open var managerIdentifier: String { + "LibreTransmitterManagerV3" + } public required convenience init?(rawState: CGMManager.RawStateValue) { @@ -195,7 +198,7 @@ public final class LibreTransmitterManagerV3: CGMManager, LibreTransmitterDelega [:] } - public let localizedTitle = LocalizedString("Libre Bluetooth", comment: "Title for the CGMManager option") + open var localizedTitle: String { "FreeStyle Libre" } public let appURL: URL? = nil // URL(string: "spikeapp://") @@ -204,13 +207,19 @@ public final class LibreTransmitterManagerV3: CGMManager, LibreTransmitterDelega UserDefaults.standard.mmSyncToNs } - public init() { + public required init() { lastConnected = nil logger.debug("LibreTransmitterManager will be created now") NotificationHelper.requestNotificationPermissionsIfNeeded() - - proxy?.delegate = self + + if isDeviceSelected { + establishProxy() + } + } + + var isDeviceSelected: Bool { + return UserDefaults.standard.preSelectedDevice != nil || UserDefaults.standard.preSelectedUid != nil || SelectionState.shared.selectedUID != nil } public func resetManager() { @@ -232,12 +241,11 @@ public final class LibreTransmitterManagerV3: CGMManager, LibreTransmitterDelega lastDirectUpdate = nil } - public func reEstablishProxy() { - logger.debug("LibreTransmitterManager re-establish called") + open func establishProxy() { + logger.debug("LibreTransmitterManager establishProxy called") proxy = LibreTransmitterProxyManager() proxy?.delegate = self - } deinit { @@ -246,8 +254,7 @@ public final class LibreTransmitterManagerV3: CGMManager, LibreTransmitterDelega disconnect() } - // lazy because we don't want to scan immediately - public lazy var proxy: LibreTransmitterProxyManager? = LibreTransmitterProxyManager() + public var proxy: LibreTransmitterProxyManager? /* These properties are mostly useful for swiftui @@ -256,14 +263,6 @@ public final class LibreTransmitterManagerV3: CGMManager, LibreTransmitterDelega public var sensorInfoObservable = SensorInfo() public var glucoseInfoObservable = GlucoseInfo() - var longDateFormatter: DateFormatter = ({ - let df = DateFormatter() - df.dateStyle = .long - df.timeStyle = .long - df.doesRelativeDateFormatting = true - return df - })() - var dateFormatter: DateFormatter = ({ let df = DateFormatter() df.dateStyle = .long @@ -276,8 +275,14 @@ public final class LibreTransmitterManagerV3: CGMManager, LibreTransmitterDelega var lastDirectUpdate: Date? internal var countTimesWithoutData: Int = 0 - + open var pairingService: SensorPairingProtocol { + return SensorPairingService() + } + + open var bluetoothSearcher: BluetoothSearcher { + return BluetoothSearchManager() + } } // MARK: - Convenience functions @@ -298,16 +303,16 @@ extension LibreTransmitterManagerV3 { if let predicted = allGlucoses.predictBloodSugar(glucosePredictionMinutes) { let currentBg = predicted.calibratedGlucose(calibrationInfo: calibration) let bgDate = predicted.date.addingTimeInterval(60 * -glucosePredictionMinutes) - return LibreGlucose(unsmoothedGlucose: currentBg, glucoseDouble: currentBg, timestamp: bgDate) logger.debug("Predicted glucose (not used) was: \(currentBg)") + return LibreGlucose(unsmoothedGlucose: currentBg, glucoseDouble: currentBg, timestamp: bgDate) } else { - return nil logger.debug("Tried to predict glucose value but failed!") + return nil } } - func setObservables(sensorData: SensorData?, bleData: Libre2.LibreBLEResponse?, metaData: LibreTransmitterMetadata?) { + public func setObservables(sensorData: SensorDataProtocol?, bleData: Libre2.LibreBLEResponse?, metaData: LibreTransmitterMetadata?) { logger.debug("setObservables called") DispatchQueue.main.async { @@ -418,45 +423,21 @@ extension LibreTransmitterManagerV3 { } - let formatter = QuantityFormatter() - let preferredUnit = UserDefaults.standard.mmGlucoseUnit ?? .millimolesPerLiter - if let d = self.latestBackfill { self.logger.debug("will set glucoseInfoObservable") - - formatter.setPreferredNumberFormatter(for: .millimolesPerLiter) - self.glucoseInfoObservable.glucoseMMOL = formatter.string(from: d.quantity, for: .millimolesPerLiter) ?? "-" - - formatter.setPreferredNumberFormatter(for: .milligramsPerDeciliter) - self.glucoseInfoObservable.glucoseMGDL = formatter.string(from: d.quantity, for: .milligramsPerDeciliter) ?? "-" - - // backward compat - if preferredUnit == .millimolesPerLiter { - self.glucoseInfoObservable.glucose = self.glucoseInfoObservable.glucoseMMOL - } else if preferredUnit == .milligramsPerDeciliter { - self.glucoseInfoObservable.glucose = self.glucoseInfoObservable.glucoseMGDL - } - - self.glucoseInfoObservable.date = self.longDateFormatter.string(from: d.timestamp) + self.glucoseInfoObservable.glucose = d.quantity + self.glucoseInfoObservable.date = d.timestamp } if let d = self.latestPrediction { - formatter.setPreferredNumberFormatter(for: .millimolesPerLiter) - self.glucoseInfoObservable.predictionMMOL = formatter.string(from: d.quantity, for: .millimolesPerLiter) ?? "-" - - formatter.setPreferredNumberFormatter(for: .milligramsPerDeciliter) - self.glucoseInfoObservable.predictionMGDL = formatter.string(from: d.quantity, for: .milligramsPerDeciliter) ?? "-" - self.glucoseInfoObservable.predictionDate = self.longDateFormatter.string(from: d.timestamp) + self.glucoseInfoObservable.prediction = d.quantity + self.glucoseInfoObservable.predictionDate = d.timestamp } else { - self.glucoseInfoObservable.predictionMMOL = "" - self.glucoseInfoObservable.predictionMGDL = "" - self.glucoseInfoObservable.predictionDate = "" - + self.glucoseInfoObservable.prediction = nil + self.glucoseInfoObservable.predictionDate = nil } - } - } func getStartDateForFilter() -> Date? { @@ -492,22 +473,20 @@ extension LibreTransmitterManagerV3 { } logger.debug("tried creating trendarrow using \(glucoses.count) elements for trend calc") - return - glucoses - .filterDateRange(startDate, nil) - .compactMap { - return NewGlucoseSample( - date: $0.startDate, - quantity: $0.quantity, - condition: nil, - trend: trend, - trendRate: nil, - isDisplayOnly: false, - wasUserEntered: false, - syncIdentifier: $0.syncId, - device: self.proxy?.device) - } - + return glucoses + .filterDateRange(startDate, nil) + .compactMap { + return NewGlucoseSample( + date: $0.startDate, + quantity: $0.quantity, + condition: nil, + trend: trend, + trendRate: nil, + isDisplayOnly: false, + wasUserEntered: false, + syncIdentifier: $0.syncId, + device: self.proxy?.device) + } } public var calibrationData: SensorData.CalibrationInfo? { @@ -518,3 +497,10 @@ extension LibreTransmitterManagerV3 { proxy?.activePluginType?.smallImage ?? UIImage(named: "libresensor", in: Bundle.current, compatibleWith: nil)! } } + + +extension LibreTransmitterManagerV3: DisplayGlucoseUnitObserver { + public func unitDidChange(to displayGlucoseUnit: HKUnit) { + self.alertsUnitPreference.unitDidChange(to: displayGlucoseUnit) + } +} diff --git a/LibreTransmitterUI/SensorPairing/FakeSensorPairingData.swift b/LibreTransmitter/Mocks/FakeSensorPairingData.swift similarity index 94% rename from LibreTransmitterUI/SensorPairing/FakeSensorPairingData.swift rename to LibreTransmitter/Mocks/FakeSensorPairingData.swift index b5f000d..14b1e58 100644 --- a/LibreTransmitterUI/SensorPairing/FakeSensorPairingData.swift +++ b/LibreTransmitter/Mocks/FakeSensorPairingData.swift @@ -8,17 +8,19 @@ import Foundation - // https://github.com/NightscoutFoundation/xDrip/blob/579d365a94cb1fa2ad28b692efb05036928a5dd3/wear/src/main/java/com/eveningoutpost/dexdrip/NFCReaderX.java#L95 -struct FakeSensorPairingData { +public struct FakeSensorPairingData { let de_new_packet: [UInt8] = [0x87, 0x88, 0xd2, 0xe8, 0xdd, 0x28, 0x9b, 0x95, 0xb5, 0x9d, 0xe1, 0x1f, 0x47, 0x2c, 0x61, 0x4f, 0xcb, 0x81, 0x5e, 0xc8, 0x36, 0x4a, 0x4c, 0x1f, 0xa4, 0xc8, 0x59, 0x81, 0x72, 0xbf, 0x9e, 0xae, 0xa4, 0x1b, 0x51, 0x5b, 0xb9, 0x9f, 0x6a, 0x6b, 0xf4, 0x55, 0x78, 0xe1, 0xa3, 0x4f, 0x3a, 0x60, 0x49, 0x8f, 0x1f, 0xcb, 0xdf, 0x2e, 0xdc, 0xbe, 0x59, 0xc9, 0x28, 0x8e, 0xf4, 0x83, 0x16, 0x11, 0xa, 0xd2, 0x74, 0x6f, 0x9d, 0xbf, 0x29, 0x44, 0x37, 0x8d, 0xe9, 0xf7, 0x47, 0x1, 0x2b, 0x3, 0x5e, 0x9b, 0x72, 0x25, 0x1f, 0x82, 0x11, 0xb5, 0xdb, 0x19, 0x42, 0x9c, 0xfe, 0x91, 0x63, 0x94, 0xf7, 0x14, 0x67, 0xb6, 0x25, 0xf3, 0xf9, 0xee, 0x30, 0x54, 0xa4, 0x89, 0x2b, 0xa8, 0xe4, 0x6f, 0x7a, 0x4f, 0xa3, 0xdc, 0xc0, 0x43, 0xfc, 0x38, 0x1c, 0x32, 0x76, 0x1b, 0x17, 0xb6, 0x81, 0x87, 0xf8, 0xd3, 0x97, 0xca, 0xd5, 0x67, 0xf6, 0x4a, 0xbd, 0x6f, 0x2b, 0x90, 0xd6, 0xd2, 0x4e, 0x96, 0x87, 0x98, 0xcf, 0xf6, 0x82, 0xb3, 0x7b, 0xf1, 0xd4, 0xf2, 0x3b, 0xb3, 0xc3, 0x76, 0x33, 0xe5, 0xa3, 0xe9, 0x27, 0xde, 0x6a, 0x21, 0xc2, 0xb2, 0xfc, 0x2, 0x87, 0xb1, 0x55, 0x7c, 0xc9, 0xe0, 0x5b, 0x9f, 0x63, 0x61, 0x67, 0x18, 0x3d, 0xe9, 0x92, 0x1f, 0xed, 0xad, 0x41, 0xee, 0x8d, 0xd7, 0x5e, 0x3d, 0x4b, 0xa4, 0x20, 0xfa, 0x6c, 0xc, 0xf7, 0x68, 0xe5, 0xfb, 0x90, 0xc6, 0x54, 0x49, 0x4d, 0xfe, 0x1e, 0xa3, 0x25, 0x2b, 0xa5, 0x6f, 0xf9, 0xc0, 0xce, 0x18, 0x67, 0x6e, 0x33, 0xc1, 0x43, 0x53, 0x35, 0x44, 0x52, 0x91, 0xd2, 0x8, 0x5a, 0x9d, 0x18, 0xea, 0x2d, 0xcb, 0x11, 0x2b, 0xe0, 0xb, 0xe3, 0x84, 0x18, 0x54, 0xc0, 0xc1, 0x74, 0xfb, 0x53, 0x4d, 0x3a, 0x29, 0x56, 0x6d, 0xce, 0x7e, 0x28, 0x4, 0xf, 0xd4, 0xb7, 0xaa, 0x19, 0x4f, 0x5f, 0x60, 0x5a, 0x59, 0x9, 0x89, 0xa3, 0xed, 0x24, 0xcc, 0x6f, 0x88, 0xf8, 0x53, 0xd7, 0xe3, 0x74, 0x7, 0x6d, 0xe1, 0x6e, 0xe9, 0xed, 0x64, 0xf, 0x46, 0x58, 0xe, 0x8f, 0x30, 0x6b, 0xdb, 0xd6, 0xbd, 0x56, 0xe0, 0x89, 0x87, 0x51, 0x4e, 0xad, 0xe3, 0x63, 0xf, 0x18, 0x41, 0x45, 0x52, 0xdd, 0x3e, 0x21, 0xe, 0x74, 0x6b, 0xd9, 0xcf, 0x4f, 0xa3, 0x94, 0x62, 0xff, 0x6a, 0x52, 0xbe, 0x15, 0x37, 0xbb, 0xad, 0xd4, 0x63, 0x28, 0x23, 0x26, 0x60, 0x90, 0xe7, 0xcd, 0xf6] var de_new_patch_uid : [UInt8] = [0xd6, 0xf1, 0x0f, 0x01, 0x00, 0xa4, 0x07, 0xe0] let de_new_patch_info : [UInt8] = [0x9d, 0x08, 0x30, 0x01, 0x9c, 0x16]; let sensorName = "3MH000GUR5W" + + public init() { + } - func fakeSensorPairingInfo() -> SensorPairingInfo{ - SensorPairingInfo(uuid: Data(de_new_patch_uid), patchInfo: Data(de_new_patch_info), fram: Data(de_new_packet), streamingEnabled: true, initialIdentificationStrategy: .byFakeSensorName, sensorName: sensorName) + public func fakeSensorPairingInfo() -> SensorPairingInfo { + SensorPairingInfo(uuid: Data(de_new_patch_uid), patchInfo: Data(de_new_patch_info), fram: Data(de_new_packet), streamingEnabled: true, sensorName: sensorName) } } diff --git a/LibreTransmitter/Mocks/MockBluetoothSearcher.swift b/LibreTransmitter/Mocks/MockBluetoothSearcher.swift new file mode 100644 index 0000000..1eea875 --- /dev/null +++ b/LibreTransmitter/Mocks/MockBluetoothSearcher.swift @@ -0,0 +1,64 @@ +// +// MockBluetoothSearcher.swift +// LibreDemoPlugin +// +// Created by Pete Schwamb on 6/24/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import Foundation +import os.log +import Combine +import CoreBluetooth + +public struct MockBluetoothSearcher: BluetoothSearcher { + fileprivate lazy var logger = Logger(forType: Self.self) + + public let throttledRSSI = GenericThrottler(identificator: \RSSIInfo.bledeviceID, interval: 5) + public let passThroughMetaData = PassthroughSubject<(PeripheralProtocol, [String: Any]), Never>() + + public init() { + } + + public func disconnectManually() { + print("Mock searcher disconnecting") + } + + public func scanForCompatibleDevices() { + print("Mock searcher scanning") + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + for device in mockData { + passThroughMetaData.send((device, [:])) + } + } + } + + public func stopTimer() { + print("Mock searcher stop timer") + } + + var mockData: [PeripheralProtocol] { + [ + MockedPeripheral(name: "miaomiaoMockTransmitter"), + MockedPeripheral(name: "bubbleMockTransmitter"), + MockedPeripheral(name: "abbottMockSensor"), + ] + } +} + +public class MockedPeripheral: PeripheralProtocol, Identifiable { + public var name: String? + + public var name2: String { + name ?? "unknown-device" + } + + public var asStringIdentifier: String { + name2 + } + + public init(name: String) { + self.name = name + } +} + diff --git a/LibreTransmitter/Mocks/MockSensorData.swift b/LibreTransmitter/Mocks/MockSensorData.swift new file mode 100644 index 0000000..b907c8a --- /dev/null +++ b/LibreTransmitter/Mocks/MockSensorData.swift @@ -0,0 +1,32 @@ +// +// MockSensorData.swift +// LibreTransmitter +// +// Created by Pete Schwamb on 6/27/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import Foundation + +public struct MockSensorData: SensorDataProtocol { + public var minutesSinceStart: Int + + public var maxMinutesWearTime: Int + + public var state: SensorState + + public var serialNumber: String + + public var footerCrc: UInt16 + + public var date: Date + + public init(minutesSinceStart: Int, maxMinutesWearTime: Int, state: SensorState, serialNumber: String, footerCrc: UInt16, date: Date) { + self.minutesSinceStart = minutesSinceStart + self.maxMinutesWearTime = maxMinutesWearTime + self.state = state + self.serialNumber = serialNumber + self.footerCrc = footerCrc + self.date = date + } +} diff --git a/LibreTransmitter/Mocks/MockSensorPairingService.swift b/LibreTransmitter/Mocks/MockSensorPairingService.swift new file mode 100644 index 0000000..04178b1 --- /dev/null +++ b/LibreTransmitter/Mocks/MockSensorPairingService.swift @@ -0,0 +1,41 @@ +// +// MockSensorPairingService.swift +// LibreDemoPlugin +// +// Created by Pete Schwamb on 6/22/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import Foundation +import Combine +import os.log + +public class MockSensorPairingService: SensorPairingProtocol { + fileprivate lazy var logger = Logger(forType: Self.self) + + private var readingsSubject = PassthroughSubject() + + public var onCancel: (() -> Void)? + + public var publisher: AnyPublisher { + readingsSubject.eraseToAnyPublisher() + } + + public init() { + } + + private func sendUpdate(_ info: SensorPairingInfo) { + DispatchQueue.main.async { [weak self] in + self?.readingsSubject.send(info) + } + } + + public func pairSensor() throws { + let info = FakeSensorPairingData().fakeSensorPairingInfo() + logger.debug("Sending fake sensor pairinginfo: \(info.description)") + //delay a bit to simulate a real tag readout + DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { + self.sendUpdate(info) + } + } +} diff --git a/LibreTransmitter/NotificationHelper.swift b/LibreTransmitter/NotificationHelper.swift index 646ebc9..71389e5 100644 --- a/LibreTransmitter/NotificationHelper.swift +++ b/LibreTransmitter/NotificationHelper.swift @@ -142,18 +142,11 @@ public enum NotificationHelper { } } - static func ensureCanSendGlucoseNotification(_ completion: @escaping (_ unit: HKUnit) -> Void ) { - ensureCanSendNotification { - if let glucoseUnit = UserDefaults.standard.mmGlucoseUnit, GlucoseUnitIsSupported(unit: glucoseUnit) { - completion(glucoseUnit) - } - } - } } // MARK: Sensor related notification sendouts public extension NotificationHelper { - static func sendLibre2FirectFinishedSetupNotifcation() { + static func sendLibre2DirectFinishedSetupNotifcation() { ensureCanSendNotification { let content = UNMutableNotificationContent() content.title = "Libre 2 Direct Setup Complete" @@ -370,7 +363,7 @@ public extension NotificationHelper { private static var glucoseNotifyCalledCount = 0 - static func sendGlucoseNotitifcationIfNeeded(glucose: LibreGlucose, oldValue: LibreGlucose?, trend: GlucoseTrend?, battery: String?) { + static func sendGlucoseNotificationIfNeeded(glucose: LibreGlucose, oldValue: LibreGlucose?, trend: GlucoseTrend?, battery: String?, glucoseFormatter: QuantityFormatter) { glucoseNotifyCalledCount &+= 1 let shouldSendGlucoseAlternatingTimes = glucoseNotifyCalledCount != 0 && UserDefaults.standard.mmNotifyEveryXTimes != 0 @@ -391,84 +384,88 @@ public extension NotificationHelper { // even if glucose notifications are disabled in the UI if shouldSend || alarm.isAlarming() { - sendGlucoseNotitifcation(glucose: glucose, oldValue: oldValue, - alarm: alarm, isSnoozed: isSnoozed, - trend: trend, showPhoneBattery: shouldShowPhoneBattery, - transmitterBattery: transmitterBattery) + sendGlucoseNotification(glucose: glucose, oldValue: oldValue, + glucoseFormatter: glucoseFormatter, + alarm: alarm, isSnoozed: isSnoozed, + trend: trend, showPhoneBattery: shouldShowPhoneBattery, + transmitterBattery: transmitterBattery) } else { logger.debug("\(#function) not sending glucose, shouldSend and alarmIsActive was false") return } } - private static func sendGlucoseNotitifcation(glucose: LibreGlucose, oldValue: LibreGlucose?, - alarm: GlucoseScheduleAlarmResult = .none, - isSnoozed: Bool = false, - trend: GlucoseTrend?, - showPhoneBattery: Bool = false, - transmitterBattery: String?) { - ensureCanSendGlucoseNotification { _ in - let content = UNMutableNotificationContent() - let glucoseDesc = glucose.description - var titles = [String]() - var body = [String]() - var body2 = [String]() - - var isCritical = false - switch alarm { - case .none: - titles.append("Glucose") - case .low: - titles.append("LOWALERT!") - isCritical = true - case .high: - titles.append("HIGHALERT!") - isCritical = true - } - - if isSnoozed { - titles.append("(Snoozed)") - } else if alarm.isAlarming() { - content.sound = .default - vibrateIfNeeded() - } - titles.append(glucoseDesc) - - body.append("Glucose: \(glucoseDesc)") + private static func sendGlucoseNotification(glucose: LibreGlucose, oldValue: LibreGlucose?, + glucoseFormatter: QuantityFormatter, + alarm: GlucoseScheduleAlarmResult = .none, + isSnoozed: Bool = false, + trend: GlucoseTrend?, + showPhoneBattery: Bool = false, + transmitterBattery: String?) { + let content = UNMutableNotificationContent() + let glucoseDesc = glucoseFormatter.string(from: glucose.quantity)! + var titles = [String]() + var body = [String]() + var body2 = [String]() + + var isCritical = false + switch alarm { + case .none: + titles.append("Glucose") + case .low: + titles.append("LOWALERT!") + isCritical = true + case .high: + titles.append("HIGHALERT!") + isCritical = true + } + + if isSnoozed { + titles.append("(Snoozed)") + } else if alarm.isAlarming() { + content.sound = .default + vibrateIfNeeded() + } + titles.append(glucoseDesc) - if let oldValue { - body.append( LibreGlucose.glucoseDiffDesc(oldValue: oldValue, newValue: glucose)) - } + body.append("Glucose: \(glucoseDesc)") - if let trend = trend?.localizedDescription { - body.append("\(trend)") + if let oldValue { + let diff = glucose.glucoseDouble - oldValue.glucoseDouble + if diff >= 0 { + body.append("+") } + body.append( glucoseFormatter.string(from: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: diff))!) + } - if showPhoneBattery { - if !UIDevice.current.isBatteryMonitoringEnabled { - UIDevice.current.isBatteryMonitoringEnabled = true - } + if let trend = trend?.localizedDescription { + body.append("\(trend)") + } - let battery = Double(UIDevice.current.batteryLevel * 100 ).roundTo(places: 1) - body2.append("Phone: \(battery)%") + if showPhoneBattery { + if !UIDevice.current.isBatteryMonitoringEnabled { + UIDevice.current.isBatteryMonitoringEnabled = true } - if let transmitterBattery { - body2.append("Transmitter: \(transmitterBattery)") - } + let battery = Double(UIDevice.current.batteryLevel * 100 ).roundTo(places: 1) + body2.append("Phone: \(battery)%") + } - // these are texts that naturally fit on their own line in the body - var body2s = "" - if !body2.isEmpty { - body2s = "\n" + body2.joined(separator: "\n") - } + if let transmitterBattery { + body2.append("Transmitter: \(transmitterBattery)") + } - content.title = titles.joined(separator: " ") - content.body = body.joined(separator: ", ") + body2s - addRequest(identifier: .glucocoseNotifications, - content: content, - deleteOld: true, isCritical: isCritical && !isSnoozed) + // these are texts that naturally fit on their own line in the body + var body2s = "" + if !body2.isEmpty { + body2s = "\n" + body2.joined(separator: "\n") } + + content.title = titles.joined(separator: " ") + content.body = body.joined(separator: ", ") + body2s + addRequest(identifier: .glucocoseNotifications, + content: content, + deleteOld: true, isCritical: isCritical && !isSnoozed) } private static var lastBatteryWarning: Date? diff --git a/LibreTransmitter/Observables/GlucoseInfo.swift b/LibreTransmitter/Observables/GlucoseInfo.swift index 983f95d..2b51dd1 100644 --- a/LibreTransmitter/Observables/GlucoseInfo.swift +++ b/LibreTransmitter/Observables/GlucoseInfo.swift @@ -7,20 +7,17 @@ // import Foundation +import HealthKit public class GlucoseInfo: ObservableObject, Equatable, Hashable { - @Published public var glucose = "" // dynamic based users preference - @Published public var glucoseMMOL = "" - @Published public var glucoseMGDL = "" - @Published public var date = "" + @Published public var glucose: HKQuantity? + @Published public var date: Date? @Published public var checksum = "" // @Published var entryErrors = "" - @Published public var prediction = "" - @Published public var predictionMMOL = "" - @Published public var predictionMGDL = "" - @Published public var predictionDate = "" + @Published public var prediction: HKQuantity? + @Published public var predictionDate: Date? public static func ==(lhs: GlucoseInfo, rhs: GlucoseInfo) -> Bool { lhs.glucose == rhs.glucose && lhs.date == rhs.date && diff --git a/LibreTransmitter/Observables/SelectionState.swift b/LibreTransmitter/Observables/SelectionState.swift new file mode 100644 index 0000000..e36713f --- /dev/null +++ b/LibreTransmitter/Observables/SelectionState.swift @@ -0,0 +1,19 @@ +// +// SelectionState.swift +// LibreTransmitter +// +// Created by Pete Schwamb on 6/26/23. +// Copyright © 2023 LoopKit Authors. All rights reserved. +// + +import Foundation +import SwiftUI + +// Decided to use shared instance instead of .environmentObject() +public class SelectionState: ObservableObject { + @Published public var selectedStringIdentifier: String? = "" + + @Published public var selectedUID: Data? + + public static var shared = SelectionState() +} diff --git a/LibreTransmitterPlugin/Info.plist b/LibreTransmitterPlugin/Info.plist index d0964c4..9f17846 100644 --- a/LibreTransmitterPlugin/Info.plist +++ b/LibreTransmitterPlugin/Info.plist @@ -23,7 +23,7 @@ NSPrincipalClass LibreTransmitterPlugin com.loopkit.Loop.CGMManagerDisplayName - Libre ThirdParty Transmitter + FreeStyle Libre com.loopkit.Loop.CGMManagerIdentifier LibreTransmitterManagerV3 diff --git a/LibreTransmitterUI/Controllers/LibreTransmitterSetupViewController.swift b/LibreTransmitterUI/Controllers/LibreTransmitterSetupViewController.swift index b1085c6..19c79bb 100644 --- a/LibreTransmitterUI/Controllers/LibreTransmitterSetupViewController.swift +++ b/LibreTransmitterUI/Controllers/LibreTransmitterSetupViewController.swift @@ -19,19 +19,20 @@ class LibreTransmitterSetupViewController: UINavigationController, CGMManagerOnb fileprivate lazy var logger = Logger(forType: Self.self) - lazy var cgmManager: LibreTransmitterManagerV3? = LibreTransmitterManagerV3() + var cgmManager: LibreTransmitterManagerV3 - var modeSelection: UIHostingController! - - init() { + init(displayGlucosePreference: DisplayGlucosePreference, cgmManager: LibreTransmitterManagerV3) { SelectionState.shared.selectedStringIdentifier = UserDefaults.standard.preSelectedDevice + self.cgmManager = cgmManager + let cancelNotifier = GenericObservableObject() let saveNotifier = GenericObservableObject() - modeSelection = UIHostingController(rootView: ModeSelectionView(cancelNotifier: cancelNotifier, saveNotifier: saveNotifier)) + let myView = ModeSelectionView(cancelNotifier: cancelNotifier, saveNotifier: saveNotifier, pairingService: cgmManager.pairingService, bluetoothSearcher: cgmManager.bluetoothSearcher) + .environmentObject(displayGlucosePreference) - super.init(rootViewController: modeSelection) + super.init(rootViewController: UIHostingController(rootView: myView)) cancelNotifier.listenOnce { [weak self] in self?.cancel() @@ -43,10 +44,6 @@ class LibreTransmitterSetupViewController: UINavigationController, CGMManagerOnb } - override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { - super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - } - deinit { logger.debug("LibreTransmitterSetupViewController() deinit was called") // cgmManager = nil @@ -76,7 +73,7 @@ class LibreTransmitterSetupViewController: UINavigationController, CGMManagerOnb } else if let newUID = SelectionState.shared.selectedUID { // this one is only temporary, // as we don't know the bluetooth identifier during nfc setup - logger.debug("Setupcontroller will set new libre2 device to \(newUID)") + logger.debug("Setupcontroller will set new libre2 device to \(newUID.hex)") UserDefaults.standard.preSelectedUid = newUID SelectionState.shared.selectedUID = nil @@ -88,14 +85,12 @@ class LibreTransmitterSetupViewController: UINavigationController, CGMManagerOnb // stored both preSelectedDevice and selectedUID ! } - if let cgmManager { - logger.debug("Setupcontroller Saving from setup") - cgmManagerOnboardingDelegate?.cgmManagerOnboarding(didCreateCGMManager: cgmManager) - cgmManagerOnboardingDelegate?.cgmManagerOnboarding(didOnboardCGMManager: cgmManager) - } else { - logger.debug("Setupcontroller not Saving from setup") - } + cgmManager.establishProxy() + + logger.debug("Setupcontroller Saving from setup") + cgmManagerOnboardingDelegate?.cgmManagerOnboarding(didCreateCGMManager: cgmManager) + cgmManagerOnboardingDelegate?.cgmManagerOnboarding(didOnboardCGMManager: cgmManager) completionDelegate?.completionNotifyingDidComplete(self) } diff --git a/LibreTransmitterUI/LibreTransmitterManager+UI.swift b/LibreTransmitterUI/LibreTransmitterManager+UI.swift index 77d4f4b..7db319b 100644 --- a/LibreTransmitterUI/LibreTransmitterManager+UI.swift +++ b/LibreTransmitterUI/LibreTransmitterManager+UI.swift @@ -24,12 +24,15 @@ extension LibreTransmitterManagerV3: CGMManagerUI { nil } - public static func setupViewController(bluetoothProvider: BluetoothProvider, displayGlucoseUnitObservable: DisplayGlucoseUnitObservable, colorPalette: LoopUIColorPalette, allowDebugFeatures: Bool) -> SetupUIResult { + public static func setupViewController(bluetoothProvider: BluetoothProvider, displayGlucosePreference: DisplayGlucosePreference, colorPalette: LoopUIColorPalette, allowDebugFeatures: Bool, prefersToSkipUserInteraction: Bool) -> SetupUIResult + { + let cgmManager = self.init() + let vc = LibreTransmitterSetupViewController(displayGlucosePreference: displayGlucosePreference, cgmManager: cgmManager) - return .userInteractionRequired(LibreTransmitterSetupViewController()) + return .userInteractionRequired(vc) } - public func settingsViewController(bluetoothProvider: BluetoothProvider, displayGlucoseUnitObservable: DisplayGlucoseUnitObservable, colorPalette: LoopUIColorPalette, allowDebugFeatures: Bool) -> CGMManagerViewController { + public func settingsViewController(bluetoothProvider: BluetoothProvider, displayGlucosePreference: DisplayGlucosePreference, colorPalette: LoopUIColorPalette, allowDebugFeatures: Bool) -> CGMManagerViewController { let doneNotifier = GenericObservableObject() let wantToTerminateNotifier = GenericObservableObject() @@ -38,16 +41,26 @@ extension LibreTransmitterManagerV3: CGMManagerUI { let wantToRestablishConnectionNotifier = GenericObservableObject() - let settings = SettingsView.asHostedViewController( - displayGlucoseUnitObservable: displayGlucoseUnitObservable, - notifyComplete: doneNotifier, notifyDelete: wantToTerminateNotifier, - notifyReset: wantToResetCGMManagerNotifier, notifyReconnect:wantToRestablishConnectionNotifier, - transmitterInfoObservable: self.transmitterInfoObservable, - sensorInfoObervable: self.sensorInfoObservable, - glucoseInfoObservable: self.glucoseInfoObservable, - alarmStatus: self.alarmStatus) - - let nav = CGMManagerSettingsNavigationViewController(rootViewController: settings) + let settingsView = SettingsView( + transmitterInfo: self.transmitterInfoObservable, + sensorInfo: self.sensorInfoObservable, + glucoseMeasurement: self.glucoseInfoObservable, + notifyComplete: doneNotifier, + notifyDelete: wantToTerminateNotifier, + notifyReset: wantToResetCGMManagerNotifier, + notifyReconnect:wantToRestablishConnectionNotifier, + alarmStatus: self.alarmStatus, + pairingService: self.pairingService, + bluetoothSearcher: self.bluetoothSearcher + ) + + let hostedView = DismissibleHostingController( + rootView: settingsView + .navigationTitle(self.localizedTitle) + .environmentObject(displayGlucosePreference) + ) + + let nav = CGMManagerSettingsNavigationViewController(rootViewController: hostedView) nav.navigationItem.largeTitleDisplayMode = .always nav.navigationBar.prefersLargeTitles = true @@ -59,7 +72,7 @@ extension LibreTransmitterManagerV3: CGMManagerUI { wantToRestablishConnectionNotifier.listenOnce { [weak self, weak nav] in self?.logger.debug("CGM wants to RestablishConnection") - self?.reEstablishProxy() + self?.establishProxy() nav?.notifyComplete() } diff --git a/LibreTransmitterUI/Views/Settings/GlucoseSettingsView.swift b/LibreTransmitterUI/Views/Settings/GlucoseSettingsView.swift index 0afa41c..eafb8d9 100644 --- a/LibreTransmitterUI/Views/Settings/GlucoseSettingsView.swift +++ b/LibreTransmitterUI/Views/Settings/GlucoseSettingsView.swift @@ -15,18 +15,6 @@ struct GlucoseSettingsView: View { @State private var presentableStatus: StatusMessage? - private var glucoseUnit: HKUnit - - public init(glucoseUnit: HKUnit) { - if let savedGlucoseUnit = UserDefaults.standard.mmGlucoseUnit { - self.glucoseUnit = savedGlucoseUnit - } else { - self.glucoseUnit = glucoseUnit - UserDefaults.standard.mmGlucoseUnit = glucoseUnit - } - - } - @AppStorage("com.loopkit.libreSyncToNs") var mmSyncToNS: Bool = true @AppStorage("com.loopkit.libreBackfillFromHistory") var mmBackfillFromHistory: Bool = true @AppStorage("com.loopkit.libreshouldPersistSensorData") var shouldPersistSensorData: Bool = false @@ -78,6 +66,6 @@ struct GlucoseSettingsView: View { struct GlucoseSettingsView_Previews: PreviewProvider { static var previews: some View { - GlucoseSettingsView(glucoseUnit: HKUnit.millimolesPerLiter) + GlucoseSettingsView() } } diff --git a/LibreTransmitterUI/Views/Settings/NotificationSettingsView.swift b/LibreTransmitterUI/Views/Settings/NotificationSettingsView.swift index 4c297c6..ee088bf 100644 --- a/LibreTransmitterUI/Views/Settings/NotificationSettingsView.swift +++ b/LibreTransmitterUI/Views/Settings/NotificationSettingsView.swift @@ -10,26 +10,16 @@ import SwiftUI import Combine import LibreTransmitter import HealthKit +import LoopKitUI struct NotificationSettingsView: View { + @EnvironmentObject private var displayGlucosePreference: DisplayGlucosePreference @State private var presentableStatus: StatusMessage? - private var glucoseUnit: HKUnit - private let glucoseSegments = [HKUnit.millimolesPerLiter, HKUnit.milligramsPerDeciliter] private lazy var glucoseSegmentStrings = self.glucoseSegments.map({ $0.localizedShortUnitString }) - public init(glucoseUnit: HKUnit) { - if let savedGlucoseUnit = UserDefaults.standard.mmGlucoseUnit { - self.glucoseUnit = savedGlucoseUnit - } else { - self.glucoseUnit = glucoseUnit - UserDefaults.standard.mmGlucoseUnit = glucoseUnit - } - - } - private enum Key: String { // case glucoseSchedules = "com.loopkit.libreglucoseschedules" @@ -142,6 +132,6 @@ struct NotificationSettingsView: View { struct NotificationSettingsView_Previews: PreviewProvider { static var previews: some View { - NotificationSettingsView(glucoseUnit: HKUnit.millimolesPerLiter) + NotificationSettingsView() } } diff --git a/LibreTransmitterUI/Views/Settings/SettingsView.swift b/LibreTransmitterUI/Views/Settings/SettingsView.swift index bee129e..af14b29 100644 --- a/LibreTransmitterUI/Views/Settings/SettingsView.swift +++ b/LibreTransmitterUI/Views/Settings/SettingsView.swift @@ -15,6 +15,7 @@ import LoopKitUI import UniformTypeIdentifiers public struct SettingsItem: View { + @State var title: String = "" // we don't want this to change after it is set @Binding var detail: String @@ -47,8 +48,16 @@ public struct SettingsItem: View { } struct SettingsView: View { + @EnvironmentObject private var displayGlucosePreference: DisplayGlucosePreference + + var longDateFormatter: DateFormatter = ({ + let df = DateFormatter() + df.dateStyle = .long + df.timeStyle = .long + df.doesRelativeDateFormatting = true + return df + })() - @ObservedObject private var displayGlucoseUnitObservable: DisplayGlucoseUnitObservable @ObservedObject private var transmitterInfo: LibreTransmitter.TransmitterInfo @ObservedObject private var sensorInfo: LibreTransmitter.SensorInfo @@ -58,7 +67,7 @@ struct SettingsView: View { @ObservedObject private var notifyDelete: GenericObservableObject @ObservedObject private var notifyReset: GenericObservableObject @ObservedObject private var notifyReconnect: GenericObservableObject - + @State private var presentableStatus: StatusMessage? @ObservedObject var alarmStatus: LibreTransmitter.AlarmStatus @@ -66,32 +75,35 @@ struct SettingsView: View { // @State private var showingExporter = false // @Environment(\.presentationMode) var presentationMode - static func asHostedViewController( - displayGlucoseUnitObservable: DisplayGlucoseUnitObservable, + var pairingService: SensorPairingProtocol + var bluetoothSearcher: BluetoothSearcher + + init( + transmitterInfo: LibreTransmitter.TransmitterInfo, + sensorInfo: LibreTransmitter.SensorInfo, + glucoseMeasurement: LibreTransmitter.GlucoseInfo, notifyComplete: GenericObservableObject, notifyDelete: GenericObservableObject, notifyReset: GenericObservableObject, notifyReconnect: GenericObservableObject, - transmitterInfoObservable: LibreTransmitter.TransmitterInfo, - sensorInfoObervable: LibreTransmitter.SensorInfo, - glucoseInfoObservable: LibreTransmitter.GlucoseInfo, - alarmStatus: LibreTransmitter.AlarmStatus) -> DismissibleHostingController { - DismissibleHostingController(rootView: self.init( - displayGlucoseUnitObservable: displayGlucoseUnitObservable, - transmitterInfo: transmitterInfoObservable, - sensorInfo: sensorInfoObervable, - glucoseMeasurement: glucoseInfoObservable, - notifyComplete: notifyComplete, - notifyDelete: notifyDelete, - notifyReset: notifyReset, - notifyReconnect: notifyReconnect, - alarmStatus: alarmStatus - - )) + alarmStatus: LibreTransmitter.AlarmStatus, + pairingService: SensorPairingProtocol, + bluetoothSearcher: BluetoothSearcher) + { + self.transmitterInfo = transmitterInfo + self.sensorInfo = sensorInfo + self.glucoseMeasurement = glucoseMeasurement + self.notifyComplete = notifyComplete + self.notifyDelete = notifyDelete + self.notifyReset = notifyReset + self.notifyReconnect = notifyReconnect + self.alarmStatus = alarmStatus + self.pairingService = pairingService + self.bluetoothSearcher = bluetoothSearcher } private var glucoseUnit: HKUnit { - displayGlucoseUnitObservable.displayGlucoseUnit + displayGlucosePreference.unit } static let formatter = NumberFormatter() @@ -104,8 +116,11 @@ struct SettingsView: View { headerSection snoozeSection measurementSection - if !glucoseMeasurement.predictionDate.isEmpty { - predictionSection + if let date = glucoseMeasurement.predictionDate, let prediction = glucoseMeasurement.prediction { + Section(header: Text(LocalizedString("Last Blood Sugar prediction", comment: "Text describing header for Blood Sugar prediction section"))) { + SettingsItem(title: "CurrentBG", detail: displayGlucosePreference.format(prediction)) + SettingsItem(title: "Date", detail: longDateFormatter.string(from: date) ) + } } NavigationLink(destination: deviceInfoSection) { @@ -122,19 +137,11 @@ struct SettingsView: View { destructSection }.listStyle(InsetGroupedListStyle()) - .onAppear { - // only override savedglucose unit if we haven't saved this locally before - if UserDefaults.standard.mmGlucoseUnit == nil { - UserDefaults.standard.mmGlucoseUnit = glucoseUnit - } - - } .toolbar { ToolbarItem(placement: .navigationBarTrailing) { doneButton } } - .navigationTitle("Libre Bluetooth") } var snoozeSection: some View { @@ -151,28 +158,19 @@ struct SettingsView: View { } var measurementSection : some View { - Section(header: Text(LocalizedString("Last measurement", comment: "Text describing header for last measurement section"))) { - if glucoseUnit == .millimolesPerLiter { - SettingsItem(title: "Glucose", detail: $glucoseMeasurement.glucoseMMOL) - } else if glucoseUnit == .milligramsPerDeciliter { - SettingsItem(title: "Glucose", detail: $glucoseMeasurement.glucoseMGDL) - } - - SettingsItem(title: "Date", detail: $glucoseMeasurement.date ) - SettingsItem(title: "Sensor Footer checksum", detail: $glucoseMeasurement.checksum ) + var glucoseText: String = "" + var glucoseDateText: String = "" + if let glucose = glucoseMeasurement.glucose { + glucoseText = displayGlucosePreference.format(glucose) + } + if let date = glucoseMeasurement.date { + glucoseDateText = longDateFormatter.string(from: date) } - } - - var predictionSection : some View { - Section(header: Text(LocalizedString("Last Blood Sugar prediction", comment: "Text describing header for Blood Sugar prediction section"))) { - if glucoseUnit == .millimolesPerLiter { - SettingsItem(title: "CurrentBG", detail: $glucoseMeasurement.predictionMMOL) - } else if glucoseUnit == .milligramsPerDeciliter { - SettingsItem(title: "CurrentBG", detail: $glucoseMeasurement.predictionMGDL) - } - - SettingsItem(title: "Date", detail: $glucoseMeasurement.predictionDate ) + return Section(header: Text(LocalizedString("Last measurement", comment: "Text describing header for last measurement section"))) { + SettingsItem(title: "Glucose", detail: glucoseText) + SettingsItem(title: "Date", detail: glucoseDateText ) + SettingsItem(title: "Sensor Footer checksum", detail: $glucoseMeasurement.checksum ) } } @@ -223,7 +221,7 @@ struct SettingsView: View { var sensorChangeSection: some View { Section { - NavigationLink(destination: AuthView(completeNotifier: notifyComplete, notifyReset: notifyReset, notifyReconnect: notifyReconnect)) { + NavigationLink(destination: AuthView(completeNotifier: notifyComplete, notifyReset: notifyReset, notifyReconnect: notifyReconnect, pairingService: pairingService, bluetoothSearcher: bluetoothSearcher)) { /*Button("Change Sensor") { }.foregroundColor(.blue)*/ SettingsItem(title: "Change Sensor").foregroundColor(.blue) @@ -245,7 +243,9 @@ struct SettingsView: View { self.authenticate { success in print("got authentication response: \(success)") if success { - notifyDelete.notify() + DispatchQueue.main.async { + notifyDelete.notify() + } } } @@ -271,11 +271,11 @@ struct SettingsView: View { } } - NavigationLink(destination: GlucoseSettingsView(glucoseUnit: self.glucoseUnit)) { + NavigationLink(destination: GlucoseSettingsView()) { SettingsItem(title: "Glucose Settings") } - NavigationLink(destination: NotificationSettingsView(glucoseUnit: self.glucoseUnit)) { + NavigationLink(destination: NotificationSettingsView()) { SettingsItem(title: "Notifications") } @@ -324,7 +324,7 @@ struct SettingsView: View { var showProgress : Bool { - if let expiresAt = sensorInfo.expiresAt,let activatedAt = sensorInfo.activatedAt { + if let expiresAt = sensorInfo.expiresAt { return expiresAt.timeIntervalSinceNow > 0 } @@ -463,6 +463,6 @@ struct SettingsView: View { struct SettingsOverview_Previews: PreviewProvider { static var previews: some View { - NotificationSettingsView(glucoseUnit: HKUnit.millimolesPerLiter) + NotificationSettingsView() } } diff --git a/LibreTransmitterUI/Views/Setup/AuthView.swift b/LibreTransmitterUI/Views/Setup/AuthView.swift index b87d3f7..f25d9c4 100644 --- a/LibreTransmitterUI/Views/Setup/AuthView.swift +++ b/LibreTransmitterUI/Views/Setup/AuthView.swift @@ -8,6 +8,8 @@ import SwiftUI import LoopKitUI +import LibreTransmitter + // this view should only be called when setting up a new device in an existing cgmmanager struct AuthView: View { @@ -21,6 +23,9 @@ struct AuthView: View { @State private var isAuthenticated = false @State private var hasSetupListeners = false + + var pairingService: SensorPairingProtocol + var bluetoothSearcher: BluetoothSearcher var exclamation: Image { Image(systemName: "exclamationmark.triangle.fill") @@ -37,7 +42,7 @@ struct AuthView: View { Text(LocalizedString("Authenticated", comment: "Text confirming user is authenticated in AuthView")) .transition(AnyTransition.opacity.combined(with: .move(edge: .bottom))) - NavigationLink(destination: ModeSelectionView(cancelNotifier: cancelNotifier, saveNotifier: saveNotifier), isActive: $isNavigationActive) { + NavigationLink(destination: ModeSelectionView(cancelNotifier: cancelNotifier, saveNotifier: saveNotifier, pairingService: pairingService, bluetoothSearcher: bluetoothSearcher), isActive: $isNavigationActive) { Button(action: { self.notifyReset.notify() self.isNavigationActive = true @@ -155,6 +160,6 @@ struct AuthView: View { struct AuthView_Previews: PreviewProvider { static var previews: some View { - AuthView(completeNotifier: GenericObservableObject(), notifyReset: GenericObservableObject(), notifyReconnect: GenericObservableObject()) + AuthView(completeNotifier: GenericObservableObject(), notifyReset: GenericObservableObject(), notifyReconnect: GenericObservableObject(), pairingService: MockSensorPairingService(), bluetoothSearcher: MockBluetoothSearcher()) } } diff --git a/LibreTransmitterUI/Views/Setup/BluetoothSelection.swift b/LibreTransmitterUI/Views/Setup/BluetoothSelection.swift index 861bbee..e1a655e 100644 --- a/LibreTransmitterUI/Views/Setup/BluetoothSelection.swift +++ b/LibreTransmitterUI/Views/Setup/BluetoothSelection.swift @@ -54,7 +54,7 @@ private struct ListFooter: View { } private struct DeviceItem: View { - var device: SomePeripheral + var device: PeripheralProtocol @Binding var rssi: RSSIInfo? var details1: String var details2: String? @@ -67,25 +67,19 @@ private struct DeviceItem: View { @ObservedObject var selection: SelectionState = .shared - func getDeviceImage(_ device: SomePeripheral) -> Image { + func getDeviceImage(_ device: PeripheralProtocol) -> Image { var image: UIImage! - switch device { - case let .Left(realDevice): - image = LibreTransmitters.getSupportedPlugins(realDevice)?.first?.smallImage - - case .Right: - image = LibreTransmitters.all.randomElement()?.smallImage - } + image = LibreTransmitters.getSupportedPlugins(device)?.first?.smallImage return image == nil ? Image(systemName: "exclamationmark.triangle") : Image(uiImage: image) } - func getRowBackground(device: SomePeripheral) -> Color { + func getRowBackground(device: PeripheralProtocol) -> Color { selection.selectedStringIdentifier == device.asStringIdentifier ? Defaults.selectedRowBackground : Defaults.rowBackground } - init(device: SomePeripheral, requiresSetup: Bool, requiresPhoneNFC: Bool, details: String, rssi: Binding) { + init(device: PeripheralProtocol, requiresSetup: Bool, requiresPhoneNFC: Bool, details: String, rssi: Binding) { self.device = device self._rssi = rssi self.requiresPhoneNFC = requiresPhoneNFC @@ -177,15 +171,6 @@ private struct DeviceItem: View { } } -// Decided to use shared instance instead of .environmentObject() -class SelectionState: ObservableObject { - @Published var selectedStringIdentifier: String? = "" - - @Published var selectedUID: Data? - - static var shared = SelectionState() -} - struct BluetoothSelection: View { @ObservedObject var selection: SelectionState = .shared @ObservedObject public var cancelNotifier: GenericObservableObject @@ -195,23 +180,16 @@ struct BluetoothSelection: View { selection.selectedStringIdentifier } - private var searcher: BluetoothSearchManager! - - /*static func asHostedViewController() -> UIHostingController { - UIHostingController(rootView: self.init()) - }*/ + var searcher: BluetoothSearcher // Should contain all discovered and compatible devices // This list is expected to contain 10 or 20 items at the most - @State var allDevices = [SomePeripheral]() + @State var allDevices = [PeripheralProtocol]() @State var deviceDetails = [String: String]() @State var deviceRequiresPhoneNFC = [String: Bool]() @State var deviceRequiresSetup = [String: Bool]() @State var rssi = [String: RSSIInfo]() - var nullPubliser: Empty! - var debugMode = false - var cancelButton: some View { Button("Cancel") { print("cancel button pressed") @@ -239,28 +217,11 @@ struct BluetoothSelection: View { #endif } - init(debugMode: Bool = false, cancelNotifier: GenericObservableObject, saveNotifier: GenericObservableObject) { - self.debugMode = debugMode + init(cancelNotifier: GenericObservableObject, saveNotifier: GenericObservableObject, searcher: BluetoothSearcher) { self.cancelNotifier = cancelNotifier self.saveNotifier = saveNotifier - - if self.debugMode { - allDevices = Self.getMockData() - nullPubliser = Empty() - - } else { - self.searcher = BluetoothSearchManager() - } - LibreTransmitter.NotificationHelper.requestNotificationPermissionsIfNeeded() - - } - - public mutating func stopScan(_ removeSearcher: Bool = false) { - self.searcher?.disconnectManually() - if removeSearcher { - self.searcher = nil - } + self.searcher = searcher } var header: some View { @@ -274,27 +235,20 @@ struct BluetoothSelection: View { } } } - var list : some View { + var deviceList : some View { List { Section(header: header) { - ForEach(allDevices) { device in - if debugMode { - let randomRSSI = RSSIInfo(bledeviceID: device.asStringIdentifier, signalStrength: -90 + (1...70).randomElement()!) - let requiresPhoneNFC = Bool.random() - DeviceItem(device: device, requiresSetup: false, requiresPhoneNFC: requiresPhoneNFC, details: "mockdatamockdata mockdata mockdata\nmockdata2 nmockdata2", rssi: .constant(randomRSSI)) - } else { - let requiresPhoneNFC = deviceRequiresPhoneNFC[device.asStringIdentifier, default: false] + ForEach(allDevices, id: \.name) { device in + let requiresPhoneNFC = deviceRequiresPhoneNFC[device.asStringIdentifier, default: false] - let requiresSetup = deviceRequiresSetup[device.asStringIdentifier, default: false] - let rssigetter = Binding(get: { - rssi[device.asStringIdentifier] - }, set: { _ in - // not ever needed - }) - - DeviceItem(device: device, requiresSetup: requiresSetup, requiresPhoneNFC: requiresPhoneNFC, details: deviceDetails[device.asStringIdentifier]!, rssi: rssigetter) - } + let requiresSetup = deviceRequiresSetup[device.asStringIdentifier, default: false] + let rssigetter = Binding(get: { + rssi[device.asStringIdentifier] + }, set: { _ in + // not ever needed + }) + DeviceItem(device: device, requiresSetup: requiresSetup, requiresPhoneNFC: requiresPhoneNFC, details: deviceDetails[device.asStringIdentifier]!, rssi: rssigetter) } } Section { @@ -302,21 +256,13 @@ struct BluetoothSelection: View { } } .onAppear { - // devices = Self.getMockData() - if debugMode { - allDevices = Self.getMockData() - } else { - print(" asking searcher to search!") - self.searcher?.scanForCompatibleDevices() - } + print(" asking searcher to search!") + self.searcher.scanForCompatibleDevices() } .onDisappear { - if !self.debugMode { - print(" asking searcher to stop searching!") - self.searcher?.stopTimer() - self.searcher?.disconnectManually() - - } + print(" asking searcher to stop searching!") + self.searcher.stopTimer() + self.searcher.disconnectManually() } .navigationBarBackButtonHidden(true) .navigationBarItems(leading: cancelButton, trailing: saveButton) @@ -330,62 +276,42 @@ struct BluetoothSelection: View { } var body: some View { - if debugMode { - list - .onReceive(nullPubliser) { _ in - print("nullpublisher received element!?") - // allDevices.append(SomePeripheral.Left(device)) - } - } else { - list - .onReceive(searcher.passThroughMetaData) { newDevice, advertisement in - print("received searcher passthrough") - - let alreadyAdded = allDevices.contains { existingDevice -> Bool in - existingDevice.asStringIdentifier == newDevice.asStringIdentifier - } - if !alreadyAdded { - if let pluginForDevice = LibreTransmitters.getSupportedPlugins(newDevice)?.first { + deviceList + .onReceive(searcher.passThroughMetaData) { newDevice, advertisement in + print("received searcher passthrough") - deviceRequiresPhoneNFC[newDevice.asStringIdentifier] = pluginForDevice.requiresPhoneNFC - deviceRequiresSetup[newDevice.asStringIdentifier] = pluginForDevice.requiresSetup + let alreadyAdded = allDevices.contains { existingDevice -> Bool in + existingDevice.asStringIdentifier == newDevice.asStringIdentifier + } + if !alreadyAdded { + if let pluginForDevice = LibreTransmitters.getSupportedPlugins(newDevice)?.first { - if let parsedAdvertisement = pluginForDevice.getDeviceDetailsFromAdvertisement(advertisementData: advertisement) { + deviceRequiresPhoneNFC[newDevice.asStringIdentifier] = pluginForDevice.requiresPhoneNFC + deviceRequiresSetup[newDevice.asStringIdentifier] = pluginForDevice.requiresSetup - deviceDetails[newDevice.asStringIdentifier] = parsedAdvertisement - } else { - deviceDetails[newDevice.asStringIdentifier] = "" - } + if let parsedAdvertisement = pluginForDevice.getDeviceDetailsFromAdvertisement(advertisementData: advertisement) { + deviceDetails[newDevice.asStringIdentifier] = parsedAdvertisement } else { - deviceDetails[newDevice.asStringIdentifier] = newDevice.asStringIdentifier + deviceDetails[newDevice.asStringIdentifier] = "" } - allDevices.append(SomePeripheral.Left(newDevice)) + } else { + deviceDetails[newDevice.asStringIdentifier] = newDevice.asStringIdentifier } - } - .onReceive(searcher.throttledRSSI.throttledPublisher, perform: receiveRSSI) - } - } -} - -extension BluetoothSelection { - static func getMockData() -> [SomePeripheral] { - [ - SomePeripheral.Right(MockedPeripheral(name: "device1")), - SomePeripheral.Right(MockedPeripheral(name: "device2")), - SomePeripheral.Right(MockedPeripheral(name: "device3")), - SomePeripheral.Right(MockedPeripheral(name: "device4")) - ] + allDevices.append(newDevice) + } + } + .onReceive(searcher.throttledRSSI.throttledPublisher, perform: receiveRSSI) } } struct BluetoothSelection_Previews: PreviewProvider { static var previews: some View { - var testData = SelectionState.shared + let testData = SelectionState.shared testData.selectedStringIdentifier = "device4" - return BluetoothSelection(debugMode: true, cancelNotifier: GenericObservableObject(), saveNotifier: GenericObservableObject()) + return BluetoothSelection(cancelNotifier: GenericObservableObject(), saveNotifier: GenericObservableObject(), searcher: MockBluetoothSearcher()) } } diff --git a/LibreTransmitterUI/Views/Setup/Libre2DirectSetup.swift b/LibreTransmitterUI/Views/Setup/Libre2DirectSetup.swift index 899ca63..bfb8e45 100644 --- a/LibreTransmitterUI/Views/Setup/Libre2DirectSetup.swift +++ b/LibreTransmitterUI/Views/Setup/Libre2DirectSetup.swift @@ -12,52 +12,37 @@ import LoopKitUI import LoopKit import os.log -#if canImport(CoreNFC) - fileprivate var logger = Logger(forType: "Libre2DirectSetup") struct Libre2DirectSetup: View { @State private var presentableStatus: StatusMessage? @State private var showPairingInfo = false - - @State private var service = SensorPairingService() - + @State private var isPairing = false @State private var pairingInfo = SensorPairingInfo() @ObservedObject public var cancelNotifier: GenericObservableObject @ObservedObject public var saveNotifier: GenericObservableObject - public var isMockedSensor = false - - - func pairMockedSensor() { - let info = FakeSensorPairingData().fakeSensorPairingInfo() - logger.debug("Sending fake sensor pairinginfo: \(info.description)") - //delay a bit to simulate a real tag readout - DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { - receivePairingInfo(info) - } - - - } + let pairingService: SensorPairingProtocol func pairSensor() { - - guard !isMockedSensor else { - pairMockedSensor() - return + + pairingService.onCancel = { + DispatchQueue.main.async { + isPairing = false + } } - if !Features.phoneNFCAvailable { - presentableStatus = StatusMessage(title: "Phone NFC required!", message: "Your phone or app is not enabled for NFC communications, which is needed to pair to libre2 sensors") - return - } - print("Asked to pair sensor! phoneNFCAvailable: \(Features.phoneNFCAvailable)") showPairingInfo = false + isPairing = true - service.pairSensor() - + do { + try pairingService.pairSensor() + } catch { + let message = (error as? LocalizedError)?.recoverySuggestion ?? error.localizedDescription + presentableStatus = StatusMessage(title: error.localizedDescription, message: message) + } } func receivePairingInfo(_ info: SensorPairingInfo) { @@ -66,6 +51,7 @@ struct Libre2DirectSetup: View { pairingInfo = info + isPairing = false showPairingInfo = true // calibrationdata must always be extracted from the full nfc scan @@ -87,7 +73,7 @@ struct Libre2DirectSetup: View { let max = info.sensorData?.maxMinutesWearTime ?? 0 - let sensor = Sensor(uuid: info.uuid, patchInfo: info.patchInfo, maxAge: max, initialIdentificationStrategy: info.initialIdentificationStrategy, sensorName: info.sensorName) + let sensor = Sensor(uuid: info.uuid, patchInfo: info.patchInfo, maxAge: max, sensorName: info.sensorName) UserDefaults.standard.preSelectedSensor = sensor SelectionState.shared.selectedUID = pairingInfo.uuid @@ -95,9 +81,9 @@ struct Libre2DirectSetup: View { // only relevant for launch through settings, as selectionstate can be persisted // we need to enforce libre2 by removing any selected third party transmitter SelectionState.shared.selectedStringIdentifier = nil - print(" paried and set selected UID to: \(SelectionState.shared.selectedUID?.hex)") + print("Paired and set selected UID to: \(String(describing: SelectionState.shared.selectedUID?.hex))") saveNotifier.notify() - NotificationHelper.sendLibre2FirectFinishedSetupNotifcation() + NotificationHelper.sendLibre2DirectFinishedSetupNotifcation() } @@ -127,17 +113,26 @@ struct Libre2DirectSetup: View { }) { VStack(spacing: 10) { - Button("Pair Sensor & connect") { + Button { pairSensor() + } label: { + if isPairing { + HStack(spacing: 10) { + ProgressView() + Text(LocalizedString("Pairing...", comment: "Button title for pairing sensor when pairing")) + } + } else { + Text(LocalizedString("Pair Sensor", comment: "Button title for pairing sensor")) + } } .actionButtonStyle(.primary) + .disabled(isPairing) }.padding() } .navigationTitle("Libre 2 Setup") .navigationBarBackButtonHidden(true) .navigationBarItems(leading: cancelButton) // the pair button does the save process for us! //, trailing: saveButton) - .onReceive(service.publisher, perform: receivePairingInfo) - //.onReceive(service.errorPublisher, perform: receiveError) + .onReceive(pairingService.publisher, perform: receivePairingInfo) .alert(item: $presentableStatus) { status in Alert(title: Text(status.title), message: Text(status.message), dismissButton: .default(Text("Got it!"))) @@ -147,7 +142,6 @@ struct Libre2DirectSetup: View { struct Libre2DirectSetup_Previews: PreviewProvider { static var previews: some View { - Libre2DirectSetup(cancelNotifier: GenericObservableObject(), saveNotifier: GenericObservableObject()) + Libre2DirectSetup(cancelNotifier: GenericObservableObject(), saveNotifier: GenericObservableObject(), pairingService: MockSensorPairingService()) } } -#endif diff --git a/LibreTransmitterUI/Views/Setup/ModeSelectionView.swift b/LibreTransmitterUI/Views/Setup/ModeSelectionView.swift index e368540..ac2a266 100644 --- a/LibreTransmitterUI/Views/Setup/ModeSelectionView.swift +++ b/LibreTransmitterUI/Views/Setup/ModeSelectionView.swift @@ -8,46 +8,30 @@ import SwiftUI import LoopKitUI +import LibreTransmitter struct ModeSelectionView: View { @ObservedObject public var cancelNotifier: GenericObservableObject @ObservedObject public var saveNotifier: GenericObservableObject - - var supportsFakeSensor = Features.supportsFakeSensor + + var pairingService: SensorPairingProtocol + var bluetoothSearcher: BluetoothSearcher var modeSelectSection : some View { Section(header: Text(LocalizedString("Connection options", comment: "Text describing options for connecting to sensor or transmitter"))) { - if supportsFakeSensor { - NavigationLink(destination: Libre2DirectSetup(cancelNotifier: cancelNotifier, saveNotifier: saveNotifier, isMockedSensor: true)) { - - SettingsItem(title: LocalizedString("Fake Libre 2 Direct", comment: "Fake Libre 2 connection option")) - .actionButtonStyle(.primary) - .padding([.top, .bottom], 8) - - } - } - - #if canImport(CoreNFC) - - - NavigationLink(destination: Libre2DirectSetup(cancelNotifier: cancelNotifier, saveNotifier: saveNotifier)) { - - SettingsItem(title: LocalizedString("Libre 2 Direct", comment: "Libre 2 connection option")) - .actionButtonStyle(.primary) - .padding([.top, .bottom], 8) - - } - - #endif - - NavigationLink(destination: BluetoothSelection(cancelNotifier: cancelNotifier, saveNotifier: saveNotifier)) { - SettingsItem(title: LocalizedString("Bluetooth Transmitters", comment: "Bluetooth Transmitter connection option")) - .actionButtonStyle(.primary) - .padding([.top, .bottom], 8) - } + NavigationLink(destination: Libre2DirectSetup(cancelNotifier: cancelNotifier, saveNotifier: saveNotifier, pairingService: pairingService)) { + SettingsItem(title: LocalizedString("Libre 2 Direct", comment: "Libre 2 connection option")) + .actionButtonStyle(.primary) + .padding([.top, .bottom], 8) + } + NavigationLink(destination: BluetoothSelection(cancelNotifier: cancelNotifier, saveNotifier: saveNotifier, searcher: bluetoothSearcher)) { + SettingsItem(title: LocalizedString("Bluetooth Transmitters", comment: "Bluetooth Transmitter connection option")) + .actionButtonStyle(.primary) + .padding([.top, .bottom], 8) + } } } @@ -88,6 +72,6 @@ struct ModeSelectionView: View { struct ModeSelectionView_Previews: PreviewProvider { static var previews: some View { - ModeSelectionView(cancelNotifier: GenericObservableObject(), saveNotifier: GenericObservableObject()) + ModeSelectionView(cancelNotifier: GenericObservableObject(), saveNotifier: GenericObservableObject(), pairingService: MockSensorPairingService(), bluetoothSearcher: MockBluetoothSearcher()) } } diff --git a/LibreTransmitterUI/Views/Utilities/ViewExtensions.swift b/LibreTransmitterUI/Views/Utilities/ViewExtensions.swift index edfdf81..493fc11 100644 --- a/LibreTransmitterUI/Views/Utilities/ViewExtensions.swift +++ b/LibreTransmitterUI/Views/Utilities/ViewExtensions.swift @@ -9,7 +9,6 @@ import SwiftUI import LocalAuthentication -#if canImport(UIKit) extension View { func hideKeyboardPreIos16() { if #unavailable(iOS 16.0) { @@ -17,7 +16,7 @@ extension View { } } } -#endif + struct LeadingImage: View { var image: UIImage