Flutter NRF 工具插件nrfutil的使用

Flutter NRF 工具插件nrfutil的使用

nrfutil

Pub Version analysis Star on Github License: BSD

一个用于创建nRF DFU包并为nRF51和nRF52设备进行DFU更新的Flutter插件。该插件还可以生成用于签名DFU包的密钥,并在设备上用于验证的C代码。

请注意 在继续或发布新问题之前:

  • 该插件不是由Nordic Semiconductor赞助或维护的。作者是希望使nrf设备的dfu包更容易创建的开发者。

开始使用

要在你的pubspec.yaml文件中添加nrfutil包以开始使用,请执行以下步骤:

dependencies:
  nrfutil: ^x.x.x

使用方法

nRF设备具有不同的软设备,请确保从softDeviceReqType枚举列表中选择正确的软设备。如果没有提供密钥文件,则会使用默认密钥。请勿在最终包中使用此密钥

DFU包可以包含单独的固件选项(例如应用、软设备或引导加载程序)或组合选项(例如应用+软设备+引导加载程序、应用+软设备或软设备+引导加载程序)。它不能制作应用+引导加载程序。

生成一个nRF DFU包

生成一个归档包作为Uint8List

在YAML文件中生成

在你的pubspec.yamlnrfutil.yaml中添加以下内容:

nrfutil:
  debug: true
  comment: "test comment"
  softdevice_type: "s132NRF52d611"
  export_path: "assets"
  hardware_version: 0xFFFFFFFF
  keyfile:
    generate: false
    private_key: "assets/key.pem"
    public_key: "assets/pbkey.pem"
  bootloader:
    version: 0xFFFFFFFF
    path: "assets/firmwares/foo.hex"
  application:
    version: 0xFFFFFFFF
    path: "assets/firmwares/bar.hex"
  softdevice:
    version: 0xFFFFFFFF
    path: "assets/firmwares/s132_nrf52_mini.hex"

然后运行以下命令:

dart run nrfutil --verbose

在终端中生成

仅在终端中运行以下命令:

dart run nrfutil --verbose --application assets/firmwares/bar.hex --app_version 0xFFFFFFFF --debug

在你的Flutter包中生成

生成一个归档包作为Uint8List

Uint8List package =  await NRFUTIL(
    applicationFirmware: applicationFirmware,
    softDeviceFirmware: softDeviceFirmware,
    hardwareVersion: hwVersion,
    applicationVersion: appVersion,
    keyFile: keyFile,
    sofDeviceReqType: sdReq
).generate();

生成私钥和公钥

生成一个归档包作为Uint8List

密钥生成能够提供一个PEM格式的私钥,但公钥可以是PEM或C代码形式。

Uint8List package =  await Signing.generateKey();

示例

你可以在这里找到这个API的示例:示例代码

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:nrfutil/nrfutil.dart';
import 'package:nrfutil_example/src/saveFile/saveFile.dart';

class DropDownItems{
  DropDownItems({
    required this.value,
    required this.text
  });
  SoftDeviceTypes value;
  String text;
}

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const NRFUtilWidget(),
    );
  }
}

enum NRFCall{cancel,submit}

class NRFUtilWidget extends StatefulWidget {
  const NRFUtilWidget({
    Key? key,
  }) : super(key: key);
  [@override](/user/override)
  _NRFUtilWidgetState createState() => _NRFUtilWidgetState();
}

class _NRFUtilWidgetState extends State<NRFUtilWidget> {
  TextEditingController appCont = TextEditingController();
  TextEditingController sdCont = TextEditingController();
  TextEditingController bootCont = TextEditingController();
  TextEditingController keyCont = TextEditingController();

  String? applicationFirmware;
  String? bootloaderFirmware;
  String? softDeviceFirmware;
  String? key;

  SoftDeviceTypes sdReq = SoftDeviceTypes.s132NRF52d611;
  String error = '';
  late List<DropdownMenuItem<SoftDeviceTypes>> sdDropDown;
  late double deviceWidth;

  bool loading = false;

