• 如何开发BLE数据透传应用程序?什么是BLE service和characteristic?如何开发自己的service和characteristic?如何区分ATT和GATT?有没有什么工具可以对BLE设备进行压力测试?如何提高BLE设备的数据上传速度?本文将...

    http://www.cnblogs.com/iini/p/9095622.html

     

    如何开发BLE数据透传应用程序?什么是BLE service和characteristic?如何开发自己的service和characteristic?如何区分ATT和GATT?有没有什么工具可以对BLE设备进行压力测试?如何提高BLE设备的数据上传速度?本文将对以上问题进行解答。

    在很多应用场合,BLE只是作为一个数据透传模块,即将设备端数据上传给手机,同时接收手机端下发的数据。本文将和大家一起,一步一步演示如何开发一个BLE透传应用程序。按照本文的说明,大家可以很快就实现一个BLE透传应用,BLE透传应用已经是BLE应用中比较复杂的一种,一旦大家掌握了BLE透传应用,其他BLE应用开发就更不在话下了。本文还会以BLE透传为例子,来解释BLE service和characteristic等概念,以帮助大家理解如何定义和开发自己的BLE service和characteristic等,从而彻底理解BLE协议栈中的ATT和GATT的运行原理。然后,本文还将手把手教大家如何提高BLE数据传输速度(蓝牙4.2的理论吞吐率大概为100kB/s,而我们实际达到了80kB/s,已经非常接近理论值)。最后,我们将告诉大家如何使用安卓版nRF Connect来对你的BLE设备进行压力测试,以测试设备的稳定性和可靠性。当然,文章的最后也会告诉大家如何找到安卓和iOS手机app开发参考代码。

     

    1. 开发准备

    1)     Nordic nRF52或者nRF51开发板1块。请参考“Nordic nRF51/nRF52开发流程说明”,购买相应开发板(DK)。

    2)     开发环境搭建。简述如下(详细说明请参考“Nordic nRF51/nRF52开发环境搭建”):

    1. 安装Keil5 MDK
    2. 安装SDK。如果你使用的是nRF52开发板,请安装nRF5 SDK15.0.0,下载链接:https://www.nordicsemi.com/eng/nordic/download_resource/59012/70/52858981/116085。如果你手上是nRF51开发板,请下载nRF5 SDK12.3.0:https://www.nordicsemi.com/eng/nordic/download_resource/54280/56/38442131/32925 (nRF51最高SDK版本只能到12.3.0,后续SDK就不再支持nRF51
    3. 安装ARM CMSIS4.5.0,下载链接:https://github.com/ARM-software/CMSIS/releases/download/v4.5.0/ARM.CMSIS.4.5.0.pack
    4. 安装Keil5 Device Family Pack,下载链接:https://www.nordicsemi.com/eng/nordic/download_resource/58865/28/26535159/87790
    5. 安装nRF5 Command Line Tools,下载链接(Windows版):https://www.nordicsemi.com/eng/nordic/download_resource/58850/47/60411125/53210
    6. 安装安卓版或者iOS版nRF connect。iOS版nRF connect请到苹果app store下载,搜索“nRF”即可以找到。安卓版nRF connect可以到Nordic Github官网上下载,下载链接为:https://github.com/NordicSemiconductor/Android-nRF-Connect/releases
    7. 安装PC版nRF connect或者nRFgo studio,两个选其一即可。PC版nRF connect下载链接(Windows版):https://www.nordicsemi.com/eng/nordic/download_resource/58847/15/21277021/108233

    注:如果你使用的是Linux系统/Mac系统,或者你使用的不是Keil5-MDK,请参考“Nordic nRF51/nRF52开发环境搭建”来搭建你的开发环境。

    2. 运行Nordic ble_app_uart应用程序

    Nordic SDK已经提供了一个直接就可以编译和运行的数据透传应用程序:ble_app_uart,Nordic将BLE透传服务称为Nordic UART Service(NUS),所以在Nordic SDK中,NUS就是BLE透传服务。请按照如下步骤运行SDK自带的ble_app_uart程序:

    1)     确认自己的芯片型号或者开发板。如果采用Nordic官方开发板的话,芯片型号和开发板编号对应关系如下:

    • nRF51系列对应开发板编号为PCA10028
    • nRF52832和nRF52810对应开发板编号为PCA10040。虽然52832和52810共用同一块开发板,但是他们在SDK中的项目编号是不一样的,52832对应PCA10040目录,52810对应PCA10040e目录,由于52810和52832 PIN to PIN兼容,软件也是完全兼容的,因此SDK很多项目只有PCA10040的目录,而没有PCA10040e目录,此时需要你自己来建立PCA10040e对应的目录和工程,具体说明可参考:http://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.sdk5.v15.0.0%2Fnrf52810_user_guide.html&cp=4_0_0_5_0
    • nRF52840对应开发板编号为PCA10056
    • nRF52840 dongle编号为PCA10059

    这里我会以nRF52832开发板PCA10040为例来阐述整个开发过程,其他开发板与之类似,大家自己可以举一反三来开始自己的开发之旅。

    2)     将开发板与PC机通过USB线相连,同时打开开发板电源(将左下角的拨位开关打到“ON”位置),打开桌面版nRF Connect,选择启动“Programmer”应用,由于驱动之前已经安装好了,设备可以立即识别成功。执行“full erase”操作,以擦除芯片原始内容。 

     

     

    3)     打开SDK中的ble_app_uart程序。如果是52832开发板,请打开:nRF5_SDK_15.0.0_a53641a\examples\ble_peripheral\ble_app_uart\pca10040\s132\arm5_no_packs;如果是51822开发板,请打开:nRF5_SDK_12.3.0_d7731ad\examples\ble_peripheral\ble_app_uart\pca10028\s130\arm5_no_packs

    后续将以52832开发板为例来阐述,51822与之类似就不再阐述了。

     

    注:Nordic SDK例程目录结构为:SDK版本/ examples /协议角色/例子名称/开发板型号/协议栈型号/工具链类型/具体工程,比如下面例子:

     

     

    Nordic每一个例子都支持5种工具链:Keil5/Keil4/IAR/GCC/SES,如下所示:

     

     

    4)     编译程序。如果你已经按照之前的说明配置好了开发环境,那么这里编译是不会报任何错的。(如果你遇到了编译错误,请重新按照前面说明去搭建你的开发环境,不要怀疑SDK例子代码有问题哦)

    5)     下载程序。程序下载包括2步:一先下载softdevice,二再下载应用。Softdevice是Nordic蓝牙协议栈的名称,整个开发过程中只需下载一次。应用就是我们这里的ble_app_uart程序。如果你的开发板已经下载了其他代码,那么最好先把开发板全擦一次,然后再下载softdevice和应用。

    • 芯片全擦(可选)。你可以使用nRFgo studio,或者nRF connect桌面版,或者nrfjprog,三者选其一来执行擦除操作。
      • 使用nRFgo studio执行全擦操作

     

    • 使用nRF connect桌面版执行全擦操作

      

     

    • 使用nrfjprog执行全擦操作

     

     

    • 蓝牙协议栈下载(整个开发周期只需下载一次)。在Keil ‘select target’下拉列表中,默认选择的是Keil工程对应的Target,即‘nrf52832_xxaa’。我们还可以选择另一个target ‘flash_s132_nrf52_6.0.0_softdevice’,即softdevice对应的target,然后点击“下载download”(不需要编译哦!),此时会把softdevice下载到开发板中。

     

     

    • 应用下载。重新选择Target:‘nrf52832_xxaa’,点击“下载Download”,此时会把ble_app_uart应用程序下载到开发板中。此时开发板的LED1闪烁,表示程序运行正常。

    6)     连接手机。打开手机蓝牙和手机版nRF connect。在nRF connect中,你将看到一个广播设备:Nordic_UART,这个就是开发板的广播名字。点击“CONNECT”,手机将与设备建立连接,并开始服务发现过程,连接成功后,LED1熄灭,LED2点亮,最后将得到如下界面。

     

    上图的Nordic UART Service(NUS)就是我们的数据透传服务, NUS具体包括两个characteristic:TX和RX,由于NUS是由设备提供的,所以TX表示设备发送数据给手机,RX表示设备接收手机发过来的数据。

    7)     测试NUS服务。ble_app_uart使用串口与上位机交互,选择一款串口助手软件,比如Putty,打开该串口软件,并做如下设置:

    • Baud rate: 115.200
    • 8 data bits
    • 1 stop bit
    • No parity
    • HW flow control: None

    复位开发板,你会发现串口助手会打印如下信息:

     

     

    按照第6)步,重新将开发板连上手机,然后点击右上角的“Enable CCCDs”以使能notification,如下所示:

     

     

    设备接收数据: 点击RX characteristic旁边的向上箭头,通过手机蓝牙往设备发送:12345678,如下所示:

     

         此时设备通过串口打印出刚才接收到的数据,如下所示:

     

    设备发送数据:在串口助手中输入“abcdefgh”并输入“\n”(注:在Putty中,先按“CTRL”再按“J”就会发出“\n”换行符)作为结束符,设备将把串口收到的数据通过蓝牙发送给手机,手机的TX characteristic将显示上述字符串,如下所示:

     

     

                    注:如果你的串口助手发不出“\n”换行符,那么你需要最少输入MTU-3个字符,设备才会把收到的全部字符通过蓝牙发出去

     

    通过上面的测试,大家可以发现Nordic SDK已经把蓝牙数据透传服务做好了,大家可以直接拿过来使用,下面将对其工作原理进行阐述,最后在Nordic蓝牙透传例子ble_app_uart上进行二次开发,以增加一些其他有用功能。如果大家觉得Nordic ble_app_uart已经可以满足自己的需求,而且也不想花时间去研究里面的原理,那么章节3/4/5/6/7.1可以略过不看。

    3. BLE client/server(C/S) 架构

            BLE采用了client/server (C/S)架构来进行数据交互,C/S架构是一种非常常见的架构,在我们身边随处可见,比如我们经常用到的浏览器和服务器也是一种C/S架构,这其中浏览器是客户端client,服务器是服务端server,server比如淘宝服务器,提供商品信息,广告,社交等服务,而浏览器,比如微软的IE,就可以用来请求这些服务,并使用server提供的服务。BLE与此类似,一般而言设备提供服务,因此设备是server,手机使用设备提供的服务,因此手机是client。比如蓝牙体温计,它可以提供“体温”数据服务,因此是一个server,而手机则可以请求“体温”数据以显示在手机上,因此手机是一个client。

            服务是以数据为载体的,所以说server提供服务其实就是提供各种有价值的数据。

     

    上图所示的Request和Response其实就是我们经常说的ATT命令(ATT PDU),也就是说Client和Server之间通过ATT PDU进行交互。另外,一个数据“37”,有可能是说体温“37度”,也有可能是说心率“37次”或者湿度“37%”,因此Server需要将数据进行包装和分类,在BLE中,数据是通过characteristic进行包装的,而且多个characteristic组成一个service,service是一个独立的服务单元,或者说service是一个基本的BLE应用。因此我们可以把上图细化为:

     

    如果某个service是一个蓝牙联盟定义的标准服务,也可以称其为profile,比如HID/心率计/体温计/血糖仪等,都是标准蓝牙服务,因此都有相应的profile规格书。

     

    4. BLE service, characteristic以及CCCD

    如文章“深入浅出低功耗蓝牙(BLE)协议栈”所讲,BLE协议栈架构如下所示:

     

            如上图所示,用户开发应用程序或者说service的时候,调用的都是GATT API,而GATT又调用了ATT API,前面也讲过,BLE数据最终都是通过ATT PDU来传输的,那么为什么还需要GATT层?直接操作ATT层不也可以达到同样的目的吗?

            前面也提过,Server是通过characteristic来表示数据的,虽然一条数据最有价值的部分是它的值(value),但是仅有value是不够,比如27,到底是表示27°温度还是27%湿度;如果表示的是温度,那么它的单位是摄氏度还是华氏度。同时每个value还有相应的读写属性以及权限属性,因此一个characteristic包含三种条目:characteristic声明,characteristic的值以及characteristic的描述符(可以有多个描述符),如下所示:

     

            由于一个service可以包含多个characteristic,characteristic declaration就是每个characteristic的分界符,解析时一旦遇到characteristic declaration,就可以认为接下来又是一个新的characteristic了,同时characteristic declaration还将包含value的读写属性等。Characteristic value就是数据的值了,这个比较好理解就不再说了。Characteristic descriptor就是数据的额外信息,比如温度的单位是什么,数据是用小数表示还是百分比表示等之类的数据描述信息。CCCD是一种特殊的characteristic descriptor,一般而言,都是client来访问server的characteristic,我们把这种操作称为读或者写。另外,server可以直接把自己的characteristic的值告诉client,我们称其为notify或者indicate,跟read操作相比,只有需要传输数据的时候或者说只有当数据有效时,server才开始notify或者indicate数据到client,因此这种操作方式可以大大节省server的功耗。有时候client不想监听characteristic notify或者indicate过来的数据,那么就可以使用CCCD来关闭characteristic的notify或者indicate功能;如果client又需要监听characteristic的notify或者indicate,那么它可以重新使能CCCD来打开相关操作。总结一下,当characteristic具有notify或者indicate操作功能时,那么必须为其添加相应CCCD,以方便client来使能或者禁止notify或者indicate功能。

            不管是characteristic declaration,characteristic value还是characteristic descriptor,实现的时候,我们都是用attribute来表达的,也就是说,他们每一个都是一个attribute,attribute可以用下图来表示:

     

    • Attribute handle,Attribute句柄,16-bit长度。Client要访问Server的Attribute,都是通过这个句柄来访问的,也就是说ATT PDU一般都包含handle的值。用户在软件代码添加characteristic的时候,系统会自动按顺序地为相关attribute生成句柄。
    • Attribute type,Attribute类型,2字节或者16字节长。在BLE中我们使用UUID来定义数据的类型,UUID是128 bit的,所以我们有足够的UUID来表达万事万物。其中有一个UUID非常特殊,它被蓝牙联盟采用为官方UUID,这个UUID如下所示:

     

     由于这个UUID众所周知,蓝牙联盟将自己定义的attribute或者数据只用16bit UUID来表示,比如0x1234,其实它也是128bit,完整表示为:

     

    Attribute type一般是由service和characteristic规格来定义,站在蓝牙协议栈角度来看,ATT层定义了一个通信的基本框架,数据的基本结构,以及通信的指令,而GATT层就是前文所述的service和characteristic,GATT层用来赋予每个数据一个具体的内涵,让数据变得有结构和意义。换句话说,没有GATT层,低功耗蓝牙也可以通信起来,但会产生兼容性问题以及通信的低效率。

    • Attribute value,就是数据真正的值,0到512字节长。
    • Attribute permissions,Attribute的权限属性,权限属性不会直接在空中包中体现,而是隐含在ATT命令的操作结果中。假设一个attribute read属性设为open(即读操作不需要任何权限),那么client去读这个attribute时server将直接返回attribute的值;如果这个attribute read属性设为authentication(即需要配对才能访问),如果client没有与server配对而直接去访问这个attribute,那么server会返回一个错误码:告诉client你的权限不够,此时client会对server发起配对请求,以满足这个attribute的读属性要求。目前主要有如下四种权限属性:
      • Open,直接可以读或者写
      • No Access,禁止读或者写
      •  Authentication,需要配对才能读或者写,由于配对有多种类型,因此authentication又衍生多种子类型,比如带不带MITM,有没有LESC
      • Authorization,跟open一样,不过server返回attribute的值之前需要应用先授权,也就是说应用可以在回调函数里面去修改读或者写的原始值。
      • Signed,签名后才能读或者写,这个用得比较少。

             大家还记不记得设备与手机nRF connect连接成功后呈现的界面,我这里再贴一下:

     

             可以看到手机呈现的就是上文讲的service和characteristic,nRF Connect为了让整个界面变得更美观,将访问属性,UUID,handle都分列来表示了,以致于很多初学者会把理论和现实二者对应不起来。Nordic之前推出过一款Master Control Panel(MCP),MCP现在已经不推荐使用了,不过MCP有一个好处,它对service和characteristic的组织方式更接近底层实现方式,对大家理解service和characteristic是非常有帮助的。还是这个设备,我用MCP跟它连接并进行服务发现,你会发现它呈现的界面如下所示:

     

    这个图就跟上面讲的理论知识可以一一对应起来了,NUS包含2个characteristic:RX和TX,每一个条目都是一个attribute,NUS服务本身就是一个attribute,而RX characteristic本身又包含2条attribute:一条是declaration attribute,一条是value本身attribute。由于TX支持notify,所以它包含3条attribute,另外一条attribute是CCCD。每个attribute都有一个handle和UUID,handle用来访问该attribute,UUID用来指明该attribute的类型。可以说,server提供数据,而数据是由attribute来表达,所有attribute组成一个attribute table,设备支持的服务不同,attribute table就不同。这里说明一下,当你在Nordic已有例程基础上再去添加新的服务或者删除已有的服务,记得一定要去修改ATTR_TAB_SIZE那个宏,否则协议栈初始化会有问题。

           

    5. 常用ATT命令

            Client和Server之间是通过ATT PDU来通信的,ATT PDU主要包括4类:读,写,notify和indicate。如果一个命令需要response,那么会在相应命令后面加上request;如果一个命令只需要ACK而不需要response,那么它的后面就不会带request。这里要特别强调一点,BLE所有命令都是“必达”的,也就是说每个命令发出去之后,会立马等ACK信息,如果收到了ACK包,发起方认为命令完成;否则发起方会一直重传该命令直到超时导致BLE连接断开。换句话说,只要你的BLE没有断开,那么你之前发送的数据包,不管它是用什么ATT PDU来发送的,它肯定被对方收到了。我估计很多人对此会产生疑问,因为他们经常碰到丢包的情况,其实大家经常碰到的“丢包”,不是空中把包丢了或者包在空中被干扰了,而是大家发送的代码写得有问题,导致你要发送的包没有被安全送达到协议栈射频FIFO中,所以以后大家碰到丢包情况,请先检查你的代码,保证你的数据包正确完整安全地送达到协议栈射频FIFO中,只要数据包放到了协议栈射频FIFO中,蓝牙协议栈就能保证该数据包“必达”对方。既然每个ATT命令都必达对方,那么还需要request做什么?如果一个命令带有request后缀,那么发起方就可以收到命令的response包,这个response包在应用层是有回调事件的,而前述的ACK包在应用层是没有回调事件的。所以采用request/response方式,应用层可以按顺序地发送一些数据包,这个在很多应用场合是非常有用的。相反,如果你对应用层数据包的顺序没有要求,那么就可以不使用request/response形式。另外Request/response有一个副作用:大大降低通信的吞吐率,因为request/response必须在不同的连接间隔中出现,也就是说,你在间隔1中发送了一个request命令,那么response包必须在间隔2或者稍后间隔中回复,而不能在间隔1中回复,这就导致两个连接间隔最多只能发一个数据包,而不带request后缀的ATT命令就没有这个问题,在同一个连接间隔中,你可以同时发多个数据包,这样将大大提高数据的吞吐率。大家可以参考下图来理解request和非request命令的区别:

     

    常用的带request的命令:所有read命令,write request,indication等,而常用的不带request的命令有write command,notification等,完整的ATT命令列表如下所示:

     

    6. 设备端固件代码一览

    现在我们一起来看一下ble_app_uart的源代码,看看它是怎么工作起来的。首先我们来看main函数:

     

    如上所述,ble_stack_init用于初始化配置和使能蓝牙协议栈,其代码如下所示:

    其中,nrf_sdh_enable_request需要选择蓝牙协议栈的低频时钟(由于蓝牙协议栈的高频时钟必须为外部32M晶振,所以高频时钟无需配置;而低频时钟可以选择为内部32K RC或者外部32K晶振,所以低频时钟需要人工配置),因此如下宏需要根据实际情况进行调整:

    复制代码

        nrf_clock_lf_cfg_t const clock_lf_cfg =
    
        {
    
            .source       = NRF_SDH_CLOCK_LF_SRC,
    
            .rc_ctiv      = NRF_SDH_CLOCK_LF_RC_CTIV,
    
            .rc_temp_ctiv = NRF_SDH_CLOCK_LF_RC_TEMP_CTIV,
    
            .accuracy     = NRF_SDH_CLOCK_LF_ACCURACY
    
    };

    复制代码

    通过sdk_config.h文件可以看到,默认是选择外部32K晶振作为低频时钟的,如果你想选择内部32K RC作为低频时钟,那么需要做如下修改

    复制代码

    NRF_SDH_CLOCK_LF_SRC = 0
    
    NRF_SDH_CLOCK_LF_RC_CTIV = 16    //每4s启动一次校准
    
    NRF_SDH_CLOCK_LF_RC_TEMP_CTIV = 2
    
    NRF_SDH_CLOCK_LF_ACCURACY = 1  //500ppm

    复制代码

    nrf_sdh_ble_default_cfg_set用来配置softdevice协议栈,如下宏是经常需要修改的:

    复制代码

    NRF_SDH_BLE_TOTAL_LINK_COUNT  //一共同时可以支持多少个连接
    
    NRF_SDH_BLE_PERIPHERAL_LINK_COUNT  //作为从模式的连接同时能有几个
    
    NRF_SDH_BLE_CENTRAL_LINK_COUNT  //作为主模式的连接同时能有几个
    
    NRF_SDH_BLE_GATT_MAX_MTU_SIZE //MTU size为多大
    
    NRF_SDH_BLE_VS_UUID_COUNT  //用户自定义的base UUID有几个
    
    NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE  //Attribute table总共占多少协议栈RAM空间
    
    NRF_SDH_BLE_SERVICE_CHANGED  //要不要包含service change characteristic

    复制代码

    nrf_sdh_ble_enable真正使能BLE功能,它的参数ram_start既是一个输入参数又是一个输出参数,作为输入参数,系统自动会把如下的RAM起始地址传入:

     

    同时nrf_sdh_ble_enable会把当前softdevice配置情况下,它实际需要占用的RAM空间通过ram_start返回,如果这个返回值不等于输入值,那么用户需要把上图的IRAM1起始地址修改成它的返回值。其中NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE这个宏的取值是需要用户不断去试错的,因此每当你添加了或者删除了BLE service,都需要去调整NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE这个宏的值,然后去查看nrf_sdh_ble_enable的返回值,看看这个参数的取值是否合理

    NRF_SDH_BLE_OBSERVER用来为本地文件(此处为main.c)注册一个BLE回调函数(此处为ble_evt_handler),NRF_SDH_BLE_OBSERVER这个宏执行成功后,所有的BLE事件都会被ble_evt_handler捕获。进入ble_evt_handler,你会发现BLE有上百个回调事件,你不需要每个都处理,你只需要处理你关心的事件即可,比如连接成功事件BLE_GAP_EVT_CONNECTED或者连接断开事件BLE_GAP_EVT_DISCONNECTED,如下所示:

    NRF_SDH_BLE_OBSERVER有一个很大的好处:某个模块如果需要捕获BLE事件,那么它自己调用NRF_SDH_BLE_OBSERVER这个宏注册相应回调函数即可,而不再需要在其它文件中去注册这个回调函数,将模块的耦合性降到最低,符合模块化编程思想。

    gap_params_init用来修改广播名字和连接间隔的。gatt_init用来修改底层数据包长度的。advertising_init用来修改广播包内容,广播间隔以及广播超时时间。conn_params_init用来请求更新连接间隔的。

    我们来重点讲一下services_init,services_init用来添加服务和characteristic,前面讲了那么多的概念和理论,现在我们就来看看services_init是如何做到跟理论一致的。services_init通过ble_nus_init添加了一个蓝牙数据透传服务:NUS,那ble_nus_init是怎么将NUS服务添加成功的呢?查看ble_nus_init函数体,你会发现它是分三步来做的:

    1. 添加服务的UUID。如果是蓝牙标准服务,这步可以省略。由于NUS不是蓝牙联盟定义的,所以需要调用sd_ble_uuid_vs_add以增加一个供应商自定义的UUID。
    2. 添加服务本身。直接调用sd_ble_gatts_service_add就可以完成。
    3. 添加服务下面的characteristics。server的characteristic一般都是通过sd_ble_gatts_characteristic_add来添加的。以NUS的RX characteristic为例,可以看到:
    sd_ble_gatts_characteristic_add(p_nus->service_handle,  &char_md,  &attr_char_value, &p_nus->rx_handles);

    其中,p_nus->service_handle表示该characteristic属于那个service,p_nus->rx_handles是输出值,由协议栈返回,以后访问该characteristic都是通过这个句柄来完成,attr_char_value这个是characteristic的value,char_md这个是characteristic的元数据(metadata),前面第4章也讲过,一个数据除了有value这个characteristic之外,它还包含其他attribute,而这些attribute全部都用char_md来表示,比如这个characteristic value能支持的ATT命令类型,CCCD信息,descriptor信息等,这里要特别指出的是,只有当支持notify或者indicate时,才需要提供cccd_md信息,其他ATT命令不需要cccd_md信息,所以RX characteristic的char_md如下所示,它同时支持write和write request两种写命令,由于它不支持notify或者indicate,所以cccd_md为NULL。

     

    attr_char_value是一个attribute,所以它包含attribute metadata,如下:

    attr_char_value具体包含的value信息由以下成员表示:

     

    由于这里把characteristic value放在了协议栈RAM中,所以协议栈会自动为这个value创建一个buffer。如果你想把characteristic value放在用户RAM中,即vloc = BLE_GATTS_VLOC_USER,那么这里你还需要把一个全局数组变量赋给attr_char_value. p_value。

    TX characteristic与之类似,就不再额外解读了。

    这里需要特别提醒大家的是,虽然Nordic API结构体参数设计得很复杂,但是大部分成员变量直接就可以使用它的默认值0,你只需对你感兴趣的成员变量进行赋值即可,所以大家经常看到如下场合,即先用memset将该结构体变量初始化为0,让其所有成员变量都采用默认值,然后再对某些需要修改的成员变量进行二次赋值。大家一定不要忘了将结构体变量清零这一步操作!

     

    ble_nus_init同时注册了nus_data_handler回调函数,当设备收到手机发过来的数据时,就会触发nus_data_handler,用户可以在nus_data_handler中对接收到的数据进行处理,本例程中nus_data_handler直接将ble收到的数据通过uart口转发出去。如果用户需要发送数据给手机,在连接成功和notify使能的情况下,直接调用ble_nus_data_send即可,而ble_nus_data_send又是通过调用协议栈API:sd_ble_gatts_hvx来实现数据发送功能的。那么什么时候需要发送数据给手机?本例程的做法是,当串口有数据过来并满足如下条件时调用ble_nus_data_send:

    if ((data_array[index - 1] == '\n') || (index >= (m_ble_nus_max_data_len)))

    main函数最后将调用API让协议栈跑起来,如果你的设备将来是一个从设备(peripheral),那么请调用ble_advertising_start,ble_advertising_start将开启可连接的广播,从而让你的设备连接成功之后成为从设备。如果你的设备将来是一个主设备(central),那么请调用sd_ble_gap_scan_start,sd_ble_gap_scan_start将开启设备的扫描功能,从而让你的设备连接成功之后变为主设备。

     

    最后我们来看main循环,它只有一个函数: idle_state_handle,idle_state_handle先把需要打印的日志打印完,然后让系统进入idle状态(Nordic SoC spec称其为System ON状态),一旦有协议栈事件或者中断事件发生,系统将唤醒,以处理相关事件回调函数,然后再执行一遍idle_state_handle。注意:idle状态下,蓝牙连接或者广播可以正常进行而不受影响,蓝牙连接或者广播都是周期性的,在一个周期中,蓝牙连接或者广播只持续很短一段时间(这段时间CPU有可能会退出idle状态),其余时间系统都是处于idle状态的,从而大大节省系统功耗。

     

    7. 定制你的BLE数据透传应用程序

    7.1 BLE数据上传吞吐率

    如何快速的把大量数据上传给手机?这是一个很常见的应用场合,现在我们尝试去修改一下Nordic的原生例程,以实现最高的数据吞吐率。下面我们通过几种不同的方法来看看每种方法下它的吞吐率能到多少。

    方法1:(通过宏METHOD1来开关)

    蓝牙spec规定,蓝牙连接间隔最小只能为7.5m,为了达到最高的吞吐率,我们创建一个timer,让其每7ms发一次数据,看一看此时吞吐率能达到多少。7ms中断服务函数代码如下所示:

    复制代码

    static void throughput_timer_handler(void * p_context)
    
    {
    
        UNUSED_PARAMETER(p_context);
    
        ret_code_t err_code;
    
        uint16_t length;
    
        m_cnt_7ms++;
    
        length = m_ble_nus_max_data_len;
    
        if (m_conn_handle != BLE_CONN_HANDLE_INVALID)
    
        {
    
            err_code = ble_nus_data_send(&m_nus, m_data_array, &length, m_conn_handle);
    
    //      if ( (err_code != NRF_ERROR_INVALID_STATE) && (err_code != NRF_ERROR_RESOURCES) &&
    
    //          (err_code != NRF_ERROR_NOT_FOUND) )
    
    //      {
    
    //                APP_ERROR_CHECK(err_code);
    
    //      }             
    
            m_len_sent += length;           
    
            m_data_array[0]++;
    
            m_data_array[length-1]++;         
    
         }
    
         NRF_LOG_INFO("time: %d *7ms == bytes send: %d Bytes == avg speed: %d B/s",m_cnt_7ms,m_len_sent,m_len_sent/(m_cnt_7ms*7));   
    
    }

    复制代码

     

    这种做法会导致ble_nus_data_send报“NRF_ERROR_RESOURCES”错误,这个错误表示协议栈无资源应付这么快的调用速度。为此我们对ble_nus_data_send返回的错误值一概不进行处理,看看会发生什么?我们发现程序可以正常运行,RTT viewer打印的日志如下所示:

     

    由上图可知,数据上传吞吐率达到了34.8kB/s,其实这个吞吐率是假的,因为中间丢了很多包,但计算吞吐率的时候把丢的包也算进去了。如下图所示,0x6E之后应该为0x6F,但实际发送的数据包编号为0x83,丢包非常严重。

     

       为了防止所谓的“丢包”(前面也提过,这里的丢包不是数据包在空中丢掉了,而是数据包没有安全送到协议栈的buffer中,从而导致丢包),我们加上如下if语句,只有ble_nus_data_send返回正确时,才认为数据包正确发送,然后才能算入到throughput中:

    复制代码

      if (err_code == NRF_SUCCESS)
    
      {
    
                 m_len_sent += length;  
    
                 m_data_array[0]++;
    
                 m_data_array[length-1]++;                                                      
    
        }

    复制代码

     

    通过查看nRF connect日志,你会发现此时不会发生丢包了,但吞吐率直接降到了1.6kB/s左右。

    方法1+:(通过宏METHOD1_PLUS来开关)

    我们对方法一稍作调整,首先我们持续往发送buffer写数据,直到返回值不是NRF_SUCCESS

    复制代码

    do

    {

        err_code = ble_nus_data_send(&m_nus, m_data_array, &length, m_conn_handle);

        if ( (err_code != NRF_ERROR_INVALID_STATE) && (err_code != NRF_ERROR_RESOURCES) &&

          (err_code != NRF_ERROR_NOT_FOUND) )

        {

            APP_ERROR_CHECK(err_code);

        }

        if (err_code == NRF_SUCCESS)

        {

            m_len_sent += length;

            m_data_array[0]++;

            m_data_array[length-1]++;

        }

    } while (err_code == NRF_SUCCESS); 

    复制代码

    然后我们把连接间隔设为尽可能小,以期提高吞吐率,如下:

    复制代码

    #ifdef CONN_INTERVAL_OPTIMIZE
    
    #define MIN_CONN_INTERVAL               MSEC_TO_UNITS(8, UNIT_1_25_MS)   
    
    #define MAX_CONN_INTERVAL               MSEC_TO_UNITS(12, UNIT_1_25_MS)
    
    #endif

    复制代码

     

    这种方法吞吐率能达到10kB/s,但离我们的目标还是很远。

    最后我们把connection event length extension和data length extension都打开(我们将在方法2+中详细阐述这2个有效提高吞吐率的利器),即定义如下宏:

     

    可以看到吞吐率将达到70kB/s,这个吞吐率还是不错的。但仔细查看nRF connect日志,你会发现这种模式下还是有小概率事件会导致“丢包”发生,而且整个发送逻辑也不是很优化,为此我们想到了METHOD2.

    方法2:(通过宏METHOD2来开关)

    ble_nus_data_send每次成功发送数据包,都会产生一个BLE_NUS_EVT_TX_RDY事件,收到这个事件后,再去调用ble_nus_data_send,丢包的情况就不会再发生了,核心代码如下所示:

    复制代码

    
     

    if (p_evt->type == BLE_NUS_EVT_TX_RDY)

    {

    #ifdef METHOD2

        err_code = ble_nus_data_send(&m_nus, m_data_array, &length, m_conn_handle);

        if ( (err_code != NRF_ERROR_INVALID_STATE) && (err_code != NRF_ERROR_RESOURCES) &&

          (err_code != NRF_ERROR_NOT_FOUND) )

        {

              APP_ERROR_CHECK(err_code);

        }

        if (err_code == NRF_SUCCESS)

        {

             m_len_sent += length;

             m_data_array[0]++;

             m_data_array[length-1]++;

         }

         NRF_LOG_INFO("time: %d *10ms == bytes send: %d Bytes == avg speed: %d B/s",m_cnt_10ms,m_len_sent,m_len_sent * 100/m_cnt_10ms);

    #endif

    复制代码

    大家可以自己去查看一下nRF  Connect的数据log,这种方式是没有丢包的,但是打开RTT viewer,你会发现他的吞吐率低得可怜,只有1kB/s。

     

    方法2+:(通过宏METHOD2_PLUS来开关)

    与方法1+类似,我们在方法2基础上,持续往发送buffer送数据直到返回值不为0,如下:

    复制代码

    #ifdef METHOD2_PLUS

    //queue multiple tx array

    do

    {

        err_code = ble_nus_data_send(&m_nus, m_data_array, &length, m_conn_handle);

        if ( (err_code != NRF_ERROR_INVALID_STATE) && (err_code != NRF_ERROR_RESOURCES) &&

         (err_code != NRF_ERROR_NOT_FOUND) )

        {

             APP_ERROR_CHECK(err_code);

        }

        if (err_code == NRF_SUCCESS)

        {

            m_len_sent += length;

            m_data_array[0]++;

            m_data_array[length-1]++;

        }

    } while (err_code == NRF_SUCCESS);

    NRF_LOG_INFO("time: %d *10ms == bytes send: %d Bytes == avg speed: %d B/s",m_cnt_10ms,m_len_sent,m_len_sent * 100/m_cnt_10ms);

    #endif

    复制代码

     

    然后我们增加gap event length extension功能,gap event length跟connection event length两者意思差不多,都是为了实现一个连接间隔可以发或收多个包的目的。为了使能gap event length extension功能,首先将gap event length修改成一个合适的值,以使其尽可能占满整个连接间隔,如下将gap event length修改为30ms

    #define NRF_SDH_BLE_GAP_EVENT_LENGTH 24

     

    然后我们再将连接间隔设为尽可能小,以保证上述connection event可以占据整个连接间隔:

    复制代码

    #ifdef CONN_INTERVAL_OPTIMIZE
    
    #define MIN_CONN_INTERVAL               MSEC_TO_UNITS(8, UNIT_1_25_MS)   
    
    #define MAX_CONN_INTERVAL               MSEC_TO_UNITS(12, UNIT_1_25_MS)
    
    #endif

    复制代码

     

    同时我们使能connection event extension功能,如下:

    复制代码

    #ifdef EVT_LEN_EXT_ON
    
        ble_opt_t  opt;
    
        memset(&opt, 0x00, sizeof(opt));
    
        opt.common_opt.conn_evt_ext.enable = true;
    
        err_code = sd_ble_opt_set(BLE_COMMON_OPT_CONN_EVT_EXT, &opt);
    
        APP_ERROR_CHECK(err_code);
    
    #endif

    复制代码

     

    我现在使用的是华为P9手机,它将把MTU设为241,在DLE不开的情况下(此时链路层每个数据包的长度还是只有27个字节!),我们可以看到throughput可以达到10kB以上,如下:

     

     

    然后我们再打开DLE功能,此时链路层每个数据包的长度将变成251字节,如下:

    复制代码

    #ifdef DLE_ON
    
            case BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST:
    
            {
    
                NRF_LOG_DEBUG("DLE update request.");
    
                ble_gap_data_length_params_t dle_param;
    
                memset(&dle_param, 0, sizeof(ble_gap_data_length_params_t));   //0 means auto select DLE                                                                    
    
                err_code = sd_ble_gap_data_length_update(p_ble_evt->evt.gap_evt.conn_handle, &dle_param, NULL);
    
                APP_ERROR_CHECK(err_code);
    
            } break;
    
    #endif

    复制代码

     

    此时我们可以看到throughput可以达到77kB/s,离蓝牙4.2的理论throughput已经很接近了。这里特别需要指出的是,当DLE使能情况下,connection interval不是越小吞吐率越高,我这里使用的connection interval大概为10ms,如果大家把这个connection interval提高到30ms,有可能吞吐率更高,这里就不再演示了。

     

     

     

     

     

    上述代码工程已经上传到百度云盘中,有需要的同学可以到如下链接下载:

    下载“tutorial_ble_app_uart_SDK15_0_0.rar”,然后解压缩到SDK15.0.0如下目录下:nRF5_SDK_15.0.0_a53641a\examples\ble_peripheral,即可成功编译运行。

     

    7.2使用安卓版nRF connect测试BLE设备的稳定性和可靠性

    先说明一下,以下内容只能通过安卓版nRF Connect来实现,iOS版nRF Connect不支持如下特性。

    手机端宏录制方式

    相信到现在大家对BLE数据上传机理和实践有个大概的了解,那如何测试BLE数据下行性能,即怎么测试数据从手机传到设备的稳定性和可靠性?我们是不是必须开发一款手机app来进行相关测试吗?答案是否定的,感谢Nordic给我们带来了nRF connect,nRF connect支持宏录制,我们可以通过nRF connect来对我们的设备进行压力测试。下面我们来讲讲宏录制是怎么工作的。

    所谓宏录制,就是把你对nRF connect的操作录制下来,然后通过宏播放实现自动化操作。由于nRF connect是一个容器,并支持JavaScript和HTML语法,宏其实就是一个XML脚本,nRF connect定义了自己的一套XML标签操作,遵守这套XML标签操作,就可以对nRF connect进行自动化操作。nRF connect支持的所有XML语法都在手机安装目录\Nordic Semiconductor中的示例中体现,只要示例中出现过的标签就支持,相反示例中没有的标签就不支持。下面具体讲一下宏录制的操作过程。

    当nRF connect连接设备成功后,你会发现右下角有一个红点,那个就是宏录制菜单。

     

     

    点击下面的红点,我们开始宏录制操作

     

     

    然后我们按照普通操作来操作nRF connect,这些操作最终对应的BLE指令会被录制下来,以便后续重复播放。我们先把“1234”发送给设备,如下:

     

    发送完上述指令后,我们加一个300ms的延时,如下:

     

    然后我们点击完成按钮,保存该宏,可以看出这个宏包括两条操作:发送“1234”到设备,然后睡眠300ms。

     

     

    将宏命名为“test”并保存:

     

     

    到此宏已经录制成功了,现在我们开始展示宏的神奇功能。如下,选择循环播放模式,然后点击“开始”按钮开始循环播放该录制宏。

     

     

    大家可以看到,nRF connect先执行“Write 0x31323334 to RX characteristic”,然后睡眠300ms,然后又执行“Write 0x31323334 to RX characteristic”,如此循环往复。打开串口助手,你会发现设备已经收到了手机发过来的一连串“1234”,如下。

     

    我们把刚才的test宏导出为XML,看一看它到底长什么样:

    复制代码

    <macro name="test" icon="PLAY">
    
       <assert-service description="Ensure Nordic UART Service" uuid="6e400001-b5a3-f393-e0a9-e50e24dcca9e">
    
          <assert-characteristic description="Ensure RX Characteristic" uuid="6e400002-b5a3-f393-e0a9-e50e24dcca9e">
    
             <property name="WRITE" requirement="MANDATORY"/>
    
          </assert-characteristic>
    
       </assert-service>
    
       <write description="Write 0x31323334 to RX Characteristic" characteristic-uuid="6e400002-b5a3-f393-e0a9-e50e24dcca9e" service-uuid="6e400001-b5a3-f393-e0a9-e50e24dcca9e" value="31323334" type="WRITE_REQUEST"/>
    
       <sleep description="Sleep 300 ms" timeout="300"/>
    
    </macro>

    复制代码

     

    大家可以看到,宏就是一些XML标记,大家也可以在此基础上,去修改该XML文件,以实现更复杂的自动化测试,然后通过nRF connect把最新的XML文件装载进来,就可以自动播放了。

    如果你还想了解宏更多的用法信息,请参考:https://github.com/NordicSemiconductor/Android-nRF-Connect/blob/master/documentation/Macros/README.md

     

    电脑端XML方式

    前面的宏录制方式,功能还是比较单一,如果要实现更复杂的自动化测试,可以通过在PC端执行XML脚本方式来实现。通过安卓调试工具ADB,我们可以直接通过PC来操作nRF connect,而nRF connect又能识别XML脚本,这样就可以让nRF connect按照XML脚本意图去执行相关自动化操作。nRF connect支持的所有XML语法都在手机安装目录中(手机内部存储/ Nordic Semiconductor目录)的示例中体现,只要示例中出现过的标签就支持,相反示例中没有的标签就不支持。

    欲了解更多信息请参考:https://github.com/NordicSemiconductor/Android-nRF-Connect/blob/master/documentation/Automated%20tests/README.md

     

    8. 开发手机端app代码

    Nordic提供很多手机端开源app供大家参考,用得最多的就是nRF Toolbox和nRF Blinky(注:nRF connect代码不开源),在nRF Toolbox和nRF Blinky中都有相关的BLE操作库,尤其是nRF Toolbox包含了很多BLE库,比如BLE管理,DFU,数据透传,蓝牙Mesh等等,大家可以参考他们来开发自己的手机端app。

    nRF Toolbox软件界面如下所示:

     

    UART就是前文说到的NUS服务,除了nRF connect,其实大家也可以通过nRF Toolbox UART模块来完成第2章所述的操作。nRF Toolbox另一个用的比较多的功能就是DFU,如果你需要通过手机BLE来实现设备固件的空中升级(OTA),那么可以参考nRF Toolbox DFU模块来编写你的手机端软件。

    展开全文
  • 我封装的是一个Okhttp工具类,有get请求,有post请求,还有上传照片文件功能,我先一步一步的解析这几个方法,最下面是一个封装好的工具类,复制粘贴即可使用: Okhttp优点: 1.支持 SPDY ,允许连接同一主机的...

    和以往的套路一样,先导入一个依赖包:
    implementation 'com.squareup.okhttp3:okhttp:3.9.0'

        我封装的是一个Okhttp工具类,有get请求,有post请求,还有上传照片文件功能,我先一步一步的解析这几个方法,最下面是一个封装好的工具类,复制粘贴即可使用:

    Okhttp优点:

        1. 支持 SPDY ,允许连接同一主机的所有请求分享一个socket。

        2. 如果SPDY不可用,会使用连接池减少请求延迟。

        3. 使用GZIP压缩下载内容,且压缩操作对用户是透明的。

        4. 利用响应缓存来避免重复的网络请求。

    Okhttp特点:

        1. 当网络出现问题的时候,OKHttp会依然有效,它将从常见的连接问题当中恢复。

        2. 如果你的服务端有多个IP地址,当第一个地址连接失败时,OKHttp会尝试连接其他的地址,这对IPV4和IPV6以及寄宿在多个数据中心的服务而言,是非常有必要的。 

        3. OKHttp利用TLS的特性初始化新的连接,如果握手失败便退回到SSLV3。

        4. OKHttp的使用很简单。其2.0API拥有流畅的构建器和稳定性。它支持同步阻塞请求和异步回调请求。

        5. 你可以试试OKHttp而不重写网络代码。okhttp-urlconnection模块实现了都很熟悉的Java.net.HttpURLConnection的API,okhttp-apache模块实现了Apache的HttpClient的API。

        6. OKHttp支持android2.3以上,Java支持最低版本1.7。

    下面给大家写一个简单好用的okhttp网络请求工具类,分为两个请求方式(get和post请求),简单说一下get和post的区别:

    1.当发送数据的时候,get的url是有长度限制的(最大长度是2048),而post是没有限制   

    2.get的安全性较差,post请求比get请求更安全get请求会把参数保留在浏览网页中,而post不会保存到浏览器的历史或服务器日志中  

    3.直接请求数据就用get , 如果需要给后台传数据那就用post

    import android.os.Handler;
    import android.os.Looper;
    import android.util.Log;
    import java.io.IOException;
    import java.net.ConnectException;
    import java.net.SocketTimeoutException;
    import java.net.UnknownHostException;
    import java.util.concurrent.TimeUnit;
    
    import okhttp3.Call;
    import okhttp3.Callback;
    import okhttp3.HttpUrl;
    import okhttp3.Interceptor;
    import okhttp3.MediaType;
    import okhttp3.OkHttpClient;
    import okhttp3.Request;
    import okhttp3.RequestBody;
    import okhttp3.Response;
    
    /**
     * Created by Administrator on 2019/7/18.
     */
    
    public class OkhttpUtils {
        private static final String TAG="OkhttpUtils";
        private OkHttpClient mClient;
        private  static volatile OkhttpUtils mOkUtils;//防止多个线程同时访问
        public static final int TIMEOUT = 1000 * 5;
        private Handler handler = new Handler(Looper.getMainLooper());
        //json请求
        public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
        //使用构造方法初始化
        public OkhttpUtils(){
            mClient=new OkHttpClient();
        }
        //使用单利模式,通过获取的方式拿到对象(还为了优化内存,单利模式只可创建一次)z
        public static OkhttpUtils getInstance(){
            if(mOkUtils==null){
                synchronized (OkhttpUtils.class){
                    if(mOkUtils==null){
                        mOkUtils=new OkhttpUtils();
                        mOkUtils.initUtils();
                    }
                }
            }
            return mOkUtils;
        }
        /**
         * 应用拦截器
         */
        Interceptor appInterceptor = new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request request = chain.request();
                HttpUrl url = request.url();
                String s = url.url().toString();
                //———请求之前—–
                Log.d(TAG,"app interceptor:begin");
                Response  response = chain.proceed(request);
                Log.d(TAG,"app interceptor:end");
                //———请求之后————
                return response;
        }
    
        };
        private void initUtils(){
            mClient.newBuilder()
                    .connectTimeout(TIMEOUT, TimeUnit.SECONDS)
                    .writeTimeout(TIMEOUT, TimeUnit.SECONDS)
                    .readTimeout(TIMEOUT, TimeUnit.SECONDS)
                    //拦截器
                    .addInterceptor(appInterceptor)
                    .build();
        }
        /**
         * okhttp     get 请求
         * **/
        public void getJson(String url,final HttpCallBack callBack){
            Request request=new Request.Builder()
                    .url(url)
                    .get()
                    .build();
            mClient.newCall(request).enqueue(new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {
                    if (e instanceof SocketTimeoutException) {
                        OnError(callBack, "连接超时");
                    } else if (e instanceof ConnectException) {
                        OnError(callBack, "连接服务器失败");
                    } else if (e instanceof UnknownHostException) {
                        OnError(callBack, "网络异常");
                    } else {
                        OnError(callBack, "未知错误");
                    }
                }
    
                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    String string = response.body().string();
                    onSuccess(callBack,string);
                }
            });
    
        }
        /**
         * okhttp    post请求
         * */
        public void postJson(String url, String json, final HttpCallBack callBack){
            RequestBody requestBody=RequestBody.create(JSON,json);
            Request request=null;
            request=new Request.Builder()
                    .url(url)
                    .post(requestBody)
                    .build();
            mClient.newCall(request).enqueue(new Callback() {
                @Override
                public void onFailure(Call call, IOException e) {
                    if (e instanceof SocketTimeoutException) {
                        OnError(callBack, "连接超时");
                    } else if (e instanceof ConnectException) {
                        OnError(callBack, "连接服务器失败");
                    } else if (e instanceof UnknownHostException) {
                        OnError(callBack, "网络异常");
                    } else {
                        OnError(callBack, "未知错误");
                    }
                }
    
                @Override
                public void onResponse(Call call, Response response) throws IOException {
                    String string = response.body().string();
                    onSuccess(callBack,string);
                }
            });
    
        }
        public void onSuccess(final HttpCallBack callBack, final String data) {
            if (callBack != null) {
                handler.post(new Runnable() {
                    @Override
                    public void run() {//在主线程操作
                        callBack.onSusscess(data);
                    }
                });
            }
        }
    
    
        public void OnError(final HttpCallBack callBack, final String msg) {
            if (callBack != null) {
                handler.post(new Runnable() {
                    @Override
    
    
                    public void run() {
                        callBack.onError(msg);
                    }
    
                });
            }
        }
        public static abstract class HttpCallBack {
            //成功回调
            public abstract void onSusscess(String data);
            //失败
            public abstract void onError(String meg);
    
        }
    
    }
    

    为了一下初学的萌生,调用方法也推荐一下,用下面的方式调用(下面只粘贴了get请求,post请求的话就把get换成post,即可,这工具类都可以更改,有的公司要session ,有的公司需要cookie,你们可以根据自己的需求在我封装的网络请求类里面,把相关参数放在请求头里面即可):

        OkhttpUtils.getInstance().getJson(str, new OkhttpUtils.HttpCallBack() {
                        @Override
                        public void onSusscess(String data) {
    
                        }
    
                        @Override
                        public void onError(String meg) {
                        }
                    });

    下面是上传照片、图片文件,多张图片带参数的例子,套路都差不多,多了就找一个方法管理就行了,我就把完整的代码展示出来了。

    我封装Okhttp用的是单例模式,单例模式的特性就是只允许创建一个实例,能减少内存的消耗,代码里面也有注释,请大家好好看一下吧。

    import java.io.File;
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import android.content.Context;
    import android.util.Log;
    import okhttp3.Call;
    import okhttp3.Callback;
    import okhttp3.FormBody;
    import okhttp3.MediaType;
    import okhttp3.MultipartBody;
    import okhttp3.OkHttpClient;
    import okhttp3.Request;
    import okhttp3.RequestBody;
    
    /**
     * OKHttp3上传工具类
     * */
    public class OkhttpUtils {
    
        private Context ctx;
        private static OkHttpClient ok = null;
        private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");
        private Map<String, String> map = new HashMap<String, String>();//存放
        private List<String> list = new ArrayList<String>();
        private static File file;
        private static String imgpath;
        private static String imageName;
    
        private OkhttpUtils() {
    
        }
    
        ;
    
        /*
         *单例获取
         * */
        public static OkHttpClient getInstance() {
            if (ok == null) {
                synchronized (OkhttpUtils.class) {
                    if (ok == null)
                        ok = new OkHttpClient();
                }
            }
            return ok;
        }
    
        public static void getString(String url, Callback callback) {
            Request request = new Request.Builder()
                    .url(url)
                    .build();
            Call call = getInstance().newCall(request);
            call.enqueue(callback);
        }
    
        /*
         * 键值对上传数据
         * */
        public static void postString(String url, Map<String, String> map, Callback callback) {
            FormBody.Builder builder = new FormBody.Builder();
            //遍历map中所有的参数到builder
            for (String key : map.keySet()) {
                builder.add(key, map.get(key));
                Log.e("", "key: " + key + "   map.get:  " + map.get(key));
            }
            Request request = new Request.Builder()
                    .url(url)
                    .post(builder.build())
                    .build();
            Call call = getInstance().newCall(request);
            call.enqueue(callback);
        }
    
        /*
         * 上传一张图片带参数
         * */
        public static void UploadFileCS(String url, String key1, String path, Map<String, String> map, Callback callback) {
    //      path.lastIndexOf是找到uri里面最后一个"你想找的东西"的位置,所以就是截取最后一个"/"和最后一个"."之间的东西,
            String imagpath = path.substring(0, path.lastIndexOf("/"));
    
            String imgName[] = path.split("/");
            for (int i = 0; i < imgName.length; i++) {
                if (i == imgName.length - 1) {
                    String name = imgName[i];
                    file = new File(imagpath, name);
                }
            }
            MultipartBody.Builder builder = new MultipartBody.Builder().setType(MultipartBody.FORM);
            RequestBody fileBody = RequestBody.create(MEDIA_TYPE_PNG, file);
            //遍历map中所有的参数到builder
            for (String key : map.keySet()) {
                builder.addFormDataPart(key, map.get(key));
            }
            //讲文件添加到builder中
            builder.addFormDataPart(key1, file.getName(), fileBody);
            //创建请求体
            RequestBody requestBody = builder.build();
    
            Request request = new Request.Builder().url(url).post(requestBody).build();
            Call call = getInstance().newCall(request);
            call.enqueue(callback);
        }
    
        /*
         *上传多个图片文件
         * */
        @SuppressWarnings("unused")
        public static void UploadFileMore(String url, List<String> paths, Callback callback) {
            if (paths != null) {
                //创建文件集合
                List<File> list = new ArrayList<File>();
                //遍历整个图片地址
                for (String str : paths) {
                    //截取图片地址:/storage/emulated/0
                    imgpath = str.substring(0, str.lastIndexOf("/"));
                    //将图片路径分解成String数组
                    String[] imgName = str.split("/");
                    for (int i = 0; i < imgName.length; i++) {
                        if (i == imgName.length - 1) {
                            imageName = imgName[i];//获取图片名称
                            File file = new File(imgpath, imageName);
                            list.add(file);
                        }
                    }
                }
                MultipartBody.Builder builder = new MultipartBody.Builder();
                builder.setType(MultipartBody.FORM);//设置表单类型
                //遍历图片文件
                for (File file : list) {
                    if (file != null) {
                        builder.addFormDataPart("acrd", file.getName(), RequestBody.create(MEDIA_TYPE_PNG, file));
                    }
                }
                //构建请求体
                MultipartBody requestBody = builder.build();
                Request request = new Request.Builder().url(url).post(requestBody).build();
                Call call = getInstance().newCall(request);
                call.enqueue(callback);
            }
    
        }
    
        /*
         * 上传多张图片带参数
         * */
        @SuppressWarnings("unused")
        public static void UploadFileSCMore(String url, String value, List<String> paths, Map<String, String> map, Callback callback) {
            if (paths != null && map != null) {
                //创建文件集合
                List<File> list = new ArrayList<File>();
                //遍历整个图片地址
                for (String str : paths) {
                    //截取图片地址:/storage/emulated/0
                    imgpath = str.substring(0, str.lastIndexOf("/"));
                    //将图片路径分解成String数组
                    String[] imgName = str.split("/");
                    for (int i = 0; i < imgName.length; i++) {
                        if (i == imgName.length - 1) {
                            imageName = imgName[i];//获取图片名称
                            File file = new File(imgpath, imageName);
                            list.add(file);
                        }
                    }
                }
                MultipartBody.Builder builder = new MultipartBody.Builder();
                builder.setType(MultipartBody.FORM);//设置表单类型
                //遍历图片文件
                for (File file : list) {
                    if (file != null) {
                        builder.addFormDataPart(value, file.getName(), RequestBody.create(MEDIA_TYPE_PNG, file));
                    }
                }
                //遍历map中所有的参数到builder
                for (String key : map.keySet()) {
                    builder.addFormDataPart(key, map.get(key));
                }
                RequestBody requestBody = builder.build();
    
                Request request = new Request.Builder().url(url).post(requestBody).build();
                Call call = getInstance().newCall(request);
                call.enqueue(callback);
            }
        }
    
    }
    

     

     

     

     

     

     


     

    展开全文
  • 如何开发BLE数据透传应用程序?什么是BLE service和characteristic?如何开发自己的service和characteristic?如何区分ATT和GATT?有没有什么工具可以对BLE设备进行压力测试?如何提高BLE设备的数据上传速度?本文将...

    如何开发BLE数据透传应用程序?什么是BLE service和characteristic?如何开发自己的service和characteristic?如何区分ATT和GATT?有没有什么工具可以对BLE设备进行压力测试?如何提高BLE设备的数据上传速度?本文将对以上问题进行解答。

    在很多应用场合,BLE只是作为一个数据透传模块,即将设备端数据上传给手机,同时接收手机端下发的数据。本文将和大家一起,一步一步演示如何开发一个BLE透传应用程序。按照本文的说明,大家可以很快就实现一个BLE透传应用,BLE透传应用已经是BLE应用中比较复杂的一种,一旦大家掌握了BLE透传应用,其他BLE应用开发就更不在话下了。本文还会以BLE透传为例子,来解释BLE service和characteristic等概念,以帮助大家理解如何定义和开发自己的BLE service和characteristic等,从而彻底理解BLE协议栈中的ATT和GATT的运行原理。然后,本文还将手把手教大家如何提高BLE数据传输速度(蓝牙4.2的理论吞吐率大概为100kB/s,而我们实际达到了80kB/s,已经非常接近理论值)。最后,我们将告诉大家如何使用安卓版nRF Connect来对你的BLE设备进行压力测试,以测试设备的稳定性和可靠性。当然,文章的最后也会告诉大家如何找到安卓和iOS手机app开发参考代码。

     

    1. 开发准备

    1)     Nordic nRF52或者nRF51开发板1块。请参考“Nordic nRF51/nRF52开发流程说明”,购买相应开发板(DK)。

    2)     开发环境搭建。简述如下(详细说明请参考“Nordic nRF51/nRF52开发环境搭建”):

    1. 安装Keil5 MDK
    2. 安装SDK。如果你使用的是nRF52开发板,请安装nRF5 SDK15.0.0,下载链接:https://www.nordicsemi.com/eng/nordic/download_resource/59012/70/52858981/116085。如果你手上是nRF51开发板,请下载nRF5 SDK12.3.0:https://www.nordicsemi.com/eng/nordic/download_resource/54280/56/38442131/32925 (nRF51最高SDK版本只能到12.3.0,后续SDK就不再支持nRF51
    3. 安装ARM CMSIS4.5.0,下载链接:https://github.com/ARM-software/CMSIS/releases/download/v4.5.0/ARM.CMSIS.4.5.0.pack
    4. 安装Keil5 Device Family Pack,下载链接:https://www.nordicsemi.com/eng/nordic/download_resource/58865/28/26535159/87790
    5. 安装nRF5 Command Line Tools,下载链接(Windows版):https://www.nordicsemi.com/eng/nordic/download_resource/58850/47/60411125/53210
    6. 安装安卓版或者iOS版nRF connect。iOS版nRF connect请到苹果app store下载,搜索“nRF”即可以找到。安卓版nRF connect可以到Nordic Github官网上下载,下载链接为:https://github.com/NordicSemiconductor/Android-nRF-Connect/releases
    7. 安装PC版nRF connect或者nRFgo studio,两个选其一即可。PC版nRF connect下载链接(Windows版):https://www.nordicsemi.com/eng/nordic/download_resource/58847/15/21277021/108233

    注:如果你使用的是Linux系统/Mac系统,或者你使用的不是Keil5-MDK,请参考“Nordic nRF51/nRF52开发环境搭建”来搭建你的开发环境。

    2. 运行Nordic ble_app_uart应用程序

    Nordic SDK已经提供了一个直接就可以编译和运行的数据透传应用程序:ble_app_uart,Nordic将BLE透传服务称为Nordic UART Service(NUS),所以在Nordic SDK中,NUS就是BLE透传服务。请按照如下步骤运行SDK自带的ble_app_uart程序:

    1)     确认自己的芯片型号或者开发板。如果采用Nordic官方开发板的话,芯片型号和开发板编号对应关系如下:

    • nRF51系列对应开发板编号为PCA10028
    • nRF52832和nRF52810对应开发板编号为PCA10040。虽然52832和52810共用同一块开发板,但是他们在SDK中的项目编号是不一样的,52832对应PCA10040目录,52810对应PCA10040e目录,由于52810和52832 PIN to PIN兼容,软件也是完全兼容的,因此SDK很多项目只有PCA10040的目录,而没有PCA10040e目录,此时需要你自己来建立PCA10040e对应的目录和工程,具体说明可参考:http://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.sdk5.v15.0.0%2Fnrf52810_user_guide.html&cp=4_0_0_5_0
    • nRF52840对应开发板编号为PCA10056
    • nRF52840 dongle编号为PCA10059

    这里我会以nRF52832开发板PCA10040为例来阐述整个开发过程,其他开发板与之类似,大家自己可以举一反三来开始自己的开发之旅。

    2)     将开发板与PC机通过USB线相连,同时打开开发板电源(将左下角的拨位开关打到“ON”位置),打开桌面版nRF Connect,选择启动“Programmer”应用,由于驱动之前已经安装好了,设备可以立即识别成功。执行“full erase”操作,以擦除芯片原始内容。 

     

     

    3)     打开SDK中的ble_app_uart程序。如果是52832开发板,请打开:nRF5_SDK_15.0.0_a53641a\examples\ble_peripheral\ble_app_uart\pca10040\s132\arm5_no_packs;如果是51822开发板,请打开:nRF5_SDK_12.3.0_d7731ad\examples\ble_peripheral\ble_app_uart\pca10028\s130\arm5_no_packs

    后续将以52832开发板为例来阐述,51822与之类似就不再阐述了。

     

    注:Nordic SDK例程目录结构为:SDK版本/ examples /协议角色/例子名称/开发板型号/协议栈型号/工具链类型/具体工程,比如下面例子:

     

     

    Nordic每一个例子都支持5种工具链:Keil5/Keil4/IAR/GCC/SES,如下所示:

     

     

    4)     编译程序。如果你已经按照之前的说明配置好了开发环境,那么这里编译是不会报任何错的。(如果你遇到了编译错误,请重新按照前面说明去搭建你的开发环境,不要怀疑SDK例子代码有问题哦)

    5)     下载程序。程序下载包括2步:一先下载softdevice,二再下载应用。Softdevice是Nordic蓝牙协议栈的名称,整个开发过程中只需下载一次。应用就是我们这里的ble_app_uart程序。如果你的开发板已经下载了其他代码,那么最好先把开发板全擦一次,然后再下载softdevice和应用。

    • 芯片全擦(可选)。你可以使用nRFgo studio,或者nRF connect桌面版,或者nrfjprog,三者选其一来执行擦除操作。
      • 使用nRFgo studio执行全擦操作

     

    • 使用nRF connect桌面版执行全擦操作

      

     

    • 使用nrfjprog执行全擦操作

     

     

    • 蓝牙协议栈下载(整个开发周期只需下载一次)。在Keil ‘select target’下拉列表中,默认选择的是Keil工程对应的Target,即‘nrf52832_xxaa’。我们还可以选择另一个target ‘flash_s132_nrf52_6.0.0_softdevice’,即softdevice对应的target,然后点击“下载download”(不需要编译哦!),此时会把softdevice下载到开发板中。

     

     

    • 应用下载。重新选择Target:‘nrf52832_xxaa’,点击“下载Download”,此时会把ble_app_uart应用程序下载到开发板中。此时开发板的LED1闪烁,表示程序运行正常。

    6)     连接手机。打开手机蓝牙和手机版nRF connect。在nRF connect中,你将看到一个广播设备:Nordic_UART,这个就是开发板的广播名字。点击“CONNECT”,手机将与设备建立连接,并开始服务发现过程,连接成功后,LED1熄灭,LED2点亮,最后将得到如下界面。

     

    上图的Nordic UART Service(NUS)就是我们的数据透传服务, NUS具体包括两个characteristic:TX和RX,由于NUS是由设备提供的,所以TX表示设备发送数据给手机,RX表示设备接收手机发过来的数据。

    7)     测试NUS服务。ble_app_uart使用串口与上位机交互,选择一款串口助手软件,比如Putty,打开该串口软件,并做如下设置:

    • Baud rate: 115.200
    • 8 data bits
    • 1 stop bit
    • No parity
    • HW flow control: None

    复位开发板,你会发现串口助手会打印如下信息:

     

     

    按照第6)步,重新将开发板连上手机,然后点击右上角的“Enable CCCDs”以使能notification,如下所示:

     

     

    设备接收数据: 点击RX characteristic旁边的向上箭头,通过手机蓝牙往设备发送:12345678,如下所示:

     

         此时设备通过串口打印出刚才接收到的数据,如下所示:

     

    设备发送数据:在串口助手中输入“abcdefgh”并输入“\n”(注:在Putty中,先按“CTRL”再按“J”就会发出“\n”换行符)作为结束符,设备将把串口收到的数据通过蓝牙发送给手机,手机的TX characteristic将显示上述字符串,如下所示:

     

     

                    注:如果你的串口助手发不出“\n”换行符,那么你需要最少输入MTU-3个字符,设备才会把收到的全部字符通过蓝牙发出去

     

    通过上面的测试,大家可以发现Nordic SDK已经把蓝牙数据透传服务做好了,大家可以直接拿过来使用,下面将对其工作原理进行阐述,最后在Nordic蓝牙透传例子ble_app_uart上进行二次开发,以增加一些其他有用功能。如果大家觉得Nordic ble_app_uart已经可以满足自己的需求,而且也不想花时间去研究里面的原理,那么章节3/4/5/6/7.1可以略过不看。

    3. BLE client/server(C/S) 架构

            BLE采用了client/server (C/S)架构来进行数据交互,C/S架构是一种非常常见的架构,在我们身边随处可见,比如我们经常用到的浏览器和服务器也是一种C/S架构,这其中浏览器是客户端client,服务器是服务端server,server比如淘宝服务器,提供商品信息,广告,社交等服务,而浏览器,比如微软的IE,就可以用来请求这些服务,并使用server提供的服务。BLE与此类似,一般而言设备提供服务,因此设备是server,手机使用设备提供的服务,因此手机是client。比如蓝牙体温计,它可以提供“体温”数据服务,因此是一个server,而手机则可以请求“体温”数据以显示在手机上,因此手机是一个client。

            服务是以数据为载体的,所以说server提供服务其实就是提供各种有价值的数据。

     

    上图所示的Request和Response其实就是我们经常说的ATT命令(ATT PDU),也就是说Client和Server之间通过ATT PDU进行交互。另外,一个数据“37”,有可能是说体温“37度”,也有可能是说心率“37次”或者湿度“37%”,因此Server需要将数据进行包装和分类,在BLE中,数据是通过characteristic进行包装的,而且多个characteristic组成一个service,service是一个独立的服务单元,或者说service是一个基本的BLE应用。因此我们可以把上图细化为:

     

    如果某个service是一个蓝牙联盟定义的标准服务,也可以称其为profile,比如HID/心率计/体温计/血糖仪等,都是标准蓝牙服务,因此都有相应的profile规格书。

     

    4. BLE service, characteristic以及CCCD

    如文章“深入浅出低功耗蓝牙(BLE)协议栈”所讲,BLE协议栈架构如下所示:

     

            如上图所示,用户开发应用程序或者说service的时候,调用的都是GATT API,而GATT又调用了ATT API,前面也讲过,BLE数据最终都是通过ATT PDU来传输的,那么为什么还需要GATT层?直接操作ATT层不也可以达到同样的目的吗?

            前面也提过,Server是通过characteristic来表示数据的,虽然一条数据最有价值的部分是它的值(value),但是仅有value是不够,比如27,到底是表示27°温度还是27%湿度;如果表示的是温度,那么它的单位是摄氏度还是华氏度。同时每个value还有相应的读写属性以及权限属性,因此一个characteristic包含三种条目:characteristic声明,characteristic的值以及characteristic的描述符(可以有多个描述符),如下所示:

     

            由于一个service可以包含多个characteristic,characteristic declaration就是每个characteristic的分界符,解析时一旦遇到characteristic declaration,就可以认为接下来又是一个新的characteristic了,同时characteristic declaration还将包含value的读写属性等。Characteristic value就是数据的值了,这个比较好理解就不再说了。Characteristic descriptor就是数据的额外信息,比如温度的单位是什么,数据是用小数表示还是百分比表示等之类的数据描述信息。CCCD是一种特殊的characteristic descriptor,一般而言,都是client来访问server的characteristic,我们把这种操作称为读或者写。另外,server可以直接把自己的characteristic的值告诉client,我们称其为notify或者indicate,跟read操作相比,只有需要传输数据的时候或者说只有当数据有效时,server才开始notify或者indicate数据到client,因此这种操作方式可以大大节省server的功耗。有时候client不想监听characteristic notify或者indicate过来的数据,那么就可以使用CCCD来关闭characteristic的notify或者indicate功能;如果client又需要监听characteristic的notify或者indicate,那么它可以重新使能CCCD来打开相关操作。总结一下,当characteristic具有notify或者indicate操作功能时,那么必须为其添加相应CCCD,以方便client来使能或者禁止notify或者indicate功能。

            不管是characteristic declaration,characteristic value还是characteristic descriptor,实现的时候,我们都是用attribute来表达的,也就是说,他们每一个都是一个attribute,attribute可以用下图来表示:

     

    • Attribute handle,Attribute句柄,16-bit长度。Client要访问Server的Attribute,都是通过这个句柄来访问的,也就是说ATT PDU一般都包含handle的值。用户在软件代码添加characteristic的时候,系统会自动按顺序地为相关attribute生成句柄。
    • Attribute type,Attribute类型,2字节或者16字节长。在BLE中我们使用UUID来定义数据的类型,UUID是128 bit的,所以我们有足够的UUID来表达万事万物。其中有一个UUID非常特殊,它被蓝牙联盟采用为官方UUID,这个UUID如下所示:

     

     由于这个UUID众所周知,蓝牙联盟将自己定义的attribute或者数据只用16bit UUID来表示,比如0x1234,其实它也是128bit,完整表示为:

     

    Attribute type一般是由service和characteristic规格来定义,站在蓝牙协议栈角度来看,ATT层定义了一个通信的基本框架,数据的基本结构,以及通信的指令,而GATT层就是前文所述的service和characteristic,GATT层用来赋予每个数据一个具体的内涵,让数据变得有结构和意义。换句话说,没有GATT层,低功耗蓝牙也可以通信起来,但会产生兼容性问题以及通信的低效率。

    • Attribute value,就是数据真正的值,0到512字节长。
    • Attribute permissions,Attribute的权限属性,权限属性不会直接在空中包中体现,而是隐含在ATT命令的操作结果中。假设一个attribute read属性设为open(即读操作不需要任何权限),那么client去读这个attribute时server将直接返回attribute的值;如果这个attribute read属性设为authentication(即需要配对才能访问),如果client没有与server配对而直接去访问这个attribute,那么server会返回一个错误码:告诉client你的权限不够,此时client会对server发起配对请求,以满足这个attribute的读属性要求。目前主要有如下四种权限属性:
      • Open,直接可以读或者写
      • No Access,禁止读或者写
      •  Authentication,需要配对才能读或者写,由于配对有多种类型,因此authentication又衍生多种子类型,比如带不带MITM,有没有LESC
      • Authorization,跟open一样,不过server返回attribute的值之前需要应用先授权,也就是说应用可以在回调函数里面去修改读或者写的原始值。
      • Signed,签名后才能读或者写,这个用得比较少。

             大家还记不记得设备与手机nRF connect连接成功后呈现的界面,我这里再贴一下:

     

             可以看到手机呈现的就是上文讲的service和characteristic,nRF Connect为了让整个界面变得更美观,将访问属性,UUID,handle都分列来表示了,以致于很多初学者会把理论和现实二者对应不起来。Nordic之前推出过一款Master Control Panel(MCP),MCP现在已经不推荐使用了,不过MCP有一个好处,它对service和characteristic的组织方式更接近底层实现方式,对大家理解service和characteristic是非常有帮助的。还是这个设备,我用MCP跟它连接并进行服务发现,你会发现它呈现的界面如下所示:

     

    这个图就跟上面讲的理论知识可以一一对应起来了,NUS包含2个characteristic:RX和TX,每一个条目都是一个attribute,NUS服务本身就是一个attribute,而RX characteristic本身又包含2条attribute:一条是declaration attribute,一条是value本身attribute。由于TX支持notify,所以它包含3条attribute,另外一条attribute是CCCD。每个attribute都有一个handle和UUID,handle用来访问该attribute,UUID用来指明该attribute的类型。可以说,server提供数据,而数据是由attribute来表达,所有attribute组成一个attribute table,设备支持的服务不同,attribute table就不同。这里说明一下,当你在Nordic已有例程基础上再去添加新的服务或者删除已有的服务,记得一定要去修改ATTR_TAB_SIZE那个宏,否则协议栈初始化会有问题。

           

    5. 常用ATT命令

            Client和Server之间是通过ATT PDU来通信的,ATT PDU主要包括4类:读,写,notify和indicate。如果一个命令需要response,那么会在相应命令后面加上request;如果一个命令只需要ACK而不需要response,那么它的后面就不会带request。这里要特别强调一点,BLE所有命令都是“必达”的,也就是说每个命令发出去之后,会立马等ACK信息,如果收到了ACK包,发起方认为命令完成;否则发起方会一直重传该命令直到超时导致BLE连接断开。换句话说,只要你的BLE没有断开,那么你之前发送的数据包,不管它是用什么ATT PDU来发送的,它肯定被对方收到了。我估计很多人对此会产生疑问,因为他们经常碰到丢包的情况,其实大家经常碰到的“丢包”,不是空中把包丢了或者包在空中被干扰了,而是大家发送的代码写得有问题,导致你要发送的包没有被安全送达到协议栈射频FIFO中,所以以后大家碰到丢包情况,请先检查你的代码,保证你的数据包正确完整安全地送达到协议栈射频FIFO中,只要数据包放到了协议栈射频FIFO中,蓝牙协议栈就能保证该数据包“必达”对方。既然每个ATT命令都必达对方,那么还需要request做什么?如果一个命令带有request后缀,那么发起方就可以收到命令的response包,这个response包在应用层是有回调事件的,而前述的ACK包在应用层是没有回调事件的。所以采用request/response方式,应用层可以按顺序地发送一些数据包,这个在很多应用场合是非常有用的。相反,如果你对应用层数据包的顺序没有要求,那么就可以不使用request/response形式。另外Request/response有一个副作用:大大降低通信的吞吐率,因为request/response必须在不同的连接间隔中出现,也就是说,你在间隔1中发送了一个request命令,那么response包必须在间隔2或者稍后间隔中回复,而不能在间隔1中回复,这就导致两个连接间隔最多只能发一个数据包,而不带request后缀的ATT命令就没有这个问题,在同一个连接间隔中,你可以同时发多个数据包,这样将大大提高数据的吞吐率。大家可以参考下图来理解request和非request命令的区别:

     

    常用的带request的命令:所有read命令,write request,indication等,而常用的不带request的命令有write command,notification等,完整的ATT命令列表如下所示:

     

    6. 设备端固件代码一览

    现在我们一起来看一下ble_app_uart的源代码,看看它是怎么工作起来的。首先我们来看main函数:

     

    如上所述,ble_stack_init用于初始化配置和使能蓝牙协议栈,其代码如下所示:

    其中,nrf_sdh_enable_request需要选择蓝牙协议栈的低频时钟(由于蓝牙协议栈的高频时钟必须为外部32M晶振,所以高频时钟无需配置;而低频时钟可以选择为内部32K RC或者外部32K晶振,所以低频时钟需要人工配置),因此如下宏需要根据实际情况进行调整:

    复制代码

        nrf_clock_lf_cfg_t const clock_lf_cfg =
    
        {
    
            .source       = NRF_SDH_CLOCK_LF_SRC,
    
            .rc_ctiv      = NRF_SDH_CLOCK_LF_RC_CTIV,
    
            .rc_temp_ctiv = NRF_SDH_CLOCK_LF_RC_TEMP_CTIV,
    
            .accuracy     = NRF_SDH_CLOCK_LF_ACCURACY
    
    };

    复制代码

    通过sdk_config.h文件可以看到,默认是选择外部32K晶振作为低频时钟的,如果你想选择内部32K RC作为低频时钟,那么需要做如下修改

    复制代码

    NRF_SDH_CLOCK_LF_SRC = 0
    
    NRF_SDH_CLOCK_LF_RC_CTIV = 16    //每4s启动一次校准
    
    NRF_SDH_CLOCK_LF_RC_TEMP_CTIV = 2
    
    NRF_SDH_CLOCK_LF_ACCURACY = 1  //500ppm

    复制代码

    nrf_sdh_ble_default_cfg_set用来配置softdevice协议栈,如下宏是经常需要修改的:

    复制代码

    NRF_SDH_BLE_TOTAL_LINK_COUNT  //一共同时可以支持多少个连接
    
    NRF_SDH_BLE_PERIPHERAL_LINK_COUNT  //作为从模式的连接同时能有几个
    
    NRF_SDH_BLE_CENTRAL_LINK_COUNT  //作为主模式的连接同时能有几个
    
    NRF_SDH_BLE_GATT_MAX_MTU_SIZE //MTU size为多大
    
    NRF_SDH_BLE_VS_UUID_COUNT  //用户自定义的base UUID有几个
    
    NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE  //Attribute table总共占多少协议栈RAM空间
    
    NRF_SDH_BLE_SERVICE_CHANGED  //要不要包含service change characteristic

    复制代码

    nrf_sdh_ble_enable真正使能BLE功能,它的参数ram_start既是一个输入参数又是一个输出参数,作为输入参数,系统自动会把如下的RAM起始地址传入:

     

    同时nrf_sdh_ble_enable会把当前softdevice配置情况下,它实际需要占用的RAM空间通过ram_start返回,如果这个返回值不等于输入值,那么用户需要把上图的IRAM1起始地址修改成它的返回值。其中NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE这个宏的取值是需要用户不断去试错的,因此每当你添加了或者删除了BLE service,都需要去调整NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE这个宏的值,然后去查看nrf_sdh_ble_enable的返回值,看看这个参数的取值是否合理

    NRF_SDH_BLE_OBSERVER用来为本地文件(此处为main.c)注册一个BLE回调函数(此处为ble_evt_handler),NRF_SDH_BLE_OBSERVER这个宏执行成功后,所有的BLE事件都会被ble_evt_handler捕获。进入ble_evt_handler,你会发现BLE有上百个回调事件,你不需要每个都处理,你只需要处理你关心的事件即可,比如连接成功事件BLE_GAP_EVT_CONNECTED或者连接断开事件BLE_GAP_EVT_DISCONNECTED,如下所示:

    NRF_SDH_BLE_OBSERVER有一个很大的好处:某个模块如果需要捕获BLE事件,那么它自己调用NRF_SDH_BLE_OBSERVER这个宏注册相应回调函数即可,而不再需要在其它文件中去注册这个回调函数,将模块的耦合性降到最低,符合模块化编程思想。

    gap_params_init用来修改广播名字和连接间隔的。gatt_init用来修改底层数据包长度的。advertising_init用来修改广播包内容,广播间隔以及广播超时时间。conn_params_init用来请求更新连接间隔的。

    我们来重点讲一下services_init,services_init用来添加服务和characteristic,前面讲了那么多的概念和理论,现在我们就来看看services_init是如何做到跟理论一致的。services_init通过ble_nus_init添加了一个蓝牙数据透传服务:NUS,那ble_nus_init是怎么将NUS服务添加成功的呢?查看ble_nus_init函数体,你会发现它是分三步来做的:

    1. 添加服务的UUID。如果是蓝牙标准服务,这步可以省略。由于NUS不是蓝牙联盟定义的,所以需要调用sd_ble_uuid_vs_add以增加一个供应商自定义的UUID。
    2. 添加服务本身。直接调用sd_ble_gatts_service_add就可以完成。
    3. 添加服务下面的characteristics。server的characteristic一般都是通过sd_ble_gatts_characteristic_add来添加的。以NUS的RX characteristic为例,可以看到:
    sd_ble_gatts_characteristic_add(p_nus->service_handle,  &char_md,  &attr_char_value, &p_nus->rx_handles);

    其中,p_nus->service_handle表示该characteristic属于那个service,p_nus->rx_handles是输出值,由协议栈返回,以后访问该characteristic都是通过这个句柄来完成,attr_char_value这个是characteristic的value,char_md这个是characteristic的元数据(metadata),前面第4章也讲过,一个数据除了有value这个characteristic之外,它还包含其他attribute,而这些attribute全部都用char_md来表示,比如这个characteristic value能支持的ATT命令类型,CCCD信息,descriptor信息等,这里要特别指出的是,只有当支持notify或者indicate时,才需要提供cccd_md信息,其他ATT命令不需要cccd_md信息,所以RX characteristic的char_md如下所示,它同时支持write和write request两种写命令,由于它不支持notify或者indicate,所以cccd_md为NULL。

     

    attr_char_value是一个attribute,所以它包含attribute metadata,如下:

    attr_char_value具体包含的value信息由以下成员表示:

     

    由于这里把characteristic value放在了协议栈RAM中,所以协议栈会自动为这个value创建一个buffer。如果你想把characteristic value放在用户RAM中,即vloc = BLE_GATTS_VLOC_USER,那么这里你还需要把一个全局数组变量赋给attr_char_value. p_value。

    TX characteristic与之类似,就不再额外解读了。

    这里需要特别提醒大家的是,虽然Nordic API结构体参数设计得很复杂,但是大部分成员变量直接就可以使用它的默认值0,你只需对你感兴趣的成员变量进行赋值即可,所以大家经常看到如下场合,即先用memset将该结构体变量初始化为0,让其所有成员变量都采用默认值,然后再对某些需要修改的成员变量进行二次赋值。大家一定不要忘了将结构体变量清零这一步操作!

     

    ble_nus_init同时注册了nus_data_handler回调函数,当设备收到手机发过来的数据时,就会触发nus_data_handler,用户可以在nus_data_handler中对接收到的数据进行处理,本例程中nus_data_handler直接将ble收到的数据通过uart口转发出去。如果用户需要发送数据给手机,在连接成功和notify使能的情况下,直接调用ble_nus_data_send即可,而ble_nus_data_send又是通过调用协议栈API:sd_ble_gatts_hvx来实现数据发送功能的。那么什么时候需要发送数据给手机?本例程的做法是,当串口有数据过来并满足如下条件时调用ble_nus_data_send:

    if ((data_array[index - 1] == '\n') || (index >= (m_ble_nus_max_data_len)))

    main函数最后将调用API让协议栈跑起来,如果你的设备将来是一个从设备(peripheral),那么请调用ble_advertising_start,ble_advertising_start将开启可连接的广播,从而让你的设备连接成功之后成为从设备。如果你的设备将来是一个主设备(central),那么请调用sd_ble_gap_scan_start,sd_ble_gap_scan_start将开启设备的扫描功能,从而让你的设备连接成功之后变为主设备。

     

    最后我们来看main循环,它只有一个函数: idle_state_handle,idle_state_handle先把需要打印的日志打印完,然后让系统进入idle状态(Nordic SoC spec称其为System ON状态),一旦有协议栈事件或者中断事件发生,系统将唤醒,以处理相关事件回调函数,然后再执行一遍idle_state_handle。注意:idle状态下,蓝牙连接或者广播可以正常进行而不受影响,蓝牙连接或者广播都是周期性的,在一个周期中,蓝牙连接或者广播只持续很短一段时间(这段时间CPU有可能会退出idle状态),其余时间系统都是处于idle状态的,从而大大节省系统功耗。

     

    7. 定制你的BLE数据透传应用程序

    7.1 BLE数据上传吞吐率

    如何快速的把大量数据上传给手机?这是一个很常见的应用场合,现在我们尝试去修改一下Nordic的原生例程,以实现最高的数据吞吐率。下面我们通过几种不同的方法来看看每种方法下它的吞吐率能到多少。

    方法1:(通过宏METHOD1来开关)

    蓝牙spec规定,蓝牙连接间隔最小只能为7.5m,为了达到最高的吞吐率,我们创建一个timer,让其每7ms发一次数据,看一看此时吞吐率能达到多少。7ms中断服务函数代码如下所示:

    复制代码

    static void throughput_timer_handler(void * p_context)
    
    {
    
        UNUSED_PARAMETER(p_context);
    
        ret_code_t err_code;
    
        uint16_t length;
    
        m_cnt_7ms++;
    
        length = m_ble_nus_max_data_len;
    
        if (m_conn_handle != BLE_CONN_HANDLE_INVALID)
    
        {
    
            err_code = ble_nus_data_send(&m_nus, m_data_array, &length, m_conn_handle);
    
    //      if ( (err_code != NRF_ERROR_INVALID_STATE) && (err_code != NRF_ERROR_RESOURCES) &&
    
    //          (err_code != NRF_ERROR_NOT_FOUND) )
    
    //      {
    
    //                APP_ERROR_CHECK(err_code);
    
    //      }             
    
            m_len_sent += length;           
    
            m_data_array[0]++;
    
            m_data_array[length-1]++;         
    
         }
    
         NRF_LOG_INFO("time: %d *7ms == bytes send: %d Bytes == avg speed: %d B/s",m_cnt_7ms,m_len_sent,m_len_sent/(m_cnt_7ms*7));   
    
    }

    复制代码

     

    这种做法会导致ble_nus_data_send报“NRF_ERROR_RESOURCES”错误,这个错误表示协议栈无资源应付这么快的调用速度。为此我们对ble_nus_data_send返回的错误值一概不进行处理,看看会发生什么?我们发现程序可以正常运行,RTT viewer打印的日志如下所示:

     

    由上图可知,数据上传吞吐率达到了34.8kB/s,其实这个吞吐率是假的,因为中间丢了很多包,但计算吞吐率的时候把丢的包也算进去了。如下图所示,0x6E之后应该为0x6F,但实际发送的数据包编号为0x83,丢包非常严重。

     

       为了防止所谓的“丢包”(前面也提过,这里的丢包不是数据包在空中丢掉了,而是数据包没有安全送到协议栈的buffer中,从而导致丢包),我们加上如下if语句,只有ble_nus_data_send返回正确时,才认为数据包正确发送,然后才能算入到throughput中:

    复制代码

      if (err_code == NRF_SUCCESS)
    
      {
    
                 m_len_sent += length;  
    
                 m_data_array[0]++;
    
                 m_data_array[length-1]++;                                                      
    
        }

    复制代码

     

    通过查看nRF connect日志,你会发现此时不会发生丢包了,但吞吐率直接降到了1.6kB/s左右。

    方法1+:(通过宏METHOD1_PLUS来开关)

    我们对方法一稍作调整,首先我们持续往发送buffer写数据,直到返回值不是NRF_SUCCESS

    复制代码

    do

    {

        err_code = ble_nus_data_send(&m_nus, m_data_array, &length, m_conn_handle);

        if ( (err_code != NRF_ERROR_INVALID_STATE) && (err_code != NRF_ERROR_RESOURCES) &&

          (err_code != NRF_ERROR_NOT_FOUND) )

        {

            APP_ERROR_CHECK(err_code);

        }

        if (err_code == NRF_SUCCESS)

        {

            m_len_sent += length;

            m_data_array[0]++;

            m_data_array[length-1]++;

        }

    } while (err_code == NRF_SUCCESS); 

    复制代码

    然后我们把连接间隔设为尽可能小,以期提高吞吐率,如下:

    复制代码

    #ifdef CONN_INTERVAL_OPTIMIZE
    
    #define MIN_CONN_INTERVAL               MSEC_TO_UNITS(8, UNIT_1_25_MS)   
    
    #define MAX_CONN_INTERVAL               MSEC_TO_UNITS(12, UNIT_1_25_MS)
    
    #endif

    复制代码

     

    这种方法吞吐率能达到10kB/s,但离我们的目标还是很远。

    最后我们把connection event length extension和data length extension都打开(我们将在方法2+中详细阐述这2个有效提高吞吐率的利器),即定义如下宏:

     

    可以看到吞吐率将达到70kB/s,这个吞吐率还是不错的。但仔细查看nRF connect日志,你会发现这种模式下还是有小概率事件会导致“丢包”发生,而且整个发送逻辑也不是很优化,为此我们想到了METHOD2.

    方法2:(通过宏METHOD2来开关)

    ble_nus_data_send每次成功发送数据包,都会产生一个BLE_NUS_EVT_TX_RDY事件,收到这个事件后,再去调用ble_nus_data_send,丢包的情况就不会再发生了,核心代码如下所示:

    复制代码

    
     

    if (p_evt->type == BLE_NUS_EVT_TX_RDY)

    {

    #ifdef METHOD2

        err_code = ble_nus_data_send(&m_nus, m_data_array, &length, m_conn_handle);

        if ( (err_code != NRF_ERROR_INVALID_STATE) && (err_code != NRF_ERROR_RESOURCES) &&

          (err_code != NRF_ERROR_NOT_FOUND) )

        {

              APP_ERROR_CHECK(err_code);

        }

        if (err_code == NRF_SUCCESS)

        {

             m_len_sent += length;

             m_data_array[0]++;

             m_data_array[length-1]++;

         }

         NRF_LOG_INFO("time: %d *10ms == bytes send: %d Bytes == avg speed: %d B/s",m_cnt_10ms,m_len_sent,m_len_sent * 100/m_cnt_10ms);

    #endif

    复制代码

    大家可以自己去查看一下nRF  Connect的数据log,这种方式是没有丢包的,但是打开RTT viewer,你会发现他的吞吐率低得可怜,只有1kB/s。

     

    方法2+:(通过宏METHOD2_PLUS来开关)

    与方法1+类似,我们在方法2基础上,持续往发送buffer送数据直到返回值不为0,如下:

    复制代码

    #ifdef METHOD2_PLUS

    //queue multiple tx array

    do

    {

        err_code = ble_nus_data_send(&m_nus, m_data_array, &length, m_conn_handle);

        if ( (err_code != NRF_ERROR_INVALID_STATE) && (err_code != NRF_ERROR_RESOURCES) &&

         (err_code != NRF_ERROR_NOT_FOUND) )

        {

             APP_ERROR_CHECK(err_code);

        }

        if (err_code == NRF_SUCCESS)

        {

            m_len_sent += length;

            m_data_array[0]++;

            m_data_array[length-1]++;

        }

    } while (err_code == NRF_SUCCESS);

    NRF_LOG_INFO("time: %d *10ms == bytes send: %d Bytes == avg speed: %d B/s",m_cnt_10ms,m_len_sent,m_len_sent * 100/m_cnt_10ms);

    #endif

    复制代码

     

    然后我们增加gap event length extension功能,gap event length跟connection event length两者意思差不多,都是为了实现一个连接间隔可以发或收多个包的目的。为了使能gap event length extension功能,首先将gap event length修改成一个合适的值,以使其尽可能占满整个连接间隔,如下将gap event length修改为30ms

    #define NRF_SDH_BLE_GAP_EVENT_LENGTH 24

     

    然后我们再将连接间隔设为尽可能小,以保证上述connection event可以占据整个连接间隔:

    复制代码

    #ifdef CONN_INTERVAL_OPTIMIZE
    
    #define MIN_CONN_INTERVAL               MSEC_TO_UNITS(8, UNIT_1_25_MS)   
    
    #define MAX_CONN_INTERVAL               MSEC_TO_UNITS(12, UNIT_1_25_MS)
    
    #endif

    复制代码

     

    同时我们使能connection event extension功能,如下:

    复制代码

    #ifdef EVT_LEN_EXT_ON
    
        ble_opt_t  opt;
    
        memset(&opt, 0x00, sizeof(opt));
    
        opt.common_opt.conn_evt_ext.enable = true;
    
        err_code = sd_ble_opt_set(BLE_COMMON_OPT_CONN_EVT_EXT, &opt);
    
        APP_ERROR_CHECK(err_code);
    
    #endif

    复制代码

     

    我现在使用的是华为P9手机,它将把MTU设为241,在DLE不开的情况下(此时链路层每个数据包的长度还是只有27个字节!),我们可以看到throughput可以达到10kB以上,如下:

     

     

    然后我们再打开DLE功能,此时链路层每个数据包的长度将变成251字节,如下:

    复制代码

    #ifdef DLE_ON
    
            case BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST:
    
            {
    
                NRF_LOG_DEBUG("DLE update request.");
    
                ble_gap_data_length_params_t dle_param;
    
                memset(&dle_param, 0, sizeof(ble_gap_data_length_params_t));   //0 means auto select DLE                                                                    
    
                err_code = sd_ble_gap_data_length_update(p_ble_evt->evt.gap_evt.conn_handle, &dle_param, NULL);
    
                APP_ERROR_CHECK(err_code);
    
            } break;
    
    #endif

    复制代码

     

    此时我们可以看到throughput可以达到77kB/s,离蓝牙4.2的理论throughput已经很接近了。这里特别需要指出的是,当DLE使能情况下,connection interval不是越小吞吐率越高,我这里使用的connection interval大概为10ms,如果大家把这个connection interval提高到30ms,有可能吞吐率更高,这里就不再演示了。

     

     

     

     

     

    上述代码工程已经上传到百度云盘中,有需要的同学可以到如下链接下载:

    下载“tutorial_ble_app_uart_SDK15_0_0.rar”,然后解压缩到SDK15.0.0如下目录下:nRF5_SDK_15.0.0_a53641a\examples\ble_peripheral,即可成功编译运行。

     

    7.2使用安卓版nRF connect测试BLE设备的稳定性和可靠性

    先说明一下,以下内容只能通过安卓版nRF Connect来实现,iOS版nRF Connect不支持如下特性。

    手机端宏录制方式

    相信到现在大家对BLE数据上传机理和实践有个大概的了解,那如何测试BLE数据下行性能,即怎么测试数据从手机传到设备的稳定性和可靠性?我们是不是必须开发一款手机app来进行相关测试吗?答案是否定的,感谢Nordic给我们带来了nRF connect,nRF connect支持宏录制,我们可以通过nRF connect来对我们的设备进行压力测试。下面我们来讲讲宏录制是怎么工作的。

    所谓宏录制,就是把你对nRF connect的操作录制下来,然后通过宏播放实现自动化操作。由于nRF connect是一个容器,并支持JavaScript和HTML语法,宏其实就是一个XML脚本,nRF connect定义了自己的一套XML标签操作,遵守这套XML标签操作,就可以对nRF connect进行自动化操作。nRF connect支持的所有XML语法都在手机安装目录\Nordic Semiconductor中的示例中体现,只要示例中出现过的标签就支持,相反示例中没有的标签就不支持。下面具体讲一下宏录制的操作过程。

    当nRF connect连接设备成功后,你会发现右下角有一个红点,那个就是宏录制菜单。

     

     

    点击下面的红点,我们开始宏录制操作

     

     

    然后我们按照普通操作来操作nRF connect,这些操作最终对应的BLE指令会被录制下来,以便后续重复播放。我们先把“1234”发送给设备,如下:

     

    发送完上述指令后,我们加一个300ms的延时,如下:

     

    然后我们点击完成按钮,保存该宏,可以看出这个宏包括两条操作:发送“1234”到设备,然后睡眠300ms。

     

     

    将宏命名为“test”并保存:

     

     

    到此宏已经录制成功了,现在我们开始展示宏的神奇功能。如下,选择循环播放模式,然后点击“开始”按钮开始循环播放该录制宏。

     

     

    大家可以看到,nRF connect先执行“Write 0x31323334 to RX characteristic”,然后睡眠300ms,然后又执行“Write 0x31323334 to RX characteristic”,如此循环往复。打开串口助手,你会发现设备已经收到了手机发过来的一连串“1234”,如下。

     

    我们把刚才的test宏导出为XML,看一看它到底长什么样:

    复制代码

    <macro name="test" icon="PLAY">
    
       <assert-service description="Ensure Nordic UART Service" uuid="6e400001-b5a3-f393-e0a9-e50e24dcca9e">
    
          <assert-characteristic description="Ensure RX Characteristic" uuid="6e400002-b5a3-f393-e0a9-e50e24dcca9e">
    
             <property name="WRITE" requirement="MANDATORY"/>
    
          </assert-characteristic>
    
       </assert-service>
    
       <write description="Write 0x31323334 to RX Characteristic" characteristic-uuid="6e400002-b5a3-f393-e0a9-e50e24dcca9e" service-uuid="6e400001-b5a3-f393-e0a9-e50e24dcca9e" value="31323334" type="WRITE_REQUEST"/>
    
       <sleep description="Sleep 300 ms" timeout="300"/>
    
    </macro>

    复制代码

     

    大家可以看到,宏就是一些XML标记,大家也可以在此基础上,去修改该XML文件,以实现更复杂的自动化测试,然后通过nRF connect把最新的XML文件装载进来,就可以自动播放了。

    如果你还想了解宏更多的用法信息,请参考:https://github.com/NordicSemiconductor/Android-nRF-Connect/blob/master/documentation/Macros/README.md

     

    电脑端XML方式

    前面的宏录制方式,功能还是比较单一,如果要实现更复杂的自动化测试,可以通过在PC端执行XML脚本方式来实现。通过安卓调试工具ADB,我们可以直接通过PC来操作nRF connect,而nRF connect又能识别XML脚本,这样就可以让nRF connect按照XML脚本意图去执行相关自动化操作。nRF connect支持的所有XML语法都在手机安装目录中(手机内部存储/ Nordic Semiconductor目录)的示例中体现,只要示例中出现过的标签就支持,相反示例中没有的标签就不支持。

    欲了解更多信息请参考:https://github.com/NordicSemiconductor/Android-nRF-Connect/blob/master/documentation/Automated%20tests/README.md

     

    8. 开发手机端app代码

    Nordic提供很多手机端开源app供大家参考,用得最多的就是nRF Toolbox和nRF Blinky(注:nRF connect代码不开源),在nRF Toolbox和nRF Blinky中都有相关的BLE操作库,尤其是nRF Toolbox包含了很多BLE库,比如BLE管理,DFU,数据透传,蓝牙Mesh等等,大家可以参考他们来开发自己的手机端app。

    nRF Toolbox软件界面如下所示:

     

    UART就是前文说到的NUS服务,除了nRF connect,其实大家也可以通过nRF Toolbox UART模块来完成第2章所述的操作。nRF Toolbox另一个用的比较多的功能就是DFU,如果你需要通过手机BLE来实现设备固件的空中升级(OTA),那么可以参考nRF Toolbox DFU模块来编写你的手机端软件。

    标签: BLE数据透传servicecharacteristic服务特征attribute tablethroughput吞吐率nRF connectmacro宏

    展开全文
  • 如何开发BLE数据透传应用程序?什么是BLE service和characteristic?如何开发自己的service和characteristic?如何区分ATT和GATT?有没有什么工具可以对BLE设备进行压力测试?如何提高BLE设备的数据上传速度?本文将...

     如何开发BLE数据透传应用程序?什么是BLE service和characteristic?如何开发自己的service和characteristic?如何区分ATT和GATT?有没有什么工具可以对BLE设备进行压力测试?如何提高BLE设备的数据上传速度?本文将对以上问题进行解答。

    在很多应用场合,BLE只是作为一个数据透传模块,即将设备端数据上传给手机,同时接收手机端下发的数据。本文将和大家一起,一步一步演示如何开发一个BLE透传应用程序。按照本文的说明,大家可以很快就实现一个BLE透传应用,BLE透传应用已经是BLE应用中比较复杂的一种,一旦大家掌握了BLE透传应用,其他BLE应用开发就更不在话下了。本文还会以BLE透传为例子,来解释BLE service和characteristic等概念,以帮助大家理解如何定义和开发自己的BLE service和characteristic等,从而彻底理解BLE协议栈中的ATT和GATT的运行原理。然后,本文还将手把手教大家如何提高BLE数据传输速度(蓝牙4.2的理论吞吐率大概为100kB/s,而我们实际达到了80kB/s,已经非常接近理论值)。最后,我们将告诉大家如何使用安卓版nRF Connect来对你的BLE设备进行压力测试,以测试设备的稳定性和可靠性。当然,文章的最后也会告诉大家如何找到安卓和iOS手机app开发参考代码。

     

    1. 开发准备

    1)     Nordic nRF52或者nRF51开发板1块。请参考“Nordic nRF51/nRF52开发流程说明”,购买相应开发板(DK)。

    2)     开发环境搭建。简述如下(详细说明请参考“Nordic nRF51/nRF52开发环境搭建”):

    1. 安装Keil5 MDK
    2. 安装SDK。如果你使用的是nRF52开发板,请安装nRF5 SDK15.0.0,下载链接:https://www.nordicsemi.com/eng/nordic/download_resource/59012/70/52858981/116085。如果你手上是nRF51开发板,请下载nRF5 SDK12.3.0:https://www.nordicsemi.com/eng/nordic/download_resource/54280/56/38442131/32925 (nRF51最高SDK版本只能到12.3.0,后续SDK就不再支持nRF51
    3. 安装ARM CMSIS4.5.0,下载链接:https://github.com/ARM-software/CMSIS/releases/download/v4.5.0/ARM.CMSIS.4.5.0.pack
    4. 安装Keil5 Device Family Pack,下载链接:https://www.nordicsemi.com/eng/nordic/download_resource/58865/28/26535159/87790
    5. 安装nRF5 Command Line Tools,下载链接(Windows版):https://www.nordicsemi.com/eng/nordic/download_resource/58850/47/60411125/53210
    6. 安装安卓版或者iOS版nRF connect。iOS版nRF connect请到苹果app store下载,搜索“nRF”即可以找到。安卓版nRF connect可以到Nordic Github官网上下载,下载链接为:https://github.com/NordicSemiconductor/Android-nRF-Connect/releases
    7. 安装PC版nRF connect或者nRFgo studio,两个选其一即可。PC版nRF connect下载链接(Windows版):https://www.nordicsemi.com/eng/nordic/download_resource/58847/15/21277021/108233

    注:如果你使用的是Linux系统/Mac系统,或者你使用的不是Keil5-MDK,请参考“Nordic nRF51/nRF52开发环境搭建”来搭建你的开发环境。

    2. 运行Nordic ble_app_uart应用程序

    Nordic SDK已经提供了一个直接就可以编译和运行的数据透传应用程序:ble_app_uart,Nordic将BLE透传服务称为Nordic UART Service(NUS),所以在Nordic SDK中,NUS就是BLE透传服务。请按照如下步骤运行SDK自带的ble_app_uart程序:

    1)     确认自己的芯片型号或者开发板。如果采用Nordic官方开发板的话,芯片型号和开发板编号对应关系如下:

    • nRF51系列对应开发板编号为PCA10028
    • nRF52832和nRF52810对应开发板编号为PCA10040。虽然52832和52810共用同一块开发板,但是他们在SDK中的项目编号是不一样的,52832对应PCA10040目录,52810对应PCA10040e目录,由于52810和52832 PIN to PIN兼容,软件也是完全兼容的,因此SDK很多项目只有PCA10040的目录,而没有PCA10040e目录,此时需要你自己来建立PCA10040e对应的目录和工程,具体说明可参考:http://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.sdk5.v15.0.0%2Fnrf52810_user_guide.html&cp=4_0_0_5_0
    • nRF52840对应开发板编号为PCA10056
    • nRF52840 dongle编号为PCA10059

    这里我会以nRF52832开发板PCA10040为例来阐述整个开发过程,其他开发板与之类似,大家自己可以举一反三来开始自己的开发之旅。

    2)     将开发板与PC机通过USB线相连,同时打开开发板电源(将左下角的拨位开关打到“ON”位置),打开桌面版nRF Connect,选择启动“Programmer”应用,由于驱动之前已经安装好了,设备可以立即识别成功。执行“full erase”操作,以擦除芯片原始内容。 

     

     

    3)     打开SDK中的ble_app_uart程序。如果是52832开发板,请打开:nRF5_SDK_15.0.0_a53641a\examples\ble_peripheral\ble_app_uart\pca10040\s132\arm5_no_packs;如果是51822开发板,请打开:nRF5_SDK_12.3.0_d7731ad\examples\ble_peripheral\ble_app_uart\pca10028\s130\arm5_no_packs

    后续将以52832开发板为例来阐述,51822与之类似就不再阐述了。

     

    注:Nordic SDK例程目录结构为:SDK版本/ examples /协议角色/例子名称/开发板型号/协议栈型号/工具链类型/具体工程,比如下面例子:

     

     

    Nordic每一个例子都支持5种工具链:Keil5/Keil4/IAR/GCC/SES,如下所示:

     

     

    4)     编译程序。如果你已经按照之前的说明配置好了开发环境,那么这里编译是不会报任何错的。(如果你遇到了编译错误,请重新按照前面说明去搭建你的开发环境,不要怀疑SDK例子代码有问题哦)

    5)     下载程序。程序下载包括2步:一先下载softdevice,二再下载应用。Softdevice是Nordic蓝牙协议栈的名称,整个开发过程中只需下载一次。应用就是我们这里的ble_app_uart程序。如果你的开发板已经下载了其他代码,那么最好先把开发板全擦一次,然后再下载softdevice和应用。

    • 芯片全擦(可选)。你可以使用nRFgo studio,或者nRF connect桌面版,或者nrfjprog,三者选其一来执行擦除操作。
      • 使用nRFgo studio执行全擦操作

     

    • 使用nRF connect桌面版执行全擦操作

      

     

    • 使用nrfjprog执行全擦操作

     

     

    • 蓝牙协议栈下载(整个开发周期只需下载一次)。在Keil ‘select target’下拉列表中,默认选择的是Keil工程对应的Target,即‘nrf52832_xxaa’。我们还可以选择另一个target ‘flash_s132_nrf52_6.0.0_softdevice’,即softdevice对应的target,然后点击“下载download”(不需要编译哦!),此时会把softdevice下载到开发板中。

     

     

    • 应用下载。重新选择Target:‘nrf52832_xxaa’,点击“下载Download”,此时会把ble_app_uart应用程序下载到开发板中。此时开发板的LED1闪烁,表示程序运行正常。

    6)     连接手机。打开手机蓝牙和手机版nRF connect。在nRF connect中,你将看到一个广播设备:Nordic_UART,这个就是开发板的广播名字。点击“CONNECT”,手机将与设备建立连接,并开始服务发现过程,连接成功后,LED1熄灭,LED2点亮,最后将得到如下界面。

     

    上图的Nordic UART Service(NUS)就是我们的数据透传服务, NUS具体包括两个characteristic:TX和RX,由于NUS是由设备提供的,所以TX表示设备发送数据给手机,RX表示设备接收手机发过来的数据。

    7)     测试NUS服务。ble_app_uart使用串口与上位机交互,选择一款串口助手软件,比如Putty,打开该串口软件,并做如下设置:

    • Baud rate: 115.200
    • 8 data bits
    • 1 stop bit
    • No parity
    • HW flow control: None

    复位开发板,你会发现串口助手会打印如下信息:

     

     

    按照第6)步,重新将开发板连上手机,然后点击右上角的“Enable CCCDs”以使能notification,如下所示:

     

     

    设备接收数据: 点击RX characteristic旁边的向上箭头,通过手机蓝牙往设备发送:12345678,如下所示:

     

         此时设备通过串口打印出刚才接收到的数据,如下所示:

     

    设备发送数据:在串口助手中输入“abcdefgh”并输入“\n”(注:在Putty中,先按“CTRL”再按“J”就会发出“\n”换行符)作为结束符,设备将把串口收到的数据通过蓝牙发送给手机,手机的TX characteristic将显示上述字符串,如下所示:

     

     

                    注:如果你的串口助手发不出“\n”换行符,那么你需要最少输入MTU-3个字符,设备才会把收到的全部字符通过蓝牙发出去

     

    通过上面的测试,大家可以发现Nordic SDK已经把蓝牙数据透传服务做好了,大家可以直接拿过来使用,下面将对其工作原理进行阐述,最后在Nordic蓝牙透传例子ble_app_uart上进行二次开发,以增加一些其他有用功能。如果大家觉得Nordic ble_app_uart已经可以满足自己的需求,而且也不想花时间去研究里面的原理,那么章节3/4/5/6/7.1可以略过不看。

    3. BLE client/server(C/S) 架构

            BLE采用了client/server (C/S)架构来进行数据交互,C/S架构是一种非常常见的架构,在我们身边随处可见,比如我们经常用到的浏览器和服务器也是一种C/S架构,这其中浏览器是客户端client,服务器是服务端server,server比如淘宝服务器,提供商品信息,广告,社交等服务,而浏览器,比如微软的IE,就可以用来请求这些服务,并使用server提供的服务。BLE与此类似,一般而言设备提供服务,因此设备是server,手机使用设备提供的服务,因此手机是client。比如蓝牙体温计,它可以提供“体温”数据服务,因此是一个server,而手机则可以请求“体温”数据以显示在手机上,因此手机是一个client。

            服务是以数据为载体的,所以说server提供服务其实就是提供各种有价值的数据。

     

    上图所示的Request和Response其实就是我们经常说的ATT命令(ATT PDU),也就是说Client和Server之间通过ATT PDU进行交互。另外,一个数据“37”,有可能是说体温“37度”,也有可能是说心率“37次”或者湿度“37%”,因此Server需要将数据进行包装和分类,在BLE中,数据是通过characteristic进行包装的,而且多个characteristic组成一个service,service是一个独立的服务单元,或者说service是一个基本的BLE应用。因此我们可以把上图细化为:

     

    如果某个service是一个蓝牙联盟定义的标准服务,也可以称其为profile,比如HID/心率计/体温计/血糖仪等,都是标准蓝牙服务,因此都有相应的profile规格书。

    ??????具体后续解答 

     

     

    4. BLE service, characteristic以及CCCD

    如文章“深入浅出低功耗蓝牙(BLE)协议栈”所讲,BLE协议栈架构如下所示:

     

            如上图所示,用户开发应用程序或者说service的时候,调用的都是GATT API,而GATT又调用了ATT API,前面也讲过,BLE数据最终都是通过ATT PDU来传输的,那么为什么还需要GATT层?直接操作ATT层不也可以达到同样的目的吗?

            前面也提过,Server是通过characteristic来表示数据的,虽然一条数据最有价值的部分是它的值(value),但是仅有value是不够,比如27,到底是表示27°温度还是27%湿度;如果表示的是温度,那么它的单位是摄氏度还是华氏度。同时每个value还有相应的读写属性以及权限属性,因此一个characteristic包含三种条目:characteristic声明,characteristic的值以及characteristic的描述符(可以有多个描述符),如下所示:

     

            由于一个service可以包含多个characteristic,characteristic declaration就是每个characteristic的分界符,解析时一旦遇到characteristic declaration,就可以认为接下来又是一个新的characteristic了,同时characteristic declaration还将包含value的读写属性等。Characteristic value就是数据的值了,这个比较好理解就不再说了。Characteristic descriptor就是数据的额外信息,比如温度的单位是什么,数据是用小数表示还是百分比表示等之类的数据描述信息。CCCD是一种特殊的characteristic descriptor,一般而言,都是client来访问server的characteristic,我们把这种操作称为读或者写。另外,server可以直接把自己的characteristic的值告诉client,我们称其为notify或者indicate,跟read操作相比,只有需要传输数据的时候或者说只有当数据有效时,server才开始notify或者indicate数据到client,因此这种操作方式可以大大节省server的功耗。有时候client不想监听characteristic notify或者indicate过来的数据,那么就可以使用CCCD来关闭characteristic的notify或者indicate功能;如果client又需要监听characteristic的notify或者indicate,那么它可以重新使能CCCD来打开相关操作。总结一下,当characteristic具有notify或者indicate操作功能时,那么必须为其添加相应CCCD,以方便client来使能或者禁止notify或者indicate功能。

            不管是characteristic declaration,characteristic value还是characteristic descriptor,实现的时候,我们都是用attribute来表达的,也就是说,他们每一个都是一个attribute,attribute可以用下图来表示:

     

    • Attribute handle,Attribute句柄,16-bit长度。Client要访问Server的Attribute,都是通过这个句柄来访问的,也就是说ATT PDU一般都包含handle的值。用户在软件代码添加characteristic的时候,系统会自动按顺序地为相关attribute生成句柄。
    • Attribute type,Attribute类型,2字节或者16字节长。在BLE中我们使用UUID来定义数据的类型,UUID是128 bit的,所以我们有足够的UUID来表达万事万物。其中有一个UUID非常特殊,它被蓝牙联盟采用为官方UUID,这个UUID如下所示:

     

     由于这个UUID众所周知,蓝牙联盟将自己定义的attribute或者数据只用16bit UUID来表示,比如0x1234,其实它也是128bit,完整表示为:

     

    Attribute type一般是由service和characteristic规格来定义,站在蓝牙协议栈角度来看,ATT层定义了一个通信的基本框架,数据的基本结构,以及通信的指令,而GATT层就是前文所述的service和characteristic,GATT层用来赋予每个数据一个具体的内涵,让数据变得有结构和意义。换句话说,没有GATT层,低功耗蓝牙也可以通信起来,但会产生兼容性问题以及通信的低效率。

    • Attribute value,就是数据真正的值,0到512字节长。
    • Attribute permissions,Attribute的权限属性,权限属性不会直接在空中包中体现,而是隐含在ATT命令的操作结果中。假设一个attribute read属性设为open(即读操作不需要任何权限),那么client去读这个attribute时server将直接返回attribute的值;如果这个attribute read属性设为authentication(即需要配对才能访问),如果client没有与server配对而直接去访问这个attribute,那么server会返回一个错误码:告诉client你的权限不够,此时client会对server发起配对请求,以满足这个attribute的读属性要求。目前主要有如下四种权限属性:
      • Open,直接可以读或者写
      • No Access,禁止读或者写
      •  Authentication,需要配对才能读或者写,由于配对有多种类型,因此authentication又衍生多种子类型,比如带不带MITM,有没有LESC
      • Authorization,跟open一样,不过server返回attribute的值之前需要应用先授权,也就是说应用可以在回调函数里面去修改读或者写的原始值。
      • Signed,签名后才能读或者写,这个用得比较少。

             大家还记不记得设备与手机nRF connect连接成功后呈现的界面,我这里再贴一下:

     

             可以看到手机呈现的就是上文讲的service和characteristic,nRF Connect为了让整个界面变得更美观,将访问属性,UUID,handle都分列来表示了,以致于很多初学者会把理论和现实二者对应不起来。Nordic之前推出过一款Master Control Panel(MCP),MCP现在已经不推荐使用了,不过MCP有一个好处,它对service和characteristic的组织方式更接近底层实现方式,对大家理解service和characteristic是非常有帮助的。还是这个设备,我用MCP跟它连接并进行服务发现,你会发现它呈现的界面如下所示:

     

    这个图就跟上面讲的理论知识可以一一对应起来了,NUS包含2个characteristic:RX和TX,每一个条目都是一个attribute,NUS服务本身就是一个attribute,而RX characteristic本身又包含2条attribute:一条是declaration attribute,一条是value本身attribute。由于TX支持notify,所以它包含3条attribute,另外一条attribute是CCCD。每个attribute都有一个handle和UUID,handle用来访问该attribute,UUID用来指明该attribute的类型。可以说,server提供数据,而数据是由attribute来表达,所有attribute组成一个attribute table,设备支持的服务不同,attribute table就不同。这里说明一下,当你在Nordic已有例程基础上再去添加新的服务或者删除已有的服务,记得一定要去修改ATTR_TAB_SIZE那个宏,否则协议栈初始化会有问题。

           

    5. 常用ATT命令

            Client和Server之间是通过ATT PDU来通信的,ATT PDU主要包括4类:读,写,notify和indicate。如果一个命令需要response,那么会在相应命令后面加上request;如果一个命令只需要ACK而不需要response,那么它的后面就不会带request。这里要特别强调一点,BLE所有命令都是“必达”的,也就是说每个命令发出去之后,会立马等ACK信息,如果收到了ACK包,发起方认为命令完成;否则发起方会一直重传该命令直到超时导致BLE连接断开。换句话说,只要你的BLE没有断开,那么你之前发送的数据包,不管它是用什么ATT PDU来发送的,它肯定被对方收到了。我估计很多人对此会产生疑问,因为他们经常碰到丢包的情况,其实大家经常碰到的“丢包”,不是空中把包丢了或者包在空中被干扰了,而是大家发送的代码写得有问题,导致你要发送的包没有被安全送达到协议栈射频FIFO中,所以以后大家碰到丢包情况,请先检查你的代码,保证你的数据包正确完整安全地送达到协议栈射频FIFO中,只要数据包放到了协议栈射频FIFO中,蓝牙协议栈就能保证该数据包“必达”对方。既然每个ATT命令都必达对方,那么还需要request做什么?如果一个命令带有request后缀,那么发起方就可以收到命令的response包,这个response包在应用层是有回调事件的,而前述的ACK包在应用层是没有回调事件的。所以采用request/response方式,应用层可以按顺序地发送一些数据包,这个在很多应用场合是非常有用的。相反,如果你对应用层数据包的顺序没有要求,那么就可以不使用request/response形式。另外Request/response有一个副作用:大大降低通信的吞吐率,因为request/response必须在不同的连接间隔中出现,也就是说,你在间隔1中发送了一个request命令,那么response包必须在间隔2或者稍后间隔中回复,而不能在间隔1中回复,这就导致两个连接间隔最多只能发一个数据包,而不带request后缀的ATT命令就没有这个问题,在同一个连接间隔中,你可以同时发多个数据包,这样将大大提高数据的吞吐率。大家可以参考下图来理解request和非request命令的区别:

     

    常用的带request的命令:所有read命令,write request,indication等,而常用的不带request的命令有write command,notification等,完整的ATT命令列表如下所示:

     

    6. 设备端固件代码一览

    现在我们一起来看一下ble_app_uart的源代码,看看它是怎么工作起来的。首先我们来看main函数:

     

    如上所述,ble_stack_init用于初始化配置和使能蓝牙协议栈,其代码如下所示:

    其中,nrf_sdh_enable_request需要选择蓝牙协议栈的低频时钟(由于蓝牙协议栈的高频时钟必须为外部32M晶振,所以高频时钟无需配置;而低频时钟可以选择为内部32K RC或者外部32K晶振,所以低频时钟需要人工配置),因此如下宏需要根据实际情况进行调整:

     

        nrf_clock_lf_cfg_t const clock_lf_cfg =
    
        {
    
            .source       = NRF_SDH_CLOCK_LF_SRC,
    
            .rc_ctiv      = NRF_SDH_CLOCK_LF_RC_CTIV,
    
            .rc_temp_ctiv = NRF_SDH_CLOCK_LF_RC_TEMP_CTIV,
    
            .accuracy     = NRF_SDH_CLOCK_LF_ACCURACY
    
    };

     

    通过sdk_config.h文件可以看到,默认是选择外部32K晶振作为低频时钟的,如果你想选择内部32K RC作为低频时钟,那么需要做如下修改

    按 Ctrl+C 复制代码

     

    按 Ctrl+C 复制代码

    nrf_sdh_ble_default_cfg_set用来配置softdevice协议栈,如下宏是经常需要修改的:

     

    NRF_SDH_BLE_TOTAL_LINK_COUNT  //一共同时可以支持多少个连接
    
    NRF_SDH_BLE_PERIPHERAL_LINK_COUNT  //作为从模式的连接同时能有几个
    
    NRF_SDH_BLE_CENTRAL_LINK_COUNT  //作为主模式的连接同时能有几个
    
    NRF_SDH_BLE_GATT_MAX_MTU_SIZE //MTU size为多大
    
    NRF_SDH_BLE_VS_UUID_COUNT  //用户自定义的base UUID有几个
    
    NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE  //Attribute table总共占多少协议栈RAM空间
    
    NRF_SDH_BLE_SERVICE_CHANGED  //要不要包含service change characteristic

     

    nrf_sdh_ble_enable真正使能BLE功能,它的参数ram_start既是一个输入参数又是一个输出参数,作为输入参数,系统自动会把如下的RAM起始地址传入:

     

    同时nrf_sdh_ble_enable会把当前softdevice配置情况下,它实际需要占用的RAM空间通过ram_start返回,如果这个返回值不等于输入值,那么用户需要把上图的IRAM1起始地址修改成它的返回值。其中NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE这个宏的取值是需要用户不断去试错的,因此每当你添加了或者删除了BLE service,都需要去调整NRF_SDH_BLE_GATTS_ATTR_TAB_SIZE这个宏的值,然后去查看nrf_sdh_ble_enable的返回值,看看这个参数的取值是否合理

    NRF_SDH_BLE_OBSERVER用来为本地文件(此处为main.c)注册一个BLE回调函数(此处为ble_evt_handler),NRF_SDH_BLE_OBSERVER这个宏执行成功后,所有的BLE事件都会被ble_evt_handler捕获。进入ble_evt_handler,你会发现BLE有上百个回调事件,你不需要每个都处理,你只需要处理你关心的事件即可,比如连接成功事件BLE_GAP_EVT_CONNECTED或者连接断开事件BLE_GAP_EVT_DISCONNECTED,如下所示:

    NRF_SDH_BLE_OBSERVER有一个很大的好处:某个模块如果需要捕获BLE事件,那么它自己调用NRF_SDH_BLE_OBSERVER这个宏注册相应回调函数即可,而不再需要在其它文件中去注册这个回调函数,将模块的耦合性降到最低,符合模块化编程思想。

    gap_params_init用来修改广播名字和连接间隔的。gatt_init用来修改底层数据包长度的。advertising_init用来修改广播包内容,广播间隔以及广播超时时间。conn_params_init用来请求更新连接间隔的。

    我们来重点讲一下services_init,services_init用来添加服务和characteristic,前面讲了那么多的概念和理论,现在我们就来看看services_init是如何做到跟理论一致的。services_init通过ble_nus_init添加了一个蓝牙数据透传服务:NUS,那ble_nus_init是怎么将NUS服务添加成功的呢?查看ble_nus_init函数体,你会发现它是分三步来做的:

    1. 添加服务的UUID。如果是蓝牙标准服务,这步可以省略。由于NUS不是蓝牙联盟定义的,所以需要调用sd_ble_uuid_vs_add以增加一个供应商自定义的UUID。
    2. 添加服务本身。直接调用sd_ble_gatts_service_add就可以完成。
    3. 添加服务下面的characteristics。server的characteristic一般都是通过sd_ble_gatts_characteristic_add来添加的。以NUS的RX characteristic为例,可以看到:
    sd_ble_gatts_characteristic_add(p_nus->service_handle,  &char_md,  &attr_char_value, &p_nus->rx_handles);

    其中,p_nus->service_handle表示该characteristic属于那个service,p_nus->rx_handles是输出值,由协议栈返回,以后访问该characteristic都是通过这个句柄来完成,attr_char_value这个是characteristic的value,char_md这个是characteristic的元数据(metadata),前面第4章也讲过,一个数据除了有value这个characteristic之外,它还包含其他attribute,而这些attribute全部都用char_md来表示,比如这个characteristic value能支持的ATT命令类型,CCCD信息,descriptor信息等,这里要特别指出的是,只有当支持notify或者indicate时,才需要提供cccd_md信息,其他ATT命令不需要cccd_md信息,所以RX characteristic的char_md如下所示,它同时支持write和write request两种写命令,由于它不支持notify或者indicate,所以cccd_md为NULL。

     

    attr_char_value是一个attribute,所以它包含attribute metadata,如下:

    attr_char_value具体包含的value信息由以下成员表示:

     

    由于这里把characteristic value放在了协议栈RAM中,所以协议栈会自动为这个value创建一个buffer。如果你想把characteristic value放在用户RAM中,即vloc = BLE_GATTS_VLOC_USER,那么这里你还需要把一个全局数组变量赋给attr_char_value. p_value。

    TX characteristic与之类似,就不再额外解读了。

    这里需要特别提醒大家的是,虽然Nordic API结构体参数设计得很复杂,但是大部分成员变量直接就可以使用它的默认值0,你只需对你感兴趣的成员变量进行赋值即可,所以大家经常看到如下场合,即先用memset将该结构体变量初始化为0,让其所有成员变量都采用默认值,然后再对某些需要修改的成员变量进行二次赋值。大家一定不要忘了将结构体变量清零这一步操作!

     

    ble_nus_init同时注册了nus_data_handler回调函数,当设备收到手机发过来的数据时,就会触发nus_data_handler,用户可以在nus_data_handler中对接收到的数据进行处理,本例程中nus_data_handler直接将ble收到的数据通过uart口转发出去。如果用户需要发送数据给手机,在连接成功和notify使能的情况下,直接调用ble_nus_data_send即可,而ble_nus_data_send又是通过调用协议栈API:sd_ble_gatts_hvx来实现数据发送功能的。那么什么时候需要发送数据给手机?本例程的做法是,当串口有数据过来并满足如下条件时调用ble_nus_data_send:

    if ((data_array[index - 1] == '\n') || (index >= (m_ble_nus_max_data_len)))

    main函数最后将调用API让协议栈跑起来,如果你的设备将来是一个从设备(peripheral),那么请调用ble_advertising_start,ble_advertising_start将开启可连接的广播,从而让你的设备连接成功之后成为从设备。如果你的设备将来是一个主设备(central),那么请调用sd_ble_gap_scan_start,sd_ble_gap_scan_start将开启设备的扫描功能,从而让你的设备连接成功之后变为主设备。

     

    最后我们来看main循环,它只有一个函数: idle_state_handle,idle_state_handle先把需要打印的日志打印完,然后让系统进入idle状态(Nordic SoC spec称其为System ON状态),一旦有协议栈事件或者中断事件发生,系统将唤醒,以处理相关事件回调函数,然后再执行一遍idle_state_handle。注意:idle状态下,蓝牙连接或者广播可以正常进行而不受影响,蓝牙连接或者广播都是周期性的,在一个周期中,蓝牙连接或者广播只持续很短一段时间(这段时间CPU有可能会退出idle状态),其余时间系统都是处于idle状态的,从而大大节省系统功耗。

     

    7. 定制你的BLE数据透传应用程序

    7.1 BLE数据上传吞吐率

    如何快速的把大量数据上传给手机?这是一个很常见的应用场合,现在我们尝试去修改一下Nordic的原生例程,以实现最高的数据吞吐率。下面我们通过几种不同的方法来看看每种方法下它的吞吐率能到多少。

    方法1:(通过宏METHOD1来开关)

    蓝牙spec规定,蓝牙连接间隔最小只能为7.5m,为了达到最高的吞吐率,我们创建一个timer,让其每7ms发一次数据,看一看此时吞吐率能达到多少。7ms中断服务函数代码如下所示:

     

    static void throughput_timer_handler(void * p_context)
    
    {
    
        UNUSED_PARAMETER(p_context);
    
        ret_code_t err_code;
    
        uint16_t length;
    
        m_cnt_7ms++;
    
        length = m_ble_nus_max_data_len;
    
        if (m_conn_handle != BLE_CONN_HANDLE_INVALID)
    
        {
    
            err_code = ble_nus_data_send(&m_nus, m_data_array, &length, m_conn_handle);
    
    //      if ( (err_code != NRF_ERROR_INVALID_STATE) && (err_code != NRF_ERROR_RESOURCES) &&
    
    //          (err_code != NRF_ERROR_NOT_FOUND) )
    
    //      {
    
    //                APP_ERROR_CHECK(err_code);
    
    //      }             
    
            m_len_sent += length;           
    
            m_data_array[0]++;
    
            m_data_array[length-1]++;         
    
         }
    
         NRF_LOG_INFO("time: %d *7ms == bytes send: %d Bytes == avg speed: %d B/s",m_cnt_7ms,m_len_sent,m_len_sent/(m_cnt_7ms*7));   
    
    }

     

     

    这种做法会导致ble_nus_data_send报“NRF_ERROR_RESOURCES”错误,这个错误表示协议栈无资源应付这么快的调用速度。为此我们对ble_nus_data_send返回的错误值一概不进行处理,看看会发生什么?我们发现程序可以正常运行,RTT viewer打印的日志如下所示:

     

    由上图可知,数据上传吞吐率达到了34.8kB/s,其实这个吞吐率是假的,因为中间丢了很多包,但计算吞吐率的时候把丢的包也算进去了。如下图所示,0x6E之后应该为0x6F,但实际发送的数据包编号为0x83,丢包非常严重。

     

       为了防止所谓的“丢包”(前面也提过,这里的丢包不是数据包在空中丢掉了,而是数据包没有安全送到协议栈的buffer中,从而导致丢包),我们加上如下if语句,只有ble_nus_data_send返回正确时,才认为数据包正确发送,然后才能算入到throughput中:

     

      if (err_code == NRF_SUCCESS)
    
      {
    
                 m_len_sent += length;  
    
                 m_data_array[0]++;
    
                 m_data_array[length-1]++;                                                      
    
        }

     

     

    通过查看nRF connect日志,你会发现此时不会发生丢包了,但吞吐率直接降到了1.6kB/s左右。

    方法1+:(通过宏METHOD1_PLUS来开关)

    我们对方法一稍作调整,首先我们持续往发送buffer写数据,直到返回值不是NRF_SUCCESS

     

    do

    {

        err_code = ble_nus_data_send(&m_nus, m_data_array, &length, m_conn_handle);

        if ( (err_code != NRF_ERROR_INVALID_STATE) && (err_code != NRF_ERROR_RESOURCES) &&

          (err_code != NRF_ERROR_NOT_FOUND) )

        {

            APP_ERROR_CHECK(err_code);

        }

        if (err_code == NRF_SUCCESS)

        {

            m_len_sent += length;

            m_data_array[0]++;

            m_data_array[length-1]++;

        }

    } while (err_code == NRF_SUCCESS); 

     

    然后我们把连接间隔设为尽可能小,以期提高吞吐率,如下:

     

    #ifdef CONN_INTERVAL_OPTIMIZE
    
    #define MIN_CONN_INTERVAL               MSEC_TO_UNITS(8, UNIT_1_25_MS)   
    
    #define MAX_CONN_INTERVAL               MSEC_TO_UNITS(12, UNIT_1_25_MS)
    
    #endif

     

     

    这种方法吞吐率能达到10kB/s,但离我们的目标还是很远。

    最后我们把connection event length extension和data length extension都打开(我们将在方法2+中详细阐述这2个有效提高吞吐率的利器),即定义如下宏:

     

    可以看到吞吐率将达到70kB/s,这个吞吐率还是不错的。但仔细查看nRF connect日志,你会发现这种模式下还是有小概率事件会导致“丢包”发生,而且整个发送逻辑也不是很优化,为此我们想到了METHOD2.

    方法2:(通过宏METHOD2来开关)

    ble_nus_data_send每次成功发送数据包,都会产生一个BLE_NUS_EVT_TX_RDY事件,收到这个事件后,再去调用ble_nus_data_send,丢包的情况就不会再发生了,核心代码如下所示:

     

    
     

    if (p_evt->type == BLE_NUS_EVT_TX_RDY)

    {

    #ifdef METHOD2

        err_code = ble_nus_data_send(&m_nus, m_data_array, &length, m_conn_handle);

        if ( (err_code != NRF_ERROR_INVALID_STATE) && (err_code != NRF_ERROR_RESOURCES) &&

          (err_code != NRF_ERROR_NOT_FOUND) )

        {

              APP_ERROR_CHECK(err_code);

        }

        if (err_code == NRF_SUCCESS)

        {

             m_len_sent += length;

             m_data_array[0]++;

             m_data_array[length-1]++;

         }

         NRF_LOG_INFO("time: %d *10ms == bytes send: %d Bytes == avg speed: %d B/s",m_cnt_10ms,m_len_sent,m_len_sent * 100/m_cnt_10ms);

    #endif

     

    大家可以自己去查看一下nRF  Connect的数据log,这种方式是没有丢包的,但是打开RTT viewer,你会发现他的吞吐率低得可怜,只有1kB/s。

     

    方法2+:(通过宏METHOD2_PLUS来开关)

    与方法1+类似,我们在方法2基础上,持续往发送buffer送数据直到返回值不为0,如下:

     

    #ifdef METHOD2_PLUS

    //queue multiple tx array

    do

    {

        err_code = ble_nus_data_send(&m_nus, m_data_array, &length, m_conn_handle);

        if ( (err_code != NRF_ERROR_INVALID_STATE) && (err_code != NRF_ERROR_RESOURCES) &&

         (err_code != NRF_ERROR_NOT_FOUND) )

        {

             APP_ERROR_CHECK(err_code);

        }

        if (err_code == NRF_SUCCESS)

        {

            m_len_sent += length;

            m_data_array[0]++;

            m_data_array[length-1]++;

        }

    } while (err_code == NRF_SUCCESS);

    NRF_LOG_INFO("time: %d *10ms == bytes send: %d Bytes == avg speed: %d B/s",m_cnt_10ms,m_len_sent,m_len_sent * 100/m_cnt_10ms);

    #endif

     

    然后我们增加gap event length extension功能,gap event length跟connection event length两者意思差不多,都是为了实现一个连接间隔可以发或收多个包的目的。为了使能gap event length extension功能,首先将gap event length修改成一个合适的值,以使其尽可能占满整个连接间隔,如下将gap event length修改为30ms

    #define NRF_SDH_BLE_GAP_EVENT_LENGTH 24

     

    然后我们再将连接间隔设为尽可能小,以保证上述connection event可以占据整个连接间隔:

     

    #ifdef CONN_INTERVAL_OPTIMIZE
    
    #define MIN_CONN_INTERVAL               MSEC_TO_UNITS(8, UNIT_1_25_MS)   
    
    #define MAX_CONN_INTERVAL               MSEC_TO_UNITS(12, UNIT_1_25_MS)
    
    #endif

     

     

    同时我们使能connection event extension功能,如下:

     

    #ifdef EVT_LEN_EXT_ON
    
        ble_opt_t  opt;
    
        memset(&opt, 0x00, sizeof(opt));
    
        opt.common_opt.conn_evt_ext.enable = true;
    
        err_code = sd_ble_opt_set(BLE_COMMON_OPT_CONN_EVT_EXT, &opt);
    
        APP_ERROR_CHECK(err_code);
    
    #endif

     

     

    我现在使用的是华为P9手机,它将把MTU设为241,在DLE不开的情况下(此时链路层每个数据包的长度还是只有27个字节!),我们可以看到throughput可以达到10kB以上,如下:

     

     

    然后我们再打开DLE功能,此时链路层每个数据包的长度将变成251字节,如下:

     

    #ifdef DLE_ON
    
            case BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST:
    
            {
    
                NRF_LOG_DEBUG("DLE update request.");
    
                ble_gap_data_length_params_t dle_param;
    
                memset(&dle_param, 0, sizeof(ble_gap_data_length_params_t));   //0 means auto select DLE                                                                    
    
                err_code = sd_ble_gap_data_length_update(p_ble_evt->evt.gap_evt.conn_handle, &dle_param, NULL);
    
                APP_ERROR_CHECK(err_code);
    
            } break;
    
    #endif

     

     

    此时我们可以看到throughput可以达到77kB/s,离蓝牙4.2的理论throughput已经很接近了。这里特别需要指出的是,当DLE使能情况下,connection interval不是越小吞吐率越高,我这里使用的connection interval大概为10ms,如果大家把这个connection interval提高到30ms,有可能吞吐率更高,这里就不再演示了。

     

     

     

     

     

    上述代码工程已经上传到百度云盘中,有需要的同学可以到如下链接下载:

    下载“tutorial_ble_app_uart_SDK15_0_0.rar”,然后解压缩到SDK15.0.0如下目录下:nRF5_SDK_15.0.0_a53641a\examples\ble_peripheral,即可成功编译运行。

     

    7.2使用安卓版nRF connect测试BLE设备的稳定性和可靠性

    先说明一下,以下内容只能通过安卓版nRF Connect来实现,iOS版nRF Connect不支持如下特性。

    手机端宏录制方式

    相信到现在大家对BLE数据上传机理和实践有个大概的了解,那如何测试BLE数据下行性能,即怎么测试数据从手机传到设备的稳定性和可靠性?我们是不是必须开发一款手机app来进行相关测试吗?答案是否定的,感谢Nordic给我们带来了nRF connect,nRF connect支持宏录制,我们可以通过nRF connect来对我们的设备进行压力测试。下面我们来讲讲宏录制是怎么工作的。

    所谓宏录制,就是把你对nRF connect的操作录制下来,然后通过宏播放实现自动化操作。由于nRF connect是一个容器,并支持JavaScript和HTML语法,宏其实就是一个XML脚本,nRF connect定义了自己的一套XML标签操作,遵守这套XML标签操作,就可以对nRF connect进行自动化操作。nRF connect支持的所有XML语法都在手机安装目录\Nordic Semiconductor中的示例中体现,只要示例中出现过的标签就支持,相反示例中没有的标签就不支持。下面具体讲一下宏录制的操作过程。

    当nRF connect连接设备成功后,你会发现右下角有一个红点,那个就是宏录制菜单。

     

     

    点击下面的红点,我们开始宏录制操作

     

     

    然后我们按照普通操作来操作nRF connect,这些操作最终对应的BLE指令会被录制下来,以便后续重复播放。我们先把“1234”发送给设备,如下:

     

    发送完上述指令后,我们加一个300ms的延时,如下:

     

    然后我们点击完成按钮,保存该宏,可以看出这个宏包括两条操作:发送“1234”到设备,然后睡眠300ms。

     

     

    将宏命名为“test”并保存:

     

     

    到此宏已经录制成功了,现在我们开始展示宏的神奇功能。如下,选择循环播放模式,然后点击“开始”按钮开始循环播放该录制宏。

     

     

    大家可以看到,nRF connect先执行“Write 0x31323334 to RX characteristic”,然后睡眠300ms,然后又执行“Write 0x31323334 to RX characteristic”,如此循环往复。打开串口助手,你会发现设备已经收到了手机发过来的一连串“1234”,如下。

     

    我们把刚才的test宏导出为XML,看一看它到底长什么样:

     

    <macro name="test" icon="PLAY">
    
       <assert-service description="Ensure Nordic UART Service" uuid="6e400001-b5a3-f393-e0a9-e50e24dcca9e">
    
          <assert-characteristic description="Ensure RX Characteristic" uuid="6e400002-b5a3-f393-e0a9-e50e24dcca9e">
    
             <property name="WRITE" requirement="MANDATORY"/>
    
          </assert-characteristic>
    
       </assert-service>
    
       <write description="Write 0x31323334 to RX Characteristic" characteristic-uuid="6e400002-b5a3-f393-e0a9-e50e24dcca9e" service-uuid="6e400001-b5a3-f393-e0a9-e50e24dcca9e" value="31323334" type="WRITE_REQUEST"/>
    
       <sleep description="Sleep 300 ms" timeout="300"/>
    
    </macro>

     

     

    大家可以看到,宏就是一些XML标记,大家也可以在此基础上,去修改该XML文件,以实现更复杂的自动化测试,然后通过nRF connect把最新的XML文件装载进来,就可以自动播放了。

    如果你还想了解宏更多的用法信息,请参考:https://github.com/NordicSemiconductor/Android-nRF-Connect/blob/master/documentation/Macros/README.md

     

    电脑端XML方式

    前面的宏录制方式,功能还是比较单一,如果要实现更复杂的自动化测试,可以通过在PC端执行XML脚本方式来实现。通过安卓调试工具ADB,我们可以直接通过PC来操作nRF connect,而nRF connect又能识别XML脚本,这样就可以让nRF connect按照XML脚本意图去执行相关自动化操作。nRF connect支持的所有XML语法都在手机安装目录中(手机内部存储/ Nordic Semiconductor目录)的示例中体现,只要示例中出现过的标签就支持,相反示例中没有的标签就不支持。

    欲了解更多信息请参考:https://github.com/NordicSemiconductor/Android-nRF-Connect/blob/master/documentation/Automated%20tests/README.md

     

    8. 开发手机端app代码

    Nordic提供很多手机端开源app供大家参考,用得最多的就是nRF Toolbox和nRF Blinky(注:nRF connect代码不开源),在nRF Toolbox和nRF Blinky中都有相关的BLE操作库,尤其是nRF Toolbox包含了很多BLE库,比如BLE管理,DFU,数据透传,蓝牙Mesh等等,大家可以参考他们来开发自己的手机端app。

    nRF Toolbox软件界面如下所示:

     

    UART就是前文说到的NUS服务,除了nRF connect,其实大家也可以通过nRF Toolbox UART模块来完成第2章所述的操作。nRF Toolbox另一个用的比较多的功能就是DFU,如果你需要通过手机BLE来实现设备固件的空中升级(OTA),那么可以参考nRF Toolbox DFU模块来编写你的手机端软件。

    展开全文
  • 文章目录Android(安卓)上传文件到阿里云点播,阿里云点播转码一:登录阿里云点播平台配置添加转码模板组1:需要什么参数,可自行填写。然后保存。(如图)2:把获取的模板 ID 保存下载,一会上传文件转码用。二:...

    Android(安卓)上传文件到阿里云点播,阿里云点播转码


    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    一:登录阿里云点播平台配置添加转码模板组

    在这里插入图片描述

    点我跳转:https://vod.console.aliyun.com/?spm=5176.8413026.1397775…2ead11cfTYJHei&aly_as=bwVll4Al#/settings/transcode/list

    在这里插入图片描述
    在这里插入图片描述

    1:需要什么参数,可自行填写。然后保存。(如图)

    在这里插入图片描述

    2:把获取的模板 ID 保存下载,一会上传文件转码用。

    在这里插入图片描述

    二:服务端获取上传地址和凭证及播放地址

    1:添加maven仓库(pom.xml)

    
        <repositories>
          <repository>
            <id>sonatype-nexus-staging</id>
            <name>Sonatype Nexus Staging</name>
            <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
          </repository>
        </repositories>
        
    

    在这里插入图片描述

    2:添加Jar包依赖

    
      <dependency>
        <groupId>com.aliyun</groupId>
        <artifactId>aliyun-java-sdk-core</artifactId>
        <version>4.3.3</version>
      </dependency>
      <dependency>
        <groupId>com.aliyun</groupId>
        <artifactId>aliyun-java-sdk-vod</artifactId>
        <version>2.15.5</version>
      </dependency>
      <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
        <version>2.8.2</version>
      </dependency>
      
    

    在这里插入图片描述

    3:媒体上传(初始化客户端,获取上传地址和凭证)

    
    public class Index {
    	
    	public static void main(String[] args) {
    		try {
    	
    			//初始化
    			DefaultAcsClient msg = initVodClient("<Your AccessKeyId>", "<Your AccessKeySecret>");
    			System.out.println(msg);
    		} catch (ClientException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    	}
    	
    	
    	
    
    	/**
    	 * 
    	 * RequestId:请求ID
    	 * VideoId:视频ID。
    	 * UploadAddress:上传地址。
    	 * UploadAuth:上传凭证。
    	 * 
    	*/
    	public static DefaultAcsClient initVodClient(String accessKeyId, String accessKeySecret) throws ClientException {
    	    String regionId = "cn-shanghai";  // 点播服务接入区域
    	    
    	    //初始化
    	    DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);
    	    DefaultAcsClient client = new DefaultAcsClient(profile);
    	    
    	    //获取上传地址和凭证
    	    CreateUploadVideoResponse response = new CreateUploadVideoResponse ();
    	    try {
    			response = createUploadVideo(client);
    			System.out.print("VideoId = " + response.getVideoId() + "\n");
    	        System.out.print("UploadAddress = " + response.getUploadAddress() + "\n");
    	        System.out.print("UploadAuth = " + response.getUploadAuth() + "\n");
    		} catch (Exception e) {
    			// TODO Auto-generated catch block
    			System.out.print("ErrorMessage = " + e.getLocalizedMessage());
    		}
    	    System.out.print("RequestId = " + response.getRequestId() + "\n");
    	    
    	    return client;
    	}
    	
    	
    	
    	
    	/**
    	 * 获取视频上传地址和凭证
    	 * @param client 发送请求客户端
    	 * @return CreateUploadVideoResponse 获取视频上传地址和凭证响应数据
    	 * @throws Exception
    	*/
    	public static CreateUploadVideoResponse createUploadVideo(DefaultAcsClient client) throws Exception {
    	    CreateUploadVideoRequest request = new CreateUploadVideoRequest();
    	    //视频标题
    	    request.setTitle("this is a sample");
    	    //视频源文件名
    	    request.setFileName("filename2.mp4");
    	    //转码模板组ID(第一部分网站部分获得,转码模板ID)
    	    request.setTemplateGroupId("XXX");
    	    //文件大小
    	    request.setFileSize(null);
    	    //视频描述
    	    request.setDescription("");
    	    //自定义视频封面URL地址
    	    request.setCoverURL("");
    	    //视频分类ID
    	    request.setCateId(null);
    	    //视频标签
    	    request.setTags(null);
    	    //存储地址(无需填写,如需更改,可登陆 点播控制台 > 存储管理 里查看存储地址)
    	    request.setStorageLocation(null);
    	    //应用ID(取值如:app-1000000。使用说明参考文档 多应用。)
    	    request.setAppId(null);
    	    //工作流ID(使用说明参考文档 工作流。)
    	    request.setWorkflowId(null);
    	    
    	    //UserData,用户自定义设置参数,用户需要单独回调URL及数据透传时设置(非必须)
    	    //JSONObject userData = new JSONObject();
    	    //UserData回调部分设置
    	    //JSONObject messageCallback = new JSONObject();
    	    //messageCallback.put("CallbackURL", "http://xxxxx");
    	    //messageCallback.put("CallbackType", "http");
    	    //userData.put("MessageCallback", messageCallback.toJSONString());
    	    //UserData透传数据部分设置
    	    //JSONObject extend = new JSONObject();
    	    //extend.put("MyId", "user-defined-id");
    	    //userData.put("Extend", extend.toJSONString());
    	    //request.setUserData(userData.toJSONString());
    	    return client.getAcsResponse(request);
    	}
    	
    }
    
    
    问题 解决
    accessKeyId 登录阿里云控制台获取accessKeyId(建议创建RAM子账号访问)
    accessKeySecret 登录阿里云控制台获取accessKeySecret(建议创建RAM子账号访问)
    request.setTemplateGroupId(“XXX”); 转码模板组ID(第一部分中创建的模板ID)
    UploadAuth 上传文件时使用(详细见第三部分)
    UploadAddress 上传文件时使用(详细见第三部分)

    4:获取视频播放地址

    
    public class GetPlay {
    	
    	
    	public static void main(String[] args) {
    		
    		try {
    			
    			//初始化
    			DefaultAcsClient msg = initVodClient("LTAI4FnKLBWt16rLcaFsjtJ4","xZSXejzr93PrlB9UXJ04L5cPcvfzlr");
    			System.out.println(msg);
    			
    			GetPlayInfoResponse response = new GetPlayInfoResponse();
    			
    			try {
    		        response = getPlayInfo(msg);
    		        List<GetPlayInfoResponse.PlayInfo> playInfoList = response.getPlayInfoList();
    		        //播放地址
    		        for (GetPlayInfoResponse.PlayInfo playInfo : playInfoList) {
    		            System.out.print("PlayInfo.PlayURL = " + playInfo.getPlayURL() + "\n");
    		        }
    		        //Base信息
    		        System.out.print("VideoBase.Title = " + response.getVideoBase().getTitle() + "\n");
    		    } catch (Exception e) {
    		        System.out.print("ErrorMessage = " + e.getLocalizedMessage());
    		    }
    			
    			System.out.print("RequestId = " + response.getRequestId() + "\n");
    			
    		} catch (ClientException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    		
    	}
    	
    	
    	public static DefaultAcsClient initVodClient(String accessKeyId, String accessKeySecret) throws ClientException {
    	    String regionId = "cn-shanghai";  // 点播服务接入区域
    	    
    	    //初始化
    	    DefaultProfile profile = DefaultProfile.getProfile(regionId, accessKeyId, accessKeySecret);
    	    DefaultAcsClient client = new DefaultAcsClient(profile);
    	    
    	    return client;
    	}
    	
    	
    	/*获取播放地址函数*/
    	public static GetPlayInfoResponse getPlayInfo(DefaultAcsClient client) throws Exception {
    	    GetPlayInfoRequest request = new GetPlayInfoRequest();
    	    
    	    //视频id
    	    request.setVideoId("bc0adcb0e3f2477a8ee65f027cc6141d");
    
    	    return client.getAcsResponse(request);
    	}
    
    }
    
    
    问题 解决
    PlayInfo.PlayURL 返回的视频播放地址(详细见上方代码)

    在这里插入图片描述

    三:客户端从服务端获取上传地址和凭证及上传文件

    1:在Maven项目中加入依赖项

    
    implementation 'com.aliyun.video.android:upload:1.5.5'
    implementation 'de.gerdi-project:GSON:6.0.6'
    
    

    在这里插入图片描述

    2:配置阿里云仓库

    
    maven{ url'http://maven.aliyun.com/nexus/content/groups/public/'}
    maven { url "https://maven.aliyun.com/nexus/content/repositories/releases" }
    
    

    在这里插入图片描述

    3:配置权限

    
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
    
    

    如果配置权限时出现 红色 下划线,请点击这里https://blog.csdn.net/qq_41974199/article/details/103622925

    4:上传媒体文件

    
    public class MainActivity extends AppCompatActivity {
    
    	//在第二部分服务端获取相关信息中获得
        String uploadAuth= “XXX”;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            if (Build.VERSION.SDK_INT >= 23) {
                int REQUEST_CODE_CONTACT = 101;
                String[] permissions = {
                        Manifest.permission.WRITE_EXTERNAL_STORAGE
                }; //验证是否许可权限
                 for (String str : permissions) {
                 if (this.checkSelfPermission(str) != PackageManager.PERMISSION_GRANTED) {
                     //申请权限
                     this.requestPermissions(permissions, REQUEST_CODE_CONTACT); return;
                 }
                 }
            }
    
    
    
            //初始化
            final VODUploadClient uploader=new VODUploadClientImpl(this.getApplicationContext());
            //初始化
    //        final VODUploadClient uploader=new VODUploadClientImpl(this);
    //        System.out.println(uploader);
            Toast.makeText(this, "开始上传", Toast.LENGTH_SHORT).show();
    
    
    
    
            // setup callback
            VODUploadCallback callback = new VODUploadCallback() {
    
                public void onUploadSucceed(UploadFileInfo info) {
                    System.out.println("onsucceed ------------------" + info.getFilePath());
                    Toast.makeText(MainActivity.this,"上传完成",Toast.LENGTH_SHORT).show();
                }
                public void onUploadFailed(UploadFileInfo info, String code, String message) {
                    System.out.println("onfailed ------------------ " + info.getFilePath() + " " + code + " " + message);
                    Toast.makeText(MainActivity.this,"上传失败",Toast.LENGTH_SHORT).show();
                }
    
                //上传进度
                public void onUploadProgress(UploadFileInfo info, long uploadedSize, long totalSize) {
                    System.out.println("onProgress ------------------ " + info.getFilePath() + " " + uploadedSize + " " + totalSize);
                }
    
                //凭证5分钟过期,过期后需要重新获取
                public void onUploadTokenExpired() {
                    System.out.println("onExpired ------------- ");
                    // 重新刷新上传凭证:RefreshUploadVideo
    //                String uploadAuth = aa;
    //                uploader.resumeWithAuth(uploadAuth);
                }
    
                //
                public void onUploadRetry(String code, String message) {
                    System.out.println("onUploadRetry ------------- ");
                    Toast.makeText(MainActivity.this,"上传开始重试",Toast.LENGTH_SHORT).show();
                }
    
                public void onUploadRetryResume() {
                    System.out.println("onUploadRetryResume ------------- ");
                    Toast.makeText(MainActivity.this,"上传结束重试,继续上传回调",Toast.LENGTH_SHORT).show();
                }
    
                /**
                 * 上传地址和凭证方式上传需要调用setUploadAuthAndAddress:uploadAuth:uploadAddress:方法设置上传地址和凭证
                 *  fileInfo 上传文件信息
                 * */
                public void onUploadStarted(UploadFileInfo uploadFileInfo) {
                    System.out.println("onUploadStarted ------------- ");
                    uploader.setUploadAuthAndAddress(uploadFileInfo, uploadAuth, uploadAddress);
                    Toast.makeText(MainActivity.this,"开始上传回调",Toast.LENGTH_SHORT).show();
                }
    
            };
    
    
    
            //上传初始化
            uploader.init(callback);
    
            File file = new File(getExternalFilesDir(null), "china.mp4");
            System.out.println(file+"++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++");
    
    		//本地文件地址
            String filePath = "/storage/emulated/0/download/china.mp4";
            VodInfo vodInfo = new VodInfo();
            vodInfo.setTitle("标题");
            vodInfo.setDesc("描述");
            vodInfo.setCateId (19);
            vodInfo.setIsProcess(true);
    
            uploader.addFile(filePath,vodInfo);
            uploader.start();
    
        }
    
    
    
    }
    
    
    问题 解决
    uploadAuth 在第二部分服务端获得,详细见第二部分
    uploadAddress 在第二部分服务端获得,详细见第二部分

    在这里插入图片描述
    在这里插入图片描述

    5:上传完成(转码以及改变清晰度)

    在这里插入图片描述

    暂时更新这么多
    复制代码可直接运行
    有问题可在下方留言

    展开全文
  • 使用Netty封装的 部标JTT808,JTT1078,渝标协议 数据上传Android端示例 项目地址:GitHub地址 已实现的功能 JTT808 注册 鉴权 心跳 位置信息汇报 设置终端参数 JTT1078 历史音视频查询、回放与下载指令 实时...
  • Android 录音数据传输

    2017-06-20 11:03:36
    MediaPlayer那边就不看了,从AudioTrack开始研究。 1、AudioTrack::write函数 调用函数obtainBuffer获取到一块buffer,然后把传入的数据copy到获取的buffer中。...该函数的主要功能就是对传入的audioBuffer进行...
  • 最新自己和朋友在做一个APP的项目,这里做个整理,一是当做记录,二是希望对大家有帮助。...废话少说,下面言归正传。 此app主要实现用智能手机对控制器(比如车载,电梯等...智能手机软件通过WIFI透传模块与控制器进行通
  • 前言:关于本篇博客主要会讲解在Android端使用HttpUrlConnection模仿Web浏览器采用表单的形式提交参数和一个或多个文件给服务器,如果本身对这方面就特别熟悉和http协议这些很了解了,那么就不需要再看了,当然如果...
  • Android实现监测网络变化以及无网络listview填充 简易新闻(十三 下) 关于 关于
  • 上一篇中我们主要介绍了如何实现数据库储存下载信息,如果你还没阅读过,建议先阅读上一篇Android多文件断点续(二)——实现数据库储存下载信息。数据库我们已经准备好,现在就可以开始来实现DownloadService进行...
  • 应公司开发要求,需要实现第一次打开应用,对某一界面的一个或多个按钮或空间进行指示说明得功能。 通过对网上代码的改写,实现了该功能。 但由于不想依赖包套多的缘故,公司要求使用JAR包的方式,于是通过研究,将...
  • SDK接入(2)之Android Google Play内支付(in-app Billing)接入 继上篇SDK接入(1)之Android Facebook SDK接入整理完Facebook接入流程之后,再来整理下Google Play in-app Billing支付的接入流程。众所周知,Google ...
  • UI作为用户体验的核心之一,始终是Android每次升级中的重点。从Androd 3.0(Honeycomb)开始,Android开始支持hwui(UI硬件加速)。到Android 4.0(ICS)时,硬件加速被默认开启。同时ICS还引入了DisplayList的概念...
  • Mainictivityimport android.content.Context; import android.content.Intent; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.drawable.ColorDrawable; import androi
  • 仿照微信,朋友圈分享图片功能 。可以进行图片的多张选择,拍照添加图片,以及进行图片的预览,预览时可以进行缩放,并且可以删除选中状态的图片 。很不错的源码,大家有需要可以下载看看 。 ...
  • 这时候就要使用消息,消息是通过透传的方式传递内容,上代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 3
  • 原文地址:https://github.com/DmrfCoder/interview/blob/master/Android/Android常见面试题.md 文章目录什么是ANR,如何避免主线程中的Looper.loop()一直无限循环为什么不会造成ANR?ListView原理与优化...
  • Android面试题目及其答案   1、Android dvm的进程和Linux的进程, 应用程序的进程是否为同一个概念    DVM指dalivk的虚拟机。每一个Android应用程序都在它自己的进程中运行,都拥有一个独立的Dalvik虚拟机实例...
  • 定制Android模拟器skin

    2013-12-09 13:37:05
    Android模拟器提供了6个标准的skin供我们使用,分别对应HVGA(横屏/竖屏)、QVGA(横屏/竖屏)、WQVGA以及WVGA,我们只要在启动模拟器时使用 -skin 参数就可以载入相应的皮肤。这不仅让我们可以随意的修改皮肤的...
1 2 3 4 5 ... 20
收藏数 2,006
精华内容 802