iOS CoreBluetooth 应用学习
目前iOS的蓝牙应用主要应用在穿戴、音箱、耳机短距离传输等领域,应用场景非常广阔。而目前对于开发者来说,应用较多的只有BLE4.0,因为苹果的2.0蓝牙是需要MFI(make for iphone)验证的,而厂商的利润本来就非常低了,还得搞个MFI认证的话就不赚钱了。所以本篇文章只是学习蓝牙4.0的开发应用。
本篇主要想从两方面来分析和学习CoreBluetooth框架,让iPhone 分别作为蓝牙外设
和作为蓝牙中央
。首先我们需要先导入CoreBluetooth.framework。然后我们先来看看iOS蓝牙使用的流程,调用规则是下图这样的:
底层的GATT和ATT、L2CAP我们是不直接调用的,感兴趣的同学可以进一步学习一下GATT和L2CAP,这两个在嵌入式用得比较多。我们主要是使用CoreBluetooth提供的方法来进行与设备的交互。
手机作为蓝牙中央
通常这种情况会多一些,因为手机具备了强大的运算能力和出色的表达能力,应用领域可以参考运动手环、心率测试仪、血压计等等。也就是下面这种情况:
但是这种情况下,如果以网络的模型来看,此刻的iOS设备是作为客户端的,而蓝牙设备就作为服务器端。这是因为蓝牙外设时需要建立一个蓝牙通信,设定好服务和特征值、描述数据等信息,然后广播到空气中,iPhone通过服务搜索,发现设备,才进行连接的。
我们先来看看外设所能提供的服务信息,这些信息是可以用来标记数据交互、或者作为通道交互数据,其实它的数据模型和网络是非常类似的,可以理解为建立了多个通道的即时通讯。
蓝牙外设段可以理解为能提供以下字段作为传输通道
1、服务 Service
1.1 特征值 Characteristic
1.1.1 描述符
1.2 特征值 Characteristic
1.2.1 描述符
2、服务 Service
2.1 特征值 Characteristic
2.1.1 描述符
2.2 特征值 Characteristic
2.2.1 描述符
当iPhone作为中心的时候,主要用到的类库有两个:CBCentralManager(外围设备管理器)、CBPeripheral(远端外围设备)
CBCentralManager 介绍
iPhone是通过这个类来进行蓝牙设备的发现、管理、连接、异常处理。
初始化:
centralMgr = CBCentralManager.init(delegate: self, queue: DispatchQueue.main, options: [CBConnectPeripheralOptionNotifyOnConnectionKey:true,CBConnectPeripheralOptionNotifyOnDisconnectionKey:true,CBConnectPeripheralOptionNotifyOnNotificationKey:true])
/* 参数说明
CBCentralManagerOptionShowPowerAlertKey
填一个Bool值,用来指定如果蓝牙设备断电的时候,系统是否会发出警告
CBCentralManagerOptionRestoreIdentifierKey
填一个String作为唯一标记
*/
扫描外设:
centralMgr.scanForPeripherals(withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey:true])
/** 参数说明
CBCentralManagerScanOptionAllowDuplicatesKey
一个布尔值,指定是否应在没有重复过滤的情况下运行扫描。
CBCentralManagerScanOptionSolicitedServiceUUIDsKey
指定服务UUID的数组扫描特定设备
*/
停止扫描:
centralMgr.stopScan()
连接设备:
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = itemArray[indexPath.row]
cbPeripheral = item
centralMgr.connect(item, options: [CBConnectPeripheralOptionNotifyOnConnectionKey:true,CBConnectPeripheralOptionNotifyOnDisconnectionKey:true])
tableView.deselectRow(at: indexPath, animated: true)
}
/** 特别说明一下连接的可选参数
CBConnectPeripheralOptionNotifyOnConnectionKey
填一个Bool值,指定后台连接外围设备时,是否告知系统,并弹窗提示
CBConnectPeripheralOptionNotifyOnDisconnectionKey
填一个Bool值,指定后台断开外围设备时,是否告知系统,并弹窗提示
CBConnectPeripheralOptionNotifyOnNotificationKey
填一个Bool值,指定系统是否对外围发过来的每一个通知都弹窗提示
CBConnectPeripheralOptionEnabTransportBridgeingKey
如果已经通过低功耗蓝牙连接,则可以桥接经典蓝牙的配置文件(GATT)
CBConnectPeripheralOptionRequiresANCS
填一个Bool值,设定连接设备时是否需要连接(ANCS)服务,接收推送服务
CBConnectPeripheralOptionStarDelayKey
填一个Bool,设置系统连接前是否要延迟
*/
CBCentralManager是通过接收代理的方式来获得设备的连接状态以及信息变动,所以可以参考以下的代理消息设置:
/// 连接上了
/// - Parameters:
/// - central: centerMgr
/// - peripheral: 蓝牙外设
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
//连接上后,选择跳到另一个界面去进行更多的通讯交互
// performSegue(withIdentifier: "peripheralSegue", sender: peripheral)
}
/// 断开连接
/// - Parameters:
/// - central: centerMgr
/// - peripheral: 蓝牙外设
/// - error: 错误原因
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
}
///ANCS授权状态回调
func centralManager(_ central: CBCentralManager, didUpdateANCSAuthorizationFor peripheral: CBPeripheral) {
}
/// 发现蓝牙设备回调
/// - Parameters:
/// - central: centerMgr
/// - peripheral: 蓝牙外设
/// - advertisementData: 描述
/// - RSSI: 信号强度
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
if !itemArray.contains(peripheral) && (peripheral.name != nil){
itemArray.append(peripheral)
listTable.reloadData()
}
}
//断开回连
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
centralMgr.connect(cbPeripheral, options: [CBConnectPeripheralOptionNotifyOnConnectionKey:true,CBConnectPeripheralOptionNotifyOnDisconnectionKey:true])
}
CBPeripheral 蓝牙设备的交互
其实这个CBPeripheral就是俄罗斯套娃的结构,一层环节一层,先去获取了服务,然后根据服务获取服务的特征值、描述符;
发现服务:
/*
1、discoverServices 发现外围设备的指定服务
2、discoverIncludedServices([CBUUID]?,for:CBService) 发现先前发现的服务中所包含的服务
3、services:[CBService]? 外围设备已发现的服务列表
*/
mainPeripheral.discoverServices(nil)
发现服务回调:
//服务发现回调
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
for service in peripheral.services! {
//根据设备回调的服务,再去请求特征值
peripheral.discoverCharacteristics(nil, for: service)
print(service.uuid.uuidString,"services")
}
}
设备特征值回调:
//设备特征回调
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
for character in service.characteristics! {
print(character.uuid.uuidString,"characteristics")
peripheral.setNotifyValue(true, for: character)//订阅所有特征值的通知
peripheral.readValue(for: character)//读取所有特征值
peripheral.discoverDescriptors(for: character)//读取所有特征值的描述符
}
peripheralTable.reloadData()
}
描述符回调:
//描述符回调
func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?) {
for descriptor in characteristic.descriptors! {
print(descriptor.uuid.uuidString,"descriptors")
}
peripheralTable.reloadData()
}
建立通讯之后,需要对订阅的值或者通道进行监听,监听如下:
//更新特征值通知
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
print("\(characteristic.uuid.uuidString): \(characteristic.uuid.data)")
}
//设备值更新通知
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
if (characteristic.value != nil) {
let k = String.init(data: characteristic.value!, encoding: String.Encoding.utf8)
print("\(characteristic.uuid.uuidString): \(k)")//收到来自通知的数据
}
}
还有其他一些状态变更,可参考下面:
//设备写入成功回调
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
print("characteristic is writed!")
}
//服务名更改
func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
}
//外设名字变更
func peripheralDidUpdateName(_ peripheral: CBPeripheral) {
}
读取特征值和描述符:
readValue(for:CBCharacteristic) 读取指定特征值
readValue(for:CBDescriptor) 读取指定特征描述符的值
写入特征值和描述符:
writeValue(Data,for:CBCharacteristic,type:CBCharacteristicWriteType)
写入特征值
writeValue(Data,for:CBDescriptor)
写入特征描述符的值
maximumWriteValueLength(for type: CBCharacteristicWriteType) -> Int
可以通过单个写入类型发送到特征的最大数据量(以字节为单位)。CBCharacteristicWriteType 表示可能写入特征值的类型的值,withResponse 写入值成功时有返回;withoutResopnse 写入值成功时不设返回值。
手机作为蓝牙外设
在实际的应用场景中,这种情况会比较少需要用上,可能会用于手机APP之间的小数据交互等等。当手机作为外设的时候,要用到的类是:CBPeripheralManager
、CBCharacteristic
作为外设意味着,需要为Central提供Service、Characteristic、Descriptor,同样的,我们也需要造一个俄罗斯套娃出来,一层一层套起来。
初始化:
phermgr = CBPeripheralManager.init(delegate: self, queue: nil)
当收到了蓝牙状态监测到成功打开的时候,需要为它添加服务,开始套娃
let serviceUUID1 = "EE00"
let notifyCharacteristicUUID = "EE01"
let readCharacteristicUUID = "EE02"
let writeCharacteristicUUID = "EE03"
let LocalNameKey = "Gt_0"
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
switch peripheral.state {
case CBManagerState.poweredOn: //蓝牙已打开正常
NSLog("启动成功,开始搜索")
//不限制
let cbu = CBUUID.init(string:CBUUIDCharacteristicUserDescriptionString)
//服务
let service1 = CBMutableService.init(type: CBUUID.init(string: serviceUUID1), primary: true)
let noti = CBMutableCharacteristic.init(type: CBUUID.init(string: notifyCharacteristicUUID), properties: .notify, value: nil, permissions: .readable)
//特征值,只读
let chart_0 = CBMutableCharacteristic.init(type: CBUUID.init(string: readCharacteristicUUID), properties: .read, value: nil, permissions: .readable)
//特征值,只写
let chart_1 = CBMutableCharacteristic.init(type: CBUUID.init(string: writeCharacteristicUUID), properties: .write, value: nil, permissions: .writeable)
//描述符
let des_0 = CBMutableDescriptor.init(type: cbu, value: "name")
let des_1 = CBMutableDescriptor.init(type: cbu, value: "name")
chart_0.descriptors = [des_0]
chart_1.descriptors = [des_1]
service1.characteristics = [chart_0,chart_1,noti]
//增加1个服务
phermgr.add(service1)
tips.isHidden = false
case CBManagerState.unauthorized: //无BLE权限
NSLog("无BLE权限")
case CBManagerState.poweredOff: //蓝牙未打开
NSLog("蓝牙未开启")
default:
NSLog("状态无变化")
}
}
当服务加入以后,就可以开始广播数据了:
func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
print("peripheralManager didAdd")
//加入服务以后再打开广播
peripheral.startAdvertising([CBAdvertisementDataServiceUUIDsKey:[CBUUID.init(string: serviceUUID1)],CBAdvertisementDataLocalNameKey:LocalNameKey])
}
只有打开了服务,Central才能搜索到设备
当它作为Peripheral的时候,我们可以理解为服务器开启了,所以我们要为它添加应答的内容,如下:
func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) {
print("peripheralManagerIsReady")
}
func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
print("peripheralManagerDidStartAdvertising")
}
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
print("subscribe")
//订阅成功之后我们给它发送一个数据
// self.sendData(characteristic: characteristic)
didSendChara = characteristic
if (timeAction != nil) {
timeAction.invalidate()
timeAction = nil
}
timeAction = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(sendData), userInfo: nil, repeats: true)
// timeAction.fire()
}
//收到读的请求
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
print("didReceiveRead")
//最好做一下是否有读权限
if request.characteristic.properties == CBCharacteristicProperties.read {
request.value = Data.init(bytes: [0x02,0x03], count: 2)
peripheral.respond(to: request, withResult: .success)
// self.sendData(characteristic: request.characteristic)
}else{
peripheral.respond(to: request, withResult: .writeNotPermitted)
}
}
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
print("didReceiveWrite")
let request = requests[0]
for req in requests {
print(req.characteristic.uuid.uuidString)
}
//先检查是否有写权限
if request.characteristic.properties == .write {
peripheral.respond(to: request, withResult: .success)
}else{
peripheral.respond(to: request, withResult: .writeNotPermitted)
}
}
//取消订阅
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
}
//数据发送
@objc func sendData(){
let date = Date.init()
let dateformate = DateFormatter.init()
dateformate.dateFormat = "yyyy-MM-dd HH:mm:ss"
let str = dateformate.string(from: date)
let data = str.data(using:String.Encoding.utf8)!
phermgr.updateValue(data, for: didSendChara as! CBMutableCharacteristic, onSubscribedCentrals: nil)
print("sendData")
}
再深入一步
为了再深入的看到整个CoreBluetooth的架构,我去整理了CoreBluetooth的结构脑图,方便后续继续开展学习和方便记忆。其实总的来看,所有的API都是在套接,一层套一层,最终到达上层的时候,我们只能看到少部分的内容了,我们在写封装库的时候可以参考着来写,那样整体库的逻辑就非常的清晰了。下面是脑图: