Flutter Bluetooth Low Energy and Circuit Python.
Nov, 05 2019
Flutter is a relatively new framework that is intended to be completely cross platform. Ideally, the same code base (written in Dart) will persist across the Android, iOS, and the web. One of Flutter’s main advantages is its already quite mature package ecosystem. Here we’re going to focus on Flutter Blue, the package used for Bluetooth Low Energy communication.
Let’s start by setting up a BLE CircuitPython device as our peripheral to keep things simple. Setup a Bluetooth Enabled CircuitPython board by following one of many guides and upload the following code to it:
from adafruit_ble.uart import UARTServer import time import board from digitalio import DigitalInOut, Direction, Pull uart = UARTServer() Rled = DigitalInOut(board.LED2_R) Bled = DigitalInOut(board.LED2_B) Gled = DigitalInOut(board.LED2_G) Rled.direction = Direction.OUTPUT Gled.direction = Direction.OUTPUT Bled.direction = Direction.OUTPUT b_cmd = str.encode("b") g_cmd = str.encode("g") r_cmd = str.encode("r") x_cmd = str.encode("x") flag = True while True: uart.start_advertising() # Wait for a connection while not uart.connected: Gled.value = True Bled.value = True Rled.value = False pass while uart.connected: if flag: flag = False Gled.value = False Bled.value = True Rled.value = True one_byte = uart.read(1) if one_byte: if one_byte == b_cmd: Gled.value = True Bled.value = False Rled.value = True print("b command") if one_byte == g_cmd: Gled.value = False Bled.value = True Rled.value = True print("g command") if one_byte == r_cmd: Gled.value = True Bled.value = True Rled.value = False print("r command") if one_byte == x_cmd: Gled.value = True Bled.value = True Rled.value = True print("x command") uart.write(one_byte)
I’m using the nrf52840 USB Dongle flashed with CircuitPython.
The setup procedure for this particular device is described here.
The USB dongle has an on-board RGB LED (this RGB module is low-side driven) which I’m controlling based on the received utf-8bit encoded commands from the Flutter app. The peripheral is broadcasting a UART service developed by Nordic acting as a UART serial port. This will matter more when we move into Dart.
While the example program in the Flutter Blue repository is useful, it’s needlessly complex when we’re trying to interact with our own Bluetooth peripheral design. Instead, we’ll start by using the application developed by 0015 called Flutter_JoyPad.
In the following section we’ll break down this code and modify it to connect with our peripheral USB dongle we deployed above. First, let’s grab some information about the CircuitPython device. Start by downloading nRFConnect on your phone. When its scan has completed, scroll through the available devices until you find something resembling “CIRCUITPY”. It might be truncated as shown below:
Connect to the device and swipe left to reveal the “Services” screen.
Take note of the:
- Nordic UART Service UUID: 6e400001-b5a3-f393-e0a9-e50e24dcca9e
- Nordic UART Rx Characteristic UUID: 6e400002-b5a3-f393-e0a9-e50e24dcca9e
- Device Name: CIRCU
We now need to modify the flutter_app_joypad_ble code to scan for the BLE peripheral we set up. Clone the repository from the link above. The following lines of code are found in the flutter_app_joypad_ble/lib/main.dart file. Notice that lines 36–38 take in the details that we discovered in the nRFConnect app. Change those to match what the nRFConnect app displayed. Make sure that the UUIDs are all lower case. The Flutter plugin will not recognize uppercase UUIDs.
import 'dart:async'; import 'dart:convert' show utf8; import 'package:control_pad/control_pad.dart'; import 'package:control_pad/models/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_blue/flutter_blue.dart'; Future<void> main() async { await SystemChrome.setPreferredOrientations( [DeviceOrientation.landscapeRight, DeviceOrientation.landscapeLeft]); runApp(MainScreen()); } class MainScreen extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Joypad with BLE', debugShowCheckedModeBanner: false, home: JoyPad(), theme: ThemeData.dark(), ); } } class JoyPad extends StatefulWidget { @override _JoyPadState createState() => _JoyPadState(); } class _JoyPadState extends State<JoyPad> { // Update the lines below to match the CircuitPython details final String SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"; final String CHARACTERISTIC_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"; final String TARGET_DEVICE_NAME = "CIRCU"; FlutterBlue flutterBlue = FlutterBlue.instance; StreamSubscription<ScanResult> scanSubScription; BluetoothDevice targetDevice; BluetoothCharacteristic targetCharacteristic; String connectionText = ""; @override void initState() { super.initState(); startScan(); } startScan() { setState(() { connectionText = "Start Scanning"; }); scanSubScription = flutterBlue.scan().listen((scanResult) { if (scanResult.device.name == TARGET_DEVICE_NAME) { print('DEVICE found'); stopScan(); setState(() { connectionText = "Found Target Device"; }); targetDevice = scanResult.device; connectToDevice(); } }, onDone: () => stopScan()); } stopScan() { scanSubScription?.cancel(); scanSubScription = null; } connectToDevice() async { if (targetDevice == null) return; setState(() { connectionText = "Device Connecting"; }); await targetDevice.connect(); print('DEVICE CONNECTED'); setState(() { connectionText = "Device Connected"; }); discoverServices(); } disconnectFromDevice() { if (targetDevice == null) return; targetDevice.disconnect(); setState(() { connectionText = "Device Disconnected"; }); } discoverServices() async { if (targetDevice == null) return; List<BluetoothService> services = await targetDevice.discoverServices(); services.forEach((service) { // do something with service if (service.uuid.toString() == SERVICE_UUID) { service.characteristics.forEach((characteristic) { if (characteristic.uuid.toString() == CHARACTERISTIC_UUID) { targetCharacteristic = characteristic; writeData("Hi there, CircuitPython"); setState(() { connectionText = "All Ready with ${targetDevice.name}"; }); } }); } }); } writeData(String data) { if (targetCharacteristic == null) return; List<int> bytes = utf8.encode(data); targetCharacteristic.write(bytes); } @override Widget build(BuildContext context) { JoystickDirectionCallback onDirectionChanged( double degrees, double distance) { String data = "Degree : ${degrees.toStringAsFixed(2)}, distance : ${distance.toStringAsFixed(2)}"; print(data); } PadButtonPressedCallback padBUttonPressedCallback( int buttonIndex, Gestures gesture) { String data = ""; /* change the data sent by the joypad buttons to correspond with the commands hardcoded into the CircuitPython device */ if (buttonIndex == 3) { data = "b"; setState(() { connectionText = "Blue LED On"; }); } else if (buttonIndex == 1) { data = "g"; setState(() { connectionText = "Green LED On"; }); } else if (buttonIndex == 2) { data = "r"; setState(() { connectionText = "Red LED On"; }); } else if (buttonIndex == 0) { data = "x"; setState(() { connectionText = "All LEDs Off"; }); } print(data); writeData(data); } return Scaffold( appBar: AppBar( title: Text(connectionText), ), body: Container( child: targetCharacteristic == null ? Center( child: Text( "Waiting...", style: TextStyle(fontSize: 24, color: Colors.red), ), ) : Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ JoystickView( onDirectionChanged: onDirectionChanged, ), PadButtonsView( padButtonPressedCallback: padBUttonPressedCallback, ), ], ), ), ); } }
We also need to change the PadButtonPressedCallback function around line 140 (reflected above) to correspond to the commands we hard coded into the CircuitPython device. I replaced the PadButtonPressedCallback function with the code that follows:
PadButtonPressedCallback padBUttonPressedCallback( int buttonIndex, Gestures gesture) { String data = ""; /* change the data sent by the joypad buttons to correspond with the commands hardcoded into the CircuitPython device */ if (buttonIndex == 3) { data = "b"; setState(() { connectionText = "Blue LED On"; }); } else if (buttonIndex == 1) { data = "g"; setState(() { connectionText = "Green LED On"; }); } else if (buttonIndex == 2) { data = "r"; setState(() { connectionText = "Red LED On"; }); } else if (buttonIndex == 0) { data = "x"; setState(() { connectionText = "All LEDs Off"; }); } print(data); writeData(data); }
When running the Flutter app on an external device, we are now able to send data to a peripheral device via BLE to control an RGB LED. This is a great starting point for customization. Check out the repository I created to control a servo motor with a similar setup.