Summary: 今天说一说iOS蓝牙相关的东西,本文背景是公司的蓝牙项目,项目要求是利用手机蓝牙模块与低功耗蓝牙卡进行通信,蓝牙卡信息解析由卡厂商提供,而我们先要做的就是建立手机与蓝牙卡的连接。
今天说一说iOS蓝牙相关的东西,本文背景是公司的蓝牙项目,项目要求是利用手机蓝牙模块与低功耗蓝牙卡进行通信,蓝牙卡信息解析由卡厂商提供,而我们先要做的就是建立手机与蓝牙卡的连接。难点主要集中在与蓝牙卡连接断开部分,因为蓝牙卡是低能耗的,每开启蓝牙卡片蓝牙后它会在8秒后自动断开连接,所以在处理蓝牙连接的的部分逻辑较为复杂。接下来我们把重点放在与蓝牙建立连接的部分,Google之,先来搞清楚与iOS有关的蓝牙库。
先说一下蓝牙版本问题,如果你的设备支持的是蓝牙4.0之前的版本,那么会涉及到一个MFI的概念,MFI(Make For ipod/ipad/iphone)是苹果的一套认证,只有少数的硬件厂商才有苹果的MFI认证,做之前需要搞定这个认证。使用蓝牙4.0的话,由于4.0苹果开放了BLE(Bluetooth Low Energy)通道,就不会有认证的问题了,而且向下兼容。
我们用到的蓝牙库为CoreBluetooth,而蓝牙库中首先要介绍下两个概念Central和Peripheral;
Central 和 Peripheral 在蓝牙交互中的角色
所有涉及蓝牙低功耗的交互中有两个主要的角色:中心Central和外围设备Perpheral。根据一些传统的客户端-服务端结构,Peripheral通常具有其他设备所需要的数据,而Central通常通过使用Perpheral的信息来实现一些特定的功能。
这里我自己理解,如果你的设备连接的是本文这种蓝牙卡或者穿戴设备等,那么你的程序就是作为Central;如果你的设备是与另外一台iPhone设备,那么它既可以作为Central也可以作为Perpheral;
想了解更详细请参照:iOS蓝牙编程指南 – 核心蓝牙概述
UUID
每个蓝牙4.0的设备都是通过服务和特征来展示自己的,一个设备必然包含一个或多个服务,每个服务下面又包含若干个特征。特征是与外界交互的最小单位。比如说,一台蓝牙4.0设备,用特征A来描述自己的出厂信息,用特征B来与收发数据等。
服务和特征都是用UUID来唯一标识的,UUID的概念如果不清楚请自行google,国际蓝牙组织为一些很典型的设备(比如测量心跳和血压的设备)规定了标准的service UUID(特征的UUID比较多,这里就不列举了);
UUID含义是通用唯一识别码 (Universally Unique Identifier),这是一个软件建构的标准,也是被开源软件基金会 (Open Software Foundation, OSF) 的组织应用在分布式计算环境 (Distributed Computing Environment, DCE) 领域的一部分。
UUID是指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的。通常平台会提供生成的API。按照开放软件基金会(OSF)制定的标准计算,用到了以太网卡地址、纳秒级时间、芯片ID码和许多可能的数字。
UUID由以下几部分的组合:
(1)当前日期和时间,UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同。
(2)时钟序列。
(3)全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。
UUID的唯一缺陷在于生成的结果串会比较长。关于UUID这个标准使用最普遍的是微软的GUID(Globals Unique Identifiers)。在ColdFusion中可以用CreateUUID()函数很简单地生成UUID,其格式为:xxxxxxxx-xxxx- xxxx-xxxxxxxxxxxxxxxx(8-4-4-16),其中每个 x 是 0-9 或 a-f 范围内的一个十六进制的数字。而标准的UUID格式为:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12),可以从cflib 下载CreateGUID() UDF进行转换。
BLE中心模式流程
1.建立中心角色
2.扫描外设(Discover Peripheral)
3.连接外设(Connect Peripheral)
4.扫描外设中的服务和特征(Discover Services And Characteristics)
5.利用特征与外设做数据交互(Explore And Interact)
6.订阅Characteristic的通知
7.断开连接(Disconnect)
代码说明
初始化 CBCentralManager
1 | dispatch_queue_t centralQ = dispatch_queue_create(BLUETOOCH_QUEUE_IDENTIFER, DISPATCH_QUEUE_CONCURRENT); |
上面的代码中,将self设置为代理,用于接收各种 central 事件。将queue设置为nil,则表示直接在主线程中运行,这里是我自己定义的任务队列。
创建Central管理器时,管理器对象会调用代理对象的centralManagerDidUpdateState:方法。我们需要实现这个方法来确保本地设备支持BLE。
初始化 central manager 之后,设置的代理会调用centralManagerDidUpdateState:方法,所以需要去遵循
搜索当前可用的 peripheral
可以使用CBCentralManager的scanForPeripheralsWithServices:options:方法来扫描周围正在发出广播的 Peripheral 设备。peripheral 每秒都在发送大量的数据包,scanForPeripheralsWithServices:options:方法会将同一 peripheral 发出的多个数据包合并为一个事件,然后每找到一个 peripheral 都会调用 centralManager:didDiscoverPeripheral:advertisementData:RSSI: 方法。另外,当已发现的 peripheral 发送的数据包有变化时,这个代理方法同样会调用。
1 | NSArray = @[[CBUUID UUIDWithString:BUSINESS_SERVICE_UUID_STRING] |
这里的services是中心要扫描的蓝牙设备类型,表示只搜索当前数组包含的设备(每个 peripheral 的 service 都有唯一标识——UUID);而scanOption中的CBCentralManagerScanOptionAllowDuplicatesKey
设置以后,每收到广播,就会调用上面的回调(无论广播数据是否一样)。关闭默认行为一般用于以下场景:根据 peripheral 的距离来初始化连接(距离可用信号强度 RSSI 来判断)。设置这个 option 会对电池寿命和 app 的性能产生不利影响,所以一定要在必要的时候,再对其进行设置。
在调用scanForPeripheralsWithServices:options:
方法之后,找到可用设备,系统会回调(每找到一个都会回调)centralManager:didDiscoverPeripheral:advertisementData:RSSI:
。该方法会已CBPeripheral返回找到的 peripheral,所以你可以使用数组将找到的 peripheral 存起来。
1 | //扫描到蓝牙后的回调 |
连接 peripheral
1 | //连接外围设备 |
当连接成功后,会回调方法centralManager:didConnectPeripheral:。在这个方法中,你可以去记录当前的连接状态等数据。
1 | -(void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral{ |
如果连接断开则会回调:
1 | //断开回调处理 |
失败的情况下则是:
1 | //连接失败回调 |
搜索 peripheral 的 service
当与 peripheral 成功建立连接以后,就可以通信了。第一步是先找到当前 peripheral 提供的 service,因为 service 广播的数据有大小限制(貌似是 31 bytes),所以你实际找到的 service 的数量可能要比它广播时候说的数量要多。调用CBPeripheral的 discoverServices:
方法可以找到当前 peripheral 的所有 service。
1 | //在搜索过程中,并不是所有的 service和characteristic 都是我们需要的,如果全部搜索,依然会造成不必要的资源浪费。 |
当找到特定的 Service 以后,会回调peripheral:didDiscoverServices:
方法。Core Bluetooth 提供了CBService类来表示 service,找到以后,它们以数组的形式存入了当前 peripheral 的services属性中,你可以在当前回调中遍历这个属性。
1 | - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{ |
搜索 service 的 characteristic
找到需要的 service 之后,下一步是找它所提供的 characteristic。如果搜索全部 characteristic,那调用CBPeripheral的discoverCharacteristics:forService:
方法即可。如果是搜索当前service的characteristic,那还应该传入相应的CBService对象:
1 | [peripheral discoverCharacteristics:nil forService:service]; |
找到所有 characteristic 之后,回调peripheral:didDiscoverCharacteristicsForService:error:
方法,此时 Core Bluetooth 提供了CBCharacteristic类来表示characteristic。可以通过以下代码来遍历找到的 characteristic :
1 | - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error{ |
读取 characteristic 数据
这里读取涉及到两个方法:
1 | [peripheral readValueForCharacteristic:characteristic]; |
read这种方法是需要主动去接收的;notify方法订阅,当有数据发送时,可以直接在回调中接收,如果 characteristic 的数据经常变化,那么采用订阅的方式更好;
1 | //获取外设发来的数据,不论是read和notify,获取数据都是从这个方法中读取。 |
所以nofify可能会被调用多次,而且它获取的是实时数据,如果你接收蓝牙信息不是一次次接收的话,那么会用到它:
1 | //中心读取外设实时数据 |
本文参考,之后会继续讲解写数据和重连等问题;