`蓝牙`标签下的文章

编程

蛋疼的GATT权限

Context

定位水壶架App的iOS端已经完成了80%,在考完矩阵之前,只要把android端的网络和蓝牙交互做完,就可以没什么太大的压力咯.

然而,在搞android端的时候,还是遇到了很多很多问题的.

第一坑

几天前,当我把iOS端蓝牙配对的代码,翻译为Java版本之后,本来想,肯定会轻轻松松,正正常常的运行了.

但是,在连接上定位器之后,总是会卡在搜索服务或者写入xxx这个阶段,然后就断线了.

当时,我觉得,可能是一些小问题,可能是我写漏了什么东西,所以就卡住了.

也怀疑过是Google的亲儿子Nexus蓝牙硬件不给力~

然后就痛苦的去复习了几天矩阵理论…………

但是,就在两个小时之前,我一步一步调试的时候,发现了一个奇怪的现象.

public void onServicesDiscovered(BluetoothGatt gatt, int status);

BluetoothGattCallback中,搜索到service的时候,竟然没有我们自己的service!

所以for循环就那么做完了……什么service都没有找到,最后就被关闭连接了.

这时候,我开始怀疑人生了

难道说,这蓝牙是为iOS设计的!?

赶紧整了一个锤子过来测测.发现锤子是可以找到所有的service的……

难道说,这蓝牙不是为Google设计的!?

这时候,我突然想到了一个问题.和之前几天的实验,可能有点儿关系.

很久很久的几天前

添哥找我说,蓝牙可以设置一个加密密钥,这样子传输数据,就是加密的了.(虽然说,蓝牙和WiFi一样,是要连接之后才能通信的,但是,无线嘛,肯定是要被窃听的)

于是我们就试验了一下,但是蓝牙加密之后,需要手动在手机上点击配对按钮,才可以进行加密通信.

由于android端,这个配对不是特别的明显,不像iOS那么粗暴地弹出来.所以我们最后没有使用这个方案.

这里还可以顺便说说加密

小时候,以为加密的意思就是,给数据加了一把锁.只要用密码把锁打开了,就可以访问数据了.(真单纯)

原来,加密是把数据都给翻译了一遍.即使知道加密解密的算法,使用错误的密钥,可以得到数据,但是这数据,肯定是错的.

当蓝牙加密通信的时候,会分配一个密钥.咱们配对蓝牙鼠标,蓝牙键盘的时候,也常常看到,需要输入个什么pin码,才能配对.一旦这配对了,之后通信就是加密的了.

(假设蓝牙键盘不用配对,不加密通信……)那就是无线的按键记录器啊!

总而言之

这搜索不出我们需要的service就是这个配对的,当我进入系统蓝牙设置,把这个设备的配对给取消了之后,就正常了~.~

第二坑的Context

BLE的GATT的Characteristic有权限控制.

大概就是Read,Write,WriteWithoutResponse,Notify这四种常见权限.

他们用一个8位的flag来控制.因为不是重点,所以懒得讲了.

第二坑

在我解决了Service Discover的问题了之后,我觉得,这下一定可以顺利地跑下去了!

结果卡在了一个写入之后的读取上…

本来,在写入一个数据之后,需要去读取一个反馈的.但是不知道为什么,没有去读取这个数据.

仔细阅读Monitor里面的log.可以发现,之前的读取,都会调用系统的readCharacteristic()函数,而这一次的读取却没有读取,这是为啥子嘞?

权限!

我们有一个Characteristic在写入之后,需要读取反馈.所以需要Read的权限.

然而,在这个Characteristic没有Read的权限的情况下,iOS端竟然还是可以正确的读取到数据!

而Android直接忽略的readCharacteristic()这个函数调用……

虽然说,这硬件是我们自己设计的,讲道理的话,在连接设备之后,并不需要去检查各种权限的.(除非这个硬件是被山寨的)

但是,加上一个权限验证的话,或许可以提高一些软件的稳健性呢?说不定可以让这个系统可以在遇到山寨货的情况下,可以继续正常的工作下去呢~

