nordic_dfu 7.1.1 copy "nordic_dfu: ^7.1.1" to clipboard
nordic_dfu: ^7.1.1 copied to clipboard

This library allows you to do a Device Firmware Update (DFU) of your nrf51 or nrf52 chip from Nordic Semiconductor. Fork of flutter-nordic-dfu.

example/lib/main.dart

import 'dart:async';
import 'dart:io';

import 'package:collection/collection.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:nordic_dfu/nordic_dfu.dart';
import 'package:permission_handler/permission_handler.dart';

void main() => runApp(const MyApp());

final dfuService = Guid('0000FE59-0000-1000-8000-00805F9B34FB');

class DfuEvent {
  DfuEvent({
    required this.timestamp,
    required this.eventName,
    required this.message,
    this.isError = false,
  });
  final DateTime timestamp;
  final String eventName;
  final String message;
  final bool isError;
}

class ExampleDfuState {
  ExampleDfuState({
    required this.dfuRunning,
    this.progressPercent,
    this.filePath,
    this.lastError,
  });
  bool dfuRunning = false;
  int? progressPercent;
  String? filePath;
  String? lastError;
  final List<DfuEvent> events = [];

  void addEvent(String eventName, String message, {bool isError = false}) {
    events.insert(
      0,
      DfuEvent(
        timestamp: DateTime.now(),
        eventName: eventName,
        message: message,
        isError: isError,
      ),
    );
  }

  void clearEvents() {
    events.clear();
  }
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  MyAppState createState() => MyAppState();
}

class MyAppState extends State<MyApp> {
  static const tag = 'nordic_dfu_example:';
  StreamSubscription<ScanResult>? scanSubscription;
  StreamSubscription<ScanResult>? dfuScanSubscription;
  List<ScanResult> scanResults = <ScanResult>[];
  Map<String, ExampleDfuState> dfuStateMap = {};
  bool get anyDfuRunning => dfuStateMap.values.any((state) => state.dfuRunning);

  // If true, only show devices advertising the DFU service
  bool onlyDfuService = true;

  Future<void> doDfu(BuildContext context, String deviceId) async {
    final messenger = ScaffoldMessenger.of(context);
    stopScan();

    // Pick ZIP file from device storage
    final result = await FilePicker.platform.pickFiles(
      type: FileType.custom,
      allowedExtensions: ['zip'],
      dialogTitle: 'Select DFU firmware file (.zip)',
    );

    if (result == null) {
      debugPrint('$tag File selection cancelled');
      return;
    }

    final filePath = result.files.single.path;
    if (filePath == null || filePath.isEmpty) {
      debugPrint('$tag Invalid file path');
      messenger.showSnackBar(
        const SnackBar(
          content: Text('Invalid file path'),
          backgroundColor: Colors.red,
        ),
      );
      return;
    }

    debugPrint('$tag Selected firmware file: $filePath');

    if (!context.mounted) return;

    // Start DFU with the selected file
    await _startDfu(context, deviceId, filePath);
  }

