rx_ble 1.0.0

Sponsor pub package

Flutter Rx BLE #

A Flutter BLE plugin, based on the wonderful RxAndroidBle and RxBluetoothKit libraries.

Batteries included. #

  • Acquire every permission and setting required for Bluetooth access, using a single method - RxBle.requestAccess().
  • No need to manually discover BLE services.
  • Automatically queues up GATT requests to avoid race conditions.

Installation #

iOS #

  1. Open iOS module in XCode
  2. Edit Info.plist
  3. Right click > Enable show Raw Keys/Values
  4. Add these entries
    • NSBluetoothAlwaysUsageDescription = Please enable location to continue.
    • NSLocationWhenInUseUsageDescription = Please enable location to continue.
    • NSBluetoothPeripheralUsageDescription = Please enable bluetooth to continue.

Or, you may add these entries maually using your editor of choice:

<dict>
    ...

    <key>NSBluetoothAlwaysUsageDescription</key>
    <string>Please enable location to continue.</string>
    <key>NSLocationWhenInUseUsageDescription</key>
    <string>Please enable location to continue.</string>
    <key>NSBluetoothPeripheralUsageDescription</key>
    <string>Please enable bluetooth to continue.</string>
</dict>

example/lib/main.dart

import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:rx_ble/rx_ble.dart';

int time() => DateTime.now().millisecondsSinceEpoch;

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter Rx BLE example'),
        ),
        body: MyApp(),
      ),
    ),
  );
}

