调试蓝牙扫码枪遇到的问题

Author Avatar
XibHe 12月 06, 2016
  • 在其它设备中阅读本文章

iOS蓝牙开发简介

蓝牙 4.0出现之前,蓝牙 2.0时只有iOS设备和苹果认证的MFI设备才可以被iOS设备检索到。蓝牙 4.0之后(硬件要4S,系统要iOS6以上才支持蓝牙 4.0),苹果开放了BLE(蓝牙4.0以低功耗著称,所以一般被称为BLE(bluetooth low energy))通道,没有MFI认证的蓝牙设备也可以连接非越狱的iOS设备了。

总结了一下,一共有五种方式可以实现蓝牙通信:

  1. 使用MFI认证的蓝牙模块的蓝牙产品 —— 开发使用ExternalAccessory框架,如果你的蓝牙模块还没设计,打算在AppStore上发布,打算使用蓝牙4.0以下(IOS设备都兼容),那你就抓紧找MFI认证的蓝牙模块吧。
  2. 使用苹果提供的CoreBluetooth framework框架 —— 只适用于支持蓝牙4.0的设备,无需越狱,无需使用MFI,可以发布在AppStore上。
  3. GameKit framework —— 只能在iOS设备之间同一个应用内连接,从iOS7开始过期了,通过蓝牙可以实现文件的共享(仅限设备沙盒中的文件),此框架一般用于游戏开发(比如五子棋对战)。
  4. Private API —— 使用私有API的应用程序不被允许在AppStore上发布。
  5. Jailbreak

这里我使用的是第二种方式 —— CoreBluetooth。

核心概念

CoreBluetooth框架的核心其实是两个东西,peripheral和central, 可以理解成外设和中心。对应他们分别有一组相关的API和类。

  • CBCentralManager:中心设备(用来连接到外部设备的管家)
  • CBPeripheralManager:外部设备(第三方的蓝牙4.0设备)

BLE

  • 这两组api分别对应不同的业务场景,左侧叫做中心模式,就是以你的app作为中心,连接其他的外设的场景,而右侧称为外设模式,使用手机作为外设别其他中心设备操作的场景。
  • 服务和特征,特征的属性(service and characteristic):
    每个设备都会有一些服务,每个服务里面都会有一些特征,特征就是具体键值对,提供数据的地方。每个特征属性分为这么几种:读,写,通知这么几种方式。

外设、服务、特征间的关系

关系图

连接测试

检测蓝牙设备的连接强度,查询一定范围内的蓝牙设备列表。可以通过一款名为LightBlue的手机端软件来确定。在开启蓝牙的状态下,打开LightBlue搜索到当前范围内的设备列表,可以点击查看当前设备的UUID以及特征和服务的UUID,如图,

系统蓝牙

设备列表

特征和服务的UUID

连接蓝牙扫码枪

项目中需要连接蓝牙扫码枪,通过扫码枪扫描商品的条码,将得到的条码值传给终端设备。终端设备根据该条码值搜索该商品的全部信息。iPad开启蓝牙,通过设置蓝牙扫描枪,连接上iPad,但发现在调试状态下,并没有调用CBCentralManager的代理方法。使用LightBlue搜索当前范围内的蓝牙设备,无法找到当前连接到iPad上的蓝牙扫描枪。蓝牙扫描枪使用的是富立叶(cilico)ci6800这款。最后,看了产品说明,发现它的蓝牙模块是蓝牙2.0 + EDR。而不是最新的蓝牙4.0。在未做任何处理的情况下,iPad端任然可以所接收到扫码枪扫描条码后的到的码值。原来是扫码枪默认开启了HID模式,在该模式下iPad默认当前通过蓝牙连接的扫码枪为一个外接键盘,因此扫描后得到码值相当于通过敲击键盘输入。

这样也造成了一个问题,iPad上所有带有输入功能的操作,都无法通过点击输入框弹出软键盘。因为系统会将扫码枪当做键盘,所有软键盘的弹出或者隐藏都是通过扫码枪做操作。但事实上扫码枪并不能控制键盘的弹出。通过google找到了一种据说是可以解决该问题的方法点击查看。但该方法极不稳定,当扫码枪的连接状态在休眠/唤醒两种状态下来回切换时就会失效。偶尔会成功弹出键盘。

最后,在联系厂家需求帮助无果的情况下,不得不停止了该款扫码枪的适配。

连接蓝牙血压计

为了验证自己的代码没有问题,是由于蓝牙扫码枪的蓝牙模块版本过低不支持CoreBluetooth。我又找来了一台据说支持蓝牙4.0的蓝牙血压计做测试。

代码实现的步骤

第一步,创建CBCentralManager。
第二步,扫描可连接的蓝牙外设(必须在蓝牙模块打开的前提下)。
第三步,连接目标蓝牙外设。
第四步,查询目标蓝牙外设下的服务。
第五步,遍历服务中的特性,获取特性中的数据或者保存某些可写的特性,或者设置某些特性值改变时,通知主动获取。
第六步,在通知更新特性中值的方法中读取特性中的数据(再设置特性的通知为YES的情况下)。
第七步,读取特性中的值。
第八步,如果有可写特性,并且需要向蓝牙外设写入数据时,写入数据发送给蓝牙外设。

  1. 引入 CoreBluetooth头文件