  Future<void> retryDfu(BuildContext context, String deviceId) async {
    final state = dfuStateMap[deviceId];
    if (state?.filePath == null) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('No previous file path found'),
          backgroundColor: Colors.red,
        ),
      );
      return;
    }

    debugPrint('$tag Retrying DFU with file: ${state!.filePath}');
    await _startDfu(context, deviceId, state.filePath!);
  }

  Future<void> _startDfu(
    BuildContext context,
    String deviceId,
    String filePath,
  ) async {
    final messenger = ScaffoldMessenger.of(context)
      ..showSnackBar(
        SnackBar(
          content: Text('Starting DFU with file: ${filePath.split('/').last}'),
          duration: const Duration(seconds: 2),
        ),
      );

    setState(() {
      dfuStateMap[deviceId] = ExampleDfuState(
        dfuRunning: true,
        filePath: filePath,
      );
      dfuStateMap[deviceId]?.clearEvents();
      dfuStateMap[deviceId]
          ?.addEvent('File Selected', 'File: ${filePath.split('/').last}');
    });

    // Auto-open timeline dialog when DFU starts
    Future.delayed(const Duration(milliseconds: 300), () {
      if (context.mounted) {
        _showEventTimeline(context, deviceId);
      }
    });
    try {
      final eventHandler = DfuEventHandler(
        onDeviceConnecting: (string) {
          debugPrint('$tag device connecting: $string');
          setState(() {
            dfuStateMap[deviceId]
                ?.addEvent('Connecting', 'Connecting to device...');
          });
        },
        onDeviceConnected: (string) {
          debugPrint('$tag device connected: $string'); //1
          setState(() {
            dfuStateMap[deviceId]
                ?.addEvent('Connected', 'Device connected successfully');
          });
        },
        onDeviceDisconnecting: (string) {
          // 3
          debugPrint('$tag device disconnecting: $string');
          setState(() {
            dfuStateMap[deviceId]
                ?.addEvent('Disconnecting', 'Disconnecting from device...');
          });
        },
        onDeviceDisconnected: (string) {
          // 4
          debugPrint('$tag device disconnected: $string');
          setState(() {
            dfuStateMap[deviceId]
                ?.addEvent('Disconnected', 'Device disconnected');
          });
        },
        onDfuAborted: (string) {
          debugPrint('$tag dfu aborted: $string');
          setState(() {
            dfuStateMap[deviceId]?.addEvent(
              'Aborted',
              'DFU process aborted by user',
              isError: true,
            );
          });
          messenger.showSnackBar(
            SnackBar(
              content: Text('DFU aborted for $string'),
              backgroundColor: Colors.orange,
            ),
          );
        },
        onDfuCompleted: (string) {
          //5
          debugPrint('$tag dfu completed: $string');
          setState(() {
            dfuStateMap[deviceId]
                ?.addEvent('Completed', 'DFU completed successfully! ✓');
            dfuStateMap[deviceId]?.lastError = null;
          });
          messenger.showSnackBar(
            SnackBar(
              content: Text('DFU completed successfully for $string'),
              backgroundColor: Colors.green,
            ),
          );
        },
        onDfuProcessStarted: (string) {
          // start
          debugPrint('$tag dfu process started: $string');
          setState(() {
            dfuStateMap[deviceId]?.addEvent(
              'Process Started',
              'DFU process started, uploading firmware...',
            );
          });
        },
        onDfuProcessStarting: (string) {
          debugPrint('$tag dfu process starting: $string'); //2
          setState(() {
            dfuStateMap[deviceId]
                ?.addEvent('Process Starting', 'Initializing DFU process...');
          });
        },
        onEnablingDfuMode: (string) {
          debugPrint('$tag dfu enabled: $string');
          setState(() {
            dfuStateMap[deviceId]?.addEvent(
              'Enabling DFU Mode',
              'Switching device to DFU mode...',
            );
          });
          // Start scanning for the DFU device
          if (string == deviceId) {
            _startScanForDfuDevice(deviceId);
          }
        },
        onFirmwareValidating: (string) {
          debugPrint('$tag firmware validating: $string');
          setState(() {
            dfuStateMap[deviceId]
                ?.addEvent('Validating', 'Validating firmware...');
          });
        },
        // ignore: deprecated_member_use
        onFirmwareUploading: (string) {
          debugPrint('$tag firmware uploading: $string');
          setState(() {
            dfuStateMap[deviceId]
                ?.addEvent('Uploading', 'Uploading firmware to device...');
          });
        },
        onError: (
          address,
          error,
          errorType,
          message,
        ) {
          debugPrint(
            '$tag error: device $address, error $error, errorType $errorType, message $message',
          );
          setState(() {
            dfuStateMap[deviceId]
                ?.addEvent('Error', 'Error $error: $message', isError: true);
            dfuStateMap[deviceId]?.lastError = message;
          });
          messenger.showSnackBar(
            SnackBar(
              content: Text('DFU Error: $message'),
              backgroundColor: Colors.red,
              duration: const Duration(seconds: 5),
            ),
          );
        },
        onProgressChanged: (
          deviceAddress,
          percent,
          speed,
          avgSpeed,
          currentPart,
          partsTotal,
        ) {
          debugPrint(
            '$tag progress changed: device $deviceAddress, percent: $percent, speed $speed, avgSpeed $avgSpeed, currentPart $currentPart, total parts: $partsTotal',
          );
          setState(() {
            dfuStateMap[deviceId]?.progressPercent = percent;
            if (percent % 10 == 0 || percent == 100) {
              dfuStateMap[deviceId]?.addEvent(
                'Progress $percent%',
                'Part $currentPart/$partsTotal - Speed: ${speed.toStringAsFixed(1)} B/s',
              );
            }
          });
        },
      );

      final s = await NordicDfu().startDfu(
        deviceId,
        filePath,
        dfuEventHandler: eventHandler,
        androidParameters: const AndroidParameters(rebootTime: 1000),
        // darwinParameters: const DarwinParameters(),
      );
      debugPrint('$tag DFU result: $s');
      setState(() {
        dfuStateMap[deviceId]?.dfuRunning = false;
      });
    } catch (e) {
      final errorMsg = e.toString();
      setState(() {
        dfuStateMap[deviceId]?.dfuRunning = false;
        dfuStateMap[deviceId]?.lastError = errorMsg;
        dfuStateMap[deviceId]
            ?.addEvent('Exception', 'DFU failed: $errorMsg', isError: true);
      });
      debugPrint('$tag DFU Exception: $e');
      messenger.showSnackBar(
        SnackBar(
          content: Text('DFU failed: $errorMsg'),
          backgroundColor: Colors.red,
          duration: const Duration(seconds: 5),
        ),
      );
    }
  }

  Future<void> startScan() async {
    // You can request multiple permissions at once.

    if (!Platform.isMacOS) {
      await [
        Permission.bluetoothAdvertise,
        Permission.bluetoothConnect,
        Permission.bluetoothScan,
        Permission.bluetooth,
      ].request();
    }

    await scanSubscription?.cancel();
    await FlutterBluePlus.startScan(
      withServices: onlyDfuService ? [dfuService] : [],
    );
    scanResults.clear();
    scanSubscription = FlutterBluePlus.scanResults.expand((e) => e).listen(
      (scanResult) {
        final exists = scanResults.firstWhereOrNull(
          (ele) => ele.device.remoteId == scanResult.device.remoteId,
        );

        if (exists != null) {
          return;
        }

        setState(() {
          scanResults
            ..add(scanResult)
            ..sort((a, b) => b.rssi.compareTo(a.rssi));
        });
      },
    );
  }

  void stopScan() {
    FlutterBluePlus.stopScan();
    scanSubscription?.cancel();
    scanSubscription = null;
    setState(() => scanSubscription = null);
  }

  // Scan for the DFU device and set address mapping
  Future<void> _startScanForDfuDevice(String deviceId) async {
    debugPrint('$tag Starting scan for DFU device...');

    // Calculate the expected DFU address (original + 1)
    final expectedDfuAddress = _incrementMacAddress(deviceId);
    debugPrint('$tag Expected DFU address: $expectedDfuAddress');

    setState(() {
      dfuStateMap[deviceId]?.addEvent(
        'Scanning for DFU',
        'Looking for device in bootloader mode...',
      );
    });

    await dfuScanSubscription?.cancel();
    await FlutterBluePlus.startScan(timeout: const Duration(seconds: 5));

    dfuScanSubscription = FlutterBluePlus.scanResults.expand((e) => e).listen(
      (scanResult) {
        final dfuDevice = scanResult.device;
        final deviceName = dfuDevice.platformName;
        final dfuAddress = dfuDevice.remoteId.str;

        // Check if this is the DFU device by:
        // 1. Name contains "dfu" or is "DfuTarg"
        // 2. Address matches the incremented address (original + 1)
        final matchesByName = deviceName.toLowerCase().contains('dfu') ||
            deviceName.toLowerCase() == 'dfutarg';
        final matchesByAddress =
            dfuAddress.toUpperCase() == expectedDfuAddress.toUpperCase();

        if (matchesByName || matchesByAddress) {
          final matchReason = matchesByName && matchesByAddress
              ? 'name and address'
              : matchesByName
                  ? 'name'
                  : 'address';
          debugPrint(
            '$tag Found DFU device by $matchReason: $deviceName at $dfuAddress',
          );

          // Set the address mapping
          NordicDfu().setAddressMapping(dfuAddress, deviceId);

          setState(() {
            dfuStateMap[deviceId]?.addEvent(
              'DFU Device Found',
              'Mapped $dfuAddress → $deviceId (by $matchReason)',
            );
          });

          // Stop scanning
          FlutterBluePlus.stopScan();
          dfuScanSubscription?.cancel();
        }
      },
    );

    // Auto-cleanup after timeout
    Future.delayed(const Duration(seconds: 5), () {
      dfuScanSubscription?.cancel();
      dfuScanSubscription = null;
    });
  }

  // Helper to increment MAC address by 1 (common DFU pattern)
  String _incrementMacAddress(String address) {
    final bytes =
        address.split(':').map((e) => int.parse(e, radix: 16)).toList();

    // Increment the last byte
    bytes[bytes.length - 1] = (bytes[bytes.length - 1] + 1) % 256;

    return bytes
        .map((e) => e.toRadixString(16).toUpperCase().padLeft(2, '0'))
        .join(':');
  }

  @override
  Widget build(BuildContext context) {
    final isScanning = scanSubscription != null;
    final hasDevice = scanResults.isNotEmpty;

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Nordic DFU Example App'),
          actions: <Widget>[
            if (anyDfuRunning)
              TextButton(
                onPressed: NordicDfu().abortDfu,
                child: const Text('Abort Dfu'),
              ),
            // Filter toggle for DFU service
            PopupMenuButton<bool>(
              icon: Icon(
                onlyDfuService ? Icons.filter_alt : Icons.filter_alt_off,
              ),
              tooltip: 'Scan filter',
              onSelected: (value) {
                setState(() {
                  onlyDfuService = value;
                });
                // Restart scan if currently scanning
                if (isScanning) {
                  stopScan();
                  startScan();
                }
              },
              itemBuilder: (context) => [
                PopupMenuItem(
                  value: true,
                  child: Row(
                    children: [
                      Icon(
                        Icons.check,
                        color:
                            onlyDfuService ? Colors.blue : Colors.transparent,
                      ),
                      const SizedBox(width: 8),
                      const Text('DFU Service Only'),
                    ],
                  ),
                ),
                PopupMenuItem(
                  value: false,
                  child: Row(
                    children: [
                      Icon(
                        Icons.check,
                        color:
                            !onlyDfuService ? Colors.blue : Colors.transparent,
                      ),
                      const SizedBox(width: 8),
                      const Text('All Devices'),
                    ],
                  ),
                ),
              ],
            ),
            if (isScanning)
              IconButton(
                icon: const Icon(Icons.pause_circle_filled),
                onPressed: anyDfuRunning ? null : stopScan,
              )
            else
              IconButton(
                icon: const Icon(Icons.play_arrow),
                onPressed: anyDfuRunning ? null : startScan,
              ),
          ],
        ),
        body: !hasDevice
            ? const Center(
                child: Text('No device'),
              )
            : ListView.separated(
                padding: const EdgeInsets.all(8),
                itemBuilder: _deviceItemBuilder,
                separatorBuilder: (context, index) => const SizedBox(height: 5),
                itemCount: scanResults.length,
              ),
      ),
    );
  }

  void _showEventTimeline(BuildContext context, String deviceId) {
    final state = dfuStateMap[deviceId];
    if (state == null) {
      return;
    }

    showDialog<void>(
      context: context,
      builder: (context) => StatefulBuilder(
        builder: (context, setDialogState) {
          // Auto-refresh dialog every 100ms to show new events in real-time
          Future.delayed(const Duration(milliseconds: 100), () {
            if (context.mounted) {
              setDialogState(() {});
            }
          });

          final currentState = dfuStateMap[deviceId];
          if (currentState == null) {
            return const SizedBox.shrink();
          }

          final isDfuRunning = currentState.dfuRunning;
          final events = currentState.events;

          return Dialog(
            shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.circular(20),
            ),
            child: SizedBox(
              width: MediaQuery.of(context).size.width * 0.9,
              height: MediaQuery.of(context).size.height * 0.7,
              child: Column(
                children: [
                  Container(
                    padding: const EdgeInsets.all(16),
                    decoration: BoxDecoration(
                      color: Colors.blue.shade100,
                      borderRadius:
                          const BorderRadius.vertical(top: Radius.circular(20)),
                    ),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Expanded(
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              const Text(
                                'DFU Event Timeline',
                                style: TextStyle(
                                  fontSize: 18,
                                  fontWeight: FontWeight.bold,
                                ),
                              ),
                              if (isDfuRunning &&
                                  currentState.progressPercent != null)
                                Padding(
                                  padding: const EdgeInsets.only(top: 4),
                                  child: Text(
                                    'Progress: ${currentState.progressPercent}%',
                                    style: const TextStyle(
                                      fontSize: 14,
                                      color: Colors.black54,
                                    ),
                                  ),
                                ),
                            ],
                          ),
                        ),
                        IconButton(
                          icon: const Icon(Icons.close),
                          onPressed: () => Navigator.pop(context),
                          tooltip: 'Close',
                        ),
                      ],
                    ),
                  ),
                  if (events.isEmpty)
                    const Expanded(
                      child: Center(
                        child: Text('No events yet...'),
                      ),
                    )
                  else
                    Expanded(
                      child: ListView.builder(
                        padding: const EdgeInsets.all(16),
                        itemCount: events.length,
                        itemBuilder: (context, index) {
                          final event = events[index];
                          final timeStr =
                              '${event.timestamp.hour.toString().padLeft(2, '0')}:'
                              '${event.timestamp.minute.toString().padLeft(2, '0')}:'
                              '${event.timestamp.second.toString().padLeft(2, '0')}';
                          return Card(
                            margin: const EdgeInsets.only(bottom: 8),
                            color: event.isError
                                ? Colors.red.shade50
                                : Colors.green.shade50,
                            child: ListTile(
                              leading: Icon(
                                event.isError
                                    ? Icons.error
                                    : Icons.check_circle,
                                color:
                                    event.isError ? Colors.red : Colors.green,
                              ),
                              title: Text(
                                event.eventName,
                                style: const TextStyle(
                                  fontWeight: FontWeight.bold,
                                ),
                              ),
                              subtitle: Text(event.message),
                              trailing: Text(
                                timeStr,
                                style: const TextStyle(
                                  fontSize: 12,
                                  color: Colors.grey,
                                ),
                              ),
                            ),
                          );
                        },
                      ),
                    ),
                  Container(
                    padding: const EdgeInsets.all(16),
                    decoration: BoxDecoration(
                      color: Colors.grey.shade100,
                      borderRadius: const BorderRadius.vertical(
                        bottom: Radius.circular(20),
                      ),
                    ),
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                      children: [
                        if (isDfuRunning)
                          Expanded(
                            child: ElevatedButton.icon(
                              onPressed: () {
                                NordicDfu().abortDfu(address: deviceId);
                                Navigator.pop(context);
                              },
                              icon: const Icon(Icons.cancel),
                              label: const Text('Abort DFU'),
                              style: ElevatedButton.styleFrom(
                                backgroundColor: Colors.red,
                                foregroundColor: Colors.white,
                                padding:
                                    const EdgeInsets.symmetric(vertical: 12),
                              ),
                            ),
                          )
                        else
                          Expanded(
                            child: ElevatedButton.icon(
                              onPressed: () => Navigator.pop(context),
                              icon: const Icon(Icons.close),
                              label: const Text('Close'),
                              style: ElevatedButton.styleFrom(
                                backgroundColor: Colors.blue,
                                foregroundColor: Colors.white,
                                padding:
                                    const EdgeInsets.symmetric(vertical: 12),
                              ),
                            ),
                          ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
          );
        },
      ),
    );
  }

  Widget _deviceItemBuilder(BuildContext context, int index) {
    final result = scanResults[index];
    final deviceId = result.device.remoteId.str;
    return DeviceItem(
      dfuState: dfuStateMap[deviceId],
      scanResult: result,
      onPress: dfuStateMap[deviceId]?.dfuRunning ?? false
          ? () => NordicDfu().abortDfu(address: deviceId)
          : () => doDfu(context, deviceId),
      onRetry: dfuStateMap[deviceId]?.lastError != null &&
              !(dfuStateMap[deviceId]?.dfuRunning ?? false)
          ? () => retryDfu(context, deviceId)
          : null,
      onShowTimeline: dfuStateMap[deviceId]?.events.isNotEmpty ?? false
          ? () => _showEventTimeline(context, deviceId)
          : null,
    );
  }
}

