前言
此篇作为对iOS蓝牙模块一个框架上的理解和概念 适合初次接触iOS蓝牙开发的同学 主要也是我为了对工作项目中的蓝牙开发做一个梳理和总结
后面我会封装一个蓝牙Demo 对CB API层做一层Block的封装 使用更方便 对外设数据的常见进制、大小端、文件解析等做一定Category的支持 对硬件DFU升级模式做一些处理等
iOS中的蓝牙
BLE:(Bluetooth low energy)蓝牙4.0设备因为低耗电,也叫BLE
iOS中提供了4个框架用于实现蓝牙连接。
- 1.GameKit.framework(用法简单)
只能用于iOS设备之间的同个应用内连接,多用于游戏(eg:棋牌类),从iOS7开始过期- 2.MultipeerConnectivity.framework(代替1)
只能用于iOS设备之间的连接,从iOS7开始引入,主要用于非联网状态下,通过wifi或者蓝牙进行文件共享(仅限于沙盒的文件),多用于附近无网聊天- 3.ExternalAccessory.framework(MFi)
可用于第三方蓝牙设备交互,但是蓝牙设备必须经过苹果MFi认证(国内很少)- 4.CoreBluetooth.framework(常用 Apple推行蓝牙的核心)
可用于第三方蓝牙设备交互,必须要支持蓝牙4.0
硬件至少是4s,系统至少是iOS6 (现iPhoneX 蓝牙5.0)
蓝牙4.0以低功耗著称,一般也叫BLE(Bluetooth Low Energy)
目前应用比较多的案例:运动手环,嵌入式设备,智能家居
我们公司对接的外设比较多 心率计、踏频、码表等运动蓝牙设备
涉及到使用的框架HealthKit/物联网HomeKit/wathOS1,2/iBeacon
使用CoreBluetooth框架做蓝牙连接传输
在iPhone上使用蓝牙有两种模式 可以一对一 也可以一对多(需服务通道不同)
现一般主要使用于第三方/自家蓝牙硬件智能产品(如:心率计设备、蓝牙音箱、蓝牙手环、蓝牙车锁等等)
蓝牙传输上限是20字节 所以大数据传输会涉及到拆包、拼包、校验等
蓝牙连接流程
- 建立中心设备管理者
- 扫描外设
- 连接外设
- 扫描外设中的服务
- 扫描外设中的特征
- 订阅或读取特征值
- 获取外设中的数据或将数据写入外设
中心模式 CenterManager (一般Phone作为中心设备)
手机可以作为中心设备也可以作为外设来使用 CenterManage主要处理对蓝牙状态的控制和对外围设备的状态处理
- 建立中心角色
- 监听蓝牙状态
- 扫描外设(Discover Peripheral) 接收外设蓝牙广播 (可以扫描包含制定服务的设备)
- 连接外设(Connect Peripheral)
- 断开连接(Disconnect)
外设模式
CenterManager 扫描链接外设成功后 启动一个Peripheral外设管理对象 负责外设数据的操作处理
- 启动一个Peripheral外设管理对象 负责外设数据的操作处理
- 扫描外设中的服务和特征(Discover Services And Characteristics)
- 获取外设的services (基本服务(电池信息和设备信息)、硬件自定服务)
- Discover指定Service下的特征 获取外设的Characteristics,
- 通过指定特征( Characteristics)订阅(Notiy)/读取(Read)/写入(Write) 等操作
- 获取Characteristics的Descriptor和Descriptor的值
- 根据业务做处理
Characteristics作为蓝牙数据传输操作的做小单元
什么是服务和特征(service and characteristic)
每个设备都会有1个or多个服务
每个服务里都会有1个or多个特征
特征就是具体键值对,提供数据的地方
每个特征属性分为:读、写、通知等等
代码Coding 使用CoreBluetooth相关API
建议先看一下官方文档Core Bluetooth Communicate with Bluetooth 4.0 low-energy devices.
调试iOS蓝牙的时候,可以下个LightBlue,非常方便,网上也有仿写LightBlue的Demo,参考这两处:DarkBlue
Lightblue
工具我使用的是LightBlue 可以直接去App Store下载 硬件同事也钟爱这个App 调试服务/特征很方便
- 导入CoreBluetooth头文件
#import <CoreBluetooth/CoreBluetooth.h>
CBCentralManager 中心设备的初始化和外设的链接
- 初始化CBCentralManager 中心设备管理者 注意初始化的参数
设置Delegate
设置制定队列Queue
设置可选参数options:可以设为nil
也可以查看CBCentralManagerOptionShowPowerAlertKey
蓝牙提醒弹窗和CBCentralManagerOptionRestoreIdentifierKey
字符串,一个唯一的标示符,用来蓝牙的恢复连接的。在后台的长连接中可能会用到
其他key可以查看这篇文章CBCentralManagerConstants
- (instancetype)init
{
if (self = = [super init]) {
dispatch_queue_t queue = dispatch_queue_create("com.imxingzhe.bici", 0);
_centralManager = [[CBCentralManager alloc] initWithDelegate:self
queue:queue
options:@{CBCentralManagerOptionShowPowerAlertKey:[NSNumber numberWithBool:NO]}];
_discoveredPeripherals = [[NSMutableArray alloc] init];
}
return self;
}
- 基本API 所有的状态回调都在delegate里做处理
//主动发起扫描外设 开始扫描符合服务serviceUUIDs的外设
- (void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs options:(nullable NSDictionary<NSString *, id> *)options;
//主动停止扫描
- (void)stopScan;
//主动连接指定外设
- (void)connectPeripheral:(CBPeripheral *)peripheral options:(nullable NSDictionary<NSString *, id> *)options;
//主动断开指定外设
- (void)cancelPeripheralConnection:(CBPeripheral *)peripheral;
- Delegate的回调使用
#pragma mark - CBCentralManagerDelegate
//更新蓝牙状态
- (void)centralManagerDidUpdateState:(CBCentralManager *)central{
// Determine the state of the peripheral
if ([central state] == CBCentralManagerStatePoweredOff){
NSLog(@"CoreBluetooth BLE hardware is powered off");
}else if ([central state] == CBCentralManagerStatePoweredOn){
NSLog(@"CoreBluetooth BLE hardware is powered on and ready");
}else if ([central state] == CBCentralManagerStateUnauthorized){
NSLog(@"CoreBluetooth BLE state is unauthorized");
}else if ([central state] == CBCentralManagerStateUnknown) {
NSLog(@"CoreBluetooth BLE state is unknown");
}else if ([central state] == CBCentralManagerStateUnsupported) {
NSLog(@"CoreBluetooth BLE hardware is unsupported on this platform");
}
}
// 扫到设备会进入到此代理方法 对应Scan方法 advertisementData:广播包 RSSI:信号强度
- (void)centralManager:(CBCentralManager *)central
didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary<NSString *,id> *)advertisementData
RSSI:(NSNumber *)RSSI
{
NSLog(@"%s, line = %d, per = %@, data = %@, rssi = %@", __FUNCTION__, __LINE__, peripheral, advertisementData, RSSI);
}
//代理返回已经链接成功的peripheral 外设
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
NSLog(@"获取链接成功的外设 可以操作外设处理数据传输");
}
//外设链接断开时会回调
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
NSLog(@"获取断开链接的外设 处理释放业务逻辑");
}
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
NSLog(@"连接失败 获取错误信息 处理");
}
CBPeripheral外设的状态和数据传输处理
//外设连接状态state
typedef NS_ENUM(NSInteger, CBPeripheralState) {
CBPeripheralStateDisconnected = 0,//断开
CBPeripheralStateConnecting,//正在连接
CBPeripheralStateConnected,//已连接
CBPeripheralStateDisconnecting NS_AVAILABLE(10_13, 9_0),//正在断开连接
} NS_AVAILABLE(10_9, 7_0);
//设备写入服务类型 一般情况下都是CBCharacteristicWriteWithResponse
typedef NS_ENUM(NSInteger, CBCharacteristicWriteType) {
CBCharacteristicWriteWithResponse = 0,//有响应
CBCharacteristicWriteWithoutResponse,//无响应
};
//CBPeripheral继承自CBPeer,独一的标示该设备的id
@property(readonly, nonatomic) NSUUID *identifier NS_AVAILABLE(10_13, 7_0);
//用来接收设备事件的代理
@property(weak, nonatomic, nullable) id<CBPeripheralDelegate> delegate;
//只读属性 设备名称
@property(retain, readonly, nullable) NSString *name;
//外设连接状态CBPeripheralState类型
@property(readonly) CBPeripheralState state;
//数组,内含扫描到的设备的服务
@property(retain, readonly, nullable) NSArray<CBService *> *services;
/*
* 1、如果值为YES,远程设备有空间发送一个没有响应的写服务.
* 2、如果值为 NO,如果值被设置为YES时,当前写服务被冲刷,方法peripheralIsReadyToSendWriteWithoutResponse:将会被调用
*/
@property(readonly) BOOL canSendWriteWithoutResponse;
/*!
* @method readRSSI
*
* @discussion 当连接成功,检索当前连接的信号强度。回调方法为 peripheral:didReadRSSI:error:
*
* @see peripheral:didReadRSSI:error:
*/
- (void)readRSSI;
/*!
* @method discoverServices:
*
* @param serviceUUIDs 需要扫描的设备的服务的id,如果为nil,则扫描所有的服务
*
* @discussion 扫描发现设备所有可用的服务
*
* @see :扫描回调方法peripheral:didDiscoverServices:
*/
- (void)discoverServices:(nullable NSArray<CBUUID *> *)serviceUUIDs;
/*!
* @method discoverIncludedServices:forService:
*
* @param includedServiceUUIDs 需要发现的服务service中的服务id列表,如果为nil,则扫描服务内所有的服务,这样的话会比较慢,不推荐
* @param service 服务
*
* @discussion 发现指定服务service内的服务
*
* @see 回调方法: peripheral:didDiscoverIncludedServicesForService:error:
*/
- (void)discoverIncludedServices:(nullable NSArray<CBUUID *> *)includedServiceUUIDs forService:(CBService *)service;
/*!
* @method discoverCharacteristics:forService:
*
* @param characteristicUUIDs 数组,内含需要被发现的所有特征值类型,如果为nil,则为所有特征值。
* @param service 服务
*
* @discussion 发现指令服务的服务特征值
*
* @see 回调方法为: peripheral:didDiscoverCharacteristicsForService:error:
*/
- (void)discoverCharacteristics:(nullable NSArray<CBUUID *> *)characteristicUUIDs forService:(CBService *)service;
/*!
* @method readValueForCharacteristic:
*
* @param characteristic 需要读取的服务特征值
*
* @discussion 读取服务特征值的值,调用该方法则读取该方法前最新的蓝牙系统缓存的从外设读取的数据
*
* @see 回调方法: peripheral:didUpdateValueForCharacteristic:error:
*/
- (void)readValueForCharacteristic:(CBCharacteristic *)characteristic;
/*!
* @method maximumWriteValueLengthForType:
*
* @discussion 获取向一个写服务可发送的最大字节数
*
* @see 该写服务可通过调用writeValue:forCharacteristic:type:写数据
*/
- (NSUInteger)maximumWriteValueLengthForType:(CBCharacteristicWriteType)type NS_AVAILABLE(10_12, 9_0);
/*!
* @method writeValue:forCharacteristic:type:
*
* @param data 待写数据
* @param characteristic 写服务特征
* @param type 写服务的类型(有/无响应)
*
* @discussion 向指定服务写数据,
* 1、如果指定CBCharacteristicWriteWithResponse类型,写入结果将会回调 peripheral:didWriteValueForCharacteristic:error:方法
* 2、如果指定为CBCharacteristicWriteWithoutResponse类型,同时canSendWriteWithoutResponse为NO时,则数据将尽最大努力,但不会被保证成功。
*
*/
- (void)writeValue:(NSData *)data forCharacteristic:(CBCharacteristic *)characteristic type:(CBCharacteristicWriteType)type;
Delegate
/*!
* @method peripheralDidUpdateName:
*
* @param peripheral 需要更新名称的设备
*
* @discussion 该方法被触发当设备的名称改变
*/
- (void)peripheralDidUpdateName:(CBPeripheral *)peripheral NS_AVAILABLE(10_9, 6_0);
/*!
* @method peripheral:didModifyServices:
*
* @param peripheral 需要更新的设备
* @param invalidatedServices The services that have been invalidated
*
* @discussion 该方法触发当设备的服务改变时候
* 服务可以被重新发现通过discoverServices: 方法
*/
- (void)peripheral:(CBPeripheral *)peripheral didModifyServices:(NSArray<CBService *> *)invalidatedServices NS_AVAILABLE(10_9, 7_0);
/*!
* @method peripheralDidUpdateRSSI:error:
*
* @param peripheral 需要更新的设备.
* @param error 返回错误原因.
*
* @discussion 该方法是readRSSI: 的回调
*
* @deprecated 使 {@link peripheral:didReadRSSI:error:}代替了
*/
- (void)peripheralDidUpdateRSSI:(CBPeripheral *)peripheral error:(nullable NSError *)error NS_DEPRECATED(10_7, 10_13, 5_0, 8_0);
/*!
* @method peripheral:didReadRSSI:error:
*
* @param peripheral 需要更新的设备
* @param RSSI 设备的RSSI.
*
* @discussion 该方法是readRSSI: 的回调
*/
- (void)peripheral:(CBPeripheral *)peripheral didReadRSSI:(NSNumber *)RSSI error:(nullable NSError *)error NS_AVAILABLE(10_13, 8_0);
/*!
* @method peripheral:didDiscoverServices:
*
* @param peripheral 当前设备.
* @param error 错误原因
*
* @discussion 该发放为 discoverServices:回调
*
*/
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error;
/*!
* @method peripheral:didDiscoverIncludedServicesForService:error:
*
* @param peripheral 当前设备
* @param service 设备服务
* @param error 错误原因.
*
* @discussion 该方法为 discoverIncludedServices:forService: 的回调
*/
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverIncludedServicesForService:(CBService *)service error:(nullable NSError *)error;
/*!
* @method peripheral:didDiscoverCharacteristicsForService:error:
*
* @param peripheral The peripheral providing this information.
* @param service The <code>CBService</code> object containing the characteristic(s).
* @param error If an error occurred, the cause of the failure.
*
* @discussion This method returns the result of a @link discoverCharacteristics:forService: @/link call. If the characteristic(s) were read successfully,
* they can be retrieved via <i>service</i>'s <code>characteristics</code> property.
*/
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(nullable NSError *)error;
/*!
* @method peripheral:didUpdateValueForCharacteristic:error:
*
* @param peripheral The peripheral providing this information.
* @param characteristic A <code>CBCharacteristic</code> object.
* @param error If an error occurred, the cause of the failure.
*
* @discussion This method is invoked after a @link readValueForCharacteristic: @/link call, or upon receipt of a notification/indication.
*/
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;
/*!
* @method peripheral:didWriteValueForCharacteristic:error:
*
* @param peripheral The peripheral providing this information.
* @param characteristic A <code>CBCharacteristic</code> object.
* @param error If an error occurred, the cause of the failure.
*
* @discussion This method returns the result of a {@link writeValue:forCharacteristic:type:} call, when the <code>CBCharacteristicWriteWithResponse</code> type is used.
*/
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;
/*!
* @method peripheral:didUpdateNotificationStateForCharacteristic:error:
*
* @param peripheral The peripheral providing this information.
* @param characteristic A <code>CBCharacteristic</code> object.
* @param error If an error occurred, the cause of the failure.
*
* @discussion This method returns the result of a @link setNotifyValue:forCharacteristic: @/link call.
*/
- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;
/*!
* @method peripheral:didDiscoverDescriptorsForCharacteristic:error:
*
* @param peripheral The peripheral providing this information.
* @param characteristic A <code>CBCharacteristic</code> object.
* @param error If an error occurred, the cause of the failure.
*
* @discussion This method returns the result of a @link discoverDescriptorsForCharacteristic: @/link call. If the descriptors were read successfully,
* they can be retrieved via <i>characteristic</i>'s <code>descriptors</code> property.
*/
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;
/*!
* @method peripheral:didUpdateValueForDescriptor:error:
*
* @param peripheral The peripheral providing this information.
* @param descriptor A <code>CBDescriptor</code> object.
* @param error If an error occurred, the cause of the failure.
*
* @discussion This method returns the result of a @link readValueForDescriptor: @/link call.
*/
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForDescriptor:(CBDescriptor *)descriptor error:(nullable NSError *)error;
/*!
* @method peripheral:didWriteValueForDescriptor:error:
*
* @param peripheral The peripheral providing this information.
* @param descriptor A <code>CBDescriptor</code> object.
* @param error If an error occurred, the cause of the failure.
*
* @discussion This method returns the result of a @link writeValue:forDescriptor: @/link call.
*/
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForDescriptor:(CBDescriptor *)descriptor error:(nullable NSError *)error;
/*!
* @method peripheralIsReadyToSendWriteWithoutResponse:
*
* @param peripheral 当前设备
*
* @discussion 该方法被调用,当 writeValue:forCharacteristic:type失败,设备再次可以发送服务特征值时调用
*
*/
- (void)peripheralIsReadyToSendWriteWithoutResponse:(CBPeripheral *)peripheral;
/*!
* @method peripheral:didOpenL2CAPChannel:error:
*
* @param peripheral 当前设备
* @param channel CBL2CAPChanne
* @param error 错误信息
*
* @discussion 该方法为openL2CAPChannel: 回调
*/
- (void)peripheral:(CBPeripheral *)peripheral didOpenL2CAPChannel:(nullable CBL2CAPChannel *)channel error:(nullable NSError *)error;
当外设与手机连接成功后 会生成一个CBPeripheral对象 用以操作外设数据等读写和监听操作
上面说过 每个外设有多个service service里有包含多个特征 层层递进下来找到我们需要操作的特征按照硬件需求进行读写或监听操作 处理外设于中心设备手机的数据命令交互
写入格式 例如
//拼接命令参数 写入硬件 一般会有文档告诉你怎么拼接 (命令串(0xB2):数据(0x04))
NSMutableData *basicData = [[NSMutableData alloc]init];
UInt8 head = 0xB2;
UInt8 leng = 0x04;
UInt8 cmd = 0x0B;
UInt8 checksum = 0xC1;
[basicData appendBytes:&head length:1];
[basicData appendBytes:&leng length:1];
[basicData appendBytes:&cmd length:1];
[basicData appendBytes:&checksum length:1];
//写入命参数数据
[self.peripheral writeValue: basicData forCharacteristic:self.writeCharacteristic type:CBCharacteristicWriteWithResponse];
//对应的回调代理
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
//对应notify监听通道 和 read读取通道 一般都是固定命令发送 写法一样 比较简单 对于外设蓝牙通讯的处理其实就是围绕着读写听这三个操作来处理的
特殊机制处理(自动重连、多设备连接等)
关于Mac地址的获取
自iOS7之后,苹果不支持获取Mac地址,只能用UUID来标识设备,要注意的是同一个设备在不同手机上显示的UUID不相同,但有的设备可以通过 “180A”这个服务来发现特征,再来读取 “2A23”这个特征值,可以获得Mac地址。如果你的蓝牙设备不支持这样获取,你可以跟硬件工程师沟通,来获得Mac地址,添加一个获取地址命令或者增加一个含地址的特征值都可以很容易的获取。上面获取地址的前提都是需要先建立连接,如果一定要在扫描的时候获得Mac地址,让硬件工程师把数据写入广播包里,根据需求决定;
未完待续....