Conexión de un sensor de frecuencia cardíaca para el pecho a través de Bluetooth a Swift

¿Cómo comenzó todo?

Hace aproximadamente un año, compré este dispositivo para controlar la frecuencia cardíaca (en adelante, FC) durante el entrenamiento. El sensor se conecta perfectamente al teléfono, reloj inteligente a través de Bluetooth, pero generalmente las aplicaciones de fitness que analizan este tipo de datos requieren una suscripción o están cargadas con analistas innecesariamente complejos que no me interesan mucho como usuario común. Por lo tanto, tuve la idea de escribir mi propia aplicación para monitorear la frecuencia cardíaca durante los entrenamientos para IOS en Swift.





Un poco de teoría sobre la tecnología Bluetooth LE

Bluetooth Low Energy es un protocolo de intercambio de datos muy popular y extendido que usamos en todas partes y que cada día es más popular. Incluso tengo una tetera en la cocina que se controla de forma remota a través de BLE. Por cierto, poca energía, consumo de energía muy reducido, en contraste con el Bluetooth "desnudo", tan reducido que el dispositivo está listo para comunicarse usando este protocolo con una batería durante varios meses, o incluso años.





Por supuesto, no tiene sentido citar y reescribir la especificación del protocolo BLE 5.2, por lo que nos limitaremos a los conceptos básicos.





Dispositivo central y periférico

Dependiendo del uso y propósito, el dispositivo Bluetooth puede ser:





  • Central (principal): recibe datos de un dispositivo periférico (nuestro teléfono)





  • - , ( )





, : , . , , , .





. , , , , , :





  • () - , . .





  • - . , .





, , - . UUID, 16- 128-, .





Xcode , Label Main.storyboard outlets labels View Controller, constraints, viewDidLoad, :





outlets "121" "", view, .





, Bluetooth.





Info.plist : Bluetooth Always Usage Description , Bluetooth . , "" . !





Bluetooth

, :





import CoreBluetooth
      
      



, , , .





() :





var centralManager: CBCentralManager!
      
      



, ViewController , CBCentralManagerDelegate. extension ViewController, .





extension ViewController: CBCentralManagerDelegate {}
      
      



Xcode : "Type 'ViewController' does not conform to protocol 'CBCentralManagerDelegate'", , : "func centralManagerDidUpdateState(_ central: CBCentralManager)". "fix", . , .





, "func centralManagerDidUpdateState(_ central: CBCentralManager)" :





 func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
        }
      
      



Xcode , . print(" "):





   extension ViewController: CBCentralManagerDelegate {
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        switch central.state {
        case .unknown:
            print ("central.state is unknown")
        case .resetting:
            print ("central.state is resetting")
        case .unsupported:
            print ("central.state is unsupported")
        case .unauthorized:
            print ("central.state is unauthorized")
        case .poweredOff:
            print ("central.state is poweredOff")
        case .poweredOn:
            print ("central.state is poweredOn")
        @unknown default:
            break
        }
    }
}
      
      



"centralManager" . "viewDidLoad", "nil", Bluetooth .





override func viewDidLoad() {
        super.viewDidLoad()
        centralManager = CBCentralManager(delegate: self, queue: nil)
        heartRateLabel.isHidden = true
        bodyLocationLabel.isHidden = true
    }
      
      



, Bluetooth, , "central.state is poweredOn", , . Bluetooth , "central.state is poweredOff".





Bluetooth

, . "centralManagerDidUpdateState" ".poweredOn" "print" :





centralManager.scanForPeripherals(withServices: nil)
      
      



, , extension ViewController "centralManagerDidUpdateState" :





 func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        print(peripheral)
    }
      
      



... . ! . , , .





UUID

Bluetooth , , UUID . UUID , : "0x180D". outlets:





let heartRateUUID = CBUUID(string: "0x180D")
      
      



"centralManager.scanForPeripherals(withServices: nil)" :





case .poweredOn:
            print ("central.state is poweredOn")
            centralManager.scanForPeripherals(withServices: [heartRateUUID] )
      
      



UUID, :





<CBPeripheral: 0x280214000, identifier = D5A5CD3E-33AC-7245-4294-4FFB9B986DFC, name = COOSPO H6 0062870, state = disconnected>





, , "var centralManager: CBCentralManager!" :





var heartRatePeripheral: CBPeripheral!
      
      



"didDiscover peripheral" :





 func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        print(peripheral)
        heartRatePeripheral = peripheral
        centralManager.stopScan()
    }
      
      



"centralManager.stopScan()":





centralManager.connect(heartRatePeripheral, options: nil)
      
      



, , "didConnect peripheral" "didDiscover peripheral", :





func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        print(" ")
    }
      
      



, " ". , .





, , (), . "heartRatePeripheral.discoverServices()" "didConnect", :





func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        print(" ")
        heartRatePeripheral.discoverServices(nil)
    }
      
      



, , "CBPeripheralDelegate" "peripheral(_:didDiscoverServices:)" :





extension ViewController: CBPeripheralDelegate {
    
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        guard let services = peripheral.services else { return }

        for service in services {
            print(service)
        }
    }
}

      
      



, . , "heartRatePeripheral". :





  func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        print(peripheral)
        heartRatePeripheral = peripheral
        
        heartRatePeripheral.delegate = self
        
        centralManager.stopScan()
        centralManager.connect(heartRatePeripheral, options: nil)
    }
      
      



, , , :





<CBService: 0x2824b4340, isPrimary = YES, UUID = Heart Rate>





