How did it all start?
About a year ago, I purchased this device to monitor the heart rate (hereinafter - HR) during training. The sensor connects perfectly to the phone, smart watch via Bluetooth, but usually fitness applications that analyze this kind of data require either a subscription or are loaded with unnecessarily complex analysts that are not very interesting to me as an ordinary user. Therefore, I had the idea to write my own application for monitoring heart rate during workouts for IOS on Swift.
A bit of theory about Bluetooth LE technology
Bluetooth Low Energy is a very popular and widespread data exchange protocol that we use everywhere and which is becoming more and more popular every day. I even have a kettle in the kitchen that is controlled remotely via BLE. Low energy, by the way, much reduced power consumption, in contrast to "bare" Bluetooth, so reduced that the device is ready to communicate using this protocol on one battery for several months, or even years.
Of course, there is no point in quoting and rewriting the BLE 5.2 protocol specification , so we will restrict ourselves to basic concepts.
Central and peripheral device
Depending on the use and purpose, the Bluetooth device can be:
Central (main) - receives data from a peripheral device (our phone)
- , ( )
, : , . , , , .
. , , , , , :
() - , . .
- . , .
, , - . 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)")
}
}
!
. :)
In general, the guide on using Bluetooth to connect a heart rate sensor came out a little large and sometimes difficult, I hope that I managed to convey the main meaning. Of course, there are a few more unimplemented methods that could be added (for example, the reconnection method when the connection is broken), but I considered this set sufficient to moderately appreciate the conciseness and convenience of the library on swift CoreBluetooth.
All success and thanks!