567 lines
19 KiB
Dart
567 lines
19 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:flutter_provisioning_for_iot/objects/cloud_service_api.dart';
|
|
import 'package:flutter_provisioning_for_iot/screens/bluetooth_device_list_entry.dart';
|
|
|
|
import 'package:app_settings/app_settings.dart';
|
|
import 'package:permission_handler/permission_handler.dart';
|
|
|
|
|
|
class BluetoothScreen extends StatefulWidget {
|
|
final bool start = true;
|
|
|
|
const BluetoothScreen({super.key});
|
|
|
|
@override
|
|
State<BluetoothScreen> createState() => _BluetoothScreen();
|
|
}
|
|
|
|
// debugPrint("r: ${r.device.name} ${r.device.address} ${r.rssi}");
|
|
class _BluetoothScreen extends State<BluetoothScreen> {
|
|
|
|
CloudServiceAPI cloudServiceAPI = CloudServiceAPI();
|
|
//BluetoothManager bluetoothManager = BluetoothManager();
|
|
|
|
late Stream<BluetoothDiscoveryResult> _stream;
|
|
late StreamSubscription<BluetoothDiscoveryResult> _streamSubscription;
|
|
late Stream<Uint8List> _connectionStream;
|
|
late StreamSubscription<Uint8List> _connectionStreamSubscription;
|
|
BluetoothDevice? _bluetoothDevice;
|
|
BluetoothConnection? _bluetoothConnection;
|
|
final List<BluetoothDiscoveryResult> _discoveryResults =
|
|
List<BluetoothDiscoveryResult>.empty(growable: true);
|
|
String _messageBuffer = "";
|
|
bool isDiscovering = false;
|
|
bool isConnecting = false;
|
|
bool isDisconnecting = false;
|
|
bool isConnected = false;
|
|
String textInput = "0123456789 abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
|
|
ButtonStyle buttonStyle = ElevatedButton.styleFrom(
|
|
foregroundColor: Colors.black,
|
|
backgroundColor: const Color(0xFFFDE100), // Text Color (Foreground color)
|
|
);
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
isDiscovering = widget.start;
|
|
if (isDiscovering) {
|
|
_startDiscovery();
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
// Avoid memory leak (`setState` after dispose) and cancel discovery
|
|
_streamSubscription.cancel();
|
|
if (isConnected) {
|
|
isDisconnecting = true;
|
|
_bluetoothConnection?.dispose();
|
|
_bluetoothConnection = null;
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _disconnectDevice(BluetoothDevice device) async {
|
|
if (_bluetoothConnection != null) {
|
|
await _bluetoothConnection!.finish();
|
|
_bluetoothConnection = null;
|
|
}
|
|
}
|
|
|
|
Future<void> _connectDevice(BluetoothDevice device) async {
|
|
String address = device.address;
|
|
if (_bluetoothConnection != null) {
|
|
await _bluetoothConnection!.finish();
|
|
_bluetoothConnection = null;
|
|
}
|
|
_bluetoothConnection = await BluetoothConnection.toAddress(address);
|
|
debugPrint("Connected to the device");
|
|
setState(() {
|
|
isConnecting = false;
|
|
isDisconnecting = false;
|
|
});
|
|
|
|
_connectionStream = _bluetoothConnection!.input!;
|
|
_connectionStreamSubscription = _connectionStream.listen(_connectionOnListen);
|
|
_connectionStreamSubscription.onDone(_connectionOnDone);
|
|
}
|
|
|
|
void _connectionOnDone() {
|
|
/*
|
|
// Example: Detect which side closed the connection
|
|
// There should be `isDisconnecting` flag to show are we are (locally)
|
|
// in middle of disconnecting process, should be set before calling
|
|
// `dispose`, `finish` or `close`, which all causes to disconnect.
|
|
// If we except the disconnection, `onDone` should be fired as result.
|
|
// If we didn't except this (no flag set), it means closing by remote.
|
|
*/
|
|
if (isDisconnecting) {
|
|
debugPrint("Disconnecting locally!");
|
|
} else {
|
|
debugPrint("Disconnected remotely!");
|
|
}
|
|
if (mounted) {
|
|
setState(() {});
|
|
}
|
|
}
|
|
|
|
void _connectionOnListen(Uint8List data) {
|
|
debugPrint("received: $data");
|
|
debugPrint("received decoded: ${const Utf8Decoder().convert(data)}");
|
|
// Allocate buffer for parsed data
|
|
int backspacesCounter = 0;
|
|
for (var byte in data) {
|
|
if (byte == 8 || byte == 127) {
|
|
backspacesCounter++;
|
|
}
|
|
}
|
|
Uint8List buffer = Uint8List(data.length - backspacesCounter);
|
|
int bufferIndex = buffer.length;
|
|
|
|
// Apply backspace control character
|
|
backspacesCounter = 0;
|
|
for (int i = data.length - 1; i >= 0; i--) {
|
|
if (data[i] == 8 || data[i] == 127) {
|
|
backspacesCounter++;
|
|
} else {
|
|
if (backspacesCounter > 0) {
|
|
backspacesCounter--;
|
|
} else {
|
|
buffer[--bufferIndex] = data[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create message if there is new line character
|
|
String dataString = String.fromCharCodes(buffer);
|
|
int index = buffer.indexOf(13);
|
|
if (~index != 0) {
|
|
setState(() {
|
|
_messageBuffer = dataString.substring(index);
|
|
});
|
|
} else {
|
|
_messageBuffer = (backspacesCounter > 0
|
|
? _messageBuffer.substring(
|
|
0, _messageBuffer.length - backspacesCounter)
|
|
: _messageBuffer + dataString);
|
|
}
|
|
}
|
|
|
|
Future<void> _startDiscovery() async {
|
|
_stream = FlutterBluetoothSerial.instance.startDiscovery();
|
|
_streamSubscription = _stream.listen(_discoveryOnListen);
|
|
_streamSubscription.onDone(_discoveryOnDone);
|
|
}
|
|
|
|
void _discoveryOnDone() {
|
|
setState(() {
|
|
isDiscovering = false;
|
|
//debugPrint(isDiscovering as String?);
|
|
});
|
|
}
|
|
|
|
void _discoveryOnListen(BluetoothDiscoveryResult event) {
|
|
setState(() {
|
|
final int existingIndex = _discoveryResults.indexWhere(
|
|
(element) => element.device.address == event.device.address);
|
|
if (existingIndex >= 0) {
|
|
_discoveryResults[existingIndex] = event;
|
|
} else {
|
|
_discoveryResults.add(event);
|
|
}
|
|
debugPrint("event: ${event.device.address} ${event.device.name}");
|
|
|
|
String deviceAddress = "64:BC:58:61:56:B0";
|
|
if (event.device.address == deviceAddress) {
|
|
_bluetoothDevice = event.device;
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> _restartDiscovery() async {
|
|
setState(() {
|
|
_discoveryResults.clear();
|
|
isDiscovering = true;
|
|
});
|
|
_startDiscovery();
|
|
}
|
|
|
|
Future<void> _sendData() async {
|
|
//String output = "0123456789 abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ";// !§%&()=?#!?";
|
|
String output = textInput;
|
|
//_bluetoothConnection!.output.add(Uint8List.fromList(const AsciiEncoder().convert("$output \r\n")));
|
|
//await _bluetoothConnection!.output.allSent;
|
|
debugPrint("sent: $output");
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
//bool isDark = true;
|
|
return Theme(
|
|
data: ThemeData.dark(), //isDark ? ThemeData.dark() : ThemeData.light(),
|
|
child: Scaffold(
|
|
appBar: AppBar(
|
|
title: isDiscovering
|
|
? const Text('Bluetooth Devices (searching...)')
|
|
: const Text('Bluetooth Devices'),
|
|
actions: <Widget>[
|
|
isDiscovering
|
|
? FittedBox(
|
|
child: Container(
|
|
margin: const EdgeInsets.all(16.0),
|
|
child: const CircularProgressIndicator(
|
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
|
),
|
|
),
|
|
)
|
|
: IconButton(
|
|
icon: const Icon(Icons.replay),
|
|
onPressed: _restartDiscovery,
|
|
)
|
|
],
|
|
),
|
|
|
|
body: // Center(
|
|
/*child:*/ SingleChildScrollView(
|
|
physics: const ScrollPhysics(),
|
|
child: Column(
|
|
children: <Widget>[
|
|
_SingleSection(
|
|
title: "Setup",
|
|
children: [
|
|
const _CustomListTile(
|
|
title: "Please Enable Bluetooth",
|
|
icon: Icons.info_outline_rounded,
|
|
),
|
|
_CustomListTile(
|
|
title: "Bluetooth Settings",
|
|
icon: Icons.bluetooth_connected,
|
|
onTap: () async {
|
|
await AppSettings.openBluetoothSettings();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
const Divider(),
|
|
TextFormField(
|
|
decoration: const InputDecoration(
|
|
labelText: "BluetoothText"
|
|
),
|
|
initialValue: textInput,
|
|
keyboardType: TextInputType.text,
|
|
onChanged: (String newValue) {
|
|
textInput = newValue;
|
|
},
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
await Permission.bluetoothConnect.request();
|
|
await Permission.bluetoothScan.request();
|
|
if (await Permission.bluetoothScan.request().isGranted) {
|
|
// Either the permission was already granted before or the user just granted it.
|
|
debugPrint("Location Permission is granted");
|
|
} else {
|
|
debugPrint("Location Permission is denied.");
|
|
}
|
|
},
|
|
style: buttonStyle,
|
|
child: const Text("Request Permissions"),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
_restartDiscovery();
|
|
},
|
|
style: buttonStyle,
|
|
child: const Text("Restart Scan"),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
_sendData();
|
|
},
|
|
style: buttonStyle,
|
|
child: const Text("Send Data"),
|
|
),
|
|
const Divider(),
|
|
const _SingleSection(
|
|
title: "Bluetooth Devices",
|
|
children: [],
|
|
),
|
|
const Divider(),
|
|
ListView.builder(
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
shrinkWrap: true,
|
|
itemCount: _discoveryResults.length,
|
|
itemBuilder: (BuildContext context, index) {
|
|
BluetoothDiscoveryResult result = _discoveryResults[index];
|
|
final device = result.device;
|
|
final address = device.address;
|
|
return BluetoothDeviceListEntry(
|
|
device: device,
|
|
rssi: result.rssi,
|
|
onTap: () {
|
|
debugPrint(_bluetoothConnection?.isConnected.toString());
|
|
if (_bluetoothConnection?.isConnected ?? false) {
|
|
_disconnectDevice(device);
|
|
} else {
|
|
_connectDevice(device);
|
|
}
|
|
|
|
//Navigator.of(context).pop(result.device);
|
|
},
|
|
onLongPress: () async {
|
|
try {
|
|
bool bonded = false;
|
|
if (device.isBonded) {
|
|
debugPrint('Unbonding from ${device.address}...');
|
|
await FlutterBluetoothSerial.instance
|
|
.removeDeviceBondWithAddress(address);
|
|
debugPrint(
|
|
'Unbonding from ${device.address} has succed');
|
|
} else {
|
|
debugPrint('Bonding with ${device.address}...');
|
|
bonded = (await FlutterBluetoothSerial.instance
|
|
.bondDeviceAtAddress(address))!;
|
|
debugPrint(
|
|
'Bonding with ${device.address} has ${bonded ? 'succed' : 'failed'}.');
|
|
}
|
|
setState(() {
|
|
_discoveryResults[_discoveryResults.indexOf(result)] =
|
|
BluetoothDiscoveryResult(
|
|
device: BluetoothDevice(
|
|
name: device.name ?? '',
|
|
address: address,
|
|
type: device.type,
|
|
bondState: bonded
|
|
? BluetoothBondState.bonded
|
|
: BluetoothBondState.none,
|
|
),
|
|
rssi: result.rssi);
|
|
});
|
|
} catch (ex) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: const Text('Error occured while bonding'),
|
|
content: Text(ex.toString()),
|
|
actions: <Widget>[
|
|
TextButton(
|
|
child: const Text("Close"),
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
},
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
//),
|
|
),
|
|
);
|
|
}
|
|
/*
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: const Text("Bluetooth Devices"),
|
|
),
|
|
body: RefreshIndicator(
|
|
onRefresh: () {
|
|
debugPrint("refreshed");
|
|
return Future(() => null);
|
|
},
|
|
child: SingleChildScrollView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
//child: Padding(
|
|
//padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Container(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
|
decoration: const BoxDecoration(
|
|
border: Border(
|
|
bottom: BorderSide(width: 1.5, color: Colors.grey),
|
|
),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
const Text(
|
|
"General",
|
|
textAlign: TextAlign.start,
|
|
),
|
|
Row(
|
|
children: [
|
|
_CustomListTile(
|
|
title: "Dark Mode",
|
|
icon: CupertinoIcons.moon,
|
|
trailing:
|
|
CupertinoSwitch(value: false, onChanged: (value) {})),
|
|
]
|
|
),
|
|
TextField(
|
|
controller: textFieldValueHolder,
|
|
decoration: const InputDecoration(
|
|
border: OutlineInputBorder(),
|
|
hintText: 'Enter the name of your new device',
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: () {
|
|
var textFieldValue = textFieldValueHolder.value.text;
|
|
checkNameAvailability(textFieldValue);
|
|
setState(() {
|
|
inputName = textFieldValue;
|
|
});
|
|
},
|
|
style: buttonStyle,
|
|
child: const Text("check name"),
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: () {
|
|
//_startDiscovery();
|
|
initScan();
|
|
},
|
|
style: buttonStyle,
|
|
child: const Text("discover devices"),
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: () {
|
|
//_startDiscovery();
|
|
_sendData();
|
|
},
|
|
style: buttonStyle,
|
|
child: const Text("send data"),
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
child: ElevatedButton(
|
|
onPressed: () {
|
|
//_startDiscovery();
|
|
dispose();
|
|
},
|
|
style: buttonStyle,
|
|
child: const Text("dispose"),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Container(
|
|
padding:
|
|
const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
|
decoration: const BoxDecoration(
|
|
border: Border(
|
|
bottom: BorderSide(width: 1.5, color: Colors.grey),
|
|
),
|
|
),
|
|
child: Row(children: [
|
|
const Text(
|
|
"Toggle Scan",
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
),
|
|
const Expanded(child: Text("")),
|
|
Switch(
|
|
value: widgetScanState,
|
|
onChanged: (bool toggleState) {
|
|
scanState = toggleState;
|
|
log("widget state: $scanState");
|
|
setState(() {
|
|
widgetScanState = toggleState;
|
|
});
|
|
})
|
|
]),
|
|
),
|
|
// BluetoothDiscovery(start: initScan, deviceID: inputName),
|
|
],
|
|
),
|
|
//),
|
|
),
|
|
),
|
|
);
|
|
}*/
|
|
}
|
|
|
|
class _CustomListTile extends StatelessWidget {
|
|
final String title;
|
|
final IconData icon;
|
|
final VoidCallback? onTap;
|
|
//final Widget? trailing;
|
|
|
|
const _CustomListTile({
|
|
Key? key,
|
|
required this.title,
|
|
required this.icon,
|
|
this.onTap,
|
|
//this.trailing,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListTile(
|
|
title: Text(title),
|
|
leading: Icon(icon),
|
|
onTap: onTap,
|
|
//trailing: trailing,
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SingleSection extends StatelessWidget {
|
|
final String? title;
|
|
final List<Widget> children;
|
|
|
|
const _SingleSection({
|
|
Key? key,
|
|
this.title,
|
|
required this.children,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Column(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (title != null)
|
|
Padding(
|
|
padding: const EdgeInsets.all(8.0),
|
|
child: Text(
|
|
title!,
|
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
|
),
|
|
),
|
|
Column(
|
|
children: children,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|