OTA 确件升级的方案有好几种, 常用的方案是通过云服务器推送 OTA 升级包到设备上更新, 或通过服务提供商的 OTA 平台推送OTA 升级包到设备. 如果只是上面二种, APP 上只需要取得升级通知后, 发送更新的通知给设备和云服务器就好了, 很简单.
但是这个设备并不能联网, 比如手环之类的, 要通过 APP 蓝牙取得数据, 那么 OTA 升级方案, 就只能从 APP 上推送 OTA 数据包到设备更新了.
步骤
- APP 与设备通信(蓝牙或 WiFi)
- APP 发送 OTA 升级包信息(包大小、文件名、校验码等)到设备, 并通知设备做好升级准备, 接收数据的回调
- APP 发送 OTA 升级包(分包)到设备
- APP 发送完成后, 等待设备升级完成后通知回调
以下代码中, OTA 使用的通信协议是 Socket, 并且分包用到 Steam 处理
固件升级包上传, 固件包使用 Socket 上传到设备上, 因为 Socket 传送的限制, 不可能一次性发送过去。因此要分包上传。
上传到设备
upFile OTA 文件
upload({@required File upFile}) async {
// 文件名 upgrade_Hi3566V100-NONESCREEN-OV2775-OV2775_1.0.0.1.20201126.appsw
upFileName = upFile.path
.split('/')
.last
.substring(0, (upFile.path.split('/').last.length - '.appsw'.length));
// 包字节数
int pktlen = await upFile.length();
// model 固件名称和型号 格式: Hi3566V100-NONESCREEN-OV2775-OV2775
String model = upFileName.split('_')[1];
// 版本号
String softVersion = upFileName.split('_').last;
print('pktlen: $pktlen model: $model softVersion: $softVersion');
// 通知设备升级 -> 发送固件到设备
OtaInfoModel otaInfoModel = await OTAService.checkupgradepktinfo(
model: model, pktlen: pktlen.toString(), softversion: softVersion);
if (otaInfoModel == null) {
Fluttertoast.showToast(msg: '设备无响应');
return;
}
// 设备定义的最大传送长度
unitSize = int.parse(otaInfoModel.unitsize);
// 已上传数量
uploadedLength = int.parse(otaInfoModel.offset);
this.upFile = upFile;
// 1. 计算固件文件长度
upFileLength = await upFile.length();
print('upFileLength: $upFileLength');
try {
// 2. 计算发送的总分包数量
if (upFileLength % unitSize <= 0) {
chuncks = upFileLength ~/ unitSize;
} else {
chuncks = upFileLength ~/ unitSize + 1;
}
print('总共分块数: $chuncks');
// 记录当前发送的分包位置
int chunck = 0;
// 3. 分块上传
chunck = await getChunck(chunck);
print('chunck: $chunck');
if (chunck >= chuncks) {
DeviceService.client(isRegister: false).then((value) {
if (value == true) {
Fluttertoast.showToast(msg: '固件上传成功,设备更新中...');
}
});
} else {
Fluttertoast.showToast(msg: '固件上传失败');
print('固件上传失败');
}
} catch (e) {
Fluttertoast.showToast(msg: '上传错误: ${e.toString()}');
print('上传错误: ${e.toString()}');
}
}
获取已经上传数块 块数,
chunck 当前块
Future<int> getChunck(int chunck) async {
// 分包文件
Uint8List block;
// 换算出第几块的 byte[]位置
// 循环分割 upFile, 先分 chunck 等份, 然后在位移读取得到 block
while (uploadedLength < upFileLength) {
// 大文件位移分割读取
block = await getBlock();
// Socket 上传到设备
await uploadFirmware(block);
uploadedLength += block.length;
chunck++;
print('当前进度: ${((uploadedLength / upFileLength) * 100).toInt()}%');
}
return chunck;
}
大文件分割读取
OTA 文件在这里分包, 这里使用一个 RandomAccessFile 类去对文件操作
Future<Uint8List> getBlock() async {
// 定义一个二进制数据(blockLength)
Uint8List block = Uint8List(unitSize);
// 随机访问类, 跳到文件任意位置读写数据
RandomAccessFile accessFile;
try {
// 访问模式
accessFile = await upFile.open(mode: FileMode.read);
// 将文件记录指针定位到 已经上传的长度 位置
accessFile.setPositionSync(uploadedLength);
// 需要要读取的长度
int readLength = await accessFile.readInto(block);
if (readLength == -1) {
// 没有读取到文件
return null;
} else if (readLength == unitSize) {
// 刚好读取大小 readLength == blockLength
return block;
} else {
// readLength < blockLength 长度
Uint8List tmpLength = Uint8List(readLength);
// 数组复制 (readLength -> tmpLength)
List.copyRange(tmpLength, 0, block, 0, readLength);
return tmpLength;
}
} catch (e) {
e.printStackTrace();
} finally {
if (accessFile != null) {
try {
accessFile.close();
} catch (e) {
e.printStackTrace();
}
}
}
return null;
}
Socket 上传升级包到设备端
这里是重点, Socket 的包头与结束字符, 是写设备那边对接好的定义. 每次发送的内容写入 ByteDataWriter buffer 里面, 然后转为 Stream 内容发送到设备. 另外, 为了防止写入速度过快, 让设备来不及处理发送过去的 buffer, 可以停留1秒在继续, sleep(Duration(seconds: 1)), 当然, 每个设备的性能不一样. 可能时间也不一样. 这个只能手动慢慢调试.
uploadFirmware(Uint8List block) async {
String filename = upFile.path.split('/').last;
// 定义 socket 传输与设备的对接协议
// 计算请求头长度 stringBufferLength
StringBuffer stringBufferLength = StringBuffer();
stringBufferLength.write("--------------------------0f063f014f74bfea\r\n");
stringBufferLength.write(
"Content-Disposition: form-data; name="filename"; filename="$filename"\r\n");
stringBufferLength.write("Content-Type: application/octet-stream\r\n\r\n");
stringBufferLength
.write("\r\n--------------------------0f063f014f74bfea--\r\n");
print("uploadFirmwares stringBuffer: >>> ${stringBufferLength.length}");
// 封装 headBuffer
StringBuffer headBuffer = StringBuffer();
headBuffer.write("POST /sd HTTP/1.1\r\n");
headBuffer.write("Host: ${Api.ip}\r\n");
headBuffer.write("User-Agent: curl/7.50.3\r\n");
headBuffer.write("Accept: */*\r\n");
headBuffer.write(
"Content-Length: ${block.length + stringBufferLength.length}\r\n");
headBuffer.write("Expect: 100-continue\r\n");
headBuffer.write(
"Content-Type: multipart/form-data; boundary=------------------------0f063f014f74bfea\r\n\r\n");
// 封装 contentBuffer
StringBuffer contentBuffer = StringBuffer();
contentBuffer.write("--------------------------0f063f014f74bfea\r\n");
contentBuffer.write(
"Content-Disposition: form-data; name="filename"; filename="$filename"\r\n");
contentBuffer.write("Content-Type: application/octet-stream\r\n\r\n");
// 封装 endBuffer
/// socket 发送完结标记
StringBuffer endBuffer = StringBuffer();
endBuffer.write("\r\n--------------------------0f063f014f74bfea--\r\n");
Socket mSocket;
Stream<List<int>> stream;
// 定义写入缓存
ByteDataWriter buffer;
// socket 建立连接
try {
mSocket = await Socket.connect(Api.ip, 80, timeout: Duration(seconds: 5));
stream = mSocket.asBroadcastStream();
// 每一次 socket 开启都 按顺序发送内容 headBuffer contentBuffer block end, 直到分包发送完全。
// headBuffer
mSocket.write(headBuffer.toString().characters);
// print(headBuffer.toString().characters);
// contentBuffer
mSocket.write(contentBuffer.toString().characters);
// print(contentBuffer.toString().characters);
if (block.length >= 1024) {
buffer = ByteDataWriter(bufferLength: 1024);
} else {
buffer = ByteDataWriter(bufferLength: block.length);
}
buffer.write(block);
stream = Stream<List<int>>.value(buffer.toBytes());
await mSocket.addStream(stream);
// socket 写入过快, 设备来不及处理, 停1秒在写入
sleep(Duration(seconds: 1));
await mSocket.flush();
mSocket.write(endBuffer.toString().characters);
// print(endBuffer.toString().characters);
await mSocket.close();
stream = null;
} catch (e) {
Fluttertoast.showToast(msg: '连接socket出现异常,e=${e.toString()}');
print("连接socket出现异常,e=${e.toString()}");
}
}
总结
对于 APP 的 OTA 到设备, 多调试, 一步步找出问题并解决问题就是最好的方法.
最后, 关于 Flutter OTA 的文章, 百度一下, 几乎是没有这方面相关的. 所以我就把这个以前写的海思设备的 OTA 发出来吧, 每种设备的情况可能不一样, 但是思路是相同的. 希望对有需要的人有帮助.