iOS蓝牙框架CoreBluetooth及小米手环Demo

章节目录

  • iOS蓝牙框架介绍
  • CoreBluetooth.framework导入
  • CoreBluetooth的基础使用
  • 蓝牙连接所涉及到的类
  • 小米手环Demo

iOS蓝牙框架介绍

CoreBluetooth介绍

在iOS开发中,实现蓝牙通信的方法有两种。分别是GameKit.framework以及CoreBluetooth.framework,前者在iOS5后基本被淘汰。

在苹果文档中,写了Communicate with Bluetooth 4.0 low-energy devices,也就是说仅支持蓝牙4.0低功耗协议(BLE)。

对于iOS10以上的设备,苹果注明以下信息:

An iOS app linked on or after iOS 10.0 must include in its Info.plist file the usage description keys for the types of data it needs to access or it will crash. To access Bluetooth peripheral data specifically, it must include NSBluetoothPeripheralUsageDescription.

也就是说需要声明并注册蓝牙权限的使用。

CoreBluetooth协议
首先提及蓝牙使用,在此引入两个概念:中心设备和外围设备。

  • 中心设备(客服端):作为中央管理器的设备,也就是本实例中的iOS设备。
  • 外围设备(服务器):也就是外部设备,扮演者产生数据的角色。许多传感器、蓝牙服务设备均是外围设备。本实例中小米手环就是外围设备。

同时数据传输还涉及到以下几个值:

  • UUID:相当与使用这个模块对映的应用的标识。
  • RSSI:信号强度,利用此信息可进行蓝牙测距,后面将进行讲解。

CoreBluetooth中涉及以下对象类:

  • CBCentralManager:中心设备类
  • CBPeripheral:外围设备类
  • CBCharacteristic:设备特征类

接下来就看一下如何导入蓝牙框架。


CoreBluetooth.framework导入

  1. 首先新建Xcode项目
  2. 在General->TARGETS->Linked Framworks and Libraries中点击添加并选择CoreBluetooth.framework导入。
    导入CoreBluetooth.framework
  3. 在代码中导入CoreBluetooth.framework
    Swift:import CoreBluetooth
    Objective-C:#import
  4. 声明协议:使用CoreBluetooth需要支持CBCentralManagerDelegate, CBPeripheralDelegate协议,即前面所说的中心设备和外围设备,并实现相应方法



CoreBluetooth的基础使用

导入框架并声明协议后,即可开始实现必要方法。
下面通过展示整个流程所需要的方法

  • 初始化

    1
    var manager = CBCentralManager.init(delegate: self as? CBCentralManagerDelegate, queue: nil)
  • 扫描设备

    1
    2
    3
    4
    5
    6
    7
    switch manager.state {
    case .poweredOn:
    NSLog("正在扫描")
    manager.scanForPeripherals(withServices: nil, options: nil)
    default:
    break
    }

注:这里为什么一定要用switch语法?
因为CBCentralManager的State属性在之前是CBCentralManagerState,但是现在变成了CBManagerState,而需要iOS10以上才支持后者(23333)。这一波强制升级我是拒绝的,找了很多方法之后,发现这样写可以被Xcode接受而不去检查

  • 处理当前中心设备蓝牙状态

    1
    2
    3
    4
    5
    6
    7
    8
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
    switch central.state {
    case .poweredOn:
    NSLog("蓝牙已开启")
    default:
    NSLog("蓝牙未开启")
    }
    }
  • 扫描到外围设备的处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {

    //peripheral.name为设备名称
    //可以调用CBCentralManager的stopScan停止扫描
    central.stopScan()
    //可以调用CBCentralManager的connect连接设备
    central.connect(peripheral, options: nil)
    }
    }
  • 成功连接到外围设备的处理

    1
    2
    3
    4
    5
    6
    7
    unc centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
    NSLog("成功连接")
    //设置代理
    peripheral.delegate = self
    //连接成功后接下来该扫描服务
    peripheral.discoverServices(nil)
    }
  • 连接失败的处理

    1
    2
    3
    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
    NSLog("连接设备失败")
    }
  • 扫描已连接外围设备服务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
    if ((error) != nil) {
    NSLog("查找服务失败")
    return
    }
    else {
    for service in peripheral.services! {
    //扫描到服务后对服务逐个扫描特征值
    peripheral.discoverCharacteristics(nil, for: service)
    }
    }
    }
  • 扫描到特征值后的操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
    if ((error) != nil) {
    NSLog("扫描特征值失败")
    }
    else {
    for characteristic in service.characteristics! {
    //设置特征值只要有更新就获取
    peripheral.setNotifyValue(true, for: characteristic)

    if (characteristic.uuid.uuidString == "你想匹配的字符串") {
    peripheral.readValue(for: characteristic)
    //或
    characSaver = characteristic
    }
    }
    }
    }

