scanner.dart 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'dart:math';
  4. import 'package:file_picker/file_picker.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
  7. import 'package:nordic_dfu/nordic_dfu.dart';
  8. import 'package:nrf/app_subscription_state.dart';
  9. import 'package:nrf/connector.dart';
  10. import 'package:nrf/dfu_list.dart';
  11. import 'package:nrf/find.dart';
  12. import 'package:path_provider/path_provider.dart';
  13. import 'package:permission_handler/permission_handler.dart';
  14. import 'package:wakelock/wakelock.dart';
  15. const SH_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
  16. Future clearCache() async {
  17. await delDir(await getTemporaryDirectory());
  18. if (Platform.isAndroid) {
  19. var dir = await getExternalStorageDirectory();
  20. if (dir != null) await delDir(dir);
  21. }
  22. }
  23. ///递归方式删除目录
  24. Future<Null> delDir(FileSystemEntity file) async {
  25. print(("del path $file"));
  26. if (file is Directory) {
  27. try {
  28. final List<FileSystemEntity> children = file.listSync();
  29. for (final FileSystemEntity child in children) {
  30. await delDir(child);
  31. }
  32. } catch (e) {
  33. print(e);
  34. }
  35. } else {
  36. await file.delete();
  37. }
  38. }
  39. class Scanner extends StatefulWidget {
  40. const Scanner({Key? key}) : super(key: key);
  41. @override
  42. State<StatefulWidget> createState() => _State();
  43. }
  44. class _State extends State<Scanner> with SubscriptionState {
  45. final flutterReactiveBle = FlutterReactiveBle();
  46. bool _selectAll = false;
  47. double _filterRssi = 35;
  48. bool _filterNameEquals = true;
  49. bool _filterSofewareEquals = true;
  50. bool _filterHardwareEquals = true;
  51. String? _filterName = "";
  52. String? _filterSofeware = "";
  53. String? _filterHardware = "";
  54. StreamSubscription? scanForDevices;
  55. List<DiscoveredDevice> scanResults = <DiscoveredDevice>[];
  56. Set<DiscoveredDevice> selectedResults = {};
  57. bool _dfuAll = false;
  58. DiscoveredDevice? _targetDevice;
  59. int _countAll = 0;
  60. int _countSucc = 0;
  61. int _countFail = 0;
  62. final ValueNotifier<String> _messageNotifier = ValueNotifier<String>("");
  63. @override
  64. void initState() {
  65. super.initState();
  66. addSubscription(flutterReactiveBle.statusStream.listen((event) {
  67. print("bluetooth -- instance state $event");
  68. if (event == BleStatus.ready) {
  69. } else if (event == BleStatus.unknown) {
  70. } else if (event == BleStatus.poweredOff) {
  71. } else {}
  72. }));
  73. _grant();
  74. clearCache();
  75. Wakelock.enable();
  76. }
  77. _grant() async {
  78. if (Platform.isAndroid) {
  79. PermissionStatus permissions = await Permission.locationWhenInUse.request();
  80. if (permissions.isGranted) {
  81. } else {
  82. showDialog(
  83. context: context,
  84. builder: (context) => const SimpleDialog(title: Text('使用蓝牙功能需授权定位权限')),
  85. );
  86. }
  87. }
  88. }
  89. _search() {
  90. FocusScope.of(context).requestFocus(FocusNode());
  91. scanForDevices?.cancel();
  92. setState(() {
  93. selectedResults.clear();
  94. scanResults.clear();
  95. });
  96. scanForDevices = flutterReactiveBle.scanForDevices(withServices: [Uuid.parse(SH_UUID)]).listen((e) {
  97. if (e.name.isEmpty) return;
  98. // print("111111111111111111111111 ${e}");
  99. final knownDeviceIndex = scanResults.indexWhere((d) => d.id == e.id);
  100. if (knownDeviceIndex >= 0) {
  101. scanResults[knownDeviceIndex] = e;
  102. } else {
  103. scanResults.add(e);
  104. }
  105. setState(() {});
  106. })
  107. ..onDone(() {
  108. setState(() {
  109. scanForDevices = null;
  110. });
  111. });
  112. }
  113. List<DiscoveredDevice> updateResults = <DiscoveredDevice>[];
  114. Map<DiscoveredDevice, int> failedResults = {};
  115. _dfu(String path, int type) async {
  116. updateResults.clear();
  117. failedResults.clear();
  118. while (mounted) {
  119. try {
  120. _messageNotifier.value = "开始搜索...(M$type)";
  121. DiscoveredDevice device;
  122. if (type == 1) {
  123. List<DiscoveredDevice> scanResults = <DiscoveredDevice>[];
  124. Completer<bool> completer = Completer();
  125. int times = 0;
  126. Timer timeout = Timer.periodic(const Duration(seconds: 5), (t) {
  127. if(scanResults.isNotEmpty) {
  128. completer.complete(true);
  129. }else{
  130. _messageNotifier.value = "开始搜索...(M$type .... ${times++})";
  131. }
  132. });
  133. var stream = flutterReactiveBle.scanForDevices(withServices: [Uuid.parse(SH_UUID)], scanMode: ScanMode.lowPower);
  134. var listen = stream.listen((event) {
  135. if (scanResults.where((element) => element.id == event.id).isEmpty && (failedResults[event] ?? 0) < 3) {
  136. List<DiscoveredDevice> results = [event];
  137. scanResults.addAll(results
  138. .where((element) => updateResults.where((event) => element.id == event.id).isEmpty)
  139. .where((element) => element.rssi.abs() < _filterRssi).where((element) => element.name.contains(_filterName ?? "") == _filterNameEquals).where((element) {
  140. if (_filterHardware?.isNotEmpty == true) {
  141. if (element.manufacturerData.length >= 6 && (_filterHardware == "${element.manufacturerData[3]}.${element.manufacturerData[4]}.${element.manufacturerData[5]}") == _filterHardwareEquals) {
  142. return true;
  143. } else {
  144. return false;
  145. }
  146. }
  147. return true;
  148. }).where((element) {
  149. if (_filterSofeware?.isNotEmpty == true) {
  150. if (element.manufacturerData.length >= 6 && (_filterSofeware == "${element.manufacturerData[3]}.${element.manufacturerData[4]}.${element.manufacturerData[0]}") == _filterSofewareEquals) {
  151. return true;
  152. } else {
  153. return false;
  154. }
  155. }
  156. return true;
  157. }));
  158. }
  159. });
  160. await completer.future;
  161. listen.cancel();
  162. timeout.cancel();
  163. _messageNotifier.value = "搜索到 ${scanResults.map((e) => e.name).join(", ")}";
  164. if (scanResults.isEmpty) {
  165. await Future.delayed(const Duration(seconds: 2));
  166. continue;
  167. }
  168. device = scanResults[Random().nextInt(scanResults.length)];
  169. } else {
  170. device = await flutterReactiveBle
  171. .scanForDevices(withServices: [Uuid.parse(SH_UUID)])
  172. .where((element) => updateResults.where((event) => element.id == event.id).isEmpty)
  173. .where((element) => failedResults.keys.where((event) => element.id == event.id).isEmpty)
  174. .where((element) => element.rssi.abs() < _filterRssi)
  175. .where((element) => element.name.contains(_filterName ?? "") == _filterNameEquals)
  176. .where((element) {
  177. if (_filterHardware?.isNotEmpty == true) {
  178. if (element.manufacturerData.length >= 6 && (_filterHardware == "${element.manufacturerData[3]}.${element.manufacturerData[4]}.${element.manufacturerData[5]}") == _filterHardwareEquals) {
  179. return true;
  180. } else {
  181. return false;
  182. }
  183. }
  184. return true;
  185. })
  186. .firstWhere((element) {
  187. if (_filterSofeware?.isNotEmpty == true) {
  188. if (element.manufacturerData.length >= 6 && (_filterSofeware == "${element.manufacturerData[3]}.${element.manufacturerData[4]}.${element.manufacturerData[0]}") == _filterSofewareEquals) {
  189. return true;
  190. } else {
  191. return false;
  192. }
  193. }
  194. return true;
  195. });
  196. }
  197. setState(() {
  198. _countAll++;
  199. _targetDevice = device;
  200. });
  201. await Future.delayed(const Duration(seconds: 2));
  202. DateTime start = DateTime.now();
  203. try {
  204. await NordicDfu().startDfu(device.id, path, fileInAsset: false, onEnablingDfuMode: (deviceAddress) {
  205. _messageNotifier.value = "EnablingDfuMode";
  206. }, onDfuCompleted: (deviceAddress) {
  207. _messageNotifier.value = "DfuCompleted use:${DateTime.now().difference(start).inSeconds}s";
  208. _countSucc++;
  209. updateResults.add(device);
  210. }, onDfuProcessStarted: (deviceAddress) {
  211. _messageNotifier.value = "DfuProcessStarted";
  212. }, onDfuProcessStarting: (deviceAddress) {
  213. _messageNotifier.value = "DfuProcessStarting";
  214. }, onDeviceConnecting: (deviceAddress) {
  215. _messageNotifier.value = "DeviceConnecting";
  216. }, onDeviceConnected: (deviceAddress) {
  217. _messageNotifier.value = "DeviceConnected";
  218. }, onProgressChanged: (
  219. deviceAddress,
  220. percent,
  221. speed,
  222. avgSpeed,
  223. currentPart,
  224. partsTotal,
  225. ) {
  226. _messageNotifier.value = "$percent% speed:${avgSpeed.toStringAsFixed(2)}kb/s";
  227. }, onError: (
  228. String? deviceAddress,
  229. int? error,
  230. int? errorType,
  231. String? message,
  232. ) {
  233. _messageNotifier.value = "Error $error $errorType $message";
  234. _countFail++;
  235. failedResults[device] = (failedResults[device] ?? 0)+1;
  236. });
  237. } catch (e) {
  238. print(e);
  239. _messageNotifier.value = "升级异常: $e";
  240. }
  241. } catch (e) {
  242. print(e);
  243. _messageNotifier.value = "异常: $e";
  244. }
  245. setState(() {
  246. _targetDevice = null;
  247. });
  248. await Future.delayed(const Duration(seconds: 2));
  249. }
  250. }
  251. @override
  252. Widget build(BuildContext context) {
  253. List<DiscoveredDevice> items = <DiscoveredDevice>[];
  254. items.addAll(scanResults.where((element) => element.rssi.abs() < _filterRssi).where((element) => element.name.contains(_filterName ?? "") == _filterNameEquals).where((element) {
  255. if (_filterHardware?.isNotEmpty == true) {
  256. if (element.manufacturerData.length >= 6 && (_filterHardware == "${element.manufacturerData[3]}.${element.manufacturerData[4]}.${element.manufacturerData[5]}") == _filterHardwareEquals) {
  257. return true;
  258. } else {
  259. return false;
  260. }
  261. }
  262. return true;
  263. }).where((element) {
  264. if (_filterSofeware?.isNotEmpty == true) {
  265. if (element.manufacturerData.length >= 6 && (_filterSofeware == "${element.manufacturerData[3]}.${element.manufacturerData[4]}.${element.manufacturerData[0]}") == _filterSofewareEquals) {
  266. return true;
  267. } else {
  268. return false;
  269. }
  270. }
  271. return true;
  272. }));
  273. items.sort((a, b) {
  274. return a.rssi.abs() > b.rssi.abs() ? 1 : -1;
  275. });
  276. return Scaffold(
  277. appBar: _dfuAll
  278. ? null
  279. : AppBar(
  280. title: const Text("发现设备"),
  281. actions: [
  282. Padding(
  283. padding: const EdgeInsets.all(8.0),
  284. child: ElevatedButton(
  285. onPressed: () {
  286. setState(() {
  287. scanForDevices?.cancel();
  288. scanForDevices = null;
  289. _selectAll = !_selectAll;
  290. });
  291. },
  292. child: _selectAll ? const Text("取消多选") : const Text("多选")),
  293. ),
  294. scanForDevices == null
  295. ? IconButton(
  296. onPressed: () {
  297. setState(() {
  298. _search();
  299. });
  300. },
  301. icon: const Icon(Icons.search))
  302. : IconButton(
  303. onPressed: () {
  304. setState(() {
  305. scanForDevices?.cancel();
  306. scanForDevices = null;
  307. });
  308. },
  309. icon: const Icon(Icons.stop))
  310. ],
  311. ),
  312. body: _dfuAll
  313. ? Container(
  314. width: double.infinity,
  315. height: double.infinity,
  316. color: Colors.white,
  317. child: Column(
  318. children: [
  319. const SizedBox(
  320. height: 50,
  321. ),
  322. Text("升级固件(总数:$_countAll/成功:$_countSucc/失败:$_countFail)"),
  323. const SizedBox(
  324. height: 25,
  325. ),
  326. ConstrainedBox(
  327. constraints: const BoxConstraints(maxWidth: 140.0),
  328. child: Text(
  329. "${_targetDevice?.name}",
  330. style: const TextStyle(fontSize: 14, color: Color(0xff333333)),
  331. softWrap: true,
  332. ),
  333. ),
  334. const SizedBox(
  335. height: 25,
  336. ),
  337. ConstrainedBox(
  338. constraints: const BoxConstraints(maxWidth: 140.0),
  339. child: Text(
  340. "${_targetDevice?.id}",
  341. style: const TextStyle(fontSize: 12, color: Color(0xff999999)),
  342. ),
  343. ),
  344. const SizedBox(
  345. height: 25,
  346. ),
  347. Text(
  348. "${_targetDevice?..manufacturerData}",
  349. style: const TextStyle(fontSize: 12, color: Color(0xff999999)),
  350. ),
  351. const SizedBox(
  352. height: 25,
  353. ),
  354. ValueListenableBuilder(
  355. valueListenable: _messageNotifier,
  356. builder: (BuildContext context, String value, Widget? child) => Text(
  357. value,
  358. ),
  359. ),
  360. const SizedBox(
  361. height: 50,
  362. ),
  363. Container(
  364. padding: const EdgeInsets.all(12.0),
  365. height: 200,
  366. child: SingleChildScrollView(
  367. child: Text("升级列表 ... ${updateResults.map((e) => e.name).join(", ")}"),
  368. ),
  369. )
  370. ],
  371. ),
  372. )
  373. : Column(
  374. children: [
  375. Container(
  376. padding: const EdgeInsets.all(12.0),
  377. child: Column(
  378. children: [
  379. Row(
  380. children: [
  381. const Text("rssi"),
  382. Padding(
  383. padding: const EdgeInsets.symmetric(horizontal: 8.0),
  384. child: Text("< ${_filterRssi.toInt()}"),
  385. ),
  386. Expanded(
  387. child: Slider(
  388. min: 20,
  389. max: 100,
  390. value: _filterRssi,
  391. onChanged: (v) {
  392. setState(() {
  393. _filterRssi = v;
  394. });
  395. },
  396. ),
  397. )
  398. ],
  399. ),
  400. Row(
  401. children: [
  402. const Text("关键字"),
  403. Checkbox(
  404. value: _filterNameEquals,
  405. onChanged: (bool? value) {
  406. setState(() {
  407. _filterNameEquals = value ?? false;
  408. });
  409. },
  410. ),
  411. Expanded(
  412. child: Padding(
  413. padding: const EdgeInsets.symmetric(horizontal: 10.0),
  414. child: TextField(
  415. controller: TextEditingController.fromValue(TextEditingValue(
  416. text: _filterName ?? "",
  417. selection: TextSelection.fromPosition(TextPosition(
  418. affinity: TextAffinity.downstream,
  419. offset: _filterName == null ? 0 : _filterName?.length ?? 0,
  420. )))),
  421. onChanged: (v) {
  422. setState(() {
  423. _filterName = v;
  424. });
  425. },
  426. ),
  427. ),
  428. ),
  429. ElevatedButton(
  430. onPressed: () {
  431. setState(() {
  432. _filterName = "FUN_";
  433. });
  434. },
  435. child: const Text("FUN"),
  436. ),
  437. const SizedBox(
  438. width: 10,
  439. ),
  440. ElevatedButton(
  441. onPressed: () {
  442. setState(() {
  443. _filterName = "";
  444. });
  445. },
  446. child: const Text("CLEAR"),
  447. ),
  448. ],
  449. ),
  450. Row(
  451. children: [
  452. Expanded(
  453. child: Row(
  454. children: [
  455. const Text("固件"),
  456. Checkbox(
  457. value: _filterSofewareEquals,
  458. onChanged: (bool? value) {
  459. setState(() {
  460. _filterSofewareEquals = value ?? false;
  461. });
  462. },
  463. ),
  464. Expanded(
  465. child: Padding(
  466. padding: const EdgeInsets.symmetric(horizontal: 10.0),
  467. child: TextField(
  468. keyboardType: const TextInputType.numberWithOptions(decimal: true),
  469. onChanged: (v) {
  470. setState(() {
  471. _filterSofeware = v;
  472. });
  473. },
  474. ),
  475. ),
  476. ),
  477. ],
  478. ),
  479. ),
  480. Expanded(
  481. child: Row(
  482. children: [
  483. const Text("硬件"),
  484. Checkbox(
  485. value: _filterHardwareEquals,
  486. onChanged: (bool? value) {
  487. setState(() {
  488. _filterHardwareEquals = value ?? false;
  489. });
  490. },
  491. ),
  492. Expanded(
  493. child: Padding(
  494. padding: const EdgeInsets.symmetric(horizontal: 10.0),
  495. child: TextField(
  496. keyboardType: const TextInputType.numberWithOptions(decimal: true),
  497. onChanged: (v) {
  498. setState(() {
  499. _filterHardware = v;
  500. });
  501. },
  502. ),
  503. ),
  504. ),
  505. ],
  506. ),
  507. ),
  508. ],
  509. ),
  510. ],
  511. ),
  512. ),
  513. if (scanForDevices == null && items.isEmpty)
  514. Padding(
  515. padding: const EdgeInsets.all(40.0),
  516. child: ElevatedButton(
  517. onPressed: () {
  518. _search();
  519. },
  520. child: const Text("搜索设备"),
  521. ),
  522. ),
  523. if (scanForDevices == null)
  524. Padding(
  525. padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20),
  526. child: Column(
  527. children: [
  528. const Padding(
  529. padding: EdgeInsets.all(16.0),
  530. child: Text("升级设备"),
  531. ),
  532. Row(
  533. mainAxisSize: MainAxisSize.min,
  534. children: [
  535. ElevatedButton(
  536. onPressed: () async {
  537. setState(() {
  538. selectedResults.clear();
  539. selectedResults.addAll(items);
  540. _dfuAll = true;
  541. });
  542. scanForDevices?.cancel();
  543. clearCache();
  544. FilePickerResult? result = await FilePicker.platform.pickFiles();
  545. String? path = result?.files.single.path ?? "";
  546. if (path.isNotEmpty == true) {
  547. _dfu(path, 0);
  548. }
  549. },
  550. child: const Text("模式 1,选取最优"),
  551. ),
  552. const SizedBox(
  553. width: 20,
  554. ),
  555. ElevatedButton(
  556. onPressed: () async {
  557. setState(() {
  558. selectedResults.clear();
  559. selectedResults.addAll(items);
  560. _dfuAll = true;
  561. });
  562. scanForDevices?.cancel();
  563. clearCache();
  564. FilePickerResult? result = await FilePicker.platform.pickFiles();
  565. String? path = result?.files.single.path ?? "";
  566. if (path.isNotEmpty == true) {
  567. _dfu(path, 1);
  568. }
  569. },
  570. child: const Text("模式 2,列表随机"),
  571. ),
  572. ],
  573. ),
  574. ],
  575. ),
  576. ),
  577. Expanded(
  578. child: ListView.builder(
  579. itemBuilder: (context, index) {
  580. var e = items[index];
  581. return GestureDetector(
  582. child: Card(
  583. child: Padding(
  584. padding: const EdgeInsets.all(12.0),
  585. child: Row(
  586. children: [
  587. Expanded(
  588. child: Column(
  589. children: [
  590. Text(
  591. e.name,
  592. style: Theme.of(context).textTheme.headline6,
  593. ),
  594. ConstrainedBox(constraints: const BoxConstraints(maxWidth: 140.0), child: Text(e.id)),
  595. Text("${e.manufacturerData}"),
  596. Row(
  597. children: [
  598. Text("${e.rssi} dBm"),
  599. const SizedBox(
  600. width: 20,
  601. ),
  602. Text("#${index + 1}"),
  603. ],
  604. ),
  605. ],
  606. crossAxisAlignment: CrossAxisAlignment.start,
  607. ),
  608. ),
  609. Padding(
  610. padding: const EdgeInsets.symmetric(horizontal: 8.0),
  611. child: ElevatedButton(
  612. onPressed: () async {
  613. await showDialog(
  614. context: context,
  615. builder: (context) => Find(device: e),
  616. );
  617. },
  618. child: const Text("找鞋"),
  619. ),
  620. ),
  621. if (!_selectAll)
  622. ElevatedButton(
  623. onPressed: () {
  624. setState(() {
  625. scanForDevices?.cancel();
  626. scanForDevices = null;
  627. });
  628. Navigator.of(context).push(MaterialPageRoute(builder: (context) {
  629. return Connector(
  630. device: e,
  631. );
  632. }));
  633. },
  634. child: const Text("CONNECT"),
  635. ),
  636. if (_selectAll)
  637. Checkbox(
  638. value: selectedResults.contains(e),
  639. onChanged: (bool? value) {
  640. setState(() {
  641. if (value == true) {
  642. selectedResults.add(e);
  643. } else {
  644. selectedResults.remove(e);
  645. }
  646. });
  647. },
  648. ),
  649. ],
  650. ),
  651. ),
  652. ),
  653. );
  654. },
  655. itemCount: items.length,
  656. )),
  657. if (_selectAll)
  658. Padding(
  659. padding: const EdgeInsets.all(24.0),
  660. child: Row(
  661. children: [
  662. ElevatedButton(
  663. onPressed: () async {
  664. scanForDevices?.cancel();
  665. clearCache();
  666. if (await showDialog(
  667. context: context,
  668. builder: (context) {
  669. return SimpleDialog(
  670. title: const Text("操作确认"),
  671. children: [
  672. if ((_filterHardware?.length ?? 0) == 0)
  673. const Padding(
  674. padding: EdgeInsets.all(20.0),
  675. child: Text("请确保所选设备是同一个硬件版本,排除升级后出现不兼容的情况"),
  676. ),
  677. if ((_filterHardware?.length ?? 0) > 0)
  678. Padding(
  679. padding: const EdgeInsets.all(20.0),
  680. child: Text("升级对应的硬件版本 v${_filterHardware}"),
  681. ),
  682. Padding(
  683. padding: const EdgeInsets.all(20.0),
  684. child: ElevatedButton(
  685. onPressed: () {
  686. Navigator.pop(context, true);
  687. },
  688. child: const Text("知道了"),
  689. ),
  690. ),
  691. ],
  692. );
  693. }) !=
  694. true) {
  695. return;
  696. }
  697. if (selectedResults.isEmpty) {
  698. ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
  699. content: Text('请选择需要升级的设备'),
  700. ));
  701. return;
  702. }
  703. FilePickerResult? result = await FilePicker.platform.pickFiles();
  704. String? path = result?.files.single.path ?? "";
  705. if (path.isNotEmpty == true) {
  706. File file = File(path);
  707. Navigator.of(context).push(MaterialPageRoute(builder: (context) {
  708. return DFUList(
  709. selectedResults: selectedResults,
  710. file: file,
  711. );
  712. }));
  713. } else {
  714. // User canceled the picker
  715. }
  716. },
  717. child: Text("批量DFU(${selectedResults.length})"),
  718. ),
  719. const SizedBox(
  720. width: 12,
  721. ),
  722. ElevatedButton(
  723. onPressed: () async {
  724. setState(() {
  725. selectedResults.clear();
  726. selectedResults.addAll(items);
  727. });
  728. },
  729. child: Text("全选"),
  730. ),
  731. const SizedBox(
  732. width: 12,
  733. ),
  734. ElevatedButton(
  735. onPressed: () async {
  736. setState(() {
  737. selectedResults.clear();
  738. });
  739. },
  740. child: Text("全不选"),
  741. ),
  742. ],
  743. ),
  744. ),
  745. ],
  746. ),
  747. );
  748. }
  749. }