BX2400 OTA on Android
第一阶段
连接设备。
读取设备服务列表。
发现服务(gatt.discoverServices())。
发现相关特征值(ctrlChar ,dataChar)。
写入ctrlChar request描述符。
设备响应ctrlChar response描述符。
手机发起OTA请求(wirite characteristic)。
设备响应OTA请求(到此OTA 开始进入准备升级状态)。
以上是OTA准备状态。
第一阶段流程图如下所示
第二阶段流程图如下所示
OTA 升级的三种情况流程图
OTA opcode 说明
ctrlChar>控制特征值
opcode:00 ->BXOTA_CTRL_PKT_START_REQ (开始请求升级)
opcode:01
opcode:02 -> BXOTA_CTRL_PKT_NEW_BLOCK_CMD (image transport)
opcode: 03 ->BXOTA_CTRL_PKT_IMAGE_TRANSFER_FINISH_CMD (app 已经将image 全部写入成功)
opcode:04 -> BXOTA_CTRL_PKT_SIGN_DATA_SEND(写入签名文件字节流,一共64bytes,一次写入16字节,一共写入四次 )
opcode:05
CRC32 ftp://bluexsh.22ip.net/misc/OTA/crc32/
App 效果图
android 代码执行步骤仅供参考
#.请查看参考代码中 setp1~11 的执行步骤
android OTA 参考代码
package com.lianrui.mesh_ota;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.util.Log;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedList;
import java.util.UUID;
import no.nordicsemi.android.ble.BleManager;
import no.nordicsemi.android.ble.Request;
import no.nordicsemi.android.ble.utils.ParserUtils;
import static com.lianrui.mesh_ota.Crc32.CRC32_INIT_VAL;
public class MeshOTAManager extends BleManager {
public final static String ERROR_CONNECTION_STATE_CHANGE = "Error on connection state change";
public final static String ERROR_DISCOVERY_SERVICE = "Error on discovering services";
public final static String ERROR_AUTH_ERROR_WHILE_BONDED = "Phone has lost bonding information";
public final static String ERROR_READ_CHARACTERISTIC = "Error on reading characteristic";
public final static String ERROR_WRITE_CHARACTERISTIC = "Error on writing characteristic";
public final static String ERROR_READ_DESCRIPTOR = "Error on reading descriptor";
public final static String ERROR_WRITE_DESCRIPTOR = "Error on writing descriptor";
public final static String ERROR_MTU_REQUEST = "Error on mtu request";
public final static String ERROR_CONNECTION_PRIORITY_REQUEST = "Error on connection priority request";
public final static String ERROR_READ_RSSI = "Error on RSSI read";
public final static String ERROR_READ_PHY = "Error on PHY read";
public final static String ERROR_PHY_UPDATE = "Error on PHY update";
public final static String ERROR_RELIABLE_WRITE = "Error on Execute Reliable Write";
/**
* The maximum packet size is 20 bytes.
*/
private static final int MAX_PACKET_SIZE = 20;
public static final int MTU_SIZE_MIN = 23;
private static final int MTU_SIZE_MAX = 40;
/**
* Mesh provisioning data in characteristic UUID
*/
/**
* Mesh OTA service UUID
*/
public final static UUID MESH_OTA_UUID = UUID.fromString("00002600-0000-1000-8000-00805F9B34FB");
private final static UUID MESH_OTA_CHAR_CTRL_UUID = UUID.fromString("00007000-0000-1000-8000-00805F9B34FB");
private final static UUID MESH_OTA_CHAR_DATA_UUID = UUID.fromString("00007001-0000-1000-8000-00805F9B34FB");
public static final UUID CCCD = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
private final String TAG = MeshOTAManager.class.getSimpleName();
private BluetoothGattCharacteristic ctrlChar;
private BluetoothGattCharacteristic dataChar;
private BluetoothGatt mBluetoothGatt;
public final static int BXOTA_CTRL_PKT_START_REQ = 0;
public final static int BXOTA_CTRL_PKT_START_RSP = 1;
public final static int BXOTA_CTRL_PKT_NEW_BLOCK_CMD = 2;
public final static int BXOTA_CTRL_PKT_IMAGE_TRANSFER_FINISH_CMD = 3;
public final static int BXOTA_CTRL_PKT_SIGN_DATA_SEND = 4;
public final static int BXOTA_CTRL_PKT_SIGN_DATA_RSP = 5;
private byte[] OTAData;
private boolean[] currentAck;
private int blockNum;
private int maxSegmentNumInBlock;
private int lastBlockSegmentNum;
private short currentBlock;
private short currentSegment;
private short maxSegmentDataSize = 19;
private final short blockHeaderSize = 1;
private final static int MAX_SIGNATURE_SEGMENT_COUNT = 4 - 1;//index from 0 so max segindex=size-1
public void setNeedCheckSign(boolean mNeedCheckSign) {
this.mNeedCheckSign = mNeedCheckSign;
}
private boolean mNeedCheckSign;
private boolean mNeedCrc32 = false;
private static final int MAX_SIGN_SEG_SIZE = 16;
public void setNeedCrc32(boolean mNeedCrc32) {
this.mNeedCrc32 = mNeedCrc32;
}
public void setSignData(byte[] signData) {
this.signData = signData;
}
private byte[] signData;
public MeshOTAManager(Context context, byte[] otaData) {
super(context);
this.OTAData = otaData;
}
BleManagerGattCallback bleManagerGattCallback = new BleManagerGattCallback() {
@Override
protected Deque initGatt(@NonNull BluetoothGatt gatt) {
isOtaAReady = false;
final LinkedList requests = new LinkedList<>();
mBluetoothGatt = gatt;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mBluetoothGatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
}
ctrlChar.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
mBluetoothGatt.setCharacteristicNotification(ctrlChar, true);
dataChar.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
mBluetoothGatt.setCharacteristicNotification(dataChar, true);
BluetoothGattDescriptor ctrlDesc = ctrlChar.getDescriptor(CCCD);
ctrlDesc.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
mBluetoothGatt.writeDescriptor(ctrlDesc);
return null;
}
@Override
protected boolean isRequiredServiceSupported(@NonNull BluetoothGatt gatt) {
for (BluetoothGattService service : gatt.getServices()) {
if (service.getUuid().toString().equals(MESH_OTA_UUID.toString())) {
log(Log.DEBUG, "Service:" + service.getUuid().toString());
for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
log(Log.DEBUG, "characteristic:" + characteristic.getUuid().toString());
}
} else {
}
}
boolean writeRequest;
BluetoothGattService meshService = gatt.getService(MESH_OTA_UUID);
if (meshService != null) {
log(Log.DEBUG, "found OTA services ");
ctrlChar = meshService.getCharacteristic(MESH_OTA_CHAR_CTRL_UUID);
dataChar = meshService.getCharacteristic(MESH_OTA_CHAR_DATA_UUID);
writeRequest = false;
if (dataChar != null) {
final int rxProperties = dataChar.getProperties();
writeRequest = (rxProperties & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) > 0;
}
return (ctrlChar != null && dataChar != null && writeRequest);
}
log(Log.DEBUG, "OTA service not support");
return false;
}
@Override
protected void onDeviceDisconnected() {
}
@Override
protected void onCharacteristicIndicated(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicIndicated(gatt, characteristic);
log(Log.DEBUG, "onCharacteristicIndicated: " + characteristic.getUuid().toString());
if (characteristic.getUuid().compareTo(MESH_OTA_CHAR_CTRL_UUID) == 0) {
ctrlPktIndicationRX(characteristic.getValue());
}
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
if (status == 0) {
super.onCharacteristicWrite(gatt, characteristic, status);
} else {
if (!isOtaAReady) {
startOtaRequest();
}
}
}
@Override
protected void onCharacteristicWrite(@NonNull BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
super.onCharacteristicWrite(gatt, characteristic);
if (characteristic.getUuid().compareTo(MESH_OTA_CHAR_CTRL_UUID) == 0) {
byte[] txBytes = characteristic.getValue();
ctrlPktSent(txBytes);
} else if (characteristic.getUuid().compareTo(MESH_OTA_CHAR_DATA_UUID) == 0) {
// step 8 每调用一次 segmentTX 就会回调这个函数
OTATransferContinue(false);
}
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
// super.onDescriptorWrite(gatt, descriptor, status);
log(Log.INFO, "onDescriptorWrite:" + descriptor.getUuid().toString());
if (descriptor.getUuid().compareTo(CCCD) == 0) {
startOtaRequest();
}
}
@Override
protected void onCharacteristicRead(@NonNull BluetoothGatt gatt, @NonNull BluetoothGattCharacteristic characteristic) {
super.onCharacteristicRead(gatt, characteristic);
final byte[] data = characteristic.getValue();
log(Log.DEBUG, "dataReceived: " + ParserUtils.parse(data));
if (characteristic.getUuid().compareTo(MESH_OTA_CHAR_DATA_UUID) == 0) {
onAckRead(characteristic.getValue());
}
}
@Override
public void onCharacteristicNotified(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
super.onCharacteristicNotified(gatt, characteristic);
}
@Override
protected void onMtuChanged(@NonNull int mtu) {
super.onMtuChanged(mtu);
maxSegmentDataSize = (short) (mtu - 3 - blockHeaderSize);
log(Log.DEBUG, "onMtuChanged: " + maxSegmentDataSize);
}
};
@Override
public void log(int priority, @NonNull String message) {
super.log(priority, message);
if (priority==Log.DEBUG)
mCallbacks.print(message);
Log.d(TAG, message);
}
@NonNull
@Override
protected BleManagerGattCallback getGattCallback() {
return bleManagerGattCallback;
}
private void segmentTX() {
//step 7 传输image
int length = getSegmentLength(currentBlock, currentSegment);
byte[] data = new byte[blockHeaderSize + length];
data[0] = (byte) currentSegment;
data[1] = (byte) (currentSegment >> 8);
System.arraycopy(OTAData, (currentBlock * maxSegmentNumInBlock + currentSegment) * maxSegmentDataSize
, data, blockHeaderSize, length);
dataChar.setValue(data);
writeCharacteristic(dataChar, data);
}
private int getSegmentLength(int blockID, int segmentID) {
if (blockID == blockNum - 1 && segmentID == lastBlockSegmentNum - 1) {
return OTAData.length - maxSegmentDataSize *
((blockNum - 1) * maxSegmentNumInBlock + (lastBlockSegmentNum - 1));
} else {
return maxSegmentDataSize;
}
}
public void readAck() {
mBluetoothGatt.readCharacteristic(dataChar);
}
public void writeDescriptor() {
}
void OTATransferContinue(boolean newBlock) {
int segmentNum = getSegmentNumOfCurrentBlock();
if (newBlock) {
// 当开启新的block 会执行这里
currentSegment = 0;
log(Log.DEBUG, "newBlock start..");
} else {
// 写入一次就++
++currentSegment;
}
while (currentSegment < segmentNum && currentAck[currentSegment]) {
++currentSegment;
}
//step 9 判断当前block 是否写入完毕
if (currentSegment == segmentNum) {
log(Log.DEBUG, "Seg 1 ~ seg " + segmentNum + " write complete then read ack");
//step 10 手机请求读取当前block 写入状态
readAck();
} else {
segmentTX();
}
}
private void ctrlPktTX(int type, byte[] param) {
byte[] ctrl;
if (param != null) {
ctrl = new byte[param.length + 1];
System.arraycopy(param, 0, ctrl, 1, param.length);
} else {
ctrl = new byte[1];
}
ctrl[0] = (byte) type;
//ctrlChar.setValue(ctrl);
if (type == BXOTA_CTRL_PKT_START_REQ) {
log(Log.DEBUG, "send start reuest:" + Arrays.toString(ctrl));
} else if (type == BXOTA_CTRL_PKT_NEW_BLOCK_CMD) {
log(Log.DEBUG, "send new block cmd:" + Arrays.toString(ctrl));
} else if (type == BXOTA_CTRL_PKT_IMAGE_TRANSFER_FINISH_CMD) {
log(Log.DEBUG, "send transfer comolete cmd:" + Arrays.toString(ctrl));
} else {
}
writeCharacteristic(ctrlChar, ctrl);
}
private void newBlockCmd() {
//step 5 传输前先用ctrlChar(控制特征值)发送新的block 开始cmd
mCallbacks.print("currentBlock:" + currentBlock);
byte[] blockIDArray = new byte[]{(byte) currentBlock, (byte) (currentBlock >> 8)};
ctrlPktTX(BXOTA_CTRL_PKT_NEW_BLOCK_CMD, blockIDArray);
}
private void imageTXFinishCmd() {
ctrlPktTX(BXOTA_CTRL_PKT_IMAGE_TRANSFER_FINISH_CMD, null);
}
private boolean isOtaAReady;
void ctrlPktSent(byte[] txData) {
switch (txData[0]) {
case BXOTA_CTRL_PKT_START_REQ:
isOtaAReady = true;
log(Log.DEBUG, "ota ready: ");
break;
case BXOTA_CTRL_PKT_NEW_BLOCK_CMD:
Arrays.fill(currentAck, false);
//step 6
OTATransferContinue(true);
break;
case BXOTA_CTRL_PKT_IMAGE_TRANSFER_FINISH_CMD:
log(Log.DEBUG, "ota complete....: ");
break;
case BXOTA_CTRL_PKT_SIGN_DATA_SEND:
mCallbacks.print(String.format("sign data:%d of 4 send complete", +txData[1]));
//step 3-2 // 这里会连续回调4次 也就是写入四次
if (txData[1] < MAX_SIGNATURE_SEGMENT_COUNT) {
transSignDataCmd(txData[1] + 1);
} else {
startOTATransfer();
}
break;
default:
break;
}
}
void transSignDataCmd(int index) {
int type = BXOTA_CTRL_PKT_SIGN_DATA_SEND;
int nextIndex = index;
byte[] data = new byte[17];
data[0] = (byte) index;
System.arraycopy(signData, nextIndex * MAX_SIGN_SEG_SIZE, data, 1, MAX_SIGN_SEG_SIZE);
log(Log.DEBUG, ParserUtils.parse(data));
ctrlPktTX(type, data);
}
private void startOtaRequest() {
//step 1
mCallbacks.onOTARequestStart();
int lenth = mNeedCrc32 ? 10 : 2;
ByteBuffer buffer = ByteBuffer.allocate(lenth).order(ByteOrder.LITTLE_ENDIAN);
byte[] maxBlockDataSizeArray = new byte[]{(byte) maxSegmentDataSize, (byte) (maxSegmentDataSize >> 8)};
buffer.put(maxBlockDataSizeArray);
if (mNeedCrc32) {
buffer.putInt(crc32());
buffer.putInt(OTAData.length);
}
log(Log.DEBUG, "startOtaRequest: " + maxSegmentDataSize);
ctrlPktTX(BXOTA_CTRL_PKT_START_REQ, buffer.array());
}
private int crc32() {
Crc32 crc32 = new Crc32();
long crc = crc32.crc32_calc(CRC32_INIT_VAL, OTAData, OTAData.length);
return (int) crc;
}
void ctrlPktIndicationRX(byte[] rxData) {
int status = rxData[1];
switch (rxData[0]) {
case BXOTA_CTRL_PKT_START_RSP:
//step 2
log(Log.DEBUG, "received OTA start Resp");
maxSegmentNumInBlock = rxData[2] * 8;
log(Log.DEBUG, "total segments size(): " + maxSegmentNumInBlock);
currentAck = new boolean[maxSegmentNumInBlock];
if (status == 0) {
//step 3 判断是否需要验证Signature data
if (mNeedCheckSign) {
//step 3-1 传输signature.bin 中的0~16 bytes
transSignDataCmd(0);
} else {
startOTATransfer();
}
}
break;
case BXOTA_CTRL_PKT_SIGN_DATA_RSP:
String mOtaStatus = status == 1 ? "OTA Status:" + "success" : "failed";
log(Log.DEBUG, mOtaStatus);
break;
default:
break;
}
}
private void startOTATransfer() {
//step 4 开始传输image
mCallbacks.onOTAStart();
new Thread(new Runnable() {
@Override
public void run() {
blockNum = (int) Math.ceil((double) OTAData.length / (maxSegmentDataSize * maxSegmentNumInBlock));
int lastBlockDataLength = OTAData.length % (maxSegmentDataSize * maxSegmentNumInBlock);
lastBlockSegmentNum = (int) Math.ceil((double) lastBlockDataLength / maxSegmentDataSize);
currentBlock = 0;
newBlockCmd();
}
}).start();
}
private int getSegmentNumOfCurrentBlock() {
if (currentBlock == blockNum - 1) {
return lastBlockSegmentNum;
} else {
return maxSegmentNumInBlock;
}
}
void onAckRead(byte[] rxBytes) {
// step 11 接收到设备返回的block 写入状态
boolean allAcked = true;
int segmentNum = getSegmentNumOfCurrentBlock();
for (int i = 0; i < segmentNum; ++i) {
//check data send
if ((rxBytes[i / 8] & (1 << i % 8)) != 0) {
currentAck[i] = true;
} else {
// transfor filed
allAcked = false;
currentAck[i] = false;
}
}
if (allAcked) {
float progress = (float) (currentBlock + 1) / blockNum;
mCallbacks.onProgress(progress);
//判断所有的block 是否全部写入完毕
if (++currentBlock == blockNum) {
log(Log.DEBUG, "OTA Complete");
//发送结束命令
imageTXFinishCmd();
} else {
log(Log.DEBUG, " next block:" + currentBlock);
// 下一个block 传输
newBlockCmd();
}
} else {
//tranfor failed >>continue transfor
//走到这里说明传输过程丢失了一部分数据,那么将当前block 重新传输一次
OTATransferContinue(true);
}
}
}