search_device.dart 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'dart:math';
  4. import 'package:android_intent/android_intent.dart';
  5. import 'package:flutter/cupertino.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter_blue/flutter_blue.dart';
  8. import 'package:provider/provider.dart';
  9. import 'package:sport/pages/my/device_info_page.dart';
  10. import 'package:sport/provider/bluetooth.dart';
  11. import 'package:sport/widgets/button_cancel.dart';
  12. import 'package:sport/widgets/button_primary.dart';
  13. import 'package:sport/widgets/image.dart';
  14. import 'package:sport/widgets/misc.dart';
  15. import 'package:url_launcher/url_launcher.dart';
  16. class SearchDeviceDialog extends StatefulWidget {
  17. @override
  18. State<StatefulWidget> createState() => _SearchDeviceDialog();
  19. }
  20. class _SearchDeviceDialog extends State<SearchDeviceDialog> {
  21. @override
  22. void dispose() {
  23. super.dispose();
  24. FlutterBlue.instance.stopScan();
  25. }
  26. @override
  27. Widget build(BuildContext context) {
  28. return Dialog(
  29. elevation: 0,
  30. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
  31. child: Padding(
  32. padding: const EdgeInsets.all(8.0),
  33. child: ConstrainedBox(
  34. constraints: BoxConstraints(maxHeight: 350, minWidth: double.infinity),
  35. child: Column(
  36. children: <Widget>[
  37. Stack(
  38. alignment: Alignment.center,
  39. children: <Widget>[
  40. Center(
  41. child: Padding(
  42. padding: const EdgeInsets.all(6.0),
  43. child: Text("请选择鞋子", style: Theme.of(context).textTheme.headline3),
  44. ),
  45. ),
  46. Positioned(
  47. right: 0,
  48. top: 0,
  49. child: GestureDetector(
  50. behavior: HitTestBehavior.opaque,
  51. onTap: () => Navigator.pop(context),
  52. child: Padding(
  53. padding: const EdgeInsets.all(6.0),
  54. child: Image.asset("lib/assets/img/btn_close_big.png"),
  55. ),
  56. ),
  57. ),
  58. ],
  59. ),
  60. Divider(),
  61. Expanded(
  62. child: StreamBuilder<BluetoothState>(
  63. stream: FlutterBlue.instance.state,
  64. initialData: BluetoothState.unknown,
  65. builder: (c, snapshot) {
  66. final state = snapshot.data;
  67. if (state == BluetoothState.on) {
  68. return FindDevicesScreen();
  69. }
  70. return BluetoothOffScreen(state: state);
  71. }),
  72. ),
  73. ],
  74. ),
  75. )),
  76. );
  77. }
  78. }
  79. class BluetoothOffScreen extends StatelessWidget {
  80. const BluetoothOffScreen({Key key, this.state}) : super(key: key);
  81. final BluetoothState state;
  82. @override
  83. Widget build(BuildContext context) {
  84. return Column(
  85. mainAxisSize: MainAxisSize.min,
  86. children: <Widget>[
  87. SizedBox(
  88. height: 60,
  89. ),
  90. Image.asset("lib/assets/img/pop_image_close.png"),
  91. SizedBox(
  92. height: 20,
  93. ),
  94. Text(
  95. '搜索失败,请确认蓝牙是否打开',
  96. style: Theme.of(context).textTheme.bodyText2.copyWith(color: Color(0xff666666)),
  97. ),
  98. SizedBox(
  99. height: 10,
  100. ),
  101. GestureDetector(
  102. behavior: HitTestBehavior.opaque,
  103. onTap: () async {
  104. if (Platform.isAndroid) {
  105. AndroidIntent intent = AndroidIntent(action: "android.bluetooth.adapter.action.REQUEST_ENABLE");
  106. await intent.launch();
  107. } else if (Platform.isIOS) {
  108. launch("App-Prefs:root=Bluetooth ");
  109. }
  110. },
  111. child: Center(
  112. child: Row(
  113. mainAxisSize: MainAxisSize.min,
  114. children: <Widget>[
  115. Text(
  116. '打开蓝牙设置',
  117. style: Theme.of(context).textTheme.bodyText2.copyWith(color: Theme.of(context).accentColor),
  118. ),
  119. SizedBox(
  120. width: 6,
  121. ),
  122. arrowRight6()
  123. ],
  124. ),
  125. ),
  126. ),
  127. ],
  128. );
  129. }
  130. }
  131. class FindDevicesScreen extends StatefulWidget {
  132. @override
  133. State<StatefulWidget> createState() => _FindDevicesScreen();
  134. }
  135. class _FindDevicesScreen extends State<FindDevicesScreen> with SingleTickerProviderStateMixin {
  136. AnimationController _animationController;
  137. Animation _animation;
  138. bool _search = true;
  139. StreamSubscription streamSubscription;
  140. @override
  141. void initState() {
  142. _animationController = AnimationController(duration: Duration(seconds: 2), vsync: this);
  143. _animation = Tween(begin: .0, end: 1.0).animate(_animationController)
  144. ..addStatusListener((status) {
  145. if (status == AnimationStatus.completed) {
  146. _animationController.repeat();
  147. }
  148. });
  149. //开始动画
  150. _animationController.forward();
  151. super.initState();
  152. _search = true;
  153. startScan();
  154. streamSubscription = Provider.of<Bluetooth>(context, listen: false).stateStream.listen((event) async {
  155. await Future.delayed(Duration(seconds: 2));
  156. Navigator.pop(context);
  157. });
  158. }
  159. startScan() async {
  160. setState(() {
  161. _search = true;
  162. });
  163. FlutterBlue.instance.startScan(timeout: Duration(seconds: 10)).whenComplete(() {
  164. setState(() {
  165. _search = false;
  166. });
  167. });
  168. }
  169. @override
  170. void dispose() {
  171. _animationController?.dispose();
  172. super.dispose();
  173. streamSubscription?.cancel();
  174. }
  175. @override
  176. Widget build(BuildContext context) {
  177. return Column(
  178. children: <Widget>[
  179. Expanded(
  180. child: SingleChildScrollView(
  181. child: Column(
  182. children: <Widget>[
  183. if (_search)
  184. Padding(
  185. padding: const EdgeInsets.symmetric(vertical: 30),
  186. child: Column(
  187. children: <Widget>[
  188. Stack(
  189. alignment: Alignment.center,
  190. children: <Widget>[
  191. RotationTransition(turns: _animation, child: Image.asset("lib/assets/img/pop_image_circle.png")),
  192. Image.asset("lib/assets/img/pop_image_shoes.png")
  193. ],
  194. ),
  195. SizedBox(
  196. height: 12,
  197. ),
  198. Text("搜索中...")
  199. ],
  200. ),
  201. ),
  202. // if (!_search)
  203. // StreamBuilder<List<BluetoothDevice>>(
  204. // stream: Stream.fromFuture(FlutterBlue.instance.connectedDevices),
  205. // initialData: [],
  206. // builder: (c, snapshot) => Column(
  207. // children: snapshot.data
  208. // .map((d) => Column(
  209. // children: <Widget>[
  210. // ListTile(
  211. // title: Column(
  212. // mainAxisAlignment: MainAxisAlignment.start,
  213. // crossAxisAlignment: CrossAxisAlignment.start,
  214. // children: <Widget>[
  215. // Text(
  216. // d.name,
  217. // overflow: TextOverflow.ellipsis,
  218. // style: Theme.of(context).textTheme.headline3,
  219. // ),
  220. // SizedBox(
  221. // height: 4,
  222. // ),
  223. // Text(
  224. // d.id.toString(),
  225. // style: Theme.of(context).textTheme.caption,
  226. // ),
  227. // ],
  228. // ),
  229. // trailing: StreamBuilder<BluetoothDeviceState>(
  230. // stream: d.state,
  231. // initialData: BluetoothDeviceState.disconnected,
  232. // builder: (c, snapshot) {
  233. // return GestureDetector(
  234. // onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => DeviceInfoPage())),
  235. // child: Container(
  236. // width: 58,
  237. // height: 30,
  238. // alignment: Alignment.center,
  239. // decoration: BoxDecoration(
  240. // shape: BoxShape.rectangle,
  241. // borderRadius: BorderRadius.all(Radius.circular(100)),
  242. // color: Theme.of(context).accentColor),
  243. // child: Text(
  244. // snapshot.data == BluetoothDeviceState.connected ? "已连接" : "未连接",
  245. // style: TextStyle(color: Colors.white, fontSize: 12),
  246. // ),
  247. // ),
  248. // );
  249. // },
  250. // ),
  251. // ),
  252. // Divider()
  253. // ],
  254. // ))
  255. // .toList(),
  256. // ),
  257. // ),
  258. if (!_search)
  259. StreamBuilder<List<ScanResult>>(
  260. stream: FlutterBlue.instance.scanResults,
  261. initialData: [],
  262. builder: (c, snapshot) {
  263. var bluetooth = Provider.of<Bluetooth>(context, listen: false);
  264. var list = snapshot.data
  265. .where((element) => element.device.name.isNotEmpty)
  266. .toList();
  267. list.sort((a, b) => bluetooth.device == a.device ? -1 : 1);
  268. return Column(
  269. children: list.isEmpty == true
  270. ? [
  271. Padding(
  272. padding: const EdgeInsets.all(30.0),
  273. child: Center(
  274. child: Column(
  275. children: <Widget>[
  276. Image.asset("lib/assets/img/pop_image_noequipment.png"),
  277. SizedBox(
  278. height: 10,
  279. ),
  280. Text("暂无设备")
  281. ],
  282. ),
  283. ))
  284. ]
  285. : list
  286. .map(
  287. (r) => Column(
  288. children: <Widget>[
  289. ScanResultTile(
  290. result: r,
  291. onTap: () async {
  292. await Provider.of<Bluetooth>(context, listen: false).saveDevice(r.device);
  293. setState(() {});
  294. },
  295. ),
  296. Divider()
  297. ],
  298. ),
  299. )
  300. .toList(),
  301. );
  302. },
  303. ),
  304. ],
  305. ),
  306. ),
  307. ),
  308. Padding(
  309. padding: const EdgeInsets.all(8.0),
  310. child: StreamBuilder<bool>(
  311. stream: FlutterBlue.instance.isScanning,
  312. initialData: false,
  313. builder: (c, snapshot) {
  314. if (snapshot.data) {
  315. return CancelButton(
  316. height: 35,
  317. content: "取消",
  318. callback: () {
  319. FlutterBlue.instance.stopScan();
  320. setState(() {
  321. _search = false;
  322. });
  323. },
  324. );
  325. } else {
  326. return PrimaryButton(height: 35, content: "重新搜索", callback: () => startScan());
  327. }
  328. },
  329. ),
  330. ),
  331. ],
  332. );
  333. }
  334. }
  335. class DeviceScreen extends StatelessWidget {
  336. const DeviceScreen({Key key, this.device}) : super(key: key);
  337. final BluetoothDevice device;
  338. List<int> _getRandomBytes() {
  339. final math = Random();
  340. return [math.nextInt(255), math.nextInt(255), math.nextInt(255), math.nextInt(255)];
  341. }
  342. List<Widget> _buildServiceTiles(List<BluetoothService> services) {
  343. return services
  344. .map(
  345. (s) => ServiceTile(
  346. service: s,
  347. characteristicTiles: s.characteristics
  348. .map(
  349. (c) => CharacteristicTile(
  350. characteristic: c,
  351. onReadPressed: () => c.read(),
  352. onWritePressed: () async {
  353. await c.write(_getRandomBytes(), withoutResponse: true);
  354. await c.read();
  355. },
  356. onNotificationPressed: () async {
  357. await c.setNotifyValue(!c.isNotifying);
  358. c.value.listen((value) {
  359. print(value.length);
  360. });
  361. },
  362. descriptorTiles: c.descriptors
  363. .map(
  364. (d) => DescriptorTile(
  365. descriptor: d,
  366. onReadPressed: () => d.read(),
  367. onWritePressed: () => d.write(_getRandomBytes()),
  368. ),
  369. )
  370. .toList(),
  371. ),
  372. )
  373. .toList(),
  374. ),
  375. )
  376. .toList();
  377. }
  378. @override
  379. Widget build(BuildContext context) {
  380. return Scaffold(
  381. appBar: AppBar(
  382. title: Text(device.name),
  383. actions: <Widget>[
  384. StreamBuilder<BluetoothDeviceState>(
  385. stream: device.state,
  386. initialData: BluetoothDeviceState.connecting,
  387. builder: (c, snapshot) {
  388. VoidCallback onPressed;
  389. String text;
  390. switch (snapshot.data) {
  391. case BluetoothDeviceState.connected:
  392. onPressed = () => device.disconnect();
  393. text = 'DISCONNECT';
  394. break;
  395. case BluetoothDeviceState.disconnected:
  396. onPressed = () => device.connect();
  397. text = 'CONNECT';
  398. break;
  399. default:
  400. onPressed = null;
  401. text = snapshot.data.toString().substring(21).toUpperCase();
  402. break;
  403. }
  404. return FlatButton(
  405. onPressed: onPressed,
  406. child: Text(
  407. text,
  408. style: Theme.of(context).primaryTextTheme.button.copyWith(color: Colors.white),
  409. ));
  410. },
  411. )
  412. ],
  413. ),
  414. body: SingleChildScrollView(
  415. child: Column(
  416. children: <Widget>[
  417. StreamBuilder<BluetoothDeviceState>(
  418. stream: device.state,
  419. initialData: BluetoothDeviceState.connecting,
  420. builder: (c, snapshot) => ListTile(
  421. leading: (snapshot.data == BluetoothDeviceState.connected) ? Icon(Icons.bluetooth_connected) : Icon(Icons.bluetooth_disabled),
  422. title: Text('Device is ${snapshot.data.toString().split('.')[1]}.'),
  423. subtitle: Text('${device.id}'),
  424. trailing: StreamBuilder<bool>(
  425. stream: device.isDiscoveringServices,
  426. initialData: false,
  427. builder: (c, snapshot) => IndexedStack(
  428. index: snapshot.data ? 1 : 0,
  429. children: <Widget>[
  430. IconButton(
  431. icon: Icon(Icons.refresh),
  432. onPressed: () => device.discoverServices(),
  433. ),
  434. IconButton(
  435. icon: SizedBox(
  436. child: CircularProgressIndicator(
  437. valueColor: AlwaysStoppedAnimation(Colors.grey),
  438. ),
  439. width: 18.0,
  440. height: 18.0,
  441. ),
  442. onPressed: null,
  443. )
  444. ],
  445. ),
  446. ),
  447. ),
  448. ),
  449. StreamBuilder<int>(
  450. stream: device.mtu,
  451. initialData: 0,
  452. builder: (c, snapshot) => ListTile(
  453. title: Text('MTU Size'),
  454. subtitle: Text('${snapshot.data} bytes'),
  455. trailing: IconButton(
  456. icon: Icon(Icons.edit),
  457. onPressed: () => device.requestMtu(223),
  458. ),
  459. ),
  460. ),
  461. StreamBuilder<List<BluetoothService>>(
  462. stream: device.services,
  463. initialData: [],
  464. builder: (c, snapshot) {
  465. return Column(
  466. children: _buildServiceTiles(snapshot.data),
  467. );
  468. },
  469. ),
  470. RaisedButton(
  471. onPressed: () {
  472. Provider.of<Bluetooth>(context, listen: false).queryDeviceStep();
  473. },
  474. child: Text("同步步数"),
  475. ),
  476. RaisedButton(
  477. onPressed: () {
  478. Provider.of<Bluetooth>(context, listen: false).resetData();
  479. },
  480. child: Text("清零"),
  481. ),
  482. RaisedButton(
  483. onPressed: () {
  484. Provider.of<Bluetooth>(context, listen: false).setupGameMode(true);
  485. },
  486. child: Text("游戏模式开"),
  487. ),
  488. RaisedButton(
  489. onPressed: () {
  490. Provider.of<Bluetooth>(context, listen: false).setupGameMode(false);
  491. },
  492. child: Text("游戏模式关"),
  493. ),
  494. ValueListenableBuilder(
  495. valueListenable: Provider.of<Bluetooth>(context, listen: false).actionNotifier,
  496. builder: (BuildContext context, int value, Widget child) => Text("当前动作: $value"),
  497. ),
  498. ValueListenableBuilder(
  499. valueListenable: Provider.of<Bluetooth>(context, listen: false).stepTotalNotifier,
  500. builder: (BuildContext context, int value, Widget child) => Text("同步步数: $value"),
  501. ),
  502. ValueListenableBuilder(
  503. valueListenable: Provider.of<Bluetooth>(context, listen: false).stepNotifier,
  504. builder: (BuildContext context, int value, Widget child) => Text("相对步数: $value"),
  505. ),
  506. ValueListenableBuilder(
  507. valueListenable: Provider.of<Bluetooth>(context, listen: false).byteNotifier,
  508. builder: (BuildContext context, List<int> value, Widget child) => Text("接收: $value"),
  509. ),
  510. ],
  511. ),
  512. ),
  513. );
  514. }
  515. }
  516. class ScanResultTile extends StatelessWidget {
  517. const ScanResultTile({Key key, this.result, this.onTap}) : super(key: key);
  518. final ScanResult result;
  519. final VoidCallback onTap;
  520. Widget _buildTitle(BuildContext context) {
  521. if (result.device.name.length > 0) {
  522. return Column(
  523. mainAxisAlignment: MainAxisAlignment.start,
  524. crossAxisAlignment: CrossAxisAlignment.start,
  525. children: <Widget>[
  526. Text(
  527. result.device.name,
  528. overflow: TextOverflow.ellipsis,
  529. style: Theme.of(context).textTheme.headline3,
  530. ),
  531. SizedBox(
  532. height: 4,
  533. ),
  534. Text(
  535. result.device.id.toString(),
  536. style: Theme.of(context).textTheme.caption,
  537. ),
  538. ],
  539. );
  540. } else {
  541. return Text(result.device.id.toString());
  542. }
  543. }
  544. Widget _buildAdvRow(BuildContext context, String title, String value) {
  545. return Padding(
  546. padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
  547. child: Row(
  548. crossAxisAlignment: CrossAxisAlignment.start,
  549. children: <Widget>[
  550. Text(title, style: Theme.of(context).textTheme.caption),
  551. SizedBox(
  552. width: 12.0,
  553. ),
  554. Expanded(
  555. child: Text(
  556. value,
  557. style: Theme.of(context).textTheme.caption.apply(color: Colors.black),
  558. softWrap: true,
  559. ),
  560. ),
  561. ],
  562. ),
  563. );
  564. }
  565. String getNiceHexArray(List<int> bytes) {
  566. return '[${bytes.map((i) => i.toRadixString(16).padLeft(2, '0')).join(', ')}]'.toUpperCase();
  567. }
  568. String getNiceManufacturerData(Map<int, List<int>> data) {
  569. if (data.isEmpty) {
  570. return null;
  571. }
  572. List<String> res = [];
  573. data.forEach((id, bytes) {
  574. res.add('${id.toRadixString(16).toUpperCase()}: ${getNiceHexArray(bytes)}');
  575. });
  576. return res.join(', ');
  577. }
  578. String getNiceServiceData(Map<String, List<int>> data) {
  579. if (data.isEmpty) {
  580. return null;
  581. }
  582. List<String> res = [];
  583. data.forEach((id, bytes) {
  584. res.add('${id.toUpperCase()}: ${getNiceHexArray(bytes)}');
  585. });
  586. return res.join(', ');
  587. }
  588. @override
  589. Widget build(BuildContext context) {
  590. return ListTile(
  591. contentPadding: EdgeInsets.symmetric(horizontal: 12),
  592. title: _buildTitle(context),
  593. // leading: Text(result.rssi.toString()),
  594. trailing: StreamBuilder<BluetoothDeviceState>(
  595. stream: result.device.state,
  596. initialData: BluetoothDeviceState.connecting,
  597. builder: (c, snapshot) {
  598. if (snapshot.data == BluetoothDeviceState.connected) {
  599. return Container(
  600. width: 64,
  601. height: 30,
  602. alignment: Alignment.center,
  603. decoration:
  604. BoxDecoration(shape: BoxShape.rectangle, borderRadius: BorderRadius.all(Radius.circular(100)), color: Theme.of(context).accentColor),
  605. child: Text(
  606. snapshot.data == BluetoothDeviceState.connected ? "已连接" : "未连接",
  607. style: TextStyle(color: Colors.white, fontSize: 12),
  608. ),
  609. );
  610. }
  611. int status = Provider.of<Bluetooth>(context, listen: false).device == result.device
  612. ? 1
  613. : snapshot.data == BluetoothDeviceState.disconnected
  614. ? 0
  615. : snapshot.data == BluetoothDeviceState.connecting ? 1 : snapshot.data == BluetoothDeviceState.connected ? 2 : 3;
  616. return GestureDetector(
  617. onTap: onTap,
  618. child: Container(
  619. width: 64,
  620. height: 30,
  621. alignment: Alignment.center,
  622. decoration: BoxDecoration(
  623. borderRadius: BorderRadius.all(Radius.circular(100)),
  624. border: Border.all(
  625. color: Theme.of(context).accentColor,
  626. )),
  627. child: Center(
  628. child: Row(
  629. mainAxisSize: MainAxisSize.min,
  630. children: <Widget>[
  631. Text(
  632. status == 1 ? "连接中" : status == 0 ? "连接" : "已连接",
  633. style: TextStyle(
  634. color: Theme.of(context).accentColor,
  635. fontSize: 12,
  636. ),
  637. strutStyle: fixedLine,
  638. ),
  639. if (status == 1)
  640. Padding(
  641. padding: const EdgeInsets.symmetric(horizontal: 4.0),
  642. child: SizedBox(
  643. width: 6,
  644. height: 6,
  645. child: CircularProgressIndicator(
  646. strokeWidth: 2,
  647. ),
  648. ),
  649. ),
  650. ],
  651. ),
  652. ),
  653. ),
  654. );
  655. },
  656. ),
  657. // children: <Widget>[
  658. // _buildAdvRow(context, 'Complete Local Name', result.advertisementData.localName),
  659. // _buildAdvRow(context, 'Tx Power Level', '${result.advertisementData.txPowerLevel ?? 'N/A'}'),
  660. // _buildAdvRow(context, 'Manufacturer Data', getNiceManufacturerData(result.advertisementData.manufacturerData) ?? 'N/A'),
  661. // _buildAdvRow(context, 'Service UUIDs',
  662. // (result.advertisementData.serviceUuids.isNotEmpty) ? result.advertisementData.serviceUuids.join(', ').toUpperCase() : 'N/A'),
  663. // _buildAdvRow(context, 'Service Data', getNiceServiceData(result.advertisementData.serviceData) ?? 'N/A'),
  664. // ],
  665. );
  666. }
  667. }
  668. class ServiceTile extends StatelessWidget {
  669. final BluetoothService service;
  670. final List<CharacteristicTile> characteristicTiles;
  671. const ServiceTile({Key key, this.service, this.characteristicTiles}) : super(key: key);
  672. @override
  673. Widget build(BuildContext context) {
  674. if (characteristicTiles.length > 0) {
  675. return ExpansionTile(
  676. title: Column(
  677. mainAxisAlignment: MainAxisAlignment.center,
  678. crossAxisAlignment: CrossAxisAlignment.start,
  679. children: <Widget>[
  680. Text('Service'),
  681. Text('0x${service.uuid.toString().toUpperCase().substring(4, 8)}',
  682. style: Theme.of(context).textTheme.body1.copyWith(color: Theme.of(context).textTheme.caption.color))
  683. ],
  684. ),
  685. children: characteristicTiles,
  686. );
  687. } else {
  688. return ListTile(
  689. title: Text('Service'),
  690. subtitle: Text('0x${service.uuid.toString().toUpperCase().substring(4, 8)}'),
  691. );
  692. }
  693. }
  694. }
  695. class CharacteristicTile extends StatelessWidget {
  696. final BluetoothCharacteristic characteristic;
  697. final List<DescriptorTile> descriptorTiles;
  698. final VoidCallback onReadPressed;
  699. final VoidCallback onWritePressed;
  700. final VoidCallback onNotificationPressed;
  701. const CharacteristicTile({Key key, this.characteristic, this.descriptorTiles, this.onReadPressed, this.onWritePressed, this.onNotificationPressed})
  702. : super(key: key);
  703. @override
  704. Widget build(BuildContext context) {
  705. return StreamBuilder<List<int>>(
  706. stream: characteristic.value,
  707. initialData: characteristic.lastValue,
  708. builder: (c, snapshot) {
  709. final value = snapshot.data;
  710. return ExpansionTile(
  711. title: ListTile(
  712. title: Column(
  713. mainAxisAlignment: MainAxisAlignment.center,
  714. crossAxisAlignment: CrossAxisAlignment.start,
  715. children: <Widget>[
  716. Text('Characteristic'),
  717. Text('0x${characteristic.uuid.toString().toUpperCase().substring(4, 8)}',
  718. style: Theme.of(context).textTheme.body1.copyWith(color: Theme.of(context).textTheme.caption.color))
  719. ],
  720. ),
  721. subtitle: Text(value.toString()),
  722. contentPadding: EdgeInsets.all(0.0),
  723. ),
  724. trailing: Row(
  725. mainAxisSize: MainAxisSize.min,
  726. children: <Widget>[
  727. IconButton(
  728. icon: Icon(
  729. Icons.file_download,
  730. color: Theme.of(context).iconTheme.color.withOpacity(0.5),
  731. ),
  732. onPressed: onReadPressed,
  733. ),
  734. IconButton(
  735. icon: Icon(Icons.file_upload, color: Theme.of(context).iconTheme.color.withOpacity(0.5)),
  736. onPressed: onWritePressed,
  737. ),
  738. IconButton(
  739. icon: Icon(characteristic.isNotifying ? Icons.sync_disabled : Icons.sync, color: Theme.of(context).iconTheme.color.withOpacity(0.5)),
  740. onPressed: onNotificationPressed,
  741. )
  742. ],
  743. ),
  744. children: descriptorTiles,
  745. );
  746. },
  747. );
  748. }
  749. }
  750. class DescriptorTile extends StatelessWidget {
  751. final BluetoothDescriptor descriptor;
  752. final VoidCallback onReadPressed;
  753. final VoidCallback onWritePressed;
  754. const DescriptorTile({Key key, this.descriptor, this.onReadPressed, this.onWritePressed}) : super(key: key);
  755. @override
  756. Widget build(BuildContext context) {
  757. return ListTile(
  758. title: Column(
  759. mainAxisAlignment: MainAxisAlignment.center,
  760. crossAxisAlignment: CrossAxisAlignment.start,
  761. children: <Widget>[
  762. Text('Descriptor'),
  763. Text('0x${descriptor.uuid.toString().toUpperCase().substring(4, 8)}',
  764. style: Theme.of(context).textTheme.body1.copyWith(color: Theme.of(context).textTheme.caption.color))
  765. ],
  766. ),
  767. subtitle: StreamBuilder<List<int>>(
  768. stream: descriptor.value,
  769. initialData: descriptor.lastValue,
  770. builder: (c, snapshot) => Text(snapshot.data.toString()),
  771. ),
  772. trailing: Row(
  773. mainAxisSize: MainAxisSize.min,
  774. children: <Widget>[
  775. IconButton(
  776. icon: Icon(
  777. Icons.file_download,
  778. color: Theme.of(context).iconTheme.color.withOpacity(0.5),
  779. ),
  780. onPressed: onReadPressed,
  781. ),
  782. IconButton(
  783. icon: Icon(
  784. Icons.file_upload,
  785. color: Theme.of(context).iconTheme.color.withOpacity(0.5),
  786. ),
  787. onPressed: onWritePressed,
  788. )
  789. ],
  790. ),
  791. );
  792. }
  793. }
  794. class AdapterStateTile extends StatelessWidget {
  795. const AdapterStateTile({Key key, @required this.state}) : super(key: key);
  796. final BluetoothState state;
  797. @override
  798. Widget build(BuildContext context) {
  799. return Container(
  800. color: Colors.redAccent,
  801. child: ListTile(
  802. title: Text(
  803. 'Bluetooth adapter is ${state.toString().substring(15)}',
  804. style: Theme.of(context).primaryTextTheme.subhead,
  805. ),
  806. trailing: Icon(
  807. Icons.error,
  808. color: Theme.of(context).primaryTextTheme.subhead.color,
  809. ),
  810. ),
  811. );
  812. }
  813. }