CC2640R2: TI BLE OAD(OTA)协议在Android和iOS上的APP流程和代码解读
[复制链接]
CC2640 R2是一款面向 Bluetooth Smart 应用的低功耗无线 MCU。该芯片运行TI的BLE协议栈,并支持OAD(Over the Air Download)空中固件升级功能,此空中固件升级功能就是利用Android或者iOS的产品对应app通过BLE对CC2640R2的产品进行固件升级。同时,TI其实提供了Anroid和iOS的源码对其支持:http://www.ti.com/tool/SENSORTAG ... earch-EN-Everything。这部分的源码是基于TI的SensorTag硬件进行开发的,包含了很多内容,对于客户来说,基本上不适合直接拿去使用,但是其中的OAD部分代码,却是可以通用的。但是对于客户的iOS或者Android app工程师来说,往往对BLE协议不熟,那就更不用说TI的OAD协议了,所以即使提供了源码,客户在这一部分开发起来还是很困难。本文就针对这一点,针对app开发过程中对OAD功能做一个流程和代码解读,用以帮助客户更方便完成此功能开发。 首先,第一步是从上面的链接下载到最新的android和iOS源码,上述链接最终会指引到github的下载地址。 有了源码之后,我们就可以解读了。 从app的角度上来看TI的OAD协议,大致是这样的: App连接上设备之后,就能发现OAD的service和characteristics(服务和特征值): 服务的UUID:0xFFC0,对应的128bit UUID: F000FFC0-0451-4000-B000-000000000000 服务下面有两个主要特征值:(其他特征值可以暂且忽略) OAD Image Identify,UUID:0xFFC1;OAD Image Block,UUID:0xFFC2,对应的128bit UUID: – OAD Image Identify F000FFC1-0451-4000-B000-000000000000,用于交互确认固件版本信息。 – OAD Image Block F000FFC2-0451-4000-B000-000000000000,用于传送新的固件。 固件更新的所有操作都是对上面这两个特征值进行操作。 用TI的SensorTag app可以看到: 用第三方app比如light blue也能看到,安卓手机的情况也是一样。 在TI提供的示例代码中,在Android中,客户唯一要用到的文件其实就是FwUpdateActivity_CC26xx.java,所有的OAD流程基本全部都在这里。另外有一个相关的文件BluetoothLeService.java,这个是TI的SensorTag App封装的BLE相关API接口集合,主要用于一些特征值的操作,比如write,read等,OAD用到的主要就是write,通过write来神奇地实现各种流程。在iOS源码中,流程相关的主要是BLETIOAD2Profile.m(注意,另外一个BLETIOADProfile.m,这是针对旧版的CC254x的,和CC26xx略有不同,这里不做讨论,有兴趣可以自己去看),基本就在这里,另外对应也有一个BLE相关接口集合BLEUtility.m。 第一步 从app来讲,开始OAD的第一步就是先使能上面两个特征值的notification功能。 简单来说就是调用android或者iOS提供的现有API,分分钟完成。(蓝牙协议上来说就是往这两个特征值的CCC句柄上写01:00,我们这里尽量不讨论具体蓝牙协议,从简考虑,有兴趣的人可以自己去研究一下)。 对应到代码里,
Android在FwUpdateActivity_CC26xx.java中,新的固件文件装载到手机内存后:public void onLoad(View v)中调用:
mLeService.setCharacteristicNotification(mCharIdentify, true); 和
mLeService.setCharacteristicNotification(mCharBlock, true);
实际上可以根据客户真实代码在适当位置加上面两个函数就行,这样第一步其实就完成了。 *更多说明: 上面两个函数,其实就是BluetoothLeService.java中的API,最终追踪下去的话是调用Android SDK 的BLE API来使能notification: mBluetoothGatt.setCharacteristicNotification(request.characteristic, request.notifyenable) 其实我们也可以直接调用这个来实现上面的功能。 iOS在BLETIOAD2Profile.m里的-(void) configureProfile 函数里调用: CBUUID *sUUID = [CBUUID UUIDWithString:TI_OAD_SERVICE]; CBUUID *cUUID = [CBUUID UUIDWithString:TI_OAD_IMAGE_NOTIFY]; [BLEUtility setNotificationForCharacteristic:self.d.p sCBUUID:sUUID cCBUUID:cUUID enable:YES]; cUUID = [CBUUID UUIDWithString:TI_OAD_IMAGE_BLOCK_REQUEST]; if (self.notifications)[BLEUtility setNotificationForCharacteristic:self.d.p sCBUUID:sUUID cCBUUID:cUUID enable:YES]; 注意,iOS代码里面OAD Image Identify和OAD Image Block对应的是TI_OAD_IMAGE_NOTIFY和TI_OAD_IMAGE_BLOCK_REQUEST。同样,在iOS里适当位置调用这个函数就行。 *更多说明: 对于iOS TI给出的源码,是BLEUtility.m中封装了Apple的BLE API来使能notification: - (void)setNotifyValue:(BOOL)enabled forCharacteristic:(CBCharacteristic *)characteristic; 一样,我们也可以直接调用这个来实现上面的功能。 空中sniffer抓包看的话,就能看到这两个使能notification的流程: 第二步 从app角度来讲,第二步就是要把新固件的版本信息从手机传送到外设上,让外设进行判断是否要升级。 这一步就要用到OAD Image Identify,这个特征值在整个OAD过程中只会用到一次,就是在这里。 - App这边先从获得到的新固件里把固件的image header (16个字节) 读出来,image header就是固件的二进制文件的开始16个字节,很容易获取到。
体现在代码里, Android:还是在FwUpdateActivity_CC26xx.java中,在打开新固件文件的时候: private boolean loadFile(String filepath, boolean isAsset) { 顺便创建要发送的16个字节image header结构: mFileImgHdr = new ImgHdr(mFileBuffer,readLen); 这个构造函数就会把image header部分给组织好放到16字节的一个buffer中去,用以接下来发送给设备。这个类的定义也在FwUpdateActivity_CC26xx.java中: private class ImgHdr {
……
ImgHdr(byte[] buf, int fileLen) {
this.len = (fileLen / (16 / 4));
this.ver = 0;
this.uid[0] = this.uid[1] = this.uid[2] = this.uid[3] = 'E';
this.addr = 0;
this.imgType = 1; //EFL_OAD_IMG_TYPE_APP
this.crc0 = calcImageCRC((int)0,buf);
crc1 = (short)0xFFFF;
……
}
可以看到构造函数里面,虽然有读取到的实际固件文件的image header作为输入参数,但我们实际的代码里面为了演示,只是写死了一些内容。这里可以根据实际情况修改一下,根据前面提到的16字节header的顺序来就行,比如: private class ImgHdr {
……
ImgHdr(byte[] buf, int fileLen) {
this.len = (fileLen / (16 / 4));
this.ver = buf[5];
this.ver = (this.ver << 8) | buf[4];
this.uid[0] = buf[8];this.uid[1] = buf[9]; this.uid[2] = buf[10]; this.uid[3] = buf[11];
this.addr = buf[13]; this.addr = (this.addr << 8) | buf[12];
this.imgType = buf[14];//EFL_OAD_IMG_TYPE_APP
this.crc0 = calcImageCRC((int)0,buf);
crc1 = (short)0xFFFF;
……
} 这样就基本能拿到实际的真实数据了。 iOS:就很简单了,直接把新固件文件开始的16字节header复制到代码里就可以了,还是在BLETIOAD2Profile.m里,在-(void) uploadImage 函数中: img_hdr_t2 imgHeader; uint32_t pages = ((uint32_t)self.imageFile.length / 4096); //做个CRC校验,放到image header中去,同时把image header的16字节内容补完整。 [self calcImageInfo:0 pages:pages imageHeader:&imgHeader buf:imageFileData]; memcpy(requestData, &imgHeader, 16); 需要注意的是iOS给的源码里面,image header内容的补充是在crc校验函数里面一并完成的:-(void) calcImageInfo里: imageHeader buf:(uint8_t *)buf { imageHeader->len = (pages * FLASH_PAGE_SIZE) / (OAD_BLOCK_SIZE / FLASH_WORD_SIZE); imageHeader->ver = 0; imageHeader->uid[0] = imageHeader->uid[1] = imageHeader->uid[2] = imageHeader->uid[3] = 'E'; imageHeader->addr = (firstpage * FLASH_PAGE_SIZE) / (OAD_BLOCK_SIZE / FLASH_WORD_SIZE); imageHeader->imgType = EFL_OAD_IMG_TYPE_APP; imageHeader->res[0] = 0xff; imageHeader->crc0 = [self calcImageCRC:firstpage imageHeader:imageHeader buf:buf]; imageHeader->crc1 = 0xffff; } 这样iOS这里也得到完整的image header信息了。 - 接下来app将通过OAD Image Identify这个特征值首先把前面得到的新的固件的版本信息发送给设备(CC2640R2),这个版本信息包含在前面得到的image header的16个字节buffer里,把这个发出去给设备就行了。
体现在代码里, Android,还是在FwUpdateActivity_CC26xx.java中,开始流程函数: private void startProgramming() { 获取image identify(其实就是image header): mCharIdentify.setValue(mFileImgHdr.getRequest()); 并通过BLE的write characteristic动作从app发送到设备端: mLeService.writeCharacteristic(mCharIdentify); *更多说明: 上面write characteristic函数其实就是BluetoothLeService.java中的API,最终追踪下去的话是调用Android SDK 的BLE API来进行BLE的write command操作: mBluetoothGatt.writeCharacteristic(request.characteristic); 其实我们也可以直接调用这个来实现上面的功能。 iOS代码中,在BLETIOAD2Profile.m里,还是在-(void) uploadImage 函数里:
CBUUID *cUUID = [CBUUID UUIDWithString:TI_OAD_IMAGE_NOTIFY]; [BLEUtility writeNoResponseCharacteristic:self.d.p sCBUUID:sUUID cCBUUID:cUUID data:[NSData dataWithBytes:requestData length:16]]; 通过 write characteristic来把image header发送到设备端。 *更多说明: 这个的write函数,其实也是在BLEUtility.m里面封装了Apple的BLE标准API: - (void)writeValue:(NSData *)data forCharacteristic:(CBCharacteristic *)characteristic type:(CBCharacteristicWriteType)type; 我们也可以直接调用这个来实现上面的功能。 空中sniffer抓包能看到write command发送image header: - 最后,设备收到image header之后,会进行和自己本身的固件版本号进行比较。如果发现image header中的版本号没有自己的版本号新,那么就直接以notification (这里notification是在前面第一步使能)的形式在OAD Image Identify上回复自己的版本号给手机,表示拒绝此次固件升级,此次升级就此结束。
空中sniffer抓包,其中标黄的部分就是设备回复自身版本号,表示拒绝此次固件升级: 如果发现image header中的版本号确实比自己本身的固件版本号要新,那么就同意这次固件更新,会在OAD Image Block这个特征值的notification (这里notification是在前面第一步使能)上回复0x0000,表示准备接受序列号第0个固件内容包。注意实在OAD Image Block这个特征值上,不是OAD Image Identify这个特征值上,OAD Image Identify这个特征值的使命在前面已经完成,后面不会再使用。 从sniffer看就会看到0x0000从外设发回手机: 总结下来第二步,就是app要在OAD Image Identify特征值上发送新固件的image header到设备端,然后设备端进行判断是否要接收新的固件进行升级,如果要升级,则在OAD Image Block特征值上向手机回复0x0000,不然回复自己目前的固件版本号表示拒绝更新。总结起来就是下面的两个流程图:
第三步 这一步从app来讲就是按照顺序发送固件内容了。 手机在OAD Image Block特征值上收到0x0000之后就代表设备端愿意接收新固件,并且意思是对方准备好接收第0个固件包。每个固件包的内容长度是16个字节,并且需要在头部放上两个字节的序列号,序列号从0x0000开始累加,一直到最后一个固件包。 *注意这个序列号是小端在前的,所以后面是0x0100,0x0200,0x0300,0x0400,…,0xFF00,0x0001,0x0101,0x0201,…这样累加上去。 从空中sniffer上看,很容易就能看到整个流程的交互: 首先是手机发送第0包的固件内容,通过BLE的Write方式在OAD Image Block特征值上发送,固件block内容是16个字节,注意下图标黄的头两个字节,是固件包序列号,第0包是0x0000,所以包的总长是16+2=18个字节。 那么设备收到APP发送过去的第0包固件后,回复请求下一个固件包,通过Notification的方式在OAD Image Block特征值上回复0x0100: APP收到设备发回的0x0100,就知道对方已经等待接收下一包固件,于是就发送下一包0x0100的固件包,以此类推,一直到固件发送结束。 具体到代码里, 在Android中,就是在programBlock()函数里,这里我们只关注核心部分: private void programBlock() {
… … 省略前面逻辑相关代码,工程里面很容易看懂。
// Prepare block 首先自然是包序列号,buffer的头两个字节:mOadBuffer[0] = Conversion.loUint16(mProgInfo.iBlocks);
mOadBuffer[1] = Conversion.hiUint16(mProgInfo.iBlocks);
然后是16个字节的固件block包内容: System.arraycopy(mFileBuffer, mProgInfo.iBytes, mOadBuffer, 2, OAD_BLOCK_SIZE);
// Send block 接着把这18个字节通过write方式在OAD Image Block特征值上发送:mCharBlock.setValue(mOadBuffer);
boolean success = mLeService.writeCharacteristicNonBlock(mCharBlock);
如果发送成功,那么就顺移相关的包序列号还有固件文件中的位移标志:if (success) {
// Update stats
packetsSent++;
mProgInfo.iBlocks++;
mProgInfo.iBytes += OAD_BLOCK_SIZE;
mProgressBar.setProgress((mProgInfo.iBlocks * 100) / mProgInfo.nBlocks); 如果最后一个固件block成功完成,那么就恭喜,OAD顺利成功!
if (mProgInfo.iBlocks == mProgInfo.nBlocks) {
…
b.setTitle("Programming finished");
b.setPositiveButton("OK",null);
AlertDialog d = b.create();
d.show();
mProgramming = false;
mLog.append("Programming finished at block " + (mProgInfo.iBlocks + 1) + "\n");
}
} else {
mProgramming = false;
msg = "GATT writeCharacteristic failed\n";
}
…
… } *更多说明: 上面boolean success = mLeService.writeCharacteristicNonBlock(mCharBlock);这个函数,其实就是BluetoothLeService.java中的API,最终追踪下去的话是调用Android SDK 的BLE API来实现的:mBluetoothGatt.writeCharacteristic(request.characteristic); iOS的代码里,体现在BLETIOAD2Profile.m里的-(void) sendOnePacket 函数,只看核心部分,和Android很像的,因为流程一样: -(void) sendOnePacket { … … //Prepare Block 首先自然是包序列号,buffer的头两个字节: uint8_t requestData[2 + OAD_BLOCK_SIZE]; requestData[0] = LO_UINT16(self.iBlocks); requestData[1] = HI_UINT16(self.iBlocks); 然后是16个字节的固件block包内容: memcpy(&requestData[2] , &imageFileData[self.iBytes], OAD_BLOCK_SIZE); CBUUID *sUUID = [CBUUID UUIDWithString:TI_OAD_SERVICE]; CBUUID *cUUID = [CBUUID UUIDWithString:TI_OAD_IMAGE_BLOCK_REQUEST]; 接着把这18个字节通过write方式在OAD Image Block特征值上发送: [BLEUtility writeNoResponseCharacteristic:self.d.p sCBUUID:sUUID cCBUUID:cUUID data:[NSData dataWithBytes:requestData length:2 + OAD_BLOCK_SIZE]]; 如果发送成功,那么就顺移相关的包序列号还有固件文件中的位移标志: dataWithBytes:requestData length:2 + OAD_BLOCK_SIZE]); self.sndDataCount ++; self.iBlocks++; self.iBytes += OAD_BLOCK_SIZE; self.sentPackets++; 如果最后一个固件block成功完成,那么就恭喜,OAD顺利成功! if(self.iBlocks == self.nBlocks) { self.inProgramming = NO; [self.oadDelegate didFinishUploading]; return; } 流程和Android完全一样,我连注释都直接复制过来了J。
这样就是完整的OAD流程了。总结就是分三步走:使能OAD Image Identify和OAD Image Block 的notification,在OAD Image Identify上发送新固件版本(image header)进行确认,最后在OAD Image Block上按顺序把固件发送完,结束。
|