#import <CoreBluetooth/CoreBluetooth.h>
  1. 声明属性
@property (nonatomic, strong) CBCentralManager *manager;
@property (nonatomic, strong) CBPeripheral *peripheral;
@property (strong ,nonatomic) CBCharacteristic *writeCharacteristic;
@property (strong,nonatomic) NSMutableArray *nDevices;
@property (strong,nonatomic) NSMutableArray *nServices;
@property (strong,nonatomic) NSMutableArray *nCharacteristics;
  1. 遵守协议
@interface ViewController () <CBCentralManagerDelegate, CBPeripheralDelegate>
  1. 初始化数据
- (void)viewDidLoad 
{
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    self.manager = [[CBCentralManager alloc] initWithDelegate:self queue:nil];

    _cbReady = false;
    _nDevices = [[NSMutableArray alloc]init];
    _nServices = [[NSMutableArray alloc]init];
    _nCharacteristics = [[NSMutableArray alloc]init];
    count = 0;
}
  1. 实现蓝牙的协议方法
  • (1)检查蓝牙状态
-(void)centralManagerDidUpdateState:(CBCentralManager *)central
{
    switch (central.state) {
        case CBCentralManagerStatePoweredOn:
        {
            [self updateLog:@"蓝牙已打开,请扫描外设"];
            [_activity startAnimating];
            [_manager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:@"FFF0"]]  options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @YES }];
        }
            break;
        case CBCentralManagerStatePoweredOff:
            [self updateLog:@"蓝牙没有打开,请先打开蓝牙"];
            break;
        default:
            break;
    }
}
注:[_manager scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:@”FF15”]] options:@{CBCentralManagerScanOptionAllowDuplicatesKey : @YES }];中间的@[[CBUUID UUIDWithString:@”FFF0”]]是为了过滤掉其他设备,可以搜索特定标示的设备。
  • (2)检测到外设后,停止扫描,连接设备
//查到外设后,停止扫描,连接设备
-(void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
{
    [self updateLog:[NSString stringWithFormat:@"已发现 peripheral: %@ rssi: %@, UUID: %@ advertisementData: %@ ", peripheral, RSSI, peripheral.identifier, advertisementData]];

    _peripheral = peripheral;
    [_manager connectPeripheral:_peripheral options:nil];

    [self.manager stopScan];
    [_activity stopAnimating];

    BOOL replace = NO;
    // Match if we have this device from before
    for (int i=0; i < _nDevices.count; i++) {
        CBPeripheral *p = [_nDevices objectAtIndex:i];
        if ([p isEqual:peripheral]) {
            [_nDevices replaceObjectAtIndex:i withObject:peripheral];
            replace = YES;
        }
    }
    if (!replace) {
        [_nDevices addObject:peripheral];
        [_bluetoothTable reloadData];
    }
}
  • (3)连接外设后的处理
//连接外设成功,开始发现服务
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
    NSLog(@"%@", [NSString stringWithFormat:@"成功连接 peripheral: %@ with UUID: %@",peripheral,peripheral.identifier]);

    [self updateLog:[NSString stringWithFormat:@"成功连接 peripheral: %@ with UUID: %@",peripheral,peripheral.identifier]];

    [self.peripheral setDelegate:self];
    [self.peripheral discoverServices:nil];
    [self updateLog:@"扫描服务"];
}
//连接外设失败
-(void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
    NSLog(@"%@",error);
}

-(void)peripheralDidUpdateRSSI:(CBPeripheral *)peripheral error:(NSError *)error
{
    NSLog(@"%s,%@",__PRETTY_FUNCTION__,peripheral);
    int rssi = abs([peripheral.RSSI intValue]);
    CGFloat ci = (rssi - 49) / (10 * 4.);
    NSString *length = [NSString stringWithFormat:@"发现BLT4.0热点:%@,距离:%.1fm",_peripheral,pow(10,ci)];
    [self updateLog:[NSString stringWithFormat:@"距离:%@", length]];
}
  • (4)发现服务和搜索到的Characteristice
//已发现服务
-(void) peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{

    [self updateLog:@"发现服务."];
    int i=0;
    for (CBService *s in peripheral.services) {
        [self.nServices addObject:s];
    }
    for (CBService *s in peripheral.services) {
        [self updateLog:[NSString stringWithFormat:@"%d :服务 UUID: %@(%@)",i,s.UUID.data,s.UUID]];
        i++;
        [peripheral discoverCharacteristics:nil forService:s];

        if ([s.UUID isEqual:[CBUUID UUIDWithString:@"FFF0"]]) {
            BOOL replace = NO;
            // Match if we have this device from before
            for (int i=0; i < _nDevices.count; i++) {
                CBPeripheral *p = [_nDevices objectAtIndex:i];
                if ([p isEqual:peripheral]) {
                    [_nDevices replaceObjectAtIndex:i withObject:peripheral];
                    replace = YES;
                }
            }
            if (!replace) {
                [_nDevices addObject:peripheral];
                [_bluetoothTable reloadData];
            }
        }
    }
}