  [@override](/user/override)
  void initState(){
    getFirmware();
    List<DropDownItems> keys = [];
    for (int i = 0; i < SoftDeviceTypes.values.length; i++) {
      keys.add(DropDownItems(value: SoftDeviceTypes.values[i], text: SoftDeviceTypes.values[i].toString()));
    }
    sdDropDown = setDropDownItems(keys);
    super.initState();
  }

  void getFirmware() async{
    String bfs = await rootBundle.loadString('assets/firmwares/bar.hex');
    String afs = await rootBundle.loadString('assets/firmwares/bar.hex');
    String sfs = await rootBundle.loadString('assets/firmwares/foo.hex');
    String ks = await rootBundle.loadString('assets/key.pem');
    applicationFirmware = afs;
    bootloaderFirmware = bfs;
    softDeviceFirmware = sfs;
    key = ks;
  }

  static Widget squareButton({
    Key? key,
    bool iconFront = false,
    Widget? icon,
    Color buttonColor = Colors.transparent,
    Color textColor = Colors.blueGrey,
    required String text,
    Function()? onTap,
    String fontFamily = 'Klavika Bold',
    double fontSize = 18.0,
    MainAxisAlignment mainAxisAlignment = MainAxisAlignment.center,
    double height = 75,
    double width = 100,
    double radius = 5,
    Alignment? alignment,
    EdgeInsets? margin,
    EdgeInsets? padding,
    List<BoxShadow>? boxShadow,
    Color? borderColor
  }){
    Widget totalIcon = (icon != null)?icon:Container();
    return InkWell(
      onTap: onTap,
      child: Container(
        alignment: alignment,
        height: height,
        width: width,
        margin: margin,
        padding: padding,
        decoration: BoxDecoration(
          color: buttonColor,
          border: Border.all(
            color: (borderColor == null)?buttonColor:borderColor,
            width: 2
          ),
          borderRadius: BorderRadius.all(Radius.circular(radius)),
          boxShadow: boxShadow
        ),
        child: Row(
          key: key,
          mainAxisAlignment: mainAxisAlignment,
          children: [
            (iconFront)?totalIcon:Container(),
            Text(
              text.toUpperCase(),
              textAlign: TextAlign.start,
              style: TextStyle(
                color: textColor,
                fontSize: fontSize,
                fontFamily: fontFamily,
                decoration: TextDecoration.none
              )
            ),
            (!iconFront)?totalIcon:Container(),
          ]
        )
      )
    );
  }

  static List<DropdownMenuItem<SoftDeviceTypes>> setDropDownItems(List<DropDownItems> info){
    List<DropdownMenuItem<SoftDeviceTypes>> items = [];
    for (int i =0; i < info.length;i++) {
      items.add(DropdownMenuItem(
        value: info[i].value,
        child: Text(
          info[i].text, 
          overflow: TextOverflow.ellipsis,
        )
      ));
    }
    return items;
  }

