Flutter 物联网硬件 OTA 升级

1,228 阅读3分钟

OTA 确件升级的方案有好几种, 常用的方案是通过云服务器推送 OTA 升级包到设备上更新, 或通过服务提供商的 OTA 平台推送OTA 升级包到设备. 如果只是上面二种, APP 上只需要取得升级通知后, 发送更新的通知给设备和云服务器就好了, 很简单.

但是这个设备并不能联网, 比如手环之类的, 要通过 APP 蓝牙取得数据, 那么 OTA 升级方案, 就只能从 APP 上推送 OTA 数据包到设备更新了.

步骤

  1. APP 与设备通信(蓝牙或 WiFi)
  2. APP 发送 OTA 升级包信息(包大小、文件名、校验码等)到设备, 并通知设备做好升级准备, 接收数据的回调
  3. APP 发送 OTA 升级包(分包)到设备
  4. 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 发出来吧, 每种设备的情况可能不一样, 但是思路是相同的. 希望对有需要的人有帮助.