End

2016马上要结束了,每年都能更加深入的了解一些蓝牙,真的是好刺激!

阅读剩下更多

编程

Android上的蓝牙

Context

如果说Android和iOS一样,一出新版本,每个用户都回去升级,那我就不用写这篇咯

先说说iOS端的蓝牙扫描

@param serviceUUIDs A list of <code>CBUUID</code> objects representing the service(s) to scan for.
//serviceUUIDs是一个过滤NSArray
//根据蓝牙外设的广播信号中的Service UUID来过滤
- (void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs options:(nullable NSDictionary<NSString *, id> *)options;

假定说,我们的app需要过滤两个产品的蓝牙信号.那么只要在serviceUUIDs里面填写他们特有Service UUID.

然后,在代理方法中做区分.

- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI{
    // 从广播信号中获取Service UUID 的列表
    NSArray *serviceUUIDs=advertisementData[CBAdvertisementDataServiceUUIDsKey];
    //做一下判断
    if ([serviceUUIDs containsObject:[CBUUID UUIDWithString:@"FFFF"]]){
        //欧耶,这是我们的产品1
    }else if ([serviceUUIDs containsObject:[CBUUID UUIDWithString:@"FFFF"]]){
        //欧耶,这是产品2
        //直接else当然也可以.但是要保险一些嘛
    }
}

方便,真是方便

正题

Android中,貌似没有那么轻松

当然,这就要提起Context里面说的事情了.

BLE是android 4.3引入的,所以最早拥有蓝牙更能的版本,就是API 18.

大概是由于API 18中,蓝牙搜索的方法,有点儿蛋疼,所以,他们改了新的方法吧?

API 21

先看看API 21怎么搜索蓝牙的.

BluetoothAdapter的一个静态方法

void startScan (List<ScanFilter> filters,ScanSettings settings,ScanCallback callback)
参数 参数 解释
filters List ScanFilters for finding exact BLE devices.
settings ScanSettings Settings for the scan.
callback ScanCallback Callback used to deliver scan results.

其中ScanFilter很牛逼

Service UUIDs which identify the bluetooth gatt services running on the device.
Name of remote Bluetooth LE device.
Mac address of the remote device.
Service data which is the data associated with a service.
Manufacturer specific data which is the data associated with a particular manufacturer.

这过滤方法,可比iOS的搜索强多了

然而,这个是API 21才有的……

API 18

由于,咱们要覆盖所有的,拥有BLE功能的手机用户

所以,只能用API 18了.

boolean startLeScan (UUID[] serviceUuids, BluetoothAdapter.LeScanCallback callback);

这过滤,和iOS一样了,只能用serviceUUID来过滤.

不过,通过我的实际测试,我发现serviceUuids填写的不能是不同设备的service uuid.

这样做,总是会显示没有match uuid list (有机会再好好研究一下)

那过滤两种设备,可以选择,做两遍这个方法,给不同的callback,好像还省去了用if去判断设备这个工作.

但是,如果有一大排设备需要去过滤呢……

boolean startLeScan (BluetoothAdapter.LeScanCallback callback);

直接全部搜索吧……还过滤啥呀

手动解析广播数据,进行分类,做不同的操作

BluetoothAdapter.LeScanCallback bluetoothScanCallback = new BluetoothAdapter.LeScanCallback() {

    @Override
    public void onLeScan(BluetoothDevice bluetoothDevice, int i, byte[] bytes) {
        //已经搜索过的设备,就不用重新解析啦
        if (locatorList.containsKey(bluetoothDevice.getAddress())){
            LocatorPeripheral peripheral=locatorList.get(bluetoothDevice.getAddress());
            peripheral.updateInformation(i);
        }else if(keyList.containsKey(bluetoothDevice.getAddress())){
            KeyPeripheral peripheral=keyList.get(bluetoothDevice.getAddress());
            peripheral.updateInformation(i);
        }else if(ignoreList.contains(bluetoothDevice)){
            return;
        } else {
            //这里开始重新解析
            VKPeripheral peripheral = VKPeripheral.createPeripheral(i,bytes,bluetoothDevice);
            if (peripheral instanceof LocatorPeripheral){
                //这是设备1
                locatorList.put(bluetoothDevice.getAddress(),(LocatorPeripheral) peripheral);
            }else if (peripheral instanceof KeyPeripheral){
                //这是设备2
                keyList.put(bluetoothDevice.getAddress(),(KeyPeripheral) peripheral);
            }else {
                //这不是咱们的东西
                ignoreList.add(bluetoothDevice);
                return;
            }
            //通知UI

        }
    }
};

我水平有限,所以就是这样写的~

这里有个VKPeripheral 类名也是直接从iOS版移植过来了

用来根据广播信息来新建我自己的蓝牙设备实例.

他生成的KeyPeripheral和LocatorPeripheral都是他的子类.如果不是我们需要的设备,就加到忽略的列表中去.

还有几个小坑

1.从android 6.0开始,蓝牙扫描如果需要得到结果,就必须获得定位权限(你逗我的吧…凭什么呀)

所以要把这个加到Manifest里面去

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

2.还是android 6.0的锅. 由于6.0可以动态获取权限,所以上面这个权限,直接写在Manifest里面,说不定还没用呢…
还要手动的去获取权限

找个Activity加上这一段(不唯一)

if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.ACCESS_FINE_LOCATION)!= PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION,Manifest.permission.ACCESS_COARSE_LOCATION},1);
}else if(ContextCompat.checkSelfPermission(MainActivity.this,Manifest.permission.BLUETOOTH)!=PackageManager.PERMISSION_GRANTED){
    ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.BLUETOOTH,Manifest.permission.BLUETOOTH_ADMIN},1);
}else {
    //你是不是不想用这个App!!
}

