search_device.dart 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'dart:math';
  4. import 'dart:typed_data';
  5. import 'package:android_intent/android_intent.dart';
  6. import 'package:broadcast/broadcast.dart';
  7. import 'package:device_info/device_info.dart';
  8. import 'package:flutter/cupertino.dart';
  9. import 'package:flutter/material.dart';
  10. import 'package:flutter/scheduler.dart';
  11. import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
  12. import 'package:get_it/get_it.dart';
  13. import 'package:permission_handler/permission_handler.dart';
  14. import 'package:sport/application.dart';
  15. import 'package:sport/db/bluetooth_db.dart';
  16. import 'package:sport/provider/bluetooth.dart';
  17. import 'package:sport/utils/toast.dart';
  18. import 'package:sport/widgets/appbar.dart';
  19. import 'package:sport/widgets/button_cancel.dart';
  20. import 'package:sport/widgets/button_primary.dart';
  21. import 'package:sport/widgets/dialog/alert_dialog.dart';
  22. import 'package:sport/widgets/dialog/request_dialog.dart';
  23. import 'package:sport/widgets/dialog/search_device_connect.dart';
  24. import 'package:sport/widgets/image.dart';
  25. import 'package:sport/widgets/misc.dart';
  26. import 'package:umeng_common_sdk/umeng_common_sdk.dart';
  27. import 'package:url_launcher/url_launcher.dart';
  28. openSearchDeviceDialog(BuildContext context) async {
  29. if (Platform.isAndroid) {
  30. DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
  31. AndroidDeviceInfo info = await deviceInfo.androidInfo;
  32. if (info.version.sdkInt < 31) {
  33. final isLocationEnabled = await Broadcast.isLocationEnabled();
  34. if (!isLocationEnabled) {
  35. if (await showDialog(
  36. context: context,
  37. builder: (context) => CustomAlertDialog(
  38. title: '打开定位信息服务才能正常连接蓝牙设备',
  39. ok: () => Navigator.of(context).pop(true),
  40. textOk: "去打开",
  41. ),
  42. ) ==
  43. true) {
  44. await Broadcast.openSetting();
  45. }
  46. return;
  47. }
  48. PermissionStatus permissions = await Permission.locationWhenInUse.request();
  49. if (permissions.isGranted) {
  50. showDialog(context: context, barrierDismissible: false, builder: (_) => SearchDeviceDialog());
  51. } else {
  52. if (await showDialog(
  53. context: context,
  54. builder: (context) => CustomAlertDialog(title: '使用蓝牙功能需授权定位权限', ok: () => Navigator.of(context).pop(true)),
  55. ) ==
  56. true) {
  57. openAppSettings();
  58. } else {
  59. ToastUtil.show('使用蓝牙设备需要授权定位权限!');
  60. }
  61. return false;
  62. }
  63. } else {
  64. Map<Permission, PermissionStatus> statuses = await [
  65. Permission.bluetoothScan,
  66. Permission.bluetoothConnect,
  67. ].request();
  68. if (statuses.isNotEmpty == true && statuses.values.every((element) => element.isGranted)) {
  69. showDialog(context: context, barrierDismissible: false, builder: (_) => SearchDeviceDialog());
  70. } else {
  71. if (await showDialog(
  72. context: context,
  73. builder: (context) => CustomAlertDialog(title: '使用蓝牙功相应的系统权限', ok: () => Navigator.of(context).pop(true)),
  74. ) ==
  75. true) {
  76. openAppSettings();
  77. } else {
  78. ToastUtil.show('使用蓝牙设备需要蓝牙搜索权限和连接权限!');
  79. }
  80. return false;
  81. }
  82. }
  83. } else if (Platform.isIOS) {
  84. showDialog(context: context, barrierDismissible: false, builder: (_) => SearchDeviceDialog());
  85. }
  86. return true;
  87. }
  88. class SearchDeviceDialog extends StatelessWidget {
  89. @override
  90. Widget build(BuildContext context) {
  91. double height = MediaQuery.of(context).size.height / 5 * 3;
  92. return Dialog(
  93. elevation: 0,
  94. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
  95. child: Padding(
  96. padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12.0),
  97. child: ConstrainedBox(
  98. constraints: BoxConstraints(minHeight: height, minWidth: double.infinity, maxHeight: height),
  99. child: Column(
  100. mainAxisSize: MainAxisSize.min,
  101. children: <Widget>[
  102. Stack(
  103. alignment: Alignment.center,
  104. children: <Widget>[
  105. Center(
  106. child: Padding(
  107. padding: const EdgeInsets.all(6.0),
  108. child: Text("请选择鞋子", style: Theme.of(context).textTheme.headline3),
  109. ),
  110. ),
  111. Positioned(
  112. right: 0,
  113. top: 0,
  114. child: GestureDetector(
  115. behavior: HitTestBehavior.opaque,
  116. onTap: () => Navigator.pop(context),
  117. child: Padding(
  118. padding: const EdgeInsets.all(6.0),
  119. child: Image.asset("lib/assets/img/btn_close_big.png"),
  120. ),
  121. ),
  122. ),
  123. ],
  124. ),
  125. Expanded(
  126. child: StreamBuilder<BleStatus>(
  127. stream: FlutterReactiveBle().statusStream,
  128. initialData: BleStatus.unknown,
  129. builder: (c, snapshot) {
  130. final state = snapshot.data ?? BleStatus.unknown;
  131. if (state == BleStatus.ready) {
  132. return FindDevicesScreen();
  133. } else if (state == BleStatus.poweredOff) {
  134. return BluetoothOffScreen(state: state);
  135. }
  136. return Container(
  137. child: Center(child: SizedBox(
  138. width: 100,
  139. height: 100,
  140. child: CircularProgressIndicator(
  141. )),),
  142. );
  143. }),
  144. ),
  145. ],
  146. ),
  147. )),
  148. );
  149. }
  150. }
  151. class BluetoothOffScreen extends StatelessWidget {
  152. final BleStatus state;
  153. const BluetoothOffScreen({Key? key, required this.state}) : super(key: key);
  154. @override
  155. Widget build(BuildContext context) {
  156. return Column(
  157. mainAxisSize: MainAxisSize.min,
  158. children: <Widget>[
  159. SizedBox(
  160. height: 20,
  161. ),
  162. Image.asset("lib/assets/img/pop_icon_bluetooth.png"),
  163. SizedBox(
  164. height: 20,
  165. ),
  166. Text(
  167. '蓝牙未开启',
  168. style: Theme.of(context).textTheme.bodyText2!,
  169. ),
  170. SizedBox(
  171. height: 10,
  172. ),
  173. GestureDetector(
  174. behavior: HitTestBehavior.opaque,
  175. onTap: () async {
  176. if (Platform.isAndroid) {
  177. AndroidIntent intent = AndroidIntent(action: "android.bluetooth.adapter.action.REQUEST_ENABLE");
  178. await intent.launch();
  179. } else if (Platform.isIOS) {
  180. launch("App-Prefs:root=Bluetooth");
  181. }
  182. },
  183. child: Center(
  184. child: Row(
  185. mainAxisSize: MainAxisSize.min,
  186. children: <Widget>[
  187. Text(
  188. '打开蓝牙设置',
  189. style: Theme.of(context).textTheme.bodyText2!.copyWith(color: Theme.of(context).accentColor),
  190. ),
  191. SizedBox(
  192. width: 6,
  193. ),
  194. arrowRight6()
  195. ],
  196. ),
  197. ),
  198. ),
  199. Divider(
  200. height: 32,
  201. ),
  202. Text(
  203. '下窗弹窗点击如图所示位置',
  204. style: Theme.of(context).textTheme.subtitle2,
  205. ),
  206. Container(
  207. padding: EdgeInsets.fromLTRB(25.0, 7, 25.0, 16.0),
  208. child: Platform.isAndroid ? Image.asset("lib/assets/img/pop_img_bluetooth_android.png") : Image.asset("lib/assets/img/pop_img_bluetooth_ios.png"),
  209. ),
  210. ],
  211. );
  212. }
  213. }
  214. class FindDevicesScreen extends StatefulWidget {
  215. @override
  216. State<StatefulWidget> createState() => _FindDevicesScreen();
  217. }
  218. class _FindDevicesScreen extends State<FindDevicesScreen> with SingleTickerProviderStateMixin {
  219. late AnimationController _animationController;
  220. late Animation<double> _animation;
  221. bool _search = true;
  222. bool _error = false;
  223. int _time = 0;
  224. int _timeout = 10;
  225. List<DiscoveredDevice> scanResults = <DiscoveredDevice>[];
  226. late Bluetooth bluetooth;
  227. Map<String, Map<String, dynamic>> history = {};
  228. Timer? timer;
  229. late FlutterReactiveBle flutterReactiveBle;
  230. StreamSubscription? scanForDevices;
  231. @override
  232. void initState() {
  233. super.initState();
  234. bluetooth = GetIt.I<Bluetooth>();
  235. flutterReactiveBle = FlutterReactiveBle();
  236. _animationController = AnimationController(duration: Duration(seconds: 2), vsync: this);
  237. _animation = Tween(begin: .0, end: 1.0).animate(_animationController)
  238. ..addStatusListener((status) {
  239. if (status == AnimationStatus.completed) {
  240. _animationController.repeat();
  241. }
  242. });
  243. //开始动画
  244. _animationController.forward();
  245. startScan();
  246. SchedulerBinding.instance?.addPostFrameCallback((timeStamp) {
  247. if (bluetooth.isRightError) bluetooth.disconnectDevice("打开搜索界面时右鞋状态异常");
  248. });
  249. UmengCommonSdk.onEvent("shoe_search", {});
  250. }
  251. @override
  252. void dispose() {
  253. scanForDevices?.cancel();
  254. _animationController.dispose();
  255. super.dispose();
  256. }
  257. startScan() {
  258. timer?.cancel();
  259. scanForDevices?.cancel();
  260. history.clear();
  261. BluetoothDB().find().then((value) {
  262. value.forEach((e) {
  263. history[e[BluetoothDB.C_ID]] = e;
  264. });
  265. setState(() {});
  266. });
  267. setState(() {
  268. _time = 0;
  269. scanResults = [];
  270. _search = true;
  271. _error = false;
  272. });
  273. timer = Timer.periodic(Duration(seconds: 1), (timer) {
  274. if (!mounted) return;
  275. int t = ++_time;
  276. if (t % 3 == 0) {
  277. _updateData();
  278. }
  279. if (_time >= 10) {
  280. scanForDevices?.cancel();
  281. scanForDevices = null;
  282. timer.cancel();
  283. _updateData();
  284. }
  285. setState(() {
  286. });
  287. });
  288. scanForDevices = flutterReactiveBle.scanForDevices(withServices: [Uuid.parse(BLE_UUID)], scanMode: ScanMode.lowPower).listen((e) {
  289. if (e.name.isEmpty) return;
  290. // if (e.id == bluetooth.deviceId) return;
  291. if (e.name.toUpperCase().startsWith("SH") || e.name.toUpperCase().startsWith("FUN")) {
  292. final knownDeviceIndex = scanResults.indexWhere((d) => d.id == e.id);
  293. if (knownDeviceIndex >= 0) {
  294. scanResults[knownDeviceIndex] = e;
  295. } else {
  296. scanResults.add(e);
  297. }
  298. }
  299. })
  300. ..onDone(() {});
  301. }
  302. _updateData() {
  303. _search = scanForDevices != null;
  304. scanResults.sort((a, b) {
  305. int r = (history[b.id]?[BluetoothDB.C_TIMES] ?? 0).compareTo(history[a.id]?[BluetoothDB.C_TIMES] ?? 0);
  306. return r == 0
  307. ? a.rssi.abs() > b.rssi.abs()
  308. ? 1
  309. : -1
  310. : r;
  311. });
  312. }
  313. @override
  314. Widget build(BuildContext context) {
  315. int maxRssi = scanResults.isEmpty ? 0 : scanResults.reduce((value, element) => value.rssi > element.rssi ? value : element).rssi;
  316. return Column(
  317. children: <Widget>[
  318. Expanded(
  319. child: RefreshIndicator(
  320. color: Theme.of(context).colorScheme.secondary,
  321. onRefresh: () async {
  322. if (!_search) startScan();
  323. },
  324. child: CustomScrollView(
  325. slivers: [
  326. SliverToBoxAdapter(
  327. child: Column(
  328. children: [
  329. ValueListenableBuilder(
  330. valueListenable: bluetooth.deviceNotifier,
  331. builder: (BuildContext context, DiscoveredDevice? d, Widget? child) {
  332. if (d == null) return Container();
  333. return Column(
  334. children: <Widget>[
  335. Container(
  336. margin: const EdgeInsets.symmetric(vertical: 12.0),
  337. decoration: BoxDecoration(border: Border.all(color: Theme.of(context).accentColor), borderRadius: const BorderRadius.all(Radius.circular(10.0))),
  338. child: ListTile(
  339. contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6.0),
  340. title: Row(
  341. children: <Widget>[
  342. (history.containsKey(d.id) && (history[d.id]?[BluetoothDB.C_MARK] as String?)?.isNotEmpty == true)
  343. ? Column(
  344. crossAxisAlignment: CrossAxisAlignment.start,
  345. children: [
  346. Text(
  347. "${(history[d.id]?[BluetoothDB.C_MARK])}",
  348. overflow: TextOverflow.ellipsis,
  349. style: Theme.of(context).textTheme.subtitle1,
  350. ),
  351. Padding(
  352. padding: const EdgeInsets.only(top: 4.0),
  353. child: Text(
  354. d.name,
  355. overflow: TextOverflow.ellipsis,
  356. style: Theme.of(context).textTheme.bodyText1,
  357. ),
  358. ),
  359. ],
  360. )
  361. : Text(
  362. d.name,
  363. overflow: TextOverflow.ellipsis,
  364. style: Theme.of(context).textTheme.subtitle1,
  365. ),
  366. const SizedBox(
  367. height: 4,
  368. ),
  369. ],
  370. ),
  371. trailing: Padding(
  372. padding: const EdgeInsets.symmetric(vertical: 8.0),
  373. child: Row(
  374. mainAxisSize: MainAxisSize.min,
  375. children: <Widget>[
  376. Icon(
  377. Icons.link_off,
  378. color: Theme.of(context).accentColor,
  379. ),
  380. SizedBox(
  381. width: 4,
  382. ),
  383. Text(
  384. "断开连接",
  385. style: TextStyle(
  386. color: Theme.of(context).accentColor,
  387. fontSize: 12,
  388. ),
  389. strutStyle: fixedLine,
  390. )
  391. ],
  392. ),
  393. ),
  394. onTap: (){GetIt.I<Bluetooth>().clearDevice(deleteStep: true);},
  395. ),
  396. ),
  397. SizedBox(
  398. height: 16.0,
  399. ),
  400. Align(
  401. alignment: Alignment.centerLeft,
  402. child: Padding(
  403. padding: const EdgeInsets.symmetric(horizontal: 8.0),
  404. child: Text(
  405. "附近的设备",
  406. style: Theme.of(context).textTheme.bodyText1,
  407. ),
  408. ),
  409. ),
  410. ],
  411. );
  412. },
  413. ),
  414. Divider(),
  415. ],
  416. ),
  417. ),
  418. for (var i = 0; i < scanResults.length; i++)
  419. SliverPadding(
  420. padding: const EdgeInsets.symmetric(horizontal: 8.0),
  421. sliver: SliverToBoxAdapter(
  422. child: Column(
  423. children: <Widget>[
  424. ListTile(
  425. contentPadding: const EdgeInsets.symmetric(horizontal: 0),
  426. title: Row(
  427. children: [
  428. (history.containsKey(scanResults[i].id) && (history[scanResults[i].id]?[BluetoothDB.C_MARK] as String?)?.isNotEmpty == true)
  429. ? Column(
  430. crossAxisAlignment: CrossAxisAlignment.start,
  431. children: [
  432. Text(
  433. "${(history[scanResults[i].id]?[BluetoothDB.C_MARK])}",
  434. overflow: TextOverflow.ellipsis,
  435. style: Theme.of(context).textTheme.subtitle1,
  436. ),
  437. Padding(
  438. padding: const EdgeInsets.only(top: 4.0),
  439. child: Text(
  440. scanResults[i].name,
  441. overflow: TextOverflow.ellipsis,
  442. style: Theme.of(context).textTheme.bodyText1,
  443. ),
  444. ),
  445. ],
  446. )
  447. : Text(
  448. scanResults[i].name,
  449. overflow: TextOverflow.ellipsis,
  450. style: Theme.of(context).textTheme.subtitle1,
  451. ),
  452. const SizedBox(
  453. width: 10,
  454. ),
  455. Rssi(
  456. rssi: scanResults[i].rssi,
  457. index: maxRssi
  458. ),
  459. if (isDebugShoe)
  460. Text(
  461. "${scanResults[i].rssi}",
  462. style: Theme.of(context).textTheme.bodyText1?.copyWith(fontSize: 8),
  463. ),
  464. if (history.containsKey(scanResults[i].id))
  465. history.keys.first == scanResults[i].id
  466. ? Container(
  467. margin: const EdgeInsets.symmetric(horizontal: 8),
  468. padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
  469. decoration: BoxDecoration(
  470. borderRadius: BorderRadius.circular(50),
  471. border: Border.all(
  472. color: Theme.of(context).accentColor,
  473. width: .5,
  474. )),
  475. child: Text(
  476. "最近使用",
  477. style: Theme.of(context).textTheme.bodyText1!.copyWith(fontSize: 8, color: Theme.of(context).accentColor),
  478. ),
  479. )
  480. : Container(
  481. margin: const EdgeInsets.symmetric(horizontal: 8),
  482. padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
  483. decoration: BoxDecoration(
  484. borderRadius: BorderRadius.circular(50),
  485. border: Border.all(
  486. color: const Color(0xff999999),
  487. width: .5,
  488. )),
  489. child: Text(
  490. "曾经使用",
  491. style: Theme.of(context).textTheme.bodyText1!.copyWith(fontSize: 8),
  492. ),
  493. ),
  494. ],
  495. ),
  496. trailing: Container(
  497. width: 64,
  498. height: 30,
  499. alignment: Alignment.center,
  500. decoration: BoxDecoration(
  501. borderRadius: BorderRadius.all(Radius.circular(100)),
  502. border: Border.all(
  503. color: Theme.of(context).accentColor,
  504. )),
  505. child: Text(
  506. "连接",
  507. style: Theme.of(context).textTheme.bodyText1?.copyWith(color: Theme.of(context).colorScheme.secondary),
  508. ),
  509. ),
  510. onTap: () async{
  511. UmengCommonSdk.onEvent("shoe_search_connect", {});
  512. // await request(context, () async {
  513. // await scanForDevices?.cancel();
  514. // await GetIt.I<Bluetooth>().clearDevice(deleteStep: true);
  515. // await Future.delayed(Duration(seconds: 2));
  516. // return 1;
  517. // });
  518. await scanForDevices?.cancel();
  519. await GetIt.I<Bluetooth>().clearDevice(deleteStep: true);
  520. Navigator.pop(context);
  521. await showDialog(context: context, barrierDismissible: false, builder: (_) => SearchDeviceConnectDialog(device: scanResults[i], alias: history[scanResults[i].id]?[BluetoothDB.C_MARK],));
  522. },
  523. ),
  524. const Divider()
  525. ],
  526. ),
  527. ),
  528. ),
  529. if (_search && scanResults.isEmpty)
  530. SliverFillRemaining(
  531. child: Center(
  532. child: Column(
  533. mainAxisSize: MainAxisSize.min,
  534. children: <Widget>[
  535. Stack(
  536. alignment: Alignment.center,
  537. children: <Widget>[RotationTransition(turns: _animation, child: Image.asset("lib/assets/img/pop_image_circle.png")), Image.asset("lib/assets/img/pop_image_shoes.png")],
  538. ),
  539. SizedBox(
  540. height: 12,
  541. ),
  542. Text("搜索中...")
  543. ],
  544. ),
  545. ),
  546. ),
  547. if (!_search && _error)
  548. SliverFillRemaining(
  549. child: Center(
  550. child: Column(
  551. mainAxisSize: MainAxisSize.min,
  552. children: <Widget>[
  553. Image.asset("lib/assets/img/pop_image_noequipment.png"),
  554. SizedBox(
  555. height: 10,
  556. ),
  557. Text("蓝牙搜索异常,请重新尝试")
  558. ],
  559. ),
  560. ),
  561. ),
  562. if (!_search && scanResults.isEmpty)
  563. SliverFillRemaining(
  564. child: Center(
  565. child: Column(
  566. mainAxisSize: MainAxisSize.min,
  567. children: <Widget>[
  568. Image.asset("lib/assets/img/pop_image_noequipment.png"),
  569. SizedBox(
  570. height: 10,
  571. ),
  572. Text("暂无设备")
  573. ],
  574. ),
  575. ),
  576. ),
  577. ],
  578. ),
  579. ),
  580. ),
  581. Padding(
  582. padding: const EdgeInsets.all(8.0),
  583. child: _search
  584. ? CancelButton(
  585. height: 35,
  586. content: "搜索中(${max(0, _timeout - _time)})...",
  587. callback: () {
  588. // FlutterBluePlus.instance.stopScan().then((value) {
  589. // setState(() {
  590. // _search = false;
  591. // });
  592. // });
  593. },
  594. )
  595. : PrimaryButton(height: 35, content: "重新搜索", callback: () => startScan()),
  596. ),
  597. ],
  598. );
  599. }
  600. }
  601. class DeviceScreen extends StatefulWidget {
  602. const DeviceScreen({Key? key, required this.device}) : super(key: key);
  603. final DiscoveredDevice device;
  604. @override
  605. State<StatefulWidget> createState() {
  606. return _DeviceScreenState();
  607. }
  608. }
  609. class _DeviceScreenState extends State<DeviceScreen> {
  610. late TextEditingController _textEditingController;
  611. @override
  612. void initState() {
  613. super.initState();
  614. _textEditingController = TextEditingController();
  615. }
  616. List<int> _getRandomBytes() {
  617. final math = Random();
  618. return [math.nextInt(255), math.nextInt(255), math.nextInt(255), math.nextInt(255)];
  619. }
  620. @override
  621. Widget build(BuildContext context) {
  622. DiscoveredDevice device = widget.device;
  623. final bluetooth = GetIt.I<Bluetooth>();
  624. return Scaffold(
  625. appBar: buildAppBar(
  626. context,
  627. title: device.name,
  628. actions: <Widget>[
  629. StreamBuilder<ConnectionStateUpdate>(
  630. stream: FlutterReactiveBle().connectedDeviceStream.where((event) => event.deviceId == device.id),
  631. initialData: ConnectionStateUpdate(
  632. deviceId: device.id,
  633. connectionState: DeviceConnectionState.disconnected,
  634. failure: null,
  635. ),
  636. builder: (c, snapshot) {
  637. VoidCallback? onPressed;
  638. String text;
  639. switch (snapshot.data?.connectionState) {
  640. case DeviceConnectionState.connected:
  641. onPressed = () => FlutterReactiveBle().connectToDevice(id: device.id);
  642. text = 'DISCONNECT';
  643. break;
  644. case DeviceConnectionState.disconnected:
  645. onPressed = () => FlutterReactiveBle().clearGattCache(device.id);
  646. text = 'CONNECT';
  647. break;
  648. default:
  649. text = snapshot.data.toString().substring(21).toUpperCase();
  650. break;
  651. }
  652. return FlatButton(
  653. onPressed: onPressed,
  654. child: Text(
  655. text,
  656. style: Theme.of(context).primaryTextTheme.button!.copyWith(color: Colors.white),
  657. ));
  658. },
  659. )
  660. ],
  661. ),
  662. body: SingleChildScrollView(
  663. child: Column(
  664. children: <Widget>[
  665. Row(
  666. mainAxisAlignment: MainAxisAlignment.spaceAround,
  667. children: [
  668. ValueListenableBuilder(
  669. valueListenable: bluetooth.deviceReadNotifier,
  670. builder: (BuildContext context, bool value, Widget? child) => Container(
  671. width: 50,
  672. height: 50,
  673. color: value ? Colors.green : Colors.red,
  674. child: Center(
  675. child: Text(
  676. "读",
  677. style: TextStyle(color: Colors.white),
  678. ))),
  679. ),
  680. ValueListenableBuilder(
  681. valueListenable: bluetooth.deviceWriteNotifier,
  682. builder: (BuildContext context, bool value, Widget? child) => Container(width: 50, height: 50, color: value ? Colors.green : Colors.red, child: Center(child: Text("写", style: TextStyle(color: Colors.white)))),
  683. ),
  684. ValueListenableBuilder(
  685. valueListenable: bluetooth.deviceReadNotifyNotifier,
  686. builder: (BuildContext context, bool value, Widget? child) => Container(width: 50, height: 50, color: value ? Colors.green : Colors.red, child: Center(child: Text("通知", style: TextStyle(color: Colors.white)))),
  687. ),
  688. ],
  689. ),
  690. Padding(
  691. padding: const EdgeInsets.all(16.0),
  692. child: Row(
  693. children: [
  694. Expanded(
  695. child: CupertinoTextField(
  696. cursorColor: const Color(0xffFFC400),
  697. controller: _textEditingController,
  698. )),
  699. SizedBox(
  700. width: 10,
  701. ),
  702. RaisedButton(
  703. onPressed: () {
  704. var data = _textEditingController.value.text;
  705. if (data.isEmpty) return;
  706. List<int> list = data.split(" ").where((element) => element.isNotEmpty).map((e) => int.parse(e, radix: 16)).toList();
  707. print("test write $list --> ${Uint8List.fromList(list)}");
  708. bluetooth.writeUintList(Uint8List.fromList(list));
  709. },
  710. child: Text("发送"))
  711. ],
  712. ),
  713. ),
  714. Row(
  715. children: [
  716. RaisedButton(
  717. onPressed: () {
  718. bluetooth.queryDeviceStep(test: true);
  719. },
  720. child: Text("同步步数"),
  721. ),
  722. ValueListenableBuilder(
  723. valueListenable: GetIt.I<Bluetooth>().stepTotalNotifier,
  724. builder: (BuildContext context, int value, Widget? child) {
  725. if (value < 0) {
  726. return CircularProgressIndicator();
  727. }
  728. return Text(" 获得步数: $value");
  729. },
  730. ),
  731. ],
  732. ),
  733. RaisedButton(
  734. onPressed: () {
  735. bluetooth.resetData();
  736. },
  737. child: Text("清零"),
  738. ),
  739. RaisedButton(
  740. onPressed: () {
  741. bluetooth.setupGameMode(true);
  742. },
  743. child: Text("游戏模式开"),
  744. ),
  745. RaisedButton(
  746. onPressed: () {
  747. bluetooth.setupGameMode(false);
  748. },
  749. child: Text("游戏模式关"),
  750. ),
  751. RaisedButton(
  752. onPressed: () async {
  753. bluetooth.vibrate(GetIt.I<Bluetooth>().vibrateNotifier.value);
  754. },
  755. child: Text("发送震动"),
  756. ),
  757. ValueListenableBuilder(
  758. valueListenable: GetIt.I<Bluetooth>().vibrateNotifier,
  759. builder: (BuildContext context, int v, Widget? child) => Row(
  760. children: <Widget>[
  761. Slider(
  762. divisions: 9,
  763. value: v.toDouble(),
  764. min: 100,
  765. max: 1000,
  766. onChanged: (double value) {
  767. bluetooth.vibrateNotifier.value = value.toInt();
  768. },
  769. ),
  770. Text("$v")
  771. ],
  772. ),
  773. ),
  774. ValueListenableBuilder(
  775. valueListenable: GetIt.I<Bluetooth>().actionNotifier,
  776. builder: (BuildContext context, int value, Widget? child) => Text("当前动作: $value"),
  777. ),
  778. ValueListenableBuilder(
  779. valueListenable: GetIt.I<Bluetooth>().stepTotalNotifier,
  780. builder: (BuildContext context, int value, Widget? child) => Text("同步步数: $value"),
  781. ),
  782. ValueListenableBuilder(
  783. valueListenable: GetIt.I<Bluetooth>().stepNotifier,
  784. builder: (BuildContext context, int value, Widget? child) => Text("相对步数: $value"),
  785. ),
  786. ValueListenableBuilder(
  787. valueListenable: GetIt.I<Bluetooth>().byteNotifier,
  788. builder: (BuildContext context, List<int> value, Widget? child) => ConstrainedBox(
  789. constraints: BoxConstraints(minHeight: 200),
  790. child: Text(
  791. "接收: $value",
  792. ),
  793. ),
  794. ),
  795. ],
  796. ),
  797. ),
  798. );
  799. }
  800. }
  801. class Rssi extends StatelessWidget {
  802. final int rssi;
  803. final int index;
  804. const Rssi({Key? key, this.rssi = 0, this.index = 0}) : super(key: key);
  805. @override
  806. Widget build(BuildContext context) {
  807. int r = this.rssi.abs();
  808. int total = 4;
  809. int level = 1;
  810. if(r == index.abs()){
  811. level = 4;
  812. } else if(r <= 60){
  813. level = 3;
  814. }else if(r <= 80){
  815. level = 2;
  816. }
  817. double height = 12;
  818. return Container(
  819. height: height,
  820. child: Row(crossAxisAlignment: CrossAxisAlignment.end, children: [
  821. for (var i = 0; i < total; i++)
  822. Container(
  823. width: 3,
  824. margin: EdgeInsets.only(right: 2),
  825. height: height - (total - i - 1) * 2,
  826. decoration: BoxDecoration(color: level > i ? Theme.of(context).colorScheme.secondary : const Color(0xffdcdcdc), borderRadius: BorderRadius.circular(3)),
  827. ),
  828. ]),
  829. );
  830. }
  831. }