class DeviceItem extends StatelessWidget {
  const DeviceItem({
    required this.scanResult,
    this.onPress,
    this.onRetry,
    this.onShowTimeline,
    this.dfuState,
    super.key,
  });
  final ScanResult scanResult;

  final VoidCallback? onPress;
  final VoidCallback? onRetry;
  final VoidCallback? onShowTimeline;

  final ExampleDfuState? dfuState;

  String _getDfuButtonText() {
    if (dfuState?.dfuRunning ?? false) {
      final progressText = dfuState?.progressPercent != null
          ? '\n(${dfuState!.progressPercent}%)'
          : '';
      return 'Abort DFU$progressText';
    }
    return 'Select ZIP\n& Start DFU';
  }

  @override
  Widget build(BuildContext context) {
    var name = 'Unknown';
    if (scanResult.device.platformName.isNotEmpty) {
      name = scanResult.device.platformName;
    }

    final hasError = dfuState?.lastError != null;
    final hasEvents = dfuState?.events.isNotEmpty ?? false;

    return Card(
      color: hasError ? Colors.red.shade50 : null,
      child: Padding(
        padding: const EdgeInsets.all(8),
        child: Column(
          children: [
            Row(
              children: <Widget>[
                const Icon(Icons.bluetooth),
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      Text(
                        name,
                        style: const TextStyle(fontWeight: FontWeight.bold),
                      ),
                      Text(
                        scanResult.device.remoteId.str,
                        style: const TextStyle(fontSize: 12),
                      ),
                      Text(
                        'RSSI: ${scanResult.rssi}',
                        style: const TextStyle(fontSize: 12),
                      ),
                      if (dfuState?.progressPercent != null)
                        Padding(
                          padding: const EdgeInsets.only(top: 4),
                          child: LinearProgressIndicator(
                            value: (dfuState!.progressPercent!) / 100,
                            backgroundColor: Colors.grey.shade300,
                            valueColor: const AlwaysStoppedAnimation<Color>(
                              Colors.blue,
                            ),
                          ),
                        ),
                    ],
                  ),
                ),
                Column(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    TextButton(
                      onPressed: onPress,
                      child: Text(
                        _getDfuButtonText(),
                        textAlign: TextAlign.center,
                      ),
                    ),
                    if (onRetry != null)
                      TextButton.icon(
                        onPressed: onRetry,
                        icon: const Icon(Icons.refresh, size: 16),
                        label: const Text('Retry'),
                        style: TextButton.styleFrom(
                          foregroundColor: Colors.orange,
                        ),
                      ),
                  ],
                ),
              ],
            ),
            if (hasEvents)
              Padding(
                padding: const EdgeInsets.only(top: 8),
                child: Row(
                  children: [
                    Expanded(
                      child: Text(
                        dfuState!.events.first.message,
                        style: TextStyle(
                          fontSize: 12,
                          color: dfuState!.events.first.isError
                              ? Colors.red
                              : Colors.green,
                          fontStyle: FontStyle.italic,
                        ),
                        maxLines: 1,
                        overflow: TextOverflow.ellipsis,
                      ),
                    ),
                    TextButton.icon(
                      onPressed: onShowTimeline,
                      icon: const Icon(Icons.timeline, size: 16),
                      label: Text('Timeline (${dfuState!.events.length})'),
                      style: TextButton.styleFrom(
                        foregroundColor: Colors.blue,
                        padding: const EdgeInsets.symmetric(horizontal: 8),
                      ),
                    ),
                  ],
                ),
              ),
          ],
        ),
      ),
    );
  }
}
45
likes
160
points
3.76k
downloads

Publisher

verified publishersteenbakker.dev

Weekly Downloads

This library allows you to do a Device Firmware Update (DFU) of your nrf51 or nrf52 chip from Nordic Semiconductor. Fork of flutter-nordic-dfu.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on nordic_dfu

Packages that implement nordic_dfu