Jonas Björkert před 1 rokem
rodič
revize
2420f5f554

+ 4 - 0
LoopFollow.xcodeproj/project.pbxproj

@@ -76,6 +76,7 @@
 		DD7E19882ACDA5DA00DBD158 /* Notes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19872ACDA5DA00DBD158 /* Notes.swift */; };
 		DD7E198A2ACDA62600DBD158 /* SensorStart.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7E19892ACDA62600DBD158 /* SensorStart.swift */; };
 		DD7FFAFD2A72953000C3A304 /* EKEventStore+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */; };
+		DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */; };
 		DD91E4DD2BDEC3F8002D9E97 /* GlucoseConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */; };
 		DD98F54424BCEFEE0007425A /* ShareClientExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */; };
 		DD9ACA042D32821400415D8A /* DeviceStatusTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD9ACA032D32821200415D8A /* DeviceStatusTask.swift */; };
@@ -354,6 +355,7 @@
 		DD7E19872ACDA5DA00DBD158 /* Notes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notes.swift; sourceTree = "<group>"; };
 		DD7E19892ACDA62600DBD158 /* SensorStart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensorStart.swift; sourceTree = "<group>"; };
 		DD7FFAFC2A72953000C3A304 /* EKEventStore+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EKEventStore+Extensions.swift"; sourceTree = "<group>"; };
+		DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodDashHeartbeatBluetoothTransmitter.swift; sourceTree = "<group>"; };
 		DD91E4DC2BDEC3F8002D9E97 /* GlucoseConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseConversion.swift; sourceTree = "<group>"; };
 		DD98F54324BCEFEE0007425A /* ShareClientExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareClientExtension.swift; sourceTree = "<group>"; };
 		DD9ACA032D32821200415D8A /* DeviceStatusTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceStatusTask.swift; sourceTree = "<group>"; };
@@ -786,6 +788,7 @@
 		DDAD16302D2EF98C0084BE10 /* Devices */ = {
 			isa = PBXGroup;
 			children = (
+				DD85E9942D739CED001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift */,
 				DDAD162E2D2EF97C0084BE10 /* RileyLinkHeartbeatBluetoothDevice.swift */,
 				DD5817192D299EF40041FB98 /* DexcomHeartbeatBluetoothDevice.swift */,
 			);
@@ -1499,6 +1502,7 @@
 				FC97881C2485969B00A7906C /* MainViewController.swift in Sources */,
 				DD6A935E2BFA6FA2003FFB8E /* DeviceStatusOpenAPS.swift in Sources */,
 				DD493AD52ACF2109009A6922 /* ResumePump.swift in Sources */,
+				DD85E9952D739CFE001C8BB7 /* OmnipodDashHeartbeatBluetoothTransmitter.swift in Sources */,
 				DDCF979624C1443C002C9752 /* GeneralSettingsViewController.swift in Sources */,
 				FCC0FAC224922A22003E610E /* DictionaryKeyPath.swift in Sources */,
 				DD493AD72ACF2139009A6922 /* SuspendPump.swift in Sources */,

+ 10 - 0
LoopFollow/BackgroundRefresh/BT/BLEManager.swift

@@ -73,6 +73,9 @@ class BLEManager: NSObject, ObservableObject {
             case .rileyLink:
                 activeDevice = RileyLinkHeartbeatBluetoothDevice(address: device.id.uuidString, name: device.name, bluetoothDeviceDelegate: self)
                 activeDevice?.connect()
+            case .omnipodDash:
+                activeDevice = OmnipodDashHeartbeatBluetoothTransmitter(address: device.id.uuidString, name: device.name, bluetoothDeviceDelegate: self)
+                activeDevice?.connect()
             case .silentTune, .none:
                 return
             }
@@ -94,6 +97,7 @@ class BLEManager: NSObject, ObservableObject {
     }
 
     private func addOrUpdateDevice(_ device: BLEDevice) {
+        LogManager.shared.log(category: .bluetooth, message: "Adding or updating BLE device: \(device)", isDebug: true)
         if let idx = devices.firstIndex(where: { $0.id == device.id }) {
             var updatedDevice = devices[idx]
             updatedDevice.rssi = device.rssi
@@ -247,6 +251,12 @@ extension BLEManager {
 
         let expectedOffset = sensorOffset + pollingDelay
 
+        // If the heartbeat interval isn't a typical 60 or 300 seconds,
+        // we simply return a string indicating that the delay is "up to" the heartbeat interval.
+        if heartBeatInterval != 60 && heartBeatInterval != 300 {
+            return "up to \(Int(heartBeatInterval)) sec"
+        }
+        
         let effectiveDelay = CycleHelper.computeDelay(sensorOffset: expectedOffset, heartbeatLast: heartbeatLast, heartbeatInterval: heartBeatInterval)
 
         return "\(Int(effectiveDelay)) sec"

+ 43 - 0
LoopFollow/BackgroundRefresh/BT/Devices/OmnipodDashHeartbeatBluetoothTransmitter.swift

@@ -0,0 +1,43 @@
+//
+//  OmnipodDashHeartbeatBluetoothTransmitter.swift
+//  LoopFollow
+//
+//  Created by Jonas Björkert on 2025-03-01.
+//  Copyright © 2025 Jon Fawcett. All rights reserved.
+//
+
+import Foundation
+import CoreBluetooth
+
+class OmnipodDashHeartbeatBluetoothTransmitter: BluetoothDevice {
+    private let CBUUID_Service: String = "1A7E4024-E3ED-4464-8B7E-751E03D0DC5F"
+    private let CBUUID_Advertisement: String = "00004024-0000-1000-8000-00805f9b34fb"
+    private let CBUUID_ReceiveCharacteristic: String = "1A7E2442-E3ED-4464-8B7E-751E03D0DC5F"
+
+    private let CBUUID_ReceiveCharacteristic_Data: String = ""
+
+    init(address:String, name:String?, bluetoothDeviceDelegate: BluetoothDeviceDelegate) {
+        super.init(
+            address: address,
+            name: name,
+            CBUUID_Advertisement: nil,
+            servicesCBUUIDs: [CBUUID(string: CBUUID_Service)],
+            CBUUID_ReceiveCharacteristic: CBUUID_ReceiveCharacteristic,
+            bluetoothDeviceDelegate: bluetoothDeviceDelegate
+        )
+    }
+
+    override func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
+        super.centralManager(central, didConnect: peripheral)
+    }
+
+    override func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
+        super.peripheral(peripheral, didUpdateValueFor: characteristic, error: error)
+
+        self.bluetoothDeviceDelegate?.heartBeat()
+    }
+
+    override func expectedHeartbeatInterval() -> TimeInterval? {
+        return 3 * 60
+    }
+}

+ 5 - 0
LoopFollow/BackgroundRefresh/BackgroundRefreshSettingsView.swift

@@ -76,6 +76,11 @@ struct BackgroundRefreshSettingsView: View {
                     Text("Requires a Dexcom G6/ONE/G7/ONE+ transmitter within Bluetooth range. Provides updates every 5 minutes and uses less battery than the silent tune method.")
                         .font(.footnote)
                         .foregroundColor(.secondary)
+
+                case .omnipodDash:
+                    Text("Requires an OmniPod DASH pod paired with this device within Bluetooth range. Provides updates once per minute and uses less battery than the silent tune method.")
+                        .font(.footnote)
+                        .foregroundColor(.secondary)
                 }
             }
         }