End

哎,灵活的Android呀.开放的Android呀.

阅读剩下更多

编程

BLE的广播帧

之前还以为BLE也就GATT那些东西嘛,没什么好说的了

其实

BLE或者说,蓝牙,的广播帧还是有很多很多奥秘的.还是有些东西可以拿来琢磨琢磨,学习学习

Context

至于为什么要突然学习这个东西,肯定是和新产品有关系的,具体的,可以等上市之后,买来一个试试看啊,哈哈哈哈哈哈

另一个原因,这个东西的官方文档真的很难找,至少在 www.bluetooth.com 上面我就没有看到讲述广播帧结构的文档,最后在TI的一个文档中,终于找到了相关的内容.

平时用BLE只要是在连接成功之后,搜索Service和Characteristc,后进行读写操作.这样的工作模式,只能一对一,不能做到一对多.但是利用了广播信息,可以做到一对多的发送消息…虽然这时候不能反过来写入.

Let’s go

我们先来上一张图片,看看Bluetooth到底广播了什么吧

AD

我是做上位机软件开发的,所以只关心应用层的东西,那么上面图片中,iOS和Android能被处理的数据是标红的那一段

一共31个字节,看起来好像不是很足够,但是由于协议是规定好了的,所以对应的位置表示对应的数据,还是够用的.

说说我们熟悉的一些广播信息吧:(iOS端CoreBluetooth的广播帧字典的Key)

1.LocalName (外设名称)
2.ServiceUUIDs (Service列表)
3.ManufacturerData (制造商数据)
4.IsConnectable (是否可连)

下面再给点儿我第一次知道的广播信息类型:

1.ServiceData (某个Service的广播数据)
2.TxPowerLevel (传输信号强度~.~,用来计算损耗用...好像暂时没啥用处,反正我也不会算)
3.OverflowServiceUUIDs (没懂他和ServiceUUIDs的区别~.~)
4.SolicitedServiceUUIDs (貌似和从设备没啥关系~.~)

说实话,平时用到的只有ServiceUUIDs,因为我们用这个来过滤出来我们需要的蓝牙外设,也用这个来区分不同的硬件.