解释一下原理:方法对每个服务进行扫描,获取特征值。辨别是否是你想要的功能的特征值就要用到UUID,用UUID去匹配。匹配到后你可以选择保存他的特征值从而在后面自行操作,或者用readValue读取它的值,并由系统自动调用下面介绍的方法

  • 获取具体值之后的操作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
    if ((error) != nil) {
    statusLabel.text = "从设备获取值失败"
    return
    }
    else {
    if(characteristic.uuid.uuidString == "特定值") {
    var infoBytes = [UInt8](characteristic.value!)
    var infoVal:Int = Int.init(infoBytes[0])
    NSLog("获取的值为:%d\n", infoVal)
    }
    }
    }

蓝牙连接所涉及到的类

CBCentralManager
此类为中心设备类,用于控制作为中心设备时的行为

  • state:获取当前中心设备状态
  • isScanning:当前中心设备是否在扫描外围设备
  • stopScan():停止扫描外围设备
  • scanForPeripherals(...):扫描外围设备(请确保蓝牙开启)
  • connect(...):连接外围设备(需要先扫描到外围设备)
  • cancelPeripheralConnection(...):断开外围设备

CBPeripheral
此类为外围设备类,用于对外围设备进行管理

  • name:获取外围设备的名称
  • rssi:获取当前外围设备的信号强度
  • state:获取外围设备的状态(disconnected/connecting/connected)
  • services:获取外围设备所提供的服务(需要先扫描到服务)
  • discoverServices(...):扫描设备所提供的服务
  • discoverCharacteristics(...):扫描特征值(需要先获取服务)
  • readValue(...):读取特征值所对应的值(需要先获取到特征值,同时要注意此方法不反回值,要用协议的didUpdateValueFor characteristic方法处理)

中心设备与外围设备关系

CBCharacteristic
外围设备服务的特征值

  • Value:获取特征值对应的值

小米手环Demo开发

现在尝试通过CoreBluetooth的内容,来实现小米手环的连接、振动与停止振动功能。特征值基于小米手环1,有兴趣可以收集其他设备特征值。

界面搭建

方便起见,该项目直接采用storyboard搭建,不再赘述项目Demo

故事板

1
2
3
4
5
6
7
8
@IBOutlet weak var scanButton: UIButton!
@IBOutlet weak var stopButton: UIButton!
@IBOutlet weak var vibrateButton: UIButton!
@IBOutlet weak var stopVibrateButton: UIButton!
@IBOutlet weak var loadingInd: UIActivityIndicatorView!
@IBOutlet weak var statusLabel: UILabel!
@IBOutlet weak var resultField: UITextView!
@IBOutlet weak var vibrateLevel: UISegmentedControl!

设置蓝牙操作过程所需对象

按上述讲解,定义所需的对象,包括中心设备外围设备等。

1
2
3
var theManager: CBCentralManager!
var thePerpher: CBPeripheral!
var theVibrator: CBCharacteristic!

实现CBCentralManagerDelegate协议

按上文所述实现CBCentralManagerDelegate协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
theManager = CBCentralManager.init(delegate: self as? CBCentralManagerDelegate, queue: nil)
self.scanButton.isEnabled = false
statusLabel.text = ""
loadingInd.isHidden = true
}

// 扫描并连接
@IBAction func startConnectAction(_ sender: UIButton) {
switch theManager.state {
case .poweredOn:
statusLabel.text = "正在扫描…"
theManager.scanForPeripherals(withServices: nil, options: nil)
self.loadingInd.startAnimating()
self.scanButton.isEnabled = false
self.isDisconnected = false
default:
break
}
}

