某硬件项目蓝牙管理类VBBluetoothManager重构

[toc]

业务背景

正如上篇iOS端智能硬件BLE通信技术实现一文所述,整个蓝牙库的最初设计实现最初都是为硬件通信服务的,19年年前突然接到需求,希望通过车载上放置的蓝牙收音设备,让用户在唤醒设备后,直接将用户的语音指令转成app里面可执行的操作,如“XXX, 播放每日XX的歌”,“XXX,上/下一首”等一些当前app用户常用的操作,产品希望通过这类车载设备来扩大app现有用户的使用场景甚至探寻增加新用户的可能。

现有问题

其实原app中蓝牙通信目前已经有两套代码了,一套用于早先的蓝牙耳机,一套用于硬件A业务,硬件A业务由于是18年11月才并入现有app的,存在两套代码也无可厚非,现在又增加了新的车载设备,难道再编写一套为车载设备而设计的蓝牙通信方案吗?作为一个有节操的程序媛,当然不可能这么做了!!
首先跟嵌入式端同学约定:车载设备的通信协议复用原有硬件A的那一套,这样数据收发处理(发送拆包接收组包)的代码就能复用,但是负责管理蓝牙一系列通信行为(扫描、停止扫描、连接、发现服务、断开连接、接收蓝牙数据等)以及提供发送数据接口和回调收到的数据给用户的VBBluetoothManager类, 耦合了扫描硬件A的标识符serviceUUID, 读写硬件A特征值的标识符characteristicUUID, 甚至有在已扫描到的设备列表中根据硬件名称或UUID查找特定设备的接口,其中的很多接口都是为硬件A通信服务的,先看下原有部分代码:

// from VBBluetoothManager.m, 以下...表示省略其他无关代码
@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去扫描(兼容嵌入式旧版本,旧版本蓝牙名称很长,超过了蓝牙广播包的长度限制,导致根据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];
    }
}

// 设备连接成功后,在发送数据前先发现服务
- (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

// 能否向外设发送数据
- (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通道都开启的情况下,才能发送数据
    BOOL canSendData = isRxCharacterNotify && isCtsCharacterNotify;
    return canSendData;
}

// 根据外设实例构造数据加工处理的桥接类
- (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

从以上未重构之前的VBBluetoothManager类的代码可以看到:该类确实耦合了大量跟特定外设信息相关的代码,导致现在新增加一种车载外设,却无法走通同样的通信流程。

解决方案

1. 抽取公共属性和方法

Untitled.gif

(1) ServiceUUIDs: 扫描外设时所用到的服务UUID数组;

(2) CharacterUUIDs: 读写数据特征和订阅特征的UUID数组;

(3) rxCharacterUUID: 嵌入式端告知的读数据的特征UUID,和txCharacterUUID一起用于构造数据加工处理类VBDataBridge实例,后者主要的作用是:收到数据处理的请求,交由VBDataBridge去决定是该接收类receiver去组装数据,还是sender类去拆包分次发送数据;

(4) txCharacterUUID: 嵌入式端告知的写数据的特征UUID,作用如上所述;

(5) peripheral: CBPeripheral实例类, 用该实例包装生成自己的蓝牙外设类,判断当前能否发送数据(canSendData)、开启订阅特征(notifyCharacter)等都需从该实例中执行相应操作;

(6) peripheralType: 外设类型,已知的外设类型枚举;

(7) prefixName: 外设的名称前缀;

(8) canSendData: 能否向外设发送数据,只有写数据通道处于notifying状态才可写;

(9) notifyCharacterForService:completionBlock::开启可读取或订阅特征的通道,并把对应的特征值通过block传回给调用方。

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,我们需要在具体的外设模型中实现上面抽象出来的属性和接口。

2.1 扩展VBUUIDType枚举类型
typedef NS_ENUM(NSInteger, VBUUIDType)
{
    VBUUIDNone = 0,
    // 车载设备的UUID
    VBCarplayServiceType = 0xFE00,
    VBCarplayTxCharacteristicType= 0xFE01,
    VBCarplayRxCharacteristicType = 0xFE02,
    // 硬件设备的UUID
    VBVboxServiceType = 0xFFF0,
    VBVboxTxCharacteristicType = 0xFFF1, // 手机向硬件发送LE数据的链路
    VBVboxRxCharacteristicType = 0xFFF2, // 手机从硬件接收LE数据的链路
    VBVboxCtsCharacteristicType = 0xFFF3, // 标识手机是否可以继续向硬件发送数据的链路,
};
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都开启的情况下,才能发送数据
    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通道打开就能发送数据
    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. 重构VBBluetoothManager

VBBluetoothManager类的原有代码中,替换原先用到serviceUUIDs、characterUUIDs以及能否判断能否向外设发送代码的业务逻辑:

@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

至此,重构蓝牙管理类以支持多种设备的蓝牙数据通信工作就完成了,至于接收到数据之后,上层怎么处理又是另外一回事了,在此不谈。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336