<CBService: 0x2824b4240, isPrimary = YES, UUID = Battery>





<CBService: 0x2824b4280, isPrimary = YES, UUID = Device Information>





<CBService: 0x2824b4200, isPrimary = YES, UUID = 8FC3FD00-F21D-11E3-976C-0002A5D5C51B>





. UUID "heartRatePeripheral.discoverServices()"





heartRatePeripheral.discoverServices([heartRateUUID])
      
      



"<CBService: 0x2824b4340, isPrimary = YES, UUID = Heart Rate>", - (№ ).





- , , . , "didDiscoverServices - peripheral" - :





extension ViewController: CBPeripheralDelegate {
    
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        guard let services = peripheral.services else { return }

        for service in services {
            peripheral.discoverCharacteristics(nil, for: service)
        }
    }
}
      
      



, "CBPeripheralDelegate" "didDiscoverCharacteristicsFor". :





func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        guard let characteristics = service.characteristics else { return }
        for characteristic in characteristics {
            print(characteristic)
        }
    }
      
      



, , , :





<CBCharacteristic: 0x28024c120, UUID = 2A37, properties = 0x10, value = {length = 2, bytes = 0x0469}, notifying = NO>





<CBCharacteristic: 0x28024c180, UUID = 2A38, properties = 0x2, value = {length = 1, bytes = 0x01}, notifying = NO>





, , . Bluetooth , UUID = 2A37 , UUID = 2A38 . , .





:





 let heartRateUUID = CBUUID(string: "0x180D")
 let heartRateCharacteristicCBUUID = CBUUID(string: "2A37")
 let bodyLocationCharacteristicCBUUID = CBUUID(string: "2A38")
      
      



. , ".notify" .. , ".read", .. . , .





, . "peripheral.readValue(for: characteristic)"





 func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        guard let characteristics = service.characteristics else { return }
        for characteristic in characteristics {
            peripheral.readValue(for: characteristic)
        }
    }
      
      



, , "peripheral(_:didUpdateValueFor:error:)" "CBPeripheralDelegate", , "switch - case", :





func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic,
                error: Error?) {
  switch characteristic.uuid {
    case bodySensorLocationCharacteristicCBUUID:
      print(characteristic.value ?? "no value")
    default:
      print("Unhandled Characteristic UUID: \(characteristic.uuid)")
  }
}
      
      



"1 bytes". , "data".





"" , , , . , :





      private func bodyLocation(from characteristic: CBCharacteristic) -> String {
        guard let characteristicData = characteristic.value,
              let byte = characteristicData.first else { return "Error" }
        switch byte {
        case 0: return ""
        case 1: return ""
        case 2: return ""
        case 3: return ""
        case 4: return ""
        case 5: return " "
        case 6: return ""
        default:
            return ""
        }
    }
      
      



"didUpdateValueFor characteristic", ( label ):





   func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic,
                    error: Error?) {
        
        switch characteristic.uuid {
        
        case bodyLocationCharacteristicCBUUID:
            let bodySensorLocation = bodyLocation(from: characteristic)
            bodyLocationLabel.text = bodySensorLocation
            bodyLocationLabel.isHidden = false
          
        default:
          print("Unhandled Characteristic UUID: \(characteristic.uuid)")
      }
        
    }
      
      



! , !





, , :)






, . , ".notify", " ", . "peripheral.setNotifyValue(true, for: characteristic)" "didDiscoverCharacteristicsFor service:





func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        guard let characteristics = service.characteristics else { return }
        for characteristic in characteristics {
            peripheral.readValue(for: characteristic)
            peripheral.setNotifyValue(true, for: characteristic)
        }
    }
      
      



, :





Unhandled Characteristic UUID: 2A37





Unhandled Characteristic UUID: 2A37





Unhandled Characteristic UUID: 2A37





. , . 1 2 . , "" "CBPeripheralDelegate".





  private func heartRate(from characteristic: CBCharacteristic) -> Int {
        guard let characteristicData = characteristic.value else { return -1 }
        let byteArray = [UInt8](characteristicData)
        
        let firstBitValue = byteArray[0] & 0x01
        if firstBitValue == 0 {
            return Int(byteArray[1])
        } else {
            return (Int(byteArray[1]) << 8) + Int(byteArray[2])
        }
    }
      
      



, , case "peripheral(_:didUpdateValueFor:error:)", , label :





   func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic,
                    error: Error?) {
        
        switch characteristic.uuid {
        
        case bodyLocationCharacteristicCBUUID:
            let bodySensorLocation = bodyLocation(from: characteristic)
            bodyLocationLabel.text = bodySensorLocation
            bodyLocationLabel.isHidden = false
            
        case heartRateCharacteristicCBUUID:
            let bpm = heartRate(from: characteristic)
            heartRateLabel.text = String(bpm)
            heartRateLabel.isHidden = false
            
        default:
          print("Unhandled Characteristic UUID: \(characteristic.uuid)")
      }
    }
      
      



!





. :)






En general, la guía sobre el uso de Bluetooth para conectar un sensor de frecuencia cardíaca resultó un poco grande y, a veces, difícil, espero haber logrado transmitir el significado principal. Por supuesto, hay algunos métodos más no implementados que podrían agregarse (por ejemplo, el método de reconexión cuando la conexión se rompe), pero consideré que este conjunto era suficiente para apreciar moderadamente la concisión y la conveniencia de la biblioteca en el rápido CoreBluetooth.





¡Todo éxito y gracias!








All Articles