[toc]
業(yè)務背景
正如上篇iOS端智能硬件BLE通信技術(shù)實現(xiàn)一文所述亿傅,整個藍牙庫的最初設計實現(xiàn)最初都是為硬件通信服務的,19年年前突然接到需求皆愉,希望通過車載上放置的藍牙收音設備,讓用戶在喚醒設備后,直接將用戶的語音指令轉(zhuǎn)成app里面可執(zhí)行的操作叫倍,如“XXX, 播放每日XX的歌”,“XXX豺瘤,上/下一首”等一些當前app用戶常用的操作吆倦,產(chǎn)品希望通過這類車載設備來擴大app現(xiàn)有用戶的使用場景甚至探尋增加新用戶的可能。
現(xiàn)有問題
其實原app中藍牙通信目前已經(jīng)有兩套代碼了坐求,一套用于早先的藍牙耳機蚕泽,一套用于硬件A業(yè)務,硬件A業(yè)務由于是18年11月才并入現(xiàn)有app的桥嗤,存在兩套代碼也無可厚非须妻,現(xiàn)在又增加了新的車載設備,難道再編寫一套為車載設備而設計的藍牙通信方案嗎泛领?作為一個有節(jié)操的程序媛荒吏,當然不可能這么做了!渊鞋!
首先跟嵌入式端同學約定:車載設備的通信協(xié)議復用原有硬件A的那一套绰更,這樣數(shù)據(jù)收發(fā)處理(發(fā)送拆包接收組包)的代碼就能復用,但是負責管理藍牙一系列通信行為(掃描锡宋、停止掃描动知、連接、發(fā)現(xiàn)服務员辩、斷開連接盒粮、接收藍牙數(shù)據(jù)等)以及提供發(fā)送數(shù)據(jù)接口和回調(diào)收到的數(shù)據(jù)給用戶的VBBluetoothManager
類, 耦合了掃描硬件A的標識符serviceUUID
, 讀寫硬件A特征值的標識符characteristicUUID
, 甚至有在已掃描到的設備列表中根據(jù)硬件名稱或UUID
查找特定設備的接口,其中的很多接口都是為硬件A通信服務的奠滑,先看下原有部分代碼:
// from VBBluetoothManager.m, 以下...表示省略其他無關(guān)代碼
@interface VBBluetoothManager ()<VBDataBridgeDelegate,CBCentralManagerDelegate,CBPeripheralDelegate>
{
...
NSArray *_serviceUUIDs;
NSMutableArray *_characterUUIDs;
...
}
@implementation VBBluetoothManager
- (instancetype)init {
if (self = [super init]) {
...
_serviceUUIDs = @[[VBUUIDUtil UUIDWithType:VBUUIDService]];
_characterUUIDs = [NSMutableArray array];
NSArray<NSNumber *> *cases = [VBUUIDUtil cases];
for (NSNumber *type in cases) {
if ([type integerValue] != VBUUIDService) {
[_characterUUIDs addObject:[VBUUIDUtil UUIDWithType:[type integerValue]]];
}
}
...
}
return self;
}
#pragma mark - Public Methods
// 掃描外設
- (void)scanWithDurationWithTimeout:(NSTimeInterval)timeout resetPrevScan:(BOOL)isReset {
if (isReset) {
[self resetScan];
}
// 前臺時不指定serviceUUID去掃描(兼容嵌入式舊版本丹皱,舊版本藍牙名稱很長妒穴,超過了藍牙廣播包的長度限制,導致根據(jù)serviceUUID無法掃描到)
// 后臺的話bluetooth state reservation and restoration要求一定要指定serviceUUID
NSArray *services = ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) ? nil : _serviceUUIDs;
[_central scanForPeripheralsWithServices: services options:@{CBCentralManagerScanOptionAllowDuplicatesKey: @NO}];
// 先將之前的掃描取消
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(endScanForPeriphers) object:nil];
[self performSelector:@selector(endScanForPeriphers) withObject:nil afterDelay:timeout];
}
// 取消所有連接
- (void)cancelAllConnections {
_isManualDisconnect = YES;
for (CBPeripheral *peripheral in [_central retrieveConnectedPeripheralsWithServices:_serviceUUIDs]) {
[_central cancelPeripheralConnection:peripheral];
}
}
// 設備連接成功后摊崭,在發(fā)送數(shù)據(jù)前先發(fā)現(xiàn)服務
- (void)startDiscoverServiceForPeripheral:(CBPeripheral *)peripheral {
BOOL canSendData = [self canSendDataForPeripheral:peripheral];
peripheral.delegate = self;
if (canSendData) {
[self constructDataBridges:peripheral];
return;
}
[peripheral discoverServices:_serviceUUIDs];
}
#pragma mark - Private Methods
// 能否向外設發(fā)送數(shù)據(jù)
- (BOOL)canSendDataForPeripheral:(CBPeripheral *)peripheral {
BOOL isRxCharacterNotify = NO;
BOOL isCtsCharacterNotify = NO;
for (CBCharacteristic *character in [peripheral.services.firstObject characteristics]) {
VBUUIDType uuidType = [VBUUIDUtil typeForUUID:character.UUID];
switch (uuidType) {
case VBUUIDRxCharacteristic:
isRxCharacterNotify = character.isNotifying;
break;
case VBUUIDCtsCharacteristic:
isCtsCharacterNotify = character.isNotifying;
default:
break;
}
}
// 只有在cts和rx通道都開啟的情況下讼油,才能發(fā)送數(shù)據(jù)
BOOL canSendData = isRxCharacterNotify && isCtsCharacterNotify;
return canSendData;
}
// 根據(jù)外設實例構(gòu)造數(shù)據(jù)加工處理的橋接類
- (void)constructDataBridges:(CBPeripheral *)peripheral {
CBService *primaryService = peripheral.services.firstObject;
NSArray<CBCharacteristic *> *characteristics = primaryService.characteristics;
if (!characteristics) {
return;
}
CBCharacteristic *txWriteCharacter;
CBCharacteristic *rxReceiveCharacter;
for (CBCharacteristic *character in characteristics) {
VBUUIDType uuidType = [VBUUIDUtil typeForUUID:character.UUID];
if (uuidType == VBUUIDNone) {
break;
} else if (uuidType == VBUUIDTxCharacteristic) {
txWriteCharacter = character;
} else {
if (uuidType == VBUUIDRxCharacteristic) {
rxReceiveCharacter = character;
}
if (!character.isNotifying) {
[peripheral setNotifyValue:YES forCharacteristic:character];
}
}
}
if (txWriteCharacter && rxReceiveCharacter) {
NSUInteger index = [self peripheralIndexAtDataBridges:peripheral];
// 移除舊的
if (index != NSNotFound) {
[_dataBridges removeObjectAtIndex:index];
}
VBDataBridge *bridge = [[VBDataBridge alloc] initWithPeripheral:peripheral writeCharacter:txWriteCharacter receiveCharacter:rxReceiveCharacter];
bridge.delegate = self;
[_dataBridges addObject:bridge];
}
}
#pragma mark - CBCentralManager delegate
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI {
if (peripheral && ![_peripherals containsObject:peripheral] && [peripheral.name hasPrefix:@"硬件A的名稱"]) {
[_peripherals addObject:peripheral];
for (id<VBBluetoothManagerDelegate> observer in _delegateObservers) {
NSAssert([NSThread isMainThread], @"非主線程");
if ([observer respondsToSelector:@selector(bluetoothManager:didFindNewPeripheral:)]) {
[observer bluetoothManager:self didFindNewPeripheral:peripheral];
}
}
}
}
#pragma mark - CBPeripheral delegate
// 1. 找到服務
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {
NSArray<CBService *> *services = peripheral.services;
if (!services) {
return;
}
for (CBService *service in services) {
[peripheral discoverCharacteristics:_characterUUIDs forService:service];
}
}
// 2. 找到特征
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
[self constructDataBridges:peripheral];
}
...
@end
從以上未重構(gòu)之前的VBBluetoothManager
類的代碼可以看到:該類確實耦合了大量跟特定外設信息相關(guān)的代碼,導致現(xiàn)在新增加一種車載外設呢簸,卻無法走通同樣的通信流程矮台。
解決方案
1. 抽取公共屬性和方法
(1) ServiceUUIDs: 掃描外設時所用到的服務UUID數(shù)組;
(2) CharacterUUIDs: 讀寫數(shù)據(jù)特征和訂閱特征的UUID數(shù)組根时;
(3) rxCharacterUUID: 嵌入式端告知的讀數(shù)據(jù)的特征UUID瘦赫,和txCharacterUUID一起用于構(gòu)造數(shù)據(jù)加工處理類VBDataBridge實例,后者主要的作用是:收到數(shù)據(jù)處理的請求蛤迎,交由VBDataBridge去決定是該接收類receiver去組裝數(shù)據(jù)确虱,還是sender類去拆包分次發(fā)送數(shù)據(jù);
(4) txCharacterUUID: 嵌入式端告知的寫數(shù)據(jù)的特征UUID替裆,作用如上所述校辩;
(5) peripheral: CBPeripheral
實例類, 用該實例包裝生成自己的藍牙外設類,判斷當前能否發(fā)送數(shù)據(jù)(canSendData)辆童、開啟訂閱特征(notifyCharacter)等都需從該實例中執(zhí)行相應操作宜咒;
(6) peripheralType: 外設類型,已知的外設類型枚舉把鉴;
(7) prefixName: 外設的名稱前綴荧呐;
(8) canSendData: 能否向外設發(fā)送數(shù)據(jù),只有寫數(shù)據(jù)通道處于notifying狀態(tài)才可寫纸镊;
(9) notifyCharacterForService:completionBlock::開啟可讀取或訂閱特征的通道倍阐,并把對應的特征值通過block傳回給調(diào)用方。
typedef void(^NotifyCharacterBlock)(CBCharacteristic *txWriteCharacter, CBCharacteristic *rxReceiveCharacter);
typedef NS_ENUM(NSInteger, VBPeripheralType) {
VBPeripheralTypeUnknown,
VBPeripheralTypeVbox,
VBPeripheralTypeCarplay,
};
@protocol VBPeripheralProtocol <NSObject>
+ (NSString *)prefixName;
@end
// interface
@interface VBPeripheral : NSObject
@property (nonatomic, assign, readonly) VBPeripheralType type;
@property (nonatomic, strong) CBPeripheral *peripheral;
@property (nonatomic, copy, readonly) NSArray<CBUUID *> *services;
@property (nonatomic, copy, readonly) NSArray<CBUUID *> *characterUUIDs;
@property (nonatomic, copy, readonly) CBUUID *rxCharacterUUID;
@property (nonatomic, copy, readonly) CBUUID *txCharacterUUID;
+ (instancetype)peripheralWithCBPeripheral:(CBPeripheral *)peripheral;
- (BOOL)canSendData;
- (void)notifyCharactersForService:(CBService *)service completionBlock:(NotifyCharacterBlock)completion;
@end
// implementation
@implementation VBPeripheral
- (instancetype)initWithPeripheral:(CBPeripheral *)peripheral {
self = [super init];
if (self) {
_peripheral = peripheral;
}
return self;
}
+ (instancetype)peripheralWithCBPeripheral:(CBPeripheral *)peripheral {
if ([peripheral.name hasPrefix:[VBVboxPeripheral prefixName]]) {
return [[VBVboxPeripheral alloc] initWithPeripheral:peripheral];
} else if ([peripheral.name hasPrefix:[VBCarplayPeripheral prefixName]]) {
return [[VBCarplayPeripheral alloc] initWithPeripheral:peripheral];
} else {
return nil;
}
}
@end
2. 子類化對應外設模型
目前已知有兩種藍牙外設:硬件A和車載逗威,每種設備有自己的服務和特征UUID峰搪,我們需要在具體的外設模型中實現(xiàn)上面抽象出來的屬性和接口。
2.1 擴展VBUUIDType
枚舉類型
typedef NS_ENUM(NSInteger, VBUUIDType)
{
VBUUIDNone = 0,
// 車載設備的UUID
VBCarplayServiceType = 0xFE00,
VBCarplayTxCharacteristicType= 0xFE01,
VBCarplayRxCharacteristicType = 0xFE02,
// 硬件設備的UUID
VBVboxServiceType = 0xFFF0,
VBVboxTxCharacteristicType = 0xFFF1, // 手機向硬件發(fā)送LE數(shù)據(jù)的鏈路
VBVboxRxCharacteristicType = 0xFFF2, // 手機從硬件接收LE數(shù)據(jù)的鏈路
VBVboxCtsCharacteristicType = 0xFFF3, // 標識手機是否可以繼續(xù)向硬件發(fā)送數(shù)據(jù)的鏈路,
};
2.2 硬件A外設模型
// interface
#import "VBPeripheralProtocol.h"
@interface VBVboxPeripheral :VBPeripheral
@end
// implementation
@implementation VBVboxPeripheral
- (VBPeripheralType)type {
return VBPeripheralTypeVbox;
}
- (NSArray<CBUUID *> *)services {
CBUUID *vboxService = [VBUUIDUtil UUIDWithType: VBVboxServiceType];
return @[vboxService];
}
- (NSArray<CBUUID *> *)characterUUIDs {
CBUUID *ctsCharacterUUID = [VBUUIDUtil UUIDWithType:VBVboxCtsCharacteristicType];
return @[self.txCharacterUUID, self.rxCharacterUUID, ctsCharacterUUID];
}
- (CBUUID *)txCharacterUUID {
return [VBUUIDUtil UUIDWithType:VBVboxTxCharacteristicType];
}
- (CBUUID *)rxCharacterUUID {
return [VBUUIDUtil UUIDWithType:VBVboxRxCharacteristicType];
}
- (BOOL)canSendData {
BOOL isRxCharacterNotify = NO;
BOOL isCtsCharacterNotify = NO;
for (CBCharacteristic *character in [self.peripheral.services.firstObject characteristics]) {
VBUUIDType uuidType = [VBUUIDUtil typeForUUID:character.UUID];
switch (uuidType) {
case VBVboxRxCharacteristicType:
isRxCharacterNotify = character.isNotifying;
break;
case VBVboxCtsCharacteristicType:
isCtsCharacterNotify = character.isNotifying;
default:
break;
}
}
// 硬件設備只有在cts和rx都開啟的情況下凯旭,才能發(fā)送數(shù)據(jù)
BOOL canSendData = isRxCharacterNotify && isCtsCharacterNotify;
return canSendData;
}
- (void)notifyCharactersForService:(CBService *)service completionBlock:(NotifyCharacterBlock)completion {
CBCharacteristic *txWriteCharacter;
CBCharacteristic *rxReceiveCharacter;
for (CBCharacteristic *character in service.characteristics)
{
VBUUIDType uuidType = [VBUUIDUtil typeForUUID:character.UUID];
switch (uuidType)
{
case VBVboxTxCharacteristicType:
txWriteCharacter = character;
break;
case VBVboxRxCharacteristicType:
rxReceiveCharacter = character;
[self.peripheral setNotifyValue:YES forCharacteristic:character];
break;
case VBVboxCtsCharacteristicType:
[self.peripheral setNotifyValue:YES forCharacteristic:character];
break;
default:
break;
}
}
completion(txWriteCharacter, rxReceiveCharacter);
}
+ (NSString *)prefixName {
return @"硬件A的名稱";
}
@end
2.3 車載外設模型
// interface
#import "VBPeripheralProtocol.h"
@interface VBCarplayPeripheral :VBPeripheral
@end
// implementation
@implementation VBCarplayPeripheral
- (VBPeripheralType)type {
return VBPeripheralTypeCarplay;
}
- (NSArray<CBUUID *> *)services {
CBUUID *carplayService = [VBUUIDUtil UUIDWithType:VBCarplayServiceType];
return @[carplayService];
}
- (NSArray<CBUUID *> *)characterUUIDs {
return @[self.txCharacterUUID, self.rxCharacterUUID];
}
- (CBUUID *)txCharacterUUID {
return [VBUUIDUtil UUIDWithType:VBCarplayTxCharacteristicType];
}
- (CBUUID *)rxCharacterUUID {
return [VBUUIDUtil UUIDWithType:VBCarplayRxCharacteristicType];
}
- (BOOL)canSendData {
// 車載設備只要rx通道打開就能發(fā)送數(shù)據(jù)
BOOL isRxNotifying = NO;
for (CBCharacteristic *character in [self.peripheral.services.firstObject characteristics]) {
VBUUIDType uuidType = [VBUUIDUtil typeForUUID:character.UUID];
if (uuidType == VBCarplayRxCharacteristicType) {
isRxNotifying = character.isNotifying;
break;
}
}
return isRxNotifying;
}
- (void)notifyCharactersForService:(CBService *)service completionBlock:(NotifyCharacterBlock)completion {
CBCharacteristic *txWriteCharacter;
CBCharacteristic *rxReceiveCharacter;
for (CBCharacteristic *character in service.characteristics)
{
VBUUIDType uuidType = [VBUUIDUtil typeForUUID:character.UUID];
switch (uuidType)
{
case VBCarplayTxCharacteristicType:
txWriteCharacter = character;
break;
case VBCarplayRxCharacteristicType:
rxReceiveCharacter = character;
[self.peripheral setNotifyValue:YES forCharacteristic:character];
break;
default:
break;
}
}
completion(txWriteCharacter, rxReceiveCharacter);
}
+ (NSString *)prefixName {
return @"NeVSPS";
}
3. 重構(gòu)VBBluetoothManager
類
在VBBluetoothManager
類的原有代碼中概耻,替換原先用到serviceUUIDs、characterUUIDs以及能否判斷能否向外設發(fā)送代碼的業(yè)務邏輯:
@interface VBBluetoothManager ()
{
NSArray<CBUUID *> *_serviceUUIDs;
}
@end
@implementation VBBluetoothManager
- (instancetype)init {
self = [super init];
if (self) {
...
_serviceUUIDs = @[
[VBUUIDUtil UUIDWithType: VBVboxServiceType],
[VBUUIDUtil UUIDWithType: VBCarplayServiceType]];
...
}
return self;
}
- (void)startDiscoverServiceForPeripheral:(CBPeripheral *)peripheral {
BOOL canSendData = [self canSendDataForPeripheral:peripheral];
NELogVerbose(@"%s %@", __func__, @(canSendData));
peripheral.delegate = self;
id<VBPeripheralProtocol> vbPeripheral = [VBPeripheral peripheralWithCBPeripheral:peripheral];
if ([vbPeripheral canSendData]) {
return;
}
[peripheral discoverServices:vbPeripheral.services];
}
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI {
BOOL isVboxPeripheral = [peripheral.name hasPrefix:[VBVboxPeripheral prefixName]];
BOOL isCarplayPeripheral = [peripheral.name hasPrefix:[VBCarplayPeripheral prefixName]];
if (peripheral && ![_peripherals containsObject:peripheral] &&
(isVboxPeripheral || isCarplayPeripheral)) {
[_peripherals addObject:peripheral];
for (id<VBBluetoothManagerDelegate> observer in _delegateObservers) {
NSAssert([NSThread isMainThread], @"非主線程");
if ([observer respondsToSelector:@selector(bluetoothManager:didFindNewPeripheral:)]) {
[observer bluetoothManager:self didFindNewPeripheral:peripheral];
}
}
}
}
#pragma mark - CBPeripheral delegate
// 1. 找到服務
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {
id<VBPeripheralProtocol> vbPeripheral = [VBPeripheral peripheralWithCBPeripheral:peripheral];
for (CBService *service in peripheral.services) {
[peripheral discoverCharacteristics:vbPeripheral.characterUUIDs forService:service];
}
}
// 2. 找到特征
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
id<VBPeripheralProtocol> vbPeripheral = [VBPeripheral peripheralWithCBPeripheral:peripheral];
[vbPeripheral notifyCharactersForService:service completionBlock:^(CBCharacteristic *txWriteCharacter, CBCharacteristic *rxReceiveCharacter) {
if (txWriteCharacter && rxReceiveCharacter) {
NSUInteger index = [self peripheralIndexAtDataBridges:peripheral];
// 移除舊的
if (index != NSNotFound) {
[_dataBridges removeObjectAtIndex:index];
}
VBDataBridge *bridge = [[VBDataBridge alloc] initWithPeripheral:peripheral writeCharacter:txWriteCharacter receiveCharacter:rxReceiveCharacter];
bridge.delegate = self;
[_dataBridges addObject:bridge];
}
}];
}
...
@end
至此罐呼,重構(gòu)藍牙管理類以支持多種設備的藍牙數(shù)據(jù)通信工作就完成了鞠柄,至于接收到數(shù)據(jù)之后,上層怎么處理又是另外一回事了嫉柴,在此不談厌杜。