class YesNoDialog extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return AlertDialog(
      title: Text('Location Permission Required'),
      content: Text(
        "This app needs location permission in order to access Bluetooth.\n"
        "Continue?",
      ),
      actions: <Widget>[
        SimpleDialogOption(
          child: Text(
            "NO",
            style: TextStyle(
              color: Colors.red,
              fontWeight: FontWeight.bold,
            ),
          ),
          onPressed: () {
            Navigator.of(context).pop(false);
          },
        ),
        SimpleDialogOption(
          child: Text(
            "YES",
            style: TextStyle(
              color: Colors.blue,
              fontWeight: FontWeight.bold,
            ),
          ),
          onPressed: () {
            Navigator.of(context).pop(true);
          },
        )
      ],
    );
  }
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  var returnValue;
  String deviceId;
  Exception returnError;
  final results = <String, ScanResult>{};
  var chars = Map<String, List<String>>();
  final uuidControl = TextEditingController();
  final mtuControl = TextEditingController();
  final writeCharValueControl = TextEditingController();
  final randomWriteNum = TextEditingController(text: '100');
  final randomWriteSize = TextEditingController(text: '100');
  var connectionState = BleConnectionState.disconnected;
  var isWorking = false;

  Function wrapCall(Function fn) {
    return () async {
      var value, error;
      setState(() {
        returnError = returnValue = null;
        isWorking = true;
      });
      try {
        value = await fn();
        print('returnValue: $value');
      } catch (e, trace) {
        print('returnError: $e\n$trace');
        error = e;
      } finally {
        if (mounted) {
          setState(() {
            isWorking = false;
            returnError = error;
            returnValue = value;
          });
        }
      }
    };
  }

  Future<void> requestAccessRationale() async {
    return await RxBle.requestAccess(
      showRationale: () async {
        return await showDialog(
              context: context,
              builder: (context) => YesNoDialog(),
            ) ??
            false;
      },
    );
  }

  Future<void> startScan() async {
    await for (final scanResult in RxBle.startScan()) {
      results[scanResult.deviceId] = scanResult;
      if (!mounted) return;
      setState(() {
        returnValue = JsonEncoder.withIndent(" " * 2, (o) {
          if (o is ScanResult) {
            return o.toString();
          } else {
            return o;
          }
        }).convert(results);
      });
    }
  }

  Future<String> discoverChars() async {
    final value = await RxBle.discoverChars(deviceId);
    if (!mounted) return null;
    setState(() {
      chars = value;
    });
    return JsonEncoder.withIndent(" " * 2).convert(chars);
  }

  Future<void> readChar() async {
    final value = await RxBle.readChar(deviceId, uuidControl.text);
    return value.toString() +
        "\n\n" +
        RxBle.charToString(value, allowMalformed: true);
  }

  Future<void> observeChar() async {
    var start = time();
    await for (final value in RxBle.observeChar(deviceId, uuidControl.text)) {
      final end = time();
      if (!mounted) return;
      setState(() {
        returnValue = value.toString() +
            "\n\n" +
            RxBle.charToString(value, allowMalformed: true) +
            "\n\nDelay: ${(end - start)} ms";
      });
      start = time();
    }
  }

  Future<void> writeChar() async {
    return await RxBle.writeChar(
      deviceId,
      uuidControl.text,
      RxBle.stringToChar(writeCharValueControl.text),
    );
  }

  Future<void> requestMtu() async {
    return await RxBle.requestMtu(deviceId, int.parse(mtuControl.text));
  }

  Future<void> randomWrite() async {
    final rand = new Random();
    final futures = List.generate(int.parse(randomWriteNum.text), (_) {
      return RxBle.writeChar(
        deviceId,
        uuidControl.text,
        Uint8List.fromList(
          List.generate(int.parse(randomWriteSize.text), (_) {
            return rand.nextInt(33) + 89;
          }),
        ),
      );
    });
    final start = time();
    await Future.wait(futures);
    final end = time();
    return "${end - start} ms";
  }

  Future<void> continuousRead() async {
    while (true) {
      final start = time();
      final value = await RxBle.readChar(deviceId, uuidControl.text);
      final end = time();
      if (!mounted) return;
      setState(() {
        returnValue = value.toString() +
            "\n\n" +
            RxBle.charToString(value, allowMalformed: true) +
            "\n\nDelay: ${start - end} ms";
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: ListView(
            children: <Widget>[
              Text("Return Value:"),
              SingleChildScrollView(
                scrollDirection: Axis.horizontal,
                child: Container(
                  color: Colors.black,
                  child: Text(
                    "$returnValue",
                    style: TextStyle(
                      fontFamily: 'DejaVuSansMono',
                      color: Colors.white,
                    ),
                  ),
                ),
              ),
              Divider(),
              Text("Error:"),
              SingleChildScrollView(
                scrollDirection: Axis.horizontal,
                child: Container(
                  color: Colors.black,
                  child: Text(
                    "$returnError",
                    style: TextStyle(
                      fontFamily: 'DejaVuSansMono',
                      color: Colors.white,
                    ),
                  ),
                ),
              ),
              Divider(),
              Container(
                color: Colors.black,
                child: Text(
                  connectionState.toString(),
                  style: TextStyle(
                    fontFamily: 'DejaVuSansMono',
                    color: Colors.white,
                  ),
                ),
              ),
              Divider(),
              RaisedButton(
                child: Text(
                  "requestAccess()",
                  style: TextStyle(fontFamily: 'DejaVuSansMono'),
                ),
                onPressed: wrapCall(RxBle.requestAccess),
              ),
              RaisedButton(
                child: Text(
                  "requestAccess(showRationale)",
                  style: TextStyle(fontFamily: 'DejaVuSansMono'),
                ),
                onPressed: wrapCall(requestAccessRationale),
              ),
              RaisedButton(
                child: Text(
                  "hasAccess()",
                  style: TextStyle(fontFamily: 'DejaVuSansMono'),
                ),
                onPressed: wrapCall(RxBle.hasAccess),
              ),
              RaisedButton(
                child: Text(
                  "openAppSettings()",
                  style: TextStyle(fontFamily: 'DejaVuSansMono'),
                ),
                onPressed: wrapCall(RxBle.openAppSettings),
              ),
              Divider(),
              RaisedButton(
                child: Text(
                  "startScan()",
                  style: TextStyle(fontFamily: 'DejaVuSansMono'),
                ),
                onPressed: wrapCall(startScan),
              ),
              RaisedButton(
                child: Text(
                  "stopScan()",
                  style: TextStyle(fontFamily: 'DejaVuSansMono'),
                ),
                onPressed: wrapCall(RxBle.stopScan),
              ),
              Divider(),
              if (results.isEmpty)
                Text('Start scanning to connect to a device'),
              for (final scanResult in results.values)
                RaisedButton(
                  child: Text(
                    "connect(${scanResult.deviceId})",
                    style: TextStyle(fontFamily: 'DejaVuSansMono'),
                  ),
                  onPressed: wrapCall(() async {
                    await RxBle.stopScan();
                    setState(() {
                      deviceId = scanResult.deviceId;
                    });
                    await for (final state in RxBle.connect(deviceId)) {
                      print("device state: $state");
                      if (!mounted) return;
                      setState(() {
                        connectionState = state;
                      });
                    }
                  }),
                ),
              Divider(),
              if (connectionState != BleConnectionState.connected)
                Text("Connect to a device to perform GATT operations.")
              else ...[
                RaisedButton(
                  child: Text(
                    "discoverChars()",
                    style: TextStyle(fontFamily: 'DejaVuSansMono'),
                  ),
                  onPressed: wrapCall(discoverChars),
                ),
                RaisedButton(
                  child: Text(
                    "disconnect()",
                    style: TextStyle(fontFamily: 'DejaVuSansMono'),
                  ),
                  onPressed: wrapCall(RxBle.disconnect),
                ),
                Divider(),
                if (chars.isNotEmpty) Text("Characteristic Picker"),
                SingleChildScrollView(
                  scrollDirection: Axis.horizontal,
                  child: Row(
                    children: <Widget>[
                      for (final i in chars.values)
                        for (final j in i)
                          Padding(
                            padding: EdgeInsets.all(5),
                            child: RaisedButton(
                              child: Text(j),
                              onPressed: () {
                                setState(() {
                                  uuidControl.text = j;
                                });
                              },
                            ),
                          ),
                    ],
                  ),
                ),
                Divider(),
                TextField(
                  controller: uuidControl,
                  decoration: InputDecoration(
                    labelText: "uuid",
                  ),
                ),
                RaisedButton(
                  child: Text(
                    "device.readChar()",
                    style: TextStyle(fontFamily: 'DejaVuSansMono'),
                  ),
                  onPressed: wrapCall(readChar),
                ),
                RaisedButton(
                  child: Text(
                    "device.observeChar()",
                    style: TextStyle(fontFamily: 'DejaVuSansMono'),
                  ),
                  onPressed: wrapCall(observeChar),
                ),
                TextField(
                  controller: writeCharValueControl,
                  decoration: InputDecoration(
                    labelText: "writeChar value",
                  ),
                ),
                RaisedButton(
                  child: Text(
                    "device.writeChar()",
                    style: TextStyle(fontFamily: 'DejaVuSansMono'),
                  ),
                  onPressed: wrapCall(writeChar),
                ),
                TextField(
                  controller: mtuControl,
                  decoration: InputDecoration(
                    labelText: "mtu",
                  ),
                ),
                RaisedButton(
                  child: Text(
                    "device.requestMtu()",
                    style: TextStyle(fontFamily: 'DejaVuSansMono'),
                  ),
                  onPressed: wrapCall(requestMtu),
                ),
                Divider(),
                TextField(
                  controller: randomWriteSize,
                  keyboardType: TextInputType.number,
                  decoration: InputDecoration(
                    labelText: "Random write batch size",
                  ),
                ),
                TextField(
                  controller: randomWriteNum,
                  keyboardType: TextInputType.number,
                  decoration: InputDecoration(
                    labelText: "Random Write no of batches",
                  ),
                ),
                RaisedButton(
                  child: Text(
                    'Test random writes',
                    style: TextStyle(fontFamily: 'DejaVuSansMono'),
                  ),
                  onPressed: wrapCall(randomWrite),
                ),
                RaisedButton(
                  child: Text(
                    'Test continuous read',
                    style: TextStyle(fontFamily: 'DejaVuSansMono'),
                  ),
                  onPressed: wrapCall(continuousRead),
                ),
              ],
            ],
          ),
        ),
        Column(
          mainAxisAlignment: MainAxisAlignment.end,
          children: <Widget>[
            if (isWorking) LinearProgressIndicator(value: null),
          ],
        ),
      ],
    );
  }
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  rx_ble: ^1.0.0

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:rx_ble/rx_ble.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
69
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
80
Overall:
Weighted score of the above. [more]
81
Learn more about scoring.

