import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_reactive_ble/flutter_reactive_ble.dart'; import 'package:nordic_dfu/nordic_dfu.dart'; import 'package:nrf/app_subscription_state.dart'; import 'package:nrf/connector.dart'; import 'package:nrf/dfu_list.dart'; import 'package:nrf/find.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:wakelock/wakelock.dart'; const SH_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"; Future clearCache() async { await delDir(await getTemporaryDirectory()); if (Platform.isAndroid) { var dir = await getExternalStorageDirectory(); if (dir != null) await delDir(dir); } } ///递归方式删除目录 Future delDir(FileSystemEntity file) async { print(("del path $file")); if (file is Directory) { try { final List children = file.listSync(); for (final FileSystemEntity child in children) { await delDir(child); } } catch (e) { print(e); } } else { await file.delete(); } } class Scanner extends StatefulWidget { const Scanner({Key? key}) : super(key: key); @override State createState() => _State(); } class _State extends State with SubscriptionState { final flutterReactiveBle = FlutterReactiveBle(); bool _selectAll = false; double _filterRssi = 35; bool _filterNameEquals = true; bool _filterSofewareEquals = true; bool _filterHardwareEquals = true; String? _filterName = ""; String? _filterSofeware = ""; String? _filterHardware = ""; StreamSubscription? scanForDevices; List scanResults = []; Set selectedResults = {}; bool _dfuAll = false; DiscoveredDevice? _targetDevice; int _countAll = 0; int _countSucc = 0; int _countFail = 0; final ValueNotifier _messageNotifier = ValueNotifier(""); @override void initState() { super.initState(); addSubscription(flutterReactiveBle.statusStream.listen((event) { print("bluetooth -- instance state $event"); if (event == BleStatus.ready) { } else if (event == BleStatus.unknown) { } else if (event == BleStatus.poweredOff) { } else {} })); _grant(); clearCache(); Wakelock.enable(); } _grant() async { if (Platform.isAndroid) { PermissionStatus permissions = await Permission.locationWhenInUse.request(); if (permissions.isGranted) { } else { showDialog( context: context, builder: (context) => const SimpleDialog(title: Text('使用蓝牙功能需授权定位权限')), ); } } } _search() { FocusScope.of(context).requestFocus(FocusNode()); scanForDevices?.cancel(); setState(() { selectedResults.clear(); scanResults.clear(); }); scanForDevices = flutterReactiveBle.scanForDevices(withServices: [Uuid.parse(SH_UUID)]).listen((e) { if (e.name.isEmpty) return; // print("111111111111111111111111 ${e}"); final knownDeviceIndex = scanResults.indexWhere((d) => d.id == e.id); if (knownDeviceIndex >= 0) { scanResults[knownDeviceIndex] = e; } else { scanResults.add(e); } setState(() {}); }) ..onDone(() { setState(() { scanForDevices = null; }); }); } List updateResults = []; Map failedResults = {}; _dfu(String path, int type) async { updateResults.clear(); failedResults.clear(); while (mounted) { try { _messageNotifier.value = "开始搜索...(M$type)"; DiscoveredDevice device; if (type == 1) { List scanResults = []; Completer completer = Completer(); int times = 0; Timer timeout = Timer.periodic(const Duration(seconds: 5), (t) { if(scanResults.isNotEmpty) { completer.complete(true); }else{ _messageNotifier.value = "开始搜索...(M$type .... ${times++})"; } }); var stream = flutterReactiveBle.scanForDevices(withServices: [Uuid.parse(SH_UUID)], scanMode: ScanMode.lowPower); var listen = stream.listen((event) { if (scanResults.where((element) => element.id == event.id).isEmpty && (failedResults[event] ?? 0) < 3) { List results = [event]; scanResults.addAll(results .where((element) => updateResults.where((event) => element.id == event.id).isEmpty) .where((element) => element.rssi.abs() < _filterRssi).where((element) => element.name.contains(_filterName ?? "") == _filterNameEquals).where((element) { if (_filterHardware?.isNotEmpty == true) { if (element.manufacturerData.length >= 6 && (_filterHardware == "${element.manufacturerData[3]}.${element.manufacturerData[4]}.${element.manufacturerData[5]}") == _filterHardwareEquals) { return true; } else { return false; } } return true; }).where((element) { if (_filterSofeware?.isNotEmpty == true) { if (element.manufacturerData.length >= 6 && (_filterSofeware == "${element.manufacturerData[3]}.${element.manufacturerData[4]}.${element.manufacturerData[0]}") == _filterSofewareEquals) { return true; } else { return false; } } return true; })); } }); await completer.future; listen.cancel(); timeout.cancel(); _messageNotifier.value = "搜索到 ${scanResults.map((e) => e.name).join(", ")}"; if (scanResults.isEmpty) { await Future.delayed(const Duration(seconds: 2)); continue; } device = scanResults[Random().nextInt(scanResults.length)]; } else { device = await flutterReactiveBle .scanForDevices(withServices: [Uuid.parse(SH_UUID)]) .where((element) => updateResults.where((event) => element.id == event.id).isEmpty) .where((element) => failedResults.keys.where((event) => element.id == event.id).isEmpty) .where((element) => element.rssi.abs() < _filterRssi) .where((element) => element.name.contains(_filterName ?? "") == _filterNameEquals) .where((element) { if (_filterHardware?.isNotEmpty == true) { if (element.manufacturerData.length >= 6 && (_filterHardware == "${element.manufacturerData[3]}.${element.manufacturerData[4]}.${element.manufacturerData[5]}") == _filterHardwareEquals) { return true; } else { return false; } } return true; }) .firstWhere((element) { if (_filterSofeware?.isNotEmpty == true) { if (element.manufacturerData.length >= 6 && (_filterSofeware == "${element.manufacturerData[3]}.${element.manufacturerData[4]}.${element.manufacturerData[0]}") == _filterSofewareEquals) { return true; } else { return false; } } return true; }); } setState(() { _countAll++; _targetDevice = device; }); await Future.delayed(const Duration(seconds: 2)); DateTime start = DateTime.now(); try { await NordicDfu().startDfu(device.id, path, fileInAsset: false, onEnablingDfuMode: (deviceAddress) { _messageNotifier.value = "EnablingDfuMode"; }, onDfuCompleted: (deviceAddress) { _messageNotifier.value = "DfuCompleted use:${DateTime.now().difference(start).inSeconds}s"; _countSucc++; updateResults.add(device); }, onDfuProcessStarted: (deviceAddress) { _messageNotifier.value = "DfuProcessStarted"; }, onDfuProcessStarting: (deviceAddress) { _messageNotifier.value = "DfuProcessStarting"; }, onDeviceConnecting: (deviceAddress) { _messageNotifier.value = "DeviceConnecting"; }, onDeviceConnected: (deviceAddress) { _messageNotifier.value = "DeviceConnected"; }, onProgressChanged: ( deviceAddress, percent, speed, avgSpeed, currentPart, partsTotal, ) { _messageNotifier.value = "$percent% speed:${avgSpeed.toStringAsFixed(2)}kb/s"; }, onError: ( String? deviceAddress, int? error, int? errorType, String? message, ) { _messageNotifier.value = "Error $error $errorType $message"; _countFail++; failedResults[device] = (failedResults[device] ?? 0)+1; }); } catch (e) { print(e); _messageNotifier.value = "升级异常: $e"; } } catch (e) { print(e); _messageNotifier.value = "异常: $e"; } setState(() { _targetDevice = null; }); await Future.delayed(const Duration(seconds: 2)); } } @override Widget build(BuildContext context) { List items = []; items.addAll(scanResults.where((element) => element.rssi.abs() < _filterRssi).where((element) => element.name.contains(_filterName ?? "") == _filterNameEquals).where((element) { if (_filterHardware?.isNotEmpty == true) { if (element.manufacturerData.length >= 6 && (_filterHardware == "${element.manufacturerData[3]}.${element.manufacturerData[4]}.${element.manufacturerData[5]}") == _filterHardwareEquals) { return true; } else { return false; } } return true; }).where((element) { if (_filterSofeware?.isNotEmpty == true) { if (element.manufacturerData.length >= 6 && (_filterSofeware == "${element.manufacturerData[3]}.${element.manufacturerData[4]}.${element.manufacturerData[0]}") == _filterSofewareEquals) { return true; } else { return false; } } return true; })); items.sort((a, b) { return a.rssi.abs() > b.rssi.abs() ? 1 : -1; }); return Scaffold( appBar: _dfuAll ? null : AppBar( title: const Text("发现设备"), actions: [ Padding( padding: const EdgeInsets.all(8.0), child: ElevatedButton( onPressed: () { setState(() { scanForDevices?.cancel(); scanForDevices = null; _selectAll = !_selectAll; }); }, child: _selectAll ? const Text("取消多选") : const Text("多选")), ), scanForDevices == null ? IconButton( onPressed: () { setState(() { _search(); }); }, icon: const Icon(Icons.search)) : IconButton( onPressed: () { setState(() { scanForDevices?.cancel(); scanForDevices = null; }); }, icon: const Icon(Icons.stop)) ], ), body: _dfuAll ? Container( width: double.infinity, height: double.infinity, color: Colors.white, child: Column( children: [ const SizedBox( height: 50, ), Text("升级固件(总数:$_countAll/成功:$_countSucc/失败:$_countFail)"), const SizedBox( height: 25, ), ConstrainedBox( constraints: const BoxConstraints(maxWidth: 140.0), child: Text( "${_targetDevice?.name}", style: const TextStyle(fontSize: 14, color: Color(0xff333333)), softWrap: true, ), ), const SizedBox( height: 25, ), ConstrainedBox( constraints: const BoxConstraints(maxWidth: 140.0), child: Text( "${_targetDevice?.id}", style: const TextStyle(fontSize: 12, color: Color(0xff999999)), ), ), const SizedBox( height: 25, ), Text( "${_targetDevice?..manufacturerData}", style: const TextStyle(fontSize: 12, color: Color(0xff999999)), ), const SizedBox( height: 25, ), ValueListenableBuilder( valueListenable: _messageNotifier, builder: (BuildContext context, String value, Widget? child) => Text( value, ), ), const SizedBox( height: 50, ), Container( padding: const EdgeInsets.all(12.0), height: 200, child: SingleChildScrollView( child: Text("升级列表 ... ${updateResults.map((e) => e.name).join(", ")}"), ), ) ], ), ) : Column( children: [ Container( padding: const EdgeInsets.all(12.0), child: Column( children: [ Row( children: [ const Text("rssi"), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text("< ${_filterRssi.toInt()}"), ), Expanded( child: Slider( min: 20, max: 100, value: _filterRssi, onChanged: (v) { setState(() { _filterRssi = v; }); }, ), ) ], ), Row( children: [ const Text("关键字"), Checkbox( value: _filterNameEquals, onChanged: (bool? value) { setState(() { _filterNameEquals = value ?? false; }); }, ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10.0), child: TextField( controller: TextEditingController.fromValue(TextEditingValue( text: _filterName ?? "", selection: TextSelection.fromPosition(TextPosition( affinity: TextAffinity.downstream, offset: _filterName == null ? 0 : _filterName?.length ?? 0, )))), onChanged: (v) { setState(() { _filterName = v; }); }, ), ), ), ElevatedButton( onPressed: () { setState(() { _filterName = "FUN_"; }); }, child: const Text("FUN"), ), const SizedBox( width: 10, ), ElevatedButton( onPressed: () { setState(() { _filterName = ""; }); }, child: const Text("CLEAR"), ), ], ), Row( children: [ Expanded( child: Row( children: [ const Text("固件"), Checkbox( value: _filterSofewareEquals, onChanged: (bool? value) { setState(() { _filterSofewareEquals = value ?? false; }); }, ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10.0), child: TextField( keyboardType: const TextInputType.numberWithOptions(decimal: true), onChanged: (v) { setState(() { _filterSofeware = v; }); }, ), ), ), ], ), ), Expanded( child: Row( children: [ const Text("硬件"), Checkbox( value: _filterHardwareEquals, onChanged: (bool? value) { setState(() { _filterHardwareEquals = value ?? false; }); }, ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 10.0), child: TextField( keyboardType: const TextInputType.numberWithOptions(decimal: true), onChanged: (v) { setState(() { _filterHardware = v; }); }, ), ), ), ], ), ), ], ), ], ), ), if (scanForDevices == null && items.isEmpty) Padding( padding: const EdgeInsets.all(40.0), child: ElevatedButton( onPressed: () { _search(); }, child: const Text("搜索设备"), ), ), if (scanForDevices == null) Padding( padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20), child: Column( children: [ const Padding( padding: EdgeInsets.all(16.0), child: Text("升级设备"), ), Row( mainAxisSize: MainAxisSize.min, children: [ ElevatedButton( onPressed: () async { setState(() { selectedResults.clear(); selectedResults.addAll(items); _dfuAll = true; }); scanForDevices?.cancel(); clearCache(); FilePickerResult? result = await FilePicker.platform.pickFiles(); String? path = result?.files.single.path ?? ""; if (path.isNotEmpty == true) { _dfu(path, 0); } }, child: const Text("模式 1,选取最优"), ), const SizedBox( width: 20, ), ElevatedButton( onPressed: () async { setState(() { selectedResults.clear(); selectedResults.addAll(items); _dfuAll = true; }); scanForDevices?.cancel(); clearCache(); FilePickerResult? result = await FilePicker.platform.pickFiles(); String? path = result?.files.single.path ?? ""; if (path.isNotEmpty == true) { _dfu(path, 1); } }, child: const Text("模式 2,列表随机"), ), ], ), ], ), ), Expanded( child: ListView.builder( itemBuilder: (context, index) { var e = items[index]; return GestureDetector( child: Card( child: Padding( padding: const EdgeInsets.all(12.0), child: Row( children: [ Expanded( child: Column( children: [ Text( e.name, style: Theme.of(context).textTheme.headline6, ), ConstrainedBox(constraints: const BoxConstraints(maxWidth: 140.0), child: Text(e.id)), Text("${e.manufacturerData}"), Row( children: [ Text("${e.rssi} dBm"), const SizedBox( width: 20, ), Text("#${index + 1}"), ], ), ], crossAxisAlignment: CrossAxisAlignment.start, ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: ElevatedButton( onPressed: () async { await showDialog( context: context, builder: (context) => Find(device: e), ); }, child: const Text("找鞋"), ), ), if (!_selectAll) ElevatedButton( onPressed: () { setState(() { scanForDevices?.cancel(); scanForDevices = null; }); Navigator.of(context).push(MaterialPageRoute(builder: (context) { return Connector( device: e, ); })); }, child: const Text("CONNECT"), ), if (_selectAll) Checkbox( value: selectedResults.contains(e), onChanged: (bool? value) { setState(() { if (value == true) { selectedResults.add(e); } else { selectedResults.remove(e); } }); }, ), ], ), ), ), ); }, itemCount: items.length, )), if (_selectAll) Padding( padding: const EdgeInsets.all(24.0), child: Row( children: [ ElevatedButton( onPressed: () async { scanForDevices?.cancel(); clearCache(); if (await showDialog( context: context, builder: (context) { return SimpleDialog( title: const Text("操作确认"), children: [ if ((_filterHardware?.length ?? 0) == 0) const Padding( padding: EdgeInsets.all(20.0), child: Text("请确保所选设备是同一个硬件版本,排除升级后出现不兼容的情况"), ), if ((_filterHardware?.length ?? 0) > 0) Padding( padding: const EdgeInsets.all(20.0), child: Text("升级对应的硬件版本 v${_filterHardware}"), ), Padding( padding: const EdgeInsets.all(20.0), child: ElevatedButton( onPressed: () { Navigator.pop(context, true); }, child: const Text("知道了"), ), ), ], ); }) != true) { return; } if (selectedResults.isEmpty) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('请选择需要升级的设备'), )); return; } FilePickerResult? result = await FilePicker.platform.pickFiles(); String? path = result?.files.single.path ?? ""; if (path.isNotEmpty == true) { File file = File(path); Navigator.of(context).push(MaterialPageRoute(builder: (context) { return DFUList( selectedResults: selectedResults, file: file, ); })); } else { // User canceled the picker } }, child: Text("批量DFU(${selectedResults.length})"), ), const SizedBox( width: 12, ), ElevatedButton( onPressed: () async { setState(() { selectedResults.clear(); selectedResults.addAll(items); }); }, child: Text("全选"), ), const SizedBox( width: 12, ), ElevatedButton( onPressed: () async { setState(() { selectedResults.clear(); }); }, child: Text("全不选"), ), ], ), ), ], ), ); } }