在本Core Bluetooth教程中,您将学习如何从兼容的设备(如戴在胸前的心率传感器)发现,连接和检索数据。
鉴于当今小工具的激增,这些设备之间的通信可以导致以更有效的方式使用这些小工具以及这些小工具提供的信息。 为此,苹果公司推出了Core Bluetooth框架,该框架可以与许多现实世界中的设备进行通信,例如心率传感器,数字恒温器和健身器材。 如果您可以通过BLE(低功耗蓝牙)无线技术连接到它,则Core Bluetooth框架可以连接到它。
在本教程中,您将学习Core Bluetooth框架的关键概念以及如何在兼容设备中发现,连接和检索数据。 您将使用这些技能来构建与蓝牙心率传感器通信的心率监测应用程序。
我们在本教程中使用的心率传感器是Polar H7蓝牙心率传感器,但是任何其他蓝牙心率传感器也应该可以工作。
首先,让我们花点时间讨论一些特定于蓝牙的术语:中心,外围设备,服务和特性。
中央和外围设备
蓝牙设备可以是中央设备或外围设备:
- 中央:从蓝牙设备接收数据的对象。
- 外围设备:发布要由其他设备使用的数据的蓝牙设备。
在本教程中,iOS设备将成为中心设备,从外围设备接收心率数据。
广告包
蓝牙外围设备以广告包的形式广播其拥有的某些数据。 这些数据包可以包含外围设备的名称和主要功能等信息。 它们还可以包括与外围设备可以提供哪种数据有关的额外信息。
中央的工作是扫描这些广告数据包,识别它发现相关的任何外围设备,并连接到各个设备以获取更多信息。
服务与特点
广告包很小,不能包含大量信息。为了共享更多数据,中央需要连接到外围设备。
外设的数据分为服务和特征:
- 服务:描述外围设备的特定功能或特征的数据和相关行为的集合。例如,心率传感器具有心率服务。一个外围设备可以具有多个服务。
- 特征:提供有关外围设备服务的更多详细信息。 例如,“心率”服务包含“心率测量”特征,其中包含“每分钟心律”数据。 服务可以具有多个特征。 心率服务可能具有的另一个特征是“身体传感器位置”,它只是一个字符串,描述了传感器的预期身体位置。
每个服务和特征都由一个UUID表示,该UUID可以是16位或128位值。
蓝牙功能实现心率监听的实现代码
import Foundation
import CoreBluetooth
enum BlueToothStatus: Int {
case Connect
case Scaning
case ScanOverUnConnet
case Connecting
case LostConnect
case ConnectFail
case StopScan
}
//用于看发送数据是否成功!
class BlueToothTool:NSObject {
//单例对象
internal static let instance = BlueToothTool()
//中心对象
var central : CBCentralManager?
//中心扫描到的设备都可以保存起来,
//扫描到新设备后可以通过通知的方式发送出去,连接设备界面可以接收通知,实时刷新设备列表
var deviceList = [CBPeripheral]()
// 存储连接过的设备
var alreadyConnectDevices: NSMutableArray?
// 当前连接的设备
var connectPeripheral:CBPeripheral!
var isConnect = false
var isScan = false
//发送数据特征(连接到设备之后可以把需要用到的特征保存起来,方便使用)
var sendCharacteristic:CBCharacteristic?
//收到心率数据特征(连接到设备之后可以把需要用到的特征保存起来,方便使用)
var heartRateCharacteristic:CBCharacteristic?
//电量
var batteryValue:Int?
var deviceBatteryChangeBlock:((_ value:Int) -> Void)?
//连接状态改变
var deviceStateChangeBlock:((_ status:BlueToothStatus) -> Void)?
//参考蓝牙规范特性 https://www.bluetooth.com/specifications/gatt/characteristics/
let heart_rate_service = "0x180D" // 心率
let battery_service = "2A19" // 电池服务
let heart_rate_mesure = "2A37" // 心率测量
let intervalSec = 60.0
let connectingIntervalSec = 20.0
let uploadInterval = 2.0
let heartRateVM = HeartRateViewModel()
var needRemindBattery = true//刚连接时通知,后面电量变化不通知
var breakLineName = ""//如果断线,可续连
var isMutipleConnect = false
lazy var timer: Timer = {
let time = Timer.scheduledTimer(timeInterval: intervalSec, target: self, selector: #selector(stopScan), userInfo: nil, repeats: true)
return time
}()
var connectingTime: Timer!
override init() {
super.init()
self.central = CBCentralManager.init(delegate:self, queue:nil, options:[CBCentralManagerOptionShowPowerAlertKey:false])
self.deviceList = [CBPeripheral]()
self.heartRateVM.openWebSocket()
}
// MARK: 扫描设备的方法
func scanForPeripheralsWithServices(){
switch central?.state {
case .unauthorized:
print("没有蓝牙功能")
self.showBluetoothUnauthorized()
return
case .poweredOff:
self.showBluetoothUnOpenService()
return
default:
print("可用")
}
let uidstr = CBUUID(string: heart_rate_service)
self.central?.scanForPeripherals(withServices: [uidstr])
isScan = true
_ = Timer.scheduledTimer(timeInterval: intervalSec, target: self, selector: #selector(stopScan), userInfo: nil, repeats: true)
}
// MARK: 停止扫描
@objc func stopScan() {
self.central?.stopScan()
isScan = false
deviceStateChangeBlock?(.StopScan)
}
@objc func stopConecting() {
self.disConnect()
LZAlert.showAlert("连接不成功", "请确认心率带设备已打开并且在通信范围内")
}
func reScan() {
deviceList.removeAll()
self.stopScan()
self.scanForPeripheralsWithServices()
}
// MARK: 写数据
func writeToPeripheral(_ data: Data) {
connectPeripheral.writeValue(data , for: sendCharacteristic!, type: CBCharacteristicWriteType.withResponse)
}
// MARK: 连接某个设备的方法
func requestConnectPeripheral(_ model:CBPeripheral) {
deviceStateChangeBlock?(.Connecting)
self.disConnect()
self.startConnectingTimer()
central?.connect(model , options: nil)
}
func startConnectingTimer() {
self.endConnectingTimer()
_ = Timer.scheduledTimer(timeInterval: connectingIntervalSec, target: self, selector: #selector(stopConecting), userInfo: nil, repeats: false)
}
func endConnectingTimer() {
if connectingTime != nil {
connectingTime.invalidate()
}
}
func disConnect() {
for device in deviceList {
if device.state == .connected || device.state == .connecting {
central?.cancelPeripheralConnection(device)
}
}
}
}
//MARK: -- 中心管理器的代理
extension BlueToothTool : CBCentralManagerDelegate{
// MARK: 检查运行这个App的设备是不是支持BLE。
func centralManagerDidUpdateState(_ central: CBCentralManager) {
if #available(iOS 10.0, *) {
switch central.state {
case CBManagerState.poweredOn:
print("蓝牙打开")
self.scanForPeripheralsWithServices()
case CBManagerState.unauthorized:
print("没有蓝牙功能")
self.showBluetoothUnauthorized()
case CBManagerState.poweredOff:
isConnect = false
timer.invalidate()
self.stopScan()
if let currentvc = UIViewController.getCurrentViewController(),currentvc.isMember(of: HeartRateConnectController.self) {
self.showBluetoothUnOpenService()
}
print("蓝牙关闭")
default:
print("蓝牙未知状态")
}
}
// 手机蓝牙状态发生变化,可以发送通知出去。提示用户
}
// 开始扫描之后会扫描到蓝牙设备,扫描到之后走到这个代理方法
// MARK: 中心管理器扫描到了设备
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
// 因为iOS目前不提供蓝牙设备的UUID的获取,所以在这里通过蓝牙名称判断是否是本公司的设备
guard peripheral.name != nil else {
return
}
if peripheral.name == breakLineName {
//断线重连
self.requestConnectPeripheral(peripheral)
breakLineName = ""//重新连接设备滞空断线设备名
}
self.addSearchDevice(newdevice: peripheral,advertisementData:advertisementData)
}
// MARK: 连接外设成功,开始发现服务
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral){
// 设置代理
peripheral.delegate = self
// 开始发现服务
peripheral.discoverServices(nil)
isConnect = true
// 保存当前连接设备
self.connectPeripheral = peripheral
// 这里可以发通知出去告诉设备连接界面连接成功
print("连接外设成功(peripheral.name ?? "")")
self.endConnectingTimer()
self.saveAlreadyConnectDevices(device: peripheral)
self.sortDevices()
self.stopScan()
deviceStateChangeBlock?(.Connect)
needRemindBattery = true
self.heartRateVM.sendBluetoothState(true)
}
// MARK: 连接外设失败
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
// 这里可以发通知出去告诉设备连接界面连接失败'
print("连接外设失败(peripheral.name ?? "")")
isConnect = false
deviceStateChangeBlock?(.ConnectFail)
timer.invalidate()
LZAlert.showAlert("连接不成功", "请确认心率带设备已打开并且在通信范围内")
self.heartRateVM.sendBluetoothState(false)
batteryValue = 0
}
// MARK: 连接丢失
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
NotificationCenter.default.post(name: Notification.Name(rawValue: DidDisConnectPeriphernalNotification), object: nil, userInfo: ["deviceList": self.deviceList as AnyObject])
print("连接外设丢失(peripheral.name ?? "")")
isConnect = false
deviceStateChangeBlock?(.LostConnect)
// 这里可以发通知出去告诉设备连接界面连接丢失
if error != nil {//出错断开,不是手动断开
LZAlert.showAlert("连接不成功", "请确认心率带设备已打开并且在通信范围内")
self.reScan()
breakLineName = peripheral.name ?? ""
}
timer.invalidate()
self.heartRateVM.sendBluetoothState(false)
batteryValue = 0
}
}
// 外设的代理
extension BlueToothTool : CBPeripheralDelegate {
//MARK: - 匹配对应服务UUID
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?){
print("发现服务UUId")
if error != nil {
return
}
for service in peripheral.services! {
peripheral.discoverCharacteristics(nil, for: service )
}
}
//MARK: - 服务下的特征
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?){
if (error != nil){
return
}
print("characteristic====(service.characteristics!)")
for characteristic in service.characteristics! {
print("特征uuid (characteristic.uuid.uuidString)")
switch characteristic.uuid.uuidString {
case heart_rate_mesure:
// 订阅特征值,心率
peripheral.setNotifyValue(true, for: characteristic)
print("扫描到其他特征")
case battery_service:
// 读区特征值
peripheral.setNotifyValue(true, for: characteristic)
peripheral.readValue(for: characteristic)
default:
print("扫描到其他特征")
}
}
}
//MARK: - 特征的订阅状体发生变化
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?){
guard error == nil else {
return
}
}
// MARK: - 获取外设发来的数据
// 注意,所有的,不管是 read , notify 的特征的值都是在这里读取
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?)-> (){
if(error != nil){
return
}
// print("外设发来的数据====(characteristic)")
print("characteristic.uuid.uuidString====(characteristic.uuid.uuidString)")
switch characteristic.uuid.uuidString {
case heart_rate_mesure:
heartRateCharacteristic = characteristic
print("bpm === (self.heartRate())")
case battery_service:
print("接收到了电池特征的值的变化")
batteryValue = self.getBattery(characteristic)
deviceBatteryChangeBlock?(batteryValue ?? 0)
if needRemindBattery {//设备为刚连接,则电量通知出去
LZToast.showToast(UIApplication.shared.keyWindow ?? UIView(), message: "连接成功,电量剩余 (self.getBattery(characteristic))%")
}
needRemindBattery = false
default:
print("收到了其他数据特征数据: (characteristic.uuid.uuidString)")
}
}
//MARK: - 检测中心向外设写数据是否成功
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
if(error != nil){
print("发送数据失败!error信息:(String(describing: error))")
}
}
}
extension BlueToothTool {
func showBluetoothUnOpenService() {
LZToast.showToast( message: "蓝牙服务未开启哦,请你进入【设置】-【蓝牙】-中打开蓝牙")
}
func showBluetoothUnauthorized() {
let alertVc = UIAlertController.init(title: "", message: "需要授予蓝牙服务权限,才能连接心率设备,确认授权吗", preferredStyle: UIAlertController.Style.alert)
let action = UIAlertAction.init(title: "确定", style: UIAlertAction.Style.default) { (ac) in
if let urlObj = URL(string:UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(urlObj) {
if #available(iOS 10.0, *) {
UIApplication.shared.open(urlObj as URL, options: [ : ], completionHandler: { Success in
})
} else {
// Fallback on earlier versions
UIApplication.shared.openURL(urlObj)
}
}
}
let cancelaction = UIAlertAction.init(title: "忽略", style: UIAlertAction.Style.cancel) { (_) in }
alertVc.addAction(cancelaction)
alertVc.addAction(action)
alertVc.show()
}
}
extension BlueToothTool {
// add搜索到的设备
func addSearchDevice(newdevice: CBPeripheral, advertisementData: [String : Any]) {
if deviceList.count > 0 {
var isAdd = false
for device in deviceList {
if device.name == newdevice.name {
isAdd = true
}
}
if !isAdd {
deviceList.append(newdevice)
}
} else {
deviceList.append(newdevice)
}
self.sortDevices()
self.deviceStateChangeBlock?(.Scaning)
}
// 设备列表排序
func sortDevices() {
if isConnect {//增加这个判断是因为心率带老设备被连接时重新scan的时候扫描不到
var isHaveConnectDevice = false
for device in deviceList {
if device.name == connectPeripheral.name {
isHaveConnectDevice = true
break
}
}
if !isHaveConnectDevice {
deviceList.insert(connectPeripheral, at: 0)
}
}
//本点有存储连接过的设备
if let saveDeviceNames = self.getAlreadyConnectDevices() {
var sameDevice = [CBPeripheral]()
if deviceList.count > 0 {
for savedevice in saveDeviceNames {
for device in deviceList {
if savedevice == device.name {
sameDevice.append(device)
deviceList.removeAll(device)
}
}
}
}
deviceList = sameDevice + deviceList
}
}
func getAlreadyConnectDevices() -> [String]? {
if let deviceNames = UserDefaults.standard.object(forKey: SaveConnectDevice) {
return deviceNames as? [String]
}
return nil
}
func saveAlreadyConnectDevices(device: CBPeripheral) {
if let olddevices = UserDefaults.standard.object(forKey: SaveConnectDevice) as? [String] {
var olds = olddevices
for name in olds {
if device.name == name {
olds.removeAll(name)
break
}
}
let newDevices = [device.name] + olds
UserDefaults.standard.set(newDevices, forKey: SaveConnectDevice)
} else {
UserDefaults.standard.set([device.name], forKey: SaveConnectDevice)
}
UserDefaults.standard.synchronize()
}
//心率转换方法
func heartRate() -> Int {
guard let characteristicData = heartRateCharacteristic?.value else { return -1 }
let byteArray = [UInt8](characteristicData)
let firstBitValue = byteArray[0] & 0x01
if firstBitValue == 0 {
// Heart Rate Value Format is in the 2nd byte
return Int(byteArray[1])
} else {
// Heart Rate Value Format is in the 2nd and 3rd bytes
return (Int(byteArray[1]) << 8) + Int(byteArray[2])
}
}
//电量转化方法
func getBattery(_ character:CBCharacteristic?) -> Int {
guard let characteristicData = character?.value else { return 0}
let value:Int = Int([UInt8](characteristicData)[0])
return value
}
}
参考
博客:http://viadean.com/CoreBluetooth-feature.html#_np=153_809
蓝牙规范特性:https://www.bluetooth.com/specifications/gatt/characteristics/