BLEManager.swift 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. //
  2. // BLEManager.swift
  3. // LoopFollow
  4. //
  5. import Foundation
  6. import CoreBluetooth
  7. import Combine
  8. class BLEManager: NSObject, ObservableObject {
  9. static let shared = BLEManager()
  10. @Published private(set) var devices: [BLEDevice] = []
  11. private var centralManager: CBCentralManager!
  12. private var activeDevice: BluetoothDevice?
  13. private override init() {
  14. super.init()
  15. centralManager = CBCentralManager(
  16. delegate: self,
  17. queue: .main
  18. )
  19. if let device = Storage.shared.selectedBLEDevice.value {
  20. connect(device: device)
  21. }
  22. }
  23. func startScanning() {
  24. guard centralManager.state == .poweredOn else {
  25. LogManager.shared.log(category: .bluetooth, message: "Not powered on, cannot start scan.")
  26. return
  27. }
  28. centralManager.scanForPeripherals(withServices: nil, options: nil)
  29. cleanupOldDevices()
  30. }
  31. func disconnect() {
  32. if let device = activeDevice {
  33. device.disconnect()
  34. activeDevice = nil
  35. }
  36. Storage.shared.selectedBLEDevice.value = nil
  37. }
  38. func connect(device: BLEDevice) {
  39. disconnect()
  40. if let matchedType = BackgroundRefreshType.allCases.first(where: { $0.matches(device) }) {
  41. Storage.shared.backgroundRefreshType.value = matchedType
  42. Storage.shared.selectedBLEDevice.value = device
  43. switch matchedType {
  44. case .dexcomG7:
  45. activeDevice = DexcomG7HeartbeatBluetoothDevice(bluetoothDeviceDelegate: self)
  46. activeDevice?.connect()
  47. case .rileyLink:
  48. activeDevice = RileyLinkHeartbeatBluetoothDevice(bluetoothDeviceDelegate: self)
  49. activeDevice?.connect()
  50. case .silentTune, .none:
  51. return
  52. }
  53. } else {
  54. LogManager.shared.log(category: .bluetooth, message: "No matching BackgroundRefreshType found for this device.")
  55. }
  56. }
  57. func stopScanning() {
  58. centralManager.stopScan()
  59. }
  60. private func addOrUpdateDevice(_ device: BLEDevice) {
  61. if let idx = devices.firstIndex(where: { $0.id == device.id }) {
  62. devices[idx] = device.updateLastSeen()
  63. } else {
  64. var newDevice = device
  65. newDevice.lastSeen = Date()
  66. devices.append(newDevice)
  67. }
  68. devices = devices
  69. }
  70. private func cleanupOldDevices() {
  71. let expirationDate = Date().addingTimeInterval(-600) // 10 minutes ago
  72. // Get the selected device's ID (if any)
  73. let selectedDeviceID = Storage.shared.selectedBLEDevice.value?.id
  74. // Filter devices, keeping those seen within the last 10 minutes or the selected device
  75. devices = devices.filter { $0.lastSeen > expirationDate || $0.id == selectedDeviceID }
  76. }
  77. }
  78. // MARK: - CBCentralManagerDelegate
  79. extension BLEManager: CBCentralManagerDelegate {
  80. func centralManagerDidUpdateState(_ central: CBCentralManager) {
  81. switch central.state {
  82. case .poweredOn:
  83. print("[BLE] Central poweredOn.")
  84. default:
  85. print("[BLE] Central state = \(central.state.rawValue), not powered on.")
  86. }
  87. }
  88. func centralManager(_ central: CBCentralManager,
  89. didDiscover peripheral: CBPeripheral,
  90. advertisementData: [String: Any],
  91. rssi RSSI: NSNumber) {
  92. let uuid = peripheral.identifier
  93. let services = (advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID])?
  94. .map { $0.uuidString }
  95. let device = BLEDevice(
  96. id: uuid,
  97. name: peripheral.name,
  98. rssi: RSSI.intValue,
  99. advertisedServices: services,
  100. lastSeen: Date()
  101. )
  102. addOrUpdateDevice(device)
  103. }
  104. }
  105. extension BLEManager: BluetoothDeviceDelegate {
  106. func didConnectTo(bluetoothDevice: BluetoothDevice) {
  107. LogManager.shared.log(category: .bluetooth, message: "Connected to: \(String(describing: bluetoothDevice.deviceName))")
  108. if var device = Storage.shared.selectedBLEDevice.value {
  109. device.isConnected = true
  110. Storage.shared.selectedBLEDevice.value = device.updateLastConnected()
  111. }
  112. }
  113. func didDisconnectFrom(bluetoothDevice: BluetoothDevice) {
  114. LogManager.shared.log(category: .bluetooth, message: "Disconnect from: \(String(describing: bluetoothDevice.deviceName))")
  115. if var device = Storage.shared.selectedBLEDevice.value {
  116. device.isConnected = false
  117. Storage.shared.selectedBLEDevice.value = device
  118. }
  119. }
  120. func heartBeat() {
  121. LogManager.shared.log(category: .bluetooth, message: "Heartbeat triggered")
  122. TaskScheduler.shared.checkTasksNow()
  123. }
  124. }