  double responsive({double? width, double smallest = 650, int total = 1}){
    width = width ?? deviceWidth;
    if(width < smallest){
      return width/total-20;
    }
    else if(width < smallest+350){
      return width/(2+(total-1))-20;
    }
    else{
      return width/(3+(total-1))-20;
    }
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    deviceWidth = MediaQuery.of(context).size.width;
    return Scaffold(
      body: Align(
        alignment: Alignment.center,
        child: SizedBox(
          width: responsive(),
          height: 360,
          child: Container(
            padding: const EdgeInsets.all(20),
            height: 360,
            width: responsive(),
            alignment: Alignment.center,
            decoration: BoxDecoration(
              color: Theme.of(context).cardColor,
              borderRadius: const BorderRadius.all(Radius.circular(10)),
              boxShadow: [
                BoxShadow(
                  color: Theme.of(context).shadowColor,
                  blurRadius: 5,
                  offset: const Offset(2, 2),
                ),
              ]
            ),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                squareButton(
                  text: 'generate key',
                  onTap: () {
                    SaveFile.saveBytes(printName: 'nrfutil_keys', fileType: 'zip', bytes: Signing.generateKey().zipFile);
                  },
                  borderColor: Colors.blueGrey,
                  height: 45,
                  radius: 10,
                  width: (responsive()-45),
                ),
                squareButton(
                  text: 'Create Application',
                  onTap: () {
                    NRFUTIL(
                      applicationFirmware: applicationFirmware,
                      hardwareVersion: 52,
                      applicationVersion: 1,
                      keyFile: key,
                      softDeviceReqType: sdReq,
                      verbose: true
                    ).generate().then((value){
                      SaveFile.saveBytes(printName: 'nrfutil_test', fileType: 'zip', bytes: value);
                    });
                  },
                  borderColor: Colors.blueGrey,
                  height: 45,
                  radius: 10,
                  width: (responsive()-45),
                ),
                squareButton(
                  text: 'Create Bootloader',
                  onTap: () {
                    NRFUTIL(
                      bootloaderFirmware: bootloaderFirmware,
                      hardwareVersion: 52,
                      bootloaderVersion: 1,
                      keyFile: key,
                      softDeviceReqType: sdReq,
                      verbose: true
                    ).generate().then((value){
                      SaveFile.saveBytes(printName: 'nrfutil_test', fileType: 'zip', bytes: value);
                    });
                  },
                  borderColor: Colors.blueGrey,
                  height: 45,
                  radius: 10,
                  width: (responsive()-45),
                ),
                squareButton(
                  text: 'Create softdevice',
                  onTap: () {
                    NRFUTIL(
                      softDeviceFirmware: softDeviceFirmware,
                      hardwareVersion: 52,
                      keyFile: key,
                      softDeviceReqType: sdReq,
                      verbose: true
                    ).generate().then((value){
                      SaveFile.saveBytes(printName: 'nrfutil_test', fileType: 'zip', bytes: value);
                    });
                  },
                  borderColor: Colors.blueGrey,
                  height: 45,
                  radius: 10,
                  width: (responsive()-45),
                ),
                squareButton(
                  text: 'Create Application and Softdevice',
                  onTap: () {
                    NRFUTIL(
                      applicationFirmware: applicationFirmware,
                      softDeviceFirmware: softDeviceFirmware,
                      hardwareVersion: 52,
                      applicationVersion: 1,
                      keyFile: key,
                      softDeviceReqType: sdReq,
                      verbose: true
                    ).generate().then((value){
                      SaveFile.saveBytes(printName: 'nrfutil_test', fileType: 'zip', bytes: value);
                    });
                  },
                  borderColor: Colors.blueGrey,
                  height: 45,
                  radius: 10,
                  width: (responsive()-45),
                ),
                squareButton(
                  text: 'Create bootloader and Softdevice',
                  onTap: () {
                    NRFUTIL(
                      softDeviceFirmware: softDeviceFirmware,
                      bootloaderFirmware: bootloaderFirmware,
                      bootloaderVersion: 1,
                      keyFile: key,
                      softDeviceReqType: sdReq
                    ).generate().then((value){
                      SaveFile.saveBytes(printName: 'nrfutil_test', fileType: 'zip', bytes: value);
                    });
                  },
                  borderColor: Colors.blueGrey,
                  height: 45,
                  radius: 10,
                  width: (responsive()-45),
                ),
                squareButton(
                  text: 'Create application, bootloader and Softdevice',
                  onTap: () {
                    NRFUTIL(
                      applicationFirmware: applicationFirmware,
                      bootloaderFirmware: bootloaderFirmware,
                      softDeviceFirmware: softDeviceFirmware,
                      applicationVersion: 1,
                      hardwareVersion: 52,
                      keyFile: key,
                      softDeviceReqType: sdReq,
                      verbose: true
                    ).generate().then((value){
                      SaveFile.saveBytes(printName: 'nrfutil_test', fileType: 'zip', bytes: value);
                    });
                  },
                  borderColor: Colors.blueGrey,
                  height: 45,
                  radius: 10,
                  width: (responsive()-45),
                )
              ]
            )
          )
        )
      )
    );
  }
}

