iOS里面的蓝牙框架是CoreBluetooth.framework,官方文档链接在这里。
这个框架只支持蓝牙4.0,所以iPhone 4s以上的设备才可以使用,而对应的iPhone蓝牙配件也必须具备蓝牙4.0。具备通话功能的手环还有蓝牙2.0或3.0的协议,这些配件是、只能跟iOS系统的蓝牙连接了,用CoreBluetooth是搜索不出设备的。
CoreBluetooth里面有两个重要的概念:Central和Peripheral,翻译过来就是中央和外设,也就是指iPhone和手环。
Central的功能主要是扫描Peripheral,连接Peripheral,还有iPhone的蓝牙状态管理。这里着重说明一下:iOS的蓝牙功能只能通过用户手动开启或关闭,不能通过写代码的方式来开启或关闭,但是你可以收到开启或关闭的回调。
Peripheral在没有连接的情况下会一直广播,包含部分信息,比如名字、服务(Service),就像某个人在大街上喊“我是张三,会Java开发”。服务(Service)其实就是功能,具备唯一标识符UUID。蓝牙联盟有一些标准的服务,比如心率,它的UUID是"180D"。服务通常包含一些特征(Characteristic),这是用来数据通讯的通道,发送数据、接收数据都在这里进行,比如标准心率服务下面的特征是“2A37”。
我们可以用网络通讯的方式来更好理解蓝牙通讯,虽然有点不严谨:
- Central是客户端
- Peripheral是服务器
- Sevice是指HTTP功能,FTP功能
- Characteristic是指端口,80端口是HTTP功能的
一般来说,App和手环要进行通讯需要经过这几个步骤:
- 搜索设备
- 连接设备
- 查找服务
- 查找特征
- 开启特征的通知功能
- 发送数据
- 接收数据
CoreBluetooth里面的方法调用有10几个,为了简化操作,将其封装在两个类里面:
- LGCentralManager,对应CoreBluetooth的CBCentralManager
- LGPeripheral,对应CoreBluetooth的CBPeripheral
LGCentralManager是个单例,因为对整个App来说,中央就是手机,主要功能就是一个:搜索LGPeripheral对象。
首先在程序启动时调用类方法+ (void)startUp
,做一些初始化操作。
#import "LGCentralManager.h"
@implementation LGAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
// Initialization of CentralManager
[LGCentralManager startUp];
return YES;
}
以后的方法调用请使用+ (LGCentralManager *)sharedInstance
按是否跟iPhone连接来区分的话,设备有两种类型
- 已连接的设备,不会广播数据
- 未连接的设备,会广播数据
已连接的设备可以通过retrieve两个方法获取,这两个方法是立即返回LGPeripheral对象数组的,不需要要花时间。
//根据服务ID来获取设备
NSArray *connectedPeripherals = [[LGCentralManager sharedInstance] retrieveConnectedPeripheralsWithServices:@[kDefaultServiceUUID]];
或
//"93E69689-5FAB-6AFF-43B1-29FCA16C1FC6"是已经保存的LGPeripheral的UUIDString
NSArray *connectedPeripherals = [[LGCentralManager sharedInstance] retrievePeripheralsWithIdentifiers:@[@"93E69689-5FAB-6AFF-43B1-29FCA16C1FC6"]];
未连接的设备可以通过scan方法来获取设备,需要传入服务ID参数。框架内有两个服务ID:
- kDefaultServiceUUID : 默认服务,一般使用这个就可以了
- kDFUServiceUUID : 固件升级服务,特殊用途,不要随意使用 用法一:
//开始扫描设备
[[LGCentralManager sharedInstance] scanPeripheralsWithServices:@[kDefaultServiceUUID]];
//扫描3秒钟
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//停止扫描, 如果不停止,将会一直扫描,占用资源。
[[LGCentralManager sharedInstance] stopScanForPeripherals];
//扫描结束之后,可以直接读取peripherals属性获取LGPeripheral对象
NSArray *scanedPeripherals = [LGCentralManager sharedInstance].peripherals;
});
用法二:
//这种扫描方式会自动超时停止扫描,不需要调用stopScanForPeripherals,在completion接收数据即可。
[[LGCentralManager sharedInstance] scanPeripheralsWithServices:@[kDefaultServiceUUID] interval:3 completion:^(LGCentralManager *manager, NSArray *scanedPeripherals) {
self.peripherals = scanedPeripherals;
[self.tableView reloadData];
}];
可以通过监听kCBCentralManagerStateNotification
通知key来获取变化,当用户关闭蓝牙或者打开蓝牙时会收到通知。
LGPeripheral对象主要用到一些属性,其中的方法普通用户是用不到,因为被其代理类LGPeripheralAgent管理了。
主要属性介绍:
- connected:是否连接状态。可能是和系统蓝牙连接,也可能是和别的手机连接
- UUIDString: 唯一标识符,iOS无法获取蓝牙设备的Mac地址,手机会分配一个唯一标识符来表示。但是不同的手机,即使是同一个手环设备,它的UUIDString也是不一样的
- name: 设备名称,如果写了序列号,每个设备的name就是序列号
- RSSI: 蓝牙信号强度,负整数,值越大,信号越好。
其他方法可以查看头文件LGPeripheral.h
,里面有详细说明。
这是LGPeripheral的代理类,用于处理断开重连、查找服务、查找特征、发送数据、接收数据、解析数据。简单来说,LGPeripheralAgent是真正进行交互的类。
解析数据是LGPeripheralAgent最主要的功能,因为蓝牙通讯是二进制数据传输,一般用户很难看懂。
数据分成两类:
- 设备主动发送的数据:操作设备上的按钮或触摸屏幕,比如SOS、拍照、音乐控制等,App接收到进行处理即可,类似于iOS的Notification。
- App主动发送的数据:发送一条数据给设备,设备会返回数据给App,返回的数据有可能只有一条,也有可能很多条。类似于http网络请求的,客户端发送request,服务器会返回数据。
初始化Agent,内部有大约1秒钟的查找蓝牙服务特征的处理,所以不能立即调用其他方法,需要延时调用。
- (instancetype)initWithPeripheral:(LGPeripheral *)aPeripheral;
写数据方法:
- (void)writeData:(NSData *)aData
intervals:(NSInteger)intervals
completion:(void(^)(NSData *data, NSError *error))aCallback;
开启实时心率
- (void)enableRealTimeHeartRateUpdate:(BOOL)enable completion:(void(^)(NSError *error))completion;
设备主动发送的数据以及被解析成LGPeripheralAgentDelegate,只要实现此协议即可。此方法包括:
- 实时心率
- 相机控制
- 音乐控制
- 查找手机
- 拨打电话
- 挂断电话
##LGBaseCommand 蓝牙命令,这是一个基类,不能直接创建使用,需要继承它实现相关方法。一个命令代表一个功能,它有很多个子类,具体查看功能说明。
初始化可以使用下面方法,命令跟具体的设备相关,所以需要传入一个agent作为参数。
+ (instancetype)commandWithAgent:(LGPeripheralAgent *)agent;
- (instancetype)initWithPeripheralAgent:(LGPeripheralAgent *)agent;
发送命令
- (void)start;
取消命令
- (void)cancel;
它有两个方法需要子类实现:
第一个是 - (NSData *)sendingData;
,命令需要发送的数据,二进制
第二个是- (BOOL)handleWithResponseData:(NSData *)responseData error:(NSError *)error;
设备返回数据接收的方法。设备可能只返回一次数据,或者连续不断的发送数据,所有数据都将经过这个方法, 请根据实际情况返回是否已完成接收。它的返回值代表是否接收完成所有数据,YES表示完成接收所有数据,NO表示还有数据需要接收。
继承于LGBaseCommand,有些比较简单的命令,比如设置参数的,它只需要返回是成功或失败,不关心其他的。这些命令可以提取共同的方法合并起来,子类只需要实现:
- (NSData *)sendingData;
命令队列类,可以批量添加LGBaseCommand的子类,然后一起执行。不需要持有对象,内部自动处理。
**这里的一起执行并不是指网络请求一样的并发,实际上内部是按照先后顺序挨个执行的。**这是因为蓝牙通道其实是阻塞式的,前面的数据还没处理完成,后面的数据如果来了,会把相关数据弄混淆。
基本使用如下,这是一个同步系统日历内容到手环的方法:
LGCommandsQueue *chainCommands = [[LGCommandsQueue alloc] init];
///指定agent,所有命令都共用
chainCommands.peripheralAgent = self.agent;
EKEventStore* store = [[EKEventStore alloc] init];
///请求日历权限
[store requestAccessToEntityType:EKEntityTypeEvent completion:^(BOOL granted, NSError *error) {
if (granted) {
//取当前时间一个月内的日历
NSDate* startDate = [NSDate date];
NSDate* endDate = [NSDate dateWithTimeInterval:3600 * 24 * 30 sinceDate:startDate];
NSPredicate *predicate = [store predicateForEventsWithStartDate:startDate
endDate:endDate
calendars:nil];
NSArray *events = [store eventsMatchingPredicate:predicate];
///最多5个日历
NSUInteger count = events.count > 5 ? 5: events.count;
for (int i = 0; i < count; i++) {
EKEvent *event = events[i];
///创建日历对象
LGCalendarEvent *ce = [[LGCalendarEvent alloc] initWithEKEvent:event atIndex:i];
///创建日历命令对象
LGCalendarEventCmd *cmd = [[[LGCalendarEventCmd alloc] init] addCalendarEvent:ce success:nil failure:nil];
///添加进队列里面
[chainCommands addCommandObject:cmd];
}
///执行
[chainCommands startWithCompletion:^(NSError *error){
[self showAlert:_commands[row] msg:error ? error.description: @"成功"];
}];
}
}];