I am currently running into an issue with my ble project for flutter. I am using the flutter blue package by Paul DeMarco and the additional page for the app is based on "ThatProject" dust sensor from youtube. I have an Adafruit Feather 32u4 board, and I am attempting to notify the client (my flutter app) that It has a series of numbers to send, but I am not getting any output. I am able to connect to the device, and seem to properly send the service UUID and characteristic UUID, but im not sure if it is coming with proper properties. I am using the adafruit BLE code to program the board, and I can get the values if I use adafruit's app. I am just trying to get the values on my own flutter app.
I am running into an error as follows:
E/flutter (32139): [ERROR:flutter/lib/ui/ui_dart_state.cc(186)] Unhandled Exception: PlatformException(set_notification_error, could not locate CCCD descriptor for characteristic: 6e400002-b5a3-f393-e0a9-e50e24dcca9e, null, null)
Here is my code. I believe the missing CCCD is coming from this part:
import 'dart:async';
import 'dart:convert' show utf8;
import 'package:flutter/material.dart';
import 'package:flutter_blue/flutter_blue.dart';
import 'package:oscilloscope/oscilloscope.dart';
class SensorPage extends StatefulWidget {
const SensorPage({Key key, this.device}) : super(key: key);
final BluetoothDevice device;
@override
_SensorPageState createState() => _SensorPageState();
}
class _SensorPageState extends State<SensorPage> {
final String SERVICE_UUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
final String CHARACTERISTIC_UUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
bool isReady;
Stream<List<int>> stream;
List<double> traceDust = List();
@override
void initState() {
super.initState();
isReady = false;
connectToDevice();
}
connectToDevice() async {
// if (widget.device == null) {
// // _Pop();
// return;
// }
//timeout timer, watchdog timer if you will
new Timer(const Duration(seconds: 15), () {
if (!isReady) {
disconnectFromDevice();
// _Pop();
}
});
await widget.device.connect();
discoverServices();
}
disconnectFromDevice() {
if (widget.device == null) {
//_Pop();
return;
}
widget.device.disconnect();
}
discoverServices() async {
// if (widget.device == null) {
// // _Pop();
// return;
// }
BluetoothCharacteristic ss;
List<BluetoothService> services = await widget.device.discoverServices();
services.forEach((service) {
debugPrint("This Service UUID is!${service.uuid.toString()}");
if (service.uuid.toString() == SERVICE_UUID) {
service.characteristics.forEach((characteristic) {
debugPrint("This char UUID is!${characteristic.uuid.toString()}");
if (characteristic.uuid.toString() == CHARACTERISTIC_UUID) {
debugPrint("Here is !isNotifying: ${!characteristic.isNotifying}");
debugPrint("Here is characteristic.value: ${characteristic.value}");
ss = characteristic;
stream = ss.value;
setState(() {
isReady = true;
});
} //this one
});
} //this one
});
await ss.setNotifyValue(true);
stream = ss.value;
if (!isReady) {
// _Pop();
}
}
Future<bool> _onWillPop() {
return showDialog(
context: context,
builder: (context) =>
new AlertDialog(
title: Text('Are you sure?'),
content: Text('Do you want to disconnect device and go back?'),
actions: <Widget>[
new FlatButton(
onPressed: () => Navigator.of(context).pop(false),
child: new Text('No')),
new FlatButton(
onPressed: () {
disconnectFromDevice();
Navigator.of(context).pop(true);
},
child: new Text('Yes')),
],
) ??
false);
}
// _Pop() {
// Navigator.of(context).pop(true);
// }
String _dataParser(List<int> dataFromDevice) {
debugPrint("current value is-> ${utf8.decode(dataFromDevice)}");
return utf8.decode(dataFromDevice);
}
@override
Widget build(BuildContext context) {
Oscilloscope oscilloscope = Oscilloscope(
showYAxis: true,
padding: 0.0,
backgroundColor: Colors.black,
traceColor: Colors.white,
yAxisMax: 3000.0,
yAxisMin: 0.0,
dataSet: traceDust,
);
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
appBar: AppBar(
title: Text('Optical Dust Sensor'),
),
body: Container(
child: !isReady
? Center(
child: Text(
"Waiting...",
style: TextStyle(fontSize: 24, color: Colors.red),
),
)
: Container(
child: StreamBuilder<List<int>>(
stream: stream,
builder: (BuildContext context,
AsyncSnapshot<List<int>> snapshot) {
if (snapshot.hasError)
return Text('Error: ${snapshot.error}');
if (snapshot.connectionState ==
ConnectionState.active) {
debugPrint("snapshot.error: ${snapshot.error}.");
debugPrint("snapshot.data: ${snapshot.error}.");
debugPrint(
"snapshot.connectionState: ${snapshot.connectionState}.");
debugPrint("snapshot.hasdata?: ${snapshot.hasData}.");
var currentValue = _dataParser(snapshot.data);
traceDust.add(double.tryParse(currentValue) ?? 0);
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
flex: 1,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Current value from Sensor',
style: TextStyle(fontSize: 14)),
Text('$currentValue ug/m3',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 24))
]),
),
Expanded(
flex: 1,
child: oscilloscope,
)
],
));
} else {
return Text('Check the stream');
}
},
),
)),
),
);
}
}
// Copyright 2017, Paul DeMarco.
// All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:K9Harness/Pages/Sensor_page.dart';
import 'package:K9Harness/Bluetooth/widgets.dart';
import 'package:flutter_blue/flutter_blue.dart';
class MyBluetoothPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
color: Colors.lightBlue,
home: StreamBuilder<BluetoothState>(
stream: FlutterBlue.instance.state,
initialData: BluetoothState.unknown,
builder: (c, snapshot) {
final state = snapshot.data;
if (state == BluetoothState.on) {
return FindDevicesScreen();
}
return BluetoothOffScreen(state: state);
}),
);
}
}
class BluetoothOffScreen extends StatelessWidget {
const BluetoothOffScreen({Key key, this.state}) : super(key: key);
final BluetoothState state;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.lightBlue,
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(
Icons.bluetooth_disabled,
size: 200.0,
color: Colors.white54,
),
Text(
'Bluetooth Adapter is ${state.toString().substring(15)}.',
style: Theme.of(context)
.primaryTextTheme
.subhead
.copyWith(color: Colors.white),
),
],
),
),
);
}
}
class FindDevicesScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Find Devices'),
),
body: RefreshIndicator(
onRefresh: () => FlutterBlue.instance
.startScan(
scanMode: ScanMode.balanced,
withServices: [
Guid("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
], //FIXME check the other ways where ".startScan" is implemented
timeout: Duration(seconds: 4))
.catchError((error) {
print("error starting scan $error");
}),
child: SingleChildScrollView(
child: Column(
children: <Widget>[
StreamBuilder<List<BluetoothDevice>>(
stream: Stream.periodic(Duration(seconds: 2))
.asyncMap((_) => FlutterBlue.instance.connectedDevices),
initialData: [],
builder: (c, snapshot) => Column(
children: snapshot.data
.map((d) => ListTile(
title: Text(d.name),
subtitle: Text(d.id.toString()),
trailing: StreamBuilder<BluetoothDeviceState>(
stream: d.state,
initialData: BluetoothDeviceState.disconnected,
builder: (c, snapshot) {
if (snapshot.data ==
BluetoothDeviceState.connected) {
return RaisedButton(
child: Text('OPEN'),
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) =>
DeviceScreen(device: d))),
);
}
return Text(snapshot.data.toString());
},
),
))
.toList(),
),
),
StreamBuilder<List<ScanResult>>(
stream: FlutterBlue.instance.scanResults,
initialData: [],
builder: (c, snapshot) => Column(
children: snapshot.data
.map(
(r) => ScanResultTile(
result: r,
onTap: () => Navigator.of(context)
.push(MaterialPageRoute(builder: (context) {
r.device.connect();
return SensorPage(device: r.device);
})),
),
)
.toList(),
),
),
],
),
),
),
floatingActionButton: StreamBuilder<bool>(
stream: FlutterBlue.instance.isScanning,
initialData: false,
builder: (c, snapshot) {
if (snapshot.data) {
return FloatingActionButton(
child: Icon(Icons.stop),
onPressed: () => FlutterBlue.instance.stopScan(),
backgroundColor: Colors.red,
);
} else {
return FloatingActionButton(
child: Icon(Icons.search),
onPressed: () => FlutterBlue.instance
.startScan(
scanMode: ScanMode.balanced,
withServices: [
Guid("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
], //FIXME check the other ways where ".startScan" is implemented
timeout: Duration(seconds: 4))
.catchError((error) {
print("error starting scan $error");
}));
}
},
),
);
}
}
class DeviceScreen extends StatelessWidget {
const DeviceScreen({Key key, this.device}) : super(key: key);
final BluetoothDevice device;
List<Widget> _buildServiceTiles(List<BluetoothService> services) {
return services
.map(
(s) => ServiceTile(
service: s,
characteristicTiles: s.characteristics
.map(
(c) => CharacteristicTile(
characteristic: c,
onReadPressed: () => c.read(),
onWritePressed: () => c.write([13, 24]),
onNotificationPressed: () =>
c.setNotifyValue(!c.isNotifying),
descriptorTiles: c.descriptors
.map(
(d) => DescriptorTile(
descriptor: d,
onReadPressed: () => d.read(),
onWritePressed: () => d.write([11, 12]),
),
)
.toList(),
),
)
.toList(),
),
)
.toList();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(device.name),
actions: <Widget>[
StreamBuilder<BluetoothDeviceState>(
stream: device.state,
initialData: BluetoothDeviceState.connecting,
builder: (c, snapshot) {
VoidCallback onPressed;
String text;
switch (snapshot.data) {
case BluetoothDeviceState.connected:
onPressed = () => device.disconnect();
text = 'DISCONNECT';
break;
case BluetoothDeviceState.disconnected:
onPressed = () => device.connect();
text = 'CONNECT';
break;
default:
onPressed = null;
text = snapshot.data.toString().substring(21).toUpperCase();
break;
}
return FlatButton(
onPressed: onPressed,
child: Text(
text,
style: Theme.of(context)
.primaryTextTheme
.button
.copyWith(color: Colors.white),
));
},
)
],
),
body: SingleChildScrollView(
child: Column(
children: <Widget>[
StreamBuilder<BluetoothDeviceState>(
stream: device.state,
initialData: BluetoothDeviceState.connecting,
builder: (c, snapshot) => ListTile(
leading: (snapshot.data == BluetoothDeviceState.connected)
? Icon(Icons.bluetooth_connected)
: Icon(Icons.bluetooth_disabled),
title: Text(
'Device is ${snapshot.data.toString().split('.')[1]}.'),
subtitle: Text('${device.id}'),
trailing: StreamBuilder<bool>(
stream: device.isDiscoveringServices,
initialData: false,
builder: (c, snapshot) => IndexedStack(
index: snapshot.data ? 1 : 0,
children: <Widget>[
IconButton(
icon: Icon(Icons.refresh),
onPressed: () => device.discoverServices(),
),
IconButton(
icon: SizedBox(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation(Colors.grey),
),
width: 18.0,
height: 18.0,
),
onPressed: null,
)
],
),
),
),
),
StreamBuilder<int>(
stream: device.mtu,
initialData: 0,
builder: (c, snapshot) => ListTile(
title: Text('MTU Size'),
subtitle: Text('${snapshot.data} bytes'),
trailing: IconButton(
icon: Icon(Icons.edit),
onPressed: () => device.requestMtu(223),
),
),
),
StreamBuilder<List<BluetoothService>>(
stream: device.services,
initialData: [],
builder: (c, snapshot) {
return Column(
children: _buildServiceTiles(snapshot.data),
);
},
),
],
),
),
);
}
}
I figured it out, I had my characteristic UUID to be 6e400002-b5a3-f393-e0a9-e50e24dcca9e
and it should have been 6e400003-b5a3-f393-e0a9-e50e24dcca9e
because for the feather, the notification/notify characteristic is mandatory to be 0x0003
instead of the 2
. It then passed the CCCD properly when I changed this value.