+ 11 - 2
LoopFollow/BackgroundRefresh/BackgroundRefreshType.swift

@@ -12,11 +12,12 @@ enum BackgroundRefreshType: String, Codable, CaseIterable {
     case silentTune = "Silent Tune"
     case rileyLink = "RileyLink"
     case dexcom = "Dexcom"
+    case omnipodDash = "Omnipod Dash"
 
     /// Indicates if the device type uses Bluetooth
     var isBluetooth: Bool {
         switch self {
-        case .rileyLink, .dexcom:
+        case .rileyLink, .dexcom, .omnipodDash:
             return true
         case .silentTune, .none:
             return false
@@ -27,6 +28,8 @@ enum BackgroundRefreshType: String, Codable, CaseIterable {
         switch self {
         case .rileyLink:
             return 60
+        case .omnipodDash:
+            return 3 * 60
         case .dexcom:
             return 5 * 60
         case .silentTune, .none:
@@ -38,7 +41,7 @@ enum BackgroundRefreshType: String, Codable, CaseIterable {
         switch self {
         case .rileyLink:
             return true
-        case .dexcom, .silentTune, .none:
+        case .dexcom, .omnipodDash, .silentTune, .none:
             return false
         }
     }
@@ -60,6 +63,12 @@ enum BackgroundRefreshType: String, Codable, CaseIterable {
             }
             return false
 
+        case .omnipodDash:
+            if let name = device.name {
+                return name == "TWI BOARD"
+            }
+            return false
+
         case .silentTune, .none:
             return false
         }