//已搜索到Characteristics
-(void) peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error{
    [self updateLog:[NSString stringWithFormat:@"发现特征的服务:%@ (%@)",service.UUID.data ,service.UUID]];

    for (CBCharacteristic *c in service.characteristics) {
        [self updateLog:[NSString stringWithFormat:@"特征 UUID: %@ (%@)",c.UUID.data,c.UUID]];

        if ([c.UUID isEqual:[CBUUID UUIDWithString:@"FF01"]]) {
            _writeCharacteristic = c;
 }

        if ([c.UUID isEqual:[CBUUID UUIDWithString:@"FF02"]])     {
            [_peripheral readValueForCharacteristic:c];
            [_peripheral setNotifyValue:YES forCharacteristic:c];
  }
 }   
 }
}

- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
    [self updateLog:[NSString stringWithFormat:@"已断开与设备:[%@]的连接", peripheral.name]];
}
  • (5)获取外设发来的数据
//获取外设发来的数据,不论是read和notify,获取数据都是从这个方法中读取。
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
    if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"FF02"]]) {
        NSData * data = characteristic.value;
        Byte * resultByte = (Byte *)[data bytes];

        for(int i=0;i<[data length];i++)
            printf("testByteFF02[%d] = %d\n",i,resultByte[i]);

        if (resultByte[1] == 0) {
        }else if (resultByte[1] == 1) {
            [self updateLog:@"未知错误"];
        }else if (resultByte[1] == 2) {
            [self updateLog:@"鉴权失败"];
        }
    }
    if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"FF04"]]) {
        NSData * data = characteristic.value;
        Byte * resultByte = (Byte *)[data bytes];

        for(int i=0;i<[data length];i++)
            printf("testByteFF04[%d] = %d\n",i,resultByte[i]);}

    if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"FF05"]]) {
        NSData * data = characteristic.value;
        Byte * resultByte = (Byte *)[data bytes];

        for(int i=0;i<[data length];i++)
            printf("testByteFF05[%d] = %d\n",i,resultByte[i]);

        if (resultByte[0] == 0) {
            // 设备加解锁状态 0 撤防     1 设防
            [self updateLog:@"当前车辆撤防状态"];
        }else if (resultByte[0] == 1) {
            // 设备加解锁状态 0 撤防     1 设防
            [self updateLog:@"当前车辆设防状态"];
        }
    }
}

//中心读取外设实时数据
- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
    if (error) {
        NSLog(@"Error changing notification state: %@", error.localizedDescription);
    }

    // Notification has started
    if (characteristic.isNotifying) {
        [peripheral readValueForCharacteristic:characteristic];

    } else { // Notification has stopped
        // so disconnect from the peripheral
        NSLog(@"Notification stopped on %@.  Disconnecting", characteristic);
        [self updateLog:[NSString stringWithFormat:@"Notification stopped on %@.  Disconnecting", characteristic]];
        [self.manager cancelPeripheralConnection:self.peripheral];
    }
}
//用于检测中心向外设写数据是否成功
-(void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
    if (error) {
        NSLog(@"=======%@",error.userInfo);
        [self updateLog:[error.userInfo JSONString]];
    }else{
        NSLog(@"发送数据成功");
        [self updateLog:@"发送数据成功"];
    }

    /* When a write occurs, need to set off a re-read of the local CBCharacteristic to update its value */
    [peripheral readValueForCharacteristic:characteristic];
}

后记

厂商一般会提供一份蓝牙血压计通信指令控制数据格式(通讯协议) 可根据该协议中定义的返回的十六进制数据,定义当前外设发送的不同数据的意义。
最主要是用UUID来确定你要干的事情,特征和服务的UUID都是外设定义好的。我们只需要读取,确定你要读取什么的时候,就去判断UUID是否相符。 一般来说我们使用的iPhone都是做centralManager的,蓝牙模块是peripheral的,所以我们是want datas,需要接受数据。

  1. 判断状态为powerOn,然后执行扫描
  2. 停止扫描,连接外设
  3. 连接成功,寻找服务
  4. 在服务里寻找特征
  5. 为特征添加通知
  6. 通知添加成功,那么就可以实时的读取value[也就是说只要外设发送数据[一般外设的频率为10Hz],代理就会调用此方法]。
  7. 处理接收到的value,[hex值,得转换] 之后就自由发挥了,在这期间都是通过代理来实现的,也就是说你只需要处理你想要做的事情,代理会帮你调用方法。[别忘了添加代理]

参考资料

蓝牙相关基础知识

hacksugar: Bringing back the on-screen keyboard

蓝牙交互CoreBlueTooth

iOS and bluetooth

iOS蓝牙,CoreBluetooth框架简介及入门使用

想做iOS Bluetooth產品但又不想過MFI的討論

CoreBluetooth cannot find devices but iOS can

请教熟悉iOS的朋友一个问题

【译】iOS蓝牙编程指南 – 核心蓝牙概述

–EOF–

若无特别说明,本站文章均为原创,转载请保留链接,谢谢