美文网首页iOS开发问题集锦
某硬件项目蓝牙管理类VBBluetoothManager重构

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

作者: 青青河边草2041 | 来源:发表于2019-03-25 23:31 被阅读0次

    [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
    

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

    相关文章

      网友评论

        本文标题:某硬件项目蓝牙管理类VBBluetoothManager重构

        本文链接:https://www.haomeiwen.com/subject/duoivqtx.html