Como é que tudo começou?
Há cerca de um ano, comprei este dispositivo para monitorar a frequência cardíaca (doravante - HR) durante o treinamento. O sensor se conecta perfeitamente ao telefone, smart watch via Bluetooth, mas geralmente os aplicativos de fitness que analisam esse tipo de dados exigem uma assinatura ou são carregados com analistas desnecessariamente complexos que não são muito interessantes para mim como um usuário comum. Portanto, tive a ideia de escrever meu próprio aplicativo para monitorar a frequência cardíaca durante os treinos para IOS no Swift.
Um pouco de teoria sobre a tecnologia Bluetooth LE
Bluetooth Low Energy é um protocolo de troca de dados muito popular e difundido que usamos em todos os lugares e que está se tornando cada vez mais popular a cada dia. Eu até tenho uma chaleira na cozinha que é controlada remotamente via BLE. A propósito, baixo consumo de energia reduziu muito o consumo de energia, ao contrário do Bluetooth "puro", tão reduzido que o dispositivo está pronto para se comunicar usando este protocolo em uma bateria por vários meses, ou até anos.
Claro, não há sentido em citar e reescrever a especificação do protocolo BLE 5.2, portanto, nos restringiremos aos conceitos básicos.
Dispositivo central e periférico
Dependendo do uso e da finalidade, o dispositivo Bluetooth pode ser:
Central (principal) - recebe dados de um dispositivo periférico (nosso telefone)
- , ( )
, : , . , , , .
. , , , , , :
() - , . .
- . , .
, , - . 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)")
}
}
!
. :)
Em geral, o guia sobre como usar o Bluetooth para conectar um sensor de frequência cardíaca saiu um pouco grande e às vezes difícil, espero ter conseguido transmitir o significado principal. Claro, existem mais alguns métodos não implementados que podem ser adicionados (por exemplo, o método de reconexão quando a conexão é interrompida), mas eu considerei este conjunto suficiente para apreciar moderadamente a concisão e conveniência da biblioteca no CoreBluetooth rápido.
Muito sucesso e obrigado!