LocalName的话iOS和Android都已经自动提取并封装成简单的接口了

上面是软件层面的东西,我们来看看硬件层面的呗~

一个广播帧会有很多类型的数据,每个类型有自己的type,上位机也是通过type字段来分辨数据是UUID还是Name或者其他的东西~

每一类数据都由3部分组成:

1.length of data (这个type的总长度)
2.type (定义type)
3.data (根据type的协议来填入数据)

这里简单的列举几个type:

0x02 不完整的16位UUID
0x04 不完整的32位UUID
0x06 不完整的128位UUID
0x08 设备全名
0x16 16位UUID的Service Data
0x20 32位UUID的Service Data
0x21 128位UUID的Service Data
0xFF 制造商自定义的数据

Example of Peripheral and Bluetooth Central

一台名叫VK的外设,公布了一个Service UUID 3344,5566;其中3344的Service Data是”0xAA,0xBB,0xCC”,制造商的自定义数据是”0x12,0x34,0x56,0x78”

那么他的广播数据是~

04 08 56 4B 05 02 44 33 66 55 06 16 44 33 cc bb aa 07 FF 00 00 78 56 34 12

拆分数据分析分析

04 08 56 4B
长度4 设备完整名称 V K

05 02 44 33 66 55
长度5 不完整的16位列表 3344 5566

06 16 44 33 cc bb aa
长度6 16位UUID的ServiceData UUID=3344 数据是 AA BB CC

07 FF 00 00 78 56 34 12
长度7 厂商自定义数据 厂商ID=0000 厂商自定义数据=12345678

如果有错误不要打我…

iOS端解析广播数据

