diff --git a/Komusou/BluetoothCadenceSensor.swift b/Komusou/BluetoothCadenceSensor.swift index d38c66e..66ee167 100644 --- a/Komusou/BluetoothCadenceSensor.swift +++ b/Komusou/BluetoothCadenceSensor.swift @@ -5,122 +5,16 @@ // Created by gurrium on 2022/03/17. // +import Combine import CoreBluetooth -import Foundation -final class BluetoothCadenceSensor: NSObject, CadenceSensor { - // CadenceSensor - var delegate: CadenceSensorDelegate? +final class BluetoothCadenceSensor: CadenceSensor { + private(set) var cadence: Published.Publisher! + @Published + private var _cadence: Int? - // TODO: スピードでも使うので一元管理する - // TODO: 複数のCBCentralManagerを作ってもいいならこのまま - private let centralManager = CBCentralManager() - - private var isBluetoothEnabled = false { - didSet { - if isBluetoothEnabled { - centralManager.scanForPeripherals(withServices: [.cyclingSpeedAndCadence], options: nil) - } - } - } - - private var connectedPeripheral: CBPeripheral? - // speed measurement - private var cadence: Double = 0 { - didSet { - delegate?.onCadenceUpdate(cadence) - } - } - - private var previousCrankEventTime: UInt16? - private var previousCumulativeCrankRevolutions: UInt16? - private var cadenceMeasurementPauseCounter = 0 { - didSet { - if cadenceMeasurementPauseCounter > 2 { - cadence = 0 - } - } - } - - override init() { - super.init() - - centralManager.delegate = self - } -} - -extension BluetoothCadenceSensor: CBCentralManagerDelegate { - func centralManagerDidUpdateState(_ central: CBCentralManager) { - isBluetoothEnabled = central.state == .poweredOn - } - - func centralManager(_: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData _: [String: Any], rssi _: NSNumber) { - // ここで参照を保持しないと破棄される - connectedPeripheral = peripheral - - centralManager.connect(peripheral, options: nil) - } - - func centralManager(_: CBCentralManager, didConnect peripheral: CBPeripheral) { - peripheral.delegate = self - peripheral.discoverServices([.cyclingSpeedAndCadence]) - } -} - -extension BluetoothCadenceSensor: CBPeripheralDelegate { - func peripheral(_ peripheral: CBPeripheral, didDiscoverServices _: Error?) { - guard let service = peripheral.services?.first(where: { $0.uuid == .cyclingSpeedAndCadence }) else { return } - - peripheral.discoverCharacteristics([.cscMeasurement], for: service) - } - - func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error _: Error?) { - guard let characteristic = service.characteristics?.first(where: { $0.uuid == .cscMeasurement }), - characteristic.properties.contains(.notify) else { return } - - peripheral.setNotifyValue(true, for: characteristic) - } - - func peripheral(_: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error _: Error?) { - guard let data = characteristic.value else { return } - - let value = [UInt8](data) - guard (value[0] & 0b0010) > 0 else { return } - - // ref: https://www.bluetooth.com/specifications/specs/gatt-specification-supplement-5/ - if let retrieved = parseCadence(from: value) { - cadenceMeasurementPauseCounter = 0 - - cadence = retrieved - } else { - cadenceMeasurementPauseCounter += 1 - } - } - - private func parseCadence(from value: [UInt8]) -> Double? { - precondition(value[0] & 0b0010 > 0, "Crank Revolution Data Present Flag is not set") - - let cumulativeCrankRevolutions = (UInt16(value[2]) << 8) + UInt16(value[1]) - let crankEventTime = (UInt16(value[4]) << 8) + UInt16(value[3]) - - defer { - previousCumulativeCrankRevolutions = cumulativeCrankRevolutions - previousCrankEventTime = crankEventTime - } - - guard let previousCumulativeCrankRevolutions = previousCumulativeCrankRevolutions, - let previousCrankEventTime = previousCrankEventTime else { return nil } - - let duration: UInt16 - - if previousCrankEventTime > crankEventTime { - duration = UInt16((UInt32(crankEventTime) + UInt32(UInt16.max) + 1) - UInt32(previousCrankEventTime)) - } else { - duration = crankEventTime - previousCrankEventTime - } - - guard duration > 0 else { return nil } - - return (Double(cumulativeCrankRevolutions - previousCumulativeCrankRevolutions) * 60) / (Double(duration) / 1024) + init() { + cadence = $_cadence + BluetoothManager.shared().$cadence.assign(to: &$_cadence) } } diff --git a/Komusou/BluetoothManager.swift b/Komusou/BluetoothManager.swift index 7c0de5b..26b7ffe 100644 --- a/Komusou/BluetoothManager.swift +++ b/Komusou/BluetoothManager.swift @@ -81,7 +81,9 @@ final class BluetoothManager: NSObject { typealias ConnectingWithPeripheralFuture = Future private static var sharedBluetoothManager: BluetoothManager! + // TODO: enum UserDefaultsKeyを作るとよさそう private static let kSavedSpeedSensorUUIDKey = "speed_sensor_uuid_key" + private static let kSavedCadenceSensorUUIDKey = "cadence_sensor_uuid_key" static func shared() -> BluetoothManager { precondition(sharedBluetoothManager != nil, "Must call setUp(centralManager:) before use") @@ -120,6 +122,26 @@ final class BluetoothManager: NSObject { private var connectingSpeedSensorUUID: UUID? private var speedSensorPromise: ConnectingWithPeripheralFuture.Promise? + // MARK: Cadence + + @Published + private(set) var cadence: Int? + @Published + private(set) var connectedCadenceSensor: Peripheral? + private var previousCrankEventTime: UInt16? + private var previousCumulativeCrankRevolutions: UInt16? + private var cadenceMeasurementPauseCounter = 0 { + didSet { + if cadenceMeasurementPauseCounter > 2 { + cadence = 0 + } + } + } + @AppStorage(kSavedCadenceSensorUUIDKey) + private var savedCadenceSensorUUID: UUID? + private var connectingCadenceSensorUUID: UUID? + private var cadenceSensorPromise: ConnectingWithPeripheralFuture.Promise? + private let centralManager: CentralManager private var cancellables = Set() private var scannedSensors = [UUID: Peripheral]() { @@ -142,20 +164,29 @@ final class BluetoothManager: NSObject { { connectToSpeedSensor(speedSensor) } - // TODO: ケイデンスセンサー + if let savedCadenceSensorUUID = savedCadenceSensorUUID, + let cadenceSensor = self.centralManager.retrievePeripherals(withIdentifiers: [savedCadenceSensorUUID]).first + { + connectToCadenceSensor(cadenceSensor) + } $connectedSpeedSensor.sink { [unowned self] sensor in self.savedSpeedSensorUUID = sensor?.identifier } .store(in: &cancellables) + $connectedCadenceSensor.sink { [unowned self] sensor in + self.savedCadenceSensorUUID = sensor?.identifier + } + .store(in: &cancellables) } deinit { - // TODO: - // ケイデンスセンサーもやる if let connectedSpeedSensor = connectedSpeedSensor { centralManager.cancelPeripheralConnection(connectedSpeedSensor) } + if let connectedCadenceSensor = connectedCadenceSensor { + centralManager.cancelPeripheralConnection(connectedCadenceSensor) + } stopScan() } @@ -192,6 +223,27 @@ final class BluetoothManager: NSObject { connectingSpeedSensorUUID = speedSensor.identifier centralManager.connect(speedSensor, options: nil) } + + func connectToCadenceSensor(uuid: UUID) -> ConnectingWithPeripheralFuture { + guard let peripheral = scannedSensors[uuid] else { + return .init { $0(.failure(.init())) } + } + + return .init { [weak self] promise in + self?.cadenceSensorPromise = promise + + self?.connectToCadenceSensor(peripheral) + } + } + + private func connectToCadenceSensor(_ cadenceSensor: Peripheral) { + if let cadenceSensor = connectedCadenceSensor { + centralManager.cancelPeripheralConnection(cadenceSensor) + } + + connectingCadenceSensorUUID = cadenceSensor.identifier + centralManager.connect(cadenceSensor, options: nil) + } } extension BluetoothManager: CentralManagerDelegate { @@ -220,6 +272,13 @@ extension BluetoothManager: CentralManagerDelegate { connectedSpeedSensor = peripheral speedSensorPromise?(.success(())) + peripheral.delegate = self + peripheral.discoverServices([.cyclingSpeedAndCadence]) + case connectingCadenceSensorUUID: + connectingCadenceSensorUUID = nil + connectedCadenceSensor = peripheral + cadenceSensorPromise?(.success(())) + peripheral.delegate = self peripheral.discoverServices([.cyclingSpeedAndCadence]) default: @@ -236,6 +295,9 @@ extension BluetoothManager: CentralManagerDelegate { case connectingSpeedSensorUUID: connectingSpeedSensorUUID = nil speedSensorPromise?(.failure(.init())) + case connectingCadenceSensorUUID: + connectingCadenceSensorUUID = nil + cadenceSensorPromise?(.failure(.init())) default: break } @@ -279,6 +341,10 @@ extension BluetoothManager: PeripheralDelegate { speedMeasurementPauseCounter = 0 speed = retrieved + } else if let retrieved = parseCadence(from: value) { + cadenceMeasurementPauseCounter = 0 + + cadence = retrieved } else { speedMeasurementPauseCounter += 1 } @@ -314,4 +380,31 @@ extension BluetoothManager: PeripheralDelegate { return revolutionsPerSec * Double(tireSize.circumference) * 3600 / 1_000_000 // [km/h] } + + private func parseCadence(from value: [UInt8]) -> Int? { + precondition(value[0] & 0b0010 > 0, "Crank Revolution Data Present Flag is not set") + + let cumulativeCrankRevolutions = (UInt16(value[2]) << 8) + UInt16(value[1]) + let crankEventTime = (UInt16(value[4]) << 8) + UInt16(value[3]) + + defer { + previousCumulativeCrankRevolutions = cumulativeCrankRevolutions + previousCrankEventTime = crankEventTime + } + + guard let previousCumulativeCrankRevolutions = previousCumulativeCrankRevolutions, + let previousCrankEventTime = previousCrankEventTime else { return nil } + + let duration: UInt16 + + if previousCrankEventTime > crankEventTime { + duration = UInt16((UInt32(crankEventTime) + UInt32(UInt16.max) + 1) - UInt32(previousCrankEventTime)) + } else { + duration = crankEventTime - previousCrankEventTime + } + + guard duration > 0 else { return nil } + + return Int((Double(cumulativeCrankRevolutions - previousCumulativeCrankRevolutions) * 60) / (Double(duration) / 1024)) + } } diff --git a/Komusou/BluetoothSpeedSensor.swift b/Komusou/BluetoothSpeedSensor.swift index a749eb1..bfec553 100644 --- a/Komusou/BluetoothSpeedSensor.swift +++ b/Komusou/BluetoothSpeedSensor.swift @@ -7,10 +7,9 @@ import Combine import CoreBluetooth -import Foundation final class BluetoothSpeedSensor: SpeedSensor { - var speed: Published.Publisher! + private(set) var speed: Published.Publisher! @Published private var _speed: Double?