更多关于Flutter NRF 工具插件nrfutil的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter NRF 工具插件nrfutil的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


nrfutil 是 Nordic Semiconductor 提供的一个命令行工具,用于为 Nordic 设备生成和操作固件升级包(DFU - Device Firmware Update)。它通常与 Nordic 的开发板(如 nRF52 系列)配合使用,用于无线固件更新。以下是如何在 Flutter 项目中使用 nrfutil 的基本步骤。

1. 安装 nrfutil

首先,你需要在你的开发环境中安装 nrfutil。你可以通过 Python 的包管理工具 pip 来安装它。

pip install nrfutil

安装完成后,你可以通过运行以下命令来验证安装是否成功:

nrfutil --version

2. 生成 DFU 包

nrfutil 可以用来生成 DFU 包。假设你已经有一个 .hex.bin 格式的固件文件,你可以使用以下命令生成一个 DFU 包:

nrfutil pkg generate --hw-version 52 --sd-req 0x00 --application-version 1 --application your_firmware.hex --key-file private.pem dfu_package.zip

其中:

  • --hw-version 指定硬件版本(例如 52 对应 nRF52 系列)。
  • --sd-req 指定 SoftDevice 版本,0x00 表示不需要特定的 SoftDevice。
  • --application-version 指定应用程序版本。
  • --application 指定你的固件文件。
  • --key-file 指定用于签名的私钥文件。
  • dfu_package.zip 是生成的 DFU 包的文件名。

3. 在 Flutter 项目中集成 DFU 功能

在 Flutter 项目中,你可以使用 flutter_blueflutter_reactive_ble 等 BLE 插件来与 Nordic 设备通信。为了执行 DFU 操作,你可以使用 flutter_blue 插件结合 nrfutil 生成的 DFU 包。

首先,添加 flutter_blue 依赖到你的 pubspec.yaml 文件中:

dependencies:
  flutter_blue: ^0.8.0

然后,使用以下代码示例来启动 DFU 过程:

import 'package:flutter_blue/flutter_blue.dart';
import 'dart:io';

Future<void> performDfu(BluetoothDevice device, String dfuPackagePath) async {
  final dfuService = Guid('00001530-1212-efde-1523-785feabcd123');
  final dfuControlPoint = Guid('00001531-1212-efde-1523-785feabcd123');
  final dfuPacket = Guid('00001532-1212-efde-1523-785feabcd123');

  await device.connect();
  List<BluetoothService> services = await device.discoverServices();
  BluetoothService dfuServiceFound = services.firstWhere(
    (service) => service.uuid == dfuService,
    orElse: () => null,
  );

  if (dfuServiceFound != null) {
    BluetoothCharacteristic controlPoint = dfuServiceFound.characteristics.firstWhere(
      (characteristic) => characteristic.uuid == dfuControlPoint,
    );

    BluetoothCharacteristic packetCharacteristic = dfuServiceFound.characteristics.firstWhere(
      (characteristic) => characteristic.uuid == dfuPacket,
    );

    // Read the DFU package file
    List<int> dfuData = await File(dfuPackagePath).readAsBytes();

    // Split the data into chunks and send it via BLE
    for (var i = 0; i < dfuData.length; i += 20) {
      List<int> chunk = dfuData.sublist(i, i + 20 < dfuData.length ? i + 20 : dfuData.length);
      await packetCharacteristic.write(chunk, withoutResponse: true);
    }

    // Trigger the DFU process
    await controlPoint.write([0x01], withoutResponse: true);
  }
}

4. 运行 DFU 过程

将生成的 dfu_package.zip 文件放到 Flutter 项目的 assets 目录中,然后在你的代码中调用 performDfu 函数来启动 DFU 过程。

void main() async {
  FlutterBlue flutterBlue = FlutterBlue.instance;
  BluetoothDevice device = // Get your BluetoothDevice object here;
  String dfuPackagePath = 'assets/dfu_package.zip';

  await performDfu(device, dfuPackagePath);
}
回到顶部