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 createState() => _BluetoothScreen(); } // debugPrint("r: ${r.device.name} ${r.device.address} ${r.rssi}"); class _BluetoothScreen extends State { CloudServiceAPI cloudServiceAPI = CloudServiceAPI(); //BluetoothManager bluetoothManager = BluetoothManager(); late Stream _stream; late StreamSubscription _streamSubscription; late Stream _connectionStream; late StreamSubscription _connectionStreamSubscription; BluetoothDevice? _bluetoothDevice; BluetoothConnection? _bluetoothConnection; final List _discoveryResults = List.empty(growable: true); String _messageBuffer = ""; bool isDiscovering = false; bool isConnecting = false; bool isDisconnecting = false; bool isConnected = false; 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 _disconnectDevice(BluetoothDevice device) async { if (_bluetoothConnection != null) { await _bluetoothConnection!.finish(); _bluetoothConnection = null; } } Future _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 _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 _restartDiscovery() async { setState(() { _discoveryResults.clear(); isDiscovering = true; }); _startDiscovery(); } Future _sendData() async { String output = "0123456789 abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ !ยง%&()=?#!?"; _bluetoothConnection!.output .add(Uint8List.fromList(const Utf8Encoder().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: [ isDiscovering ? FittedBox( child: Container( margin: const EdgeInsets.all(16.0), child: const CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Colors.white), ), ), ) : IconButton( icon: const Icon(Icons.replay), onPressed: _restartDiscovery, ) ], ), body: // Center( /*child:*/ SingleChildScrollView( physics: const ScrollPhysics(), child: Column( children: [ _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(), 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: [ 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 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, ), ], ); } }