-(void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI
{
    // 名字的字符串
    NSLog(@"Name %@",advertisementData[CBAdvertisementDataLocalNameKey]);
    // CBUUID的NSArray
    NSLog(@"Service UUID %@",advertisementData[CBAdvertisementDataServiceUUIDsKey]);
    // Key为CBUUID,Value为NSData的NSDictionary
    NSLog(@"Service Data %@",advertisementData[CBAdvertisementDataServiceDataKey]);
    // 制造商的数据NSData
    NSLog(@"ManufacturerData %@",advertisementData[CBAdvertisementDataManufacturerDataKey]);
}

Android端解析

@Override
    public void onLeScan(final BluetoothDevice device, int rssi,
            byte[] scanRecord) {
        // scanRecord 就是所有的广播数据呗
   }

iOS端为了隐私保护,做了各种各样奇怪的限定,所以不像Android那么爽,可以直接把广播帧的字节流都提取出来,所以就只能好好阅读协议,才能让iOS的App获取到想要的数据

相同的,Android虽然数据很开放,下位机可以不按照BLE协议去设置广播数据,Android App依然可以读得出来数据,但是解析数据就很头疼的样子了.

嗯,各有各的好处~

BTW

其中一个type是

0x17  Public Target Address
目标Mac地址

官方定义是

The Public Target Address data type defines the address of one or more intended recipients of an advertisement when one or more devices were bonded using a public address. This data type is intended to be used to avoid a situation where a bonded device unnecessarily responds to an advertisement intended for another bonded device.

what are you 弄啥嘞…

我YY他的用处就是,这个BLE设备只能被指定Mac地址的主机连接.

好啦,这次又学到了一点儿蓝牙的知识,写个文章当笔记.

阅读剩下更多

编程

蓝牙测速

之前的文章中介绍过蓝牙的速度和踏频协议,在那篇文章中也顺便介绍了BLE的一些小名词.这一篇文章就来说说炫轮App 3.0中的测速模块是怎么构成的吧.

Context

炫轮App可以获取到哪些传感器的数据呢?

  1. 炫轮车灯自身的码表数据
  2. 炫轮踏频器
  3. 满足公有踏频速度协议的传感器
  4. GPS

他们的数据是些什么?

1.炫轮车灯的数据

炫轮车灯会通过蓝牙得到每转一圈需要的时间

2.炫轮踏频器

这个产品不测速,所以先不多说~

3.满足公有协议的踏频速度传感器

通过蓝牙得到在第x秒转了y圈的信息

4.GPS

GPS数据本身就携带了速度信息,只不过,如果GPS信号不强,那这个数据就会不精准

再来个附加功能

大多数的骑行App的一次运动运动时间是手动点击开始,暂停,保存来记录的.这样不是很难过嘛?我想要个自动开始,自动暂停的码表!

不用往下翻,我没写~~这只是打个广告

Structure

所以和速度有关的传感器目前为止大概是那么3类: 炫轮车灯,满足公有协议的速度计,GPS

既然,GPS那么不稳定,干脆砍掉算了.

所以问题就变成了,如何根据炫轮车灯和速度计来计算出较为准确地速度.


Structure-Context

App 可以连接两个炫轮车灯,一个速度计

码表模块是和界面分离的,把他当做服务来处理(这里我写了一个单例),这个类可以收数据,也可以被获取数据.收到的自然是解析后的炫轮数据和速度计的数据,提供获取接口的是多种单位下的:当前速度,平均速度,总路程,总运动时间,(当然还有踏频和心率,只是这里不用,所以就不说了)


全局变量

计时器1枚,记录炫轮数据的数组2枚,记录速度计的数据的数组1枚.

其他的变量这篇文章用不到,就当他不存在了

-(void)updateXuanwheelSpeed:(NSTimeInterval)timeinterval ForPosition:(int)position
{
    switch (position) {
        case 0:
        {
            //########
            [xuanWheelTemporaryList addObject:@(timeinterval)];
            xuanwheelRevolution1++;
            //########
            break;
        }
        case 1:
        {
            //########
            [xuanWheelTemporaryList addObject:@(timeinterval)];
            xuanwheelRevolution2++;
            //########
            break;
        }
        default:
            break;
    }    
}

这是处理炫轮数据的方法,把一圈的时间间隔记录下来,分开写是因为要分别记录两个轮子转的圈数,帮助计算距离.

-(void)updateSpeeder:(Float32)frequency
{
    //########
    [publicTemporaryList addObject:@(frequency)];
}

公有协议的数据,我在这之前就一经计算好了转一圈的频率.已知在第X1秒转了Y1圈,在X2秒转了Y2圈.那么频率就是(X1-X2)/(Y1-Y2)

记录完毕数据,就该计算相关信息了

Calculate

-(void)optimizeSpeed
{
    if (######) {
        //....
    }else
    {
        //#####
        //Reason1:只有一组数据,还怎么计算平均速度;Reason2:#####
        if (publicTemporaryList.count>1||xuanWheelTemporaryList.count>1) {
            int count=0;
            //time用来存放这1秒内平均的转过1圈需要的时间
            NSTimeInterval time=0;
            for(NSNumber *item in publicTemporaryList)
            {
                //这里来计算公有协议的速度信息
                if (item.floatValue>5) {
                    //这么慢还是忽略吧
                    continue;
                }
                if (item.floatValue<=0) {
                    //谁解析的数据!!
                    continue;
                }
                time+=1.f/item.floatValue;
                count++;
            }
            if (publicTemporaryList.count>0) {
                if (publicTemporaryList.count>1) {
                    [publicTemporaryList removeObjectsInRange:NSMakeRange(0, publicTemporaryList.count-1)];
                }else
                {
                    [publicTemporaryList removeAllObjects];
                }
                //算完删光,留下一组数据,为下一秒做帮助,防止突变
            }
            for(NSNumber *item in xuanWheelTemporaryList)
            {
                //这里来计算炫轮的速度信息
                if (item.floatValue>5) {
                    continue;
                }
                if (item.floatValue<=0) {
                    continue;
                }
                time+=item.floatValue;
                count++;
            }
            if (xuanWheelTemporaryList.count>0) {
                if (xuanWheelTemporaryList.count>1) {
                    [xuanWheelTemporaryList removeObjectsInRange:NSMakeRange(0, xuanWheelTemporaryList.count-1)];
                }else
                {
                    [xuanWheelTemporaryList removeAllObjects];
                }
                //Again,留一组数据来备用
            }
            if (count==0||time==0) {
                currentS=0;
            }else
                currentS=[PublicResource sharedObject].roundLength*count/time*3600;
                //这个就是这1秒的速度了咯
        }else
        {
            //#####
        }
    }
//最新的最高速度
    if (currentS>max_inner_speed) {
        max_inner_speed=currentS;
    }
//防止速度突变
    if (currentS>lastSpeed+30) {
        lastSpeed=currentS;
        currentS=0;
    }
//强制限速
    if (currentS>80) {
        currentS=80;
    }
}

差不多,速度就是这么计算的,在实际测试中,感觉还是满准确地~

阅读剩下更多

编程

BLE速度踏频公有协议

BLE(Bluetooth Low Energy)

不知道从哪一年开始,BLE突然火了起来,可能是苹果推动的iBeacon,推了BLE一把吧

BLE模块越做越小,越做越省点,于是他们就纷纷往智能硬件上面去集成了,比如说什么手环啊,码表啊,门锁,窗帘啊,灯啊,炫轮啊,之类的.

How does it work

这里不是说BLE在底层如何工作,只是从上层软件开发的角度,讲讲BLE怎么工作

BLE必须提到的几个名词

  1. peripheral
  2. central
  3. service
  4. characteristic
  5. UUID

Peripheral

传说中的外设,也就是那些个手环啊,码表啊,炫轮啊之类的东西,他们在不停地广播自己存在的信号,可以让别人知道自己存在,自己的名字,自己有些什么Service.别人也可以根据这个广播计算以下RSSI(信号强度)

Central

暂且叫他主机吧,因为在定义中,并不是叫Peripheral和Central,而是Master和Slave(太难听了,还是使用iOS中的外设中心吧).

Central可以接收到广播帧,并且知道它的Mac地址啊,拥有哪些Service啊,名字啊……

Service

服务,假设一个蓝牙可以提供很多信息,比如说,电量啊,心率啊,速度啊,还有自定义的一些数据.如果通过数据帧头来区分他们,会非常的麻烦.所以BLE有Service这个概念,一个Service负责一件事情,清晰明了.

Characteristic

特征值,每个服务可以拥有多个特征值,每个特征值都有权限,比如Read,Write,Write Without Response,Notify……

UUID

好吧,这么多东西,怎么标示嘞?用UUID咯…

BLE有专用UUID是16位的,然而标准的UUID是128位的.

总结一下

每一个外设(Peripheral)可以拥有很多的服务(Service),每一个服务(Service)可以拥有多个特征值(Characteristic),使用UUID来确定Service和Characteristic.

说说公有协议

由于BLE的Service和Characteristic的UUID是16位的,那就说有很多很多很多的UUID可以被定义.选择太多,就容易出乱子.所以大概是IEEE还是IEC还是SIG之类的组织专门定了一大堆公有协议

这个链接里面的Service是他们定的,貌似已经被大家接受了.

速度和踏频

为什么只说速度与踏频的公有协议呢?

因为我们刚好在做这个……

Service UUID: 0x1816
Characteristic UUID: 0x2A5B (至少得有Notify,Read权限)

有了这两个东西,你的App或者其他智能硬件就可以进行过滤扫描和连接了

具体协议

Flag(必须):8位
    0x01==(速度数据)
    0x02==(踏频数据)
    0x03= (速度数据+踏频数据)
速度数据:
    32位:累计轮胎圈数[溢出清零]
    16位:时间(精确到1/1024秒)[溢出清零]
踏频数据:
    16位:累计曲柄圈数[溢出清零]
    16位:时间(精确到1/1024秒)[溢出清零]

举个例子

轮子在3.7353秒转动了103圈,曲柄在54.1093秒转动了202

03 67 00 00 00 f1 0e 00 ca d8 70

阅读剩下更多

返回顶部