We analyzed this package on Jul 14, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.8.4
  • pana: 0.13.15
  • Flutter: 1.17.5

Analysis suggestions

Package does not support Flutter platform Linux

Because:

  • package:rx_ble/rx_ble.dart that declares support for platforms: Android, iOS

Package does not support Flutter platform Web

Because:

  • package:rx_ble/rx_ble.dart that declares support for platforms: Android, iOS

Package does not support Flutter platform Windows

Because:

  • package:rx_ble/rx_ble.dart that declares support for platforms: Android, iOS

Package does not support Flutter platform macOS

Because:

  • package:rx_ble/rx_ble.dart that declares support for platforms: Android, iOS

Package not compatible with SDK dart

Because:

  • rx_ble that is a package requiring null.

Health suggestions

Format lib/src/exceptions.dart.

Run flutter format to format lib/src/exceptions.dart.

Maintenance issues and suggestions

Provide a file named CHANGELOG.md. (-20 points)

Changelog entries help developers follow the progress of your package. See the example generated by stagehand.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.1.0 <3.0.0
flutter 0.0.0
plugin_scaffold ^3.0.1 3.1.0
Transitive dependencies
collection 1.14.12 1.14.13
meta 1.1.8 1.2.2
sky_engine 0.0.99
typed_data 1.1.6 1.2.0
vector_math 2.0.8 2.1.0-nullsafety
Dev dependencies
flutter_test