@IBAction func disconnectAction(_ sender: UIButton) {
if ((thePerpher) != nil) {
theManager.cancelPeripheralConnection(thePerpher)
thePerpher = nil
theVibrator = nil
statusLabel.text = "设备已断开"
scanButton.isEnabled = true
isDisconnected = true
isVibrating = false
}
}

@IBAction func vibrateAction(_ sender: Any) {
if ((thePerpher != nil) && (theVibrator != nil)) {
let data: [UInt8] = [UInt8.init(vibrateLevel.selectedSegmentIndex+1)];
let theData: Data = Data.init(bytes: data)
thePerpher.writeValue(theData, for: theVibrator, type: CBCharacteristicWriteType.withoutResponse)
}
}

@IBAction func stopVibrateAction(_ sender: UIButton) {
if ((thePerpher != nil) && (theVibrator != nil)) {
let data: [UInt8] = [UInt8.init(0)];
let theData: Data = Data.init(bytes: data)
thePerpher.writeValue(theData, for: theVibrator, type: CBCharacteristicWriteType.withoutResponse)
isVibrating = false
}
}


// 处理当前蓝牙主设备状态
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOn:
statusLabel.text = "蓝牙已开启"
self.scanButton.isEnabled = true
default:
statusLabel.text = "蓝牙未开启!"
self.loadingInd.stopAnimating()
}
}

// 扫描到设备
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
if (peripheral.name?.hasSuffix("MI"))! {
thePerpher = peripheral
central.stopScan()
central.connect(peripheral, options: nil)
statusLabel.text = "搜索成功,开始连接"

}
// 特征值匹配请用 peripheral.identifier.uuidString
resultField.text = String.init(format: "发现手环\n名称:%@\nUUID:%@\n", peripheral.name!, peripheral.identifier.uuidString)
}

// 成功连接到设备
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
statusLabel.text = "连接成功,正在扫描信息..."
peripheral.delegate = self
peripheral.discoverServices(nil)
}

// 连接到设备失败
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
loadingInd.stopAnimating()
statusLabel.text = "连接设备失败"
scanButton.isEnabled = true
}

// 扫描服务
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
if ((error) != nil) {
statusLabel.text = "查找服务失败"
loadingInd.stopAnimating()
scanButton.isEnabled = true
return
}
else {
for service in peripheral.services! {
peripheral.discoverCharacteristics(nil, for: service)
}
}
}

// 扫描到特征值
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
if ((error) != nil) {
statusLabel.text = "查找服务失败"
loadingInd.stopAnimating()
scanButton.isEnabled = true
return
}
else {
for characteristic in service.characteristics! {
peripheral.setNotifyValue(true, for: characteristic)

if (characteristic.uuid.uuidString == BATTERY) {
peripheral.readValue(for: characteristic)
}
else if (characteristic.uuid.uuidString == DEVICE) {
peripheral.readValue(for: characteristic)
}
else if (characteristic.uuid.uuidString == VIBRATE) {
theVibrator = characteristic
}
}
}
}

// 扫描到具体设备
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
if ((error) != nil) {
statusLabel.text = "从设备获取值失败"
return
}
else {
if(characteristic.uuid.uuidString == BATTERY) {
var batteryBytes = [UInt8](characteristic.value!)
var batteryVal:Int = Int.init(batteryBytes[0])
self.resultField.text = String.init(format: "%@电量:%d%%\n", resultField.text, batteryVal)
}
loadingInd.stopAnimating()
scanButton.isEnabled = true
statusLabel.text = "信息扫描完成!"
if (isVibrating) {
vibrateAction(Any)
}
}
}

// 与设备断开连接
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
statusLabel.text = "设备已断开"
scanButton.isEnabled = true
if(!isDisconnected) {
theManager.scanForPeripherals(withServices: nil, options: nil)
}
}

补充:
小米手环振动的UUID是2A06,0代表不振,1为短振,2为长振。

GitHub:https://github.com/Minecodecraft
Demo链接:https://github.com/Minecodecraft/MiBandDemo