Files
universal_ble/example/lib/peripheral_details/peripheral_detail_page.dart
T
Foti Dim 3be945c3d5 Improve manufacturer data discovery on Windows (#38)
* Improve manufacturer data discovery on Windows

* Switch pairing protection level to encrypted in Windows

* Minor UI improvement in example app

* Update windows/src/universal_ble_plugin.h

* Remove minimum DevicePairingProtectionLevel

* Improve logging

* Bump version

---------

Co-authored-by: Rohit Sangwan <rohitsangwan647@gmail.com>
2024-05-02 00:09:44 +02:00

460 lines
18 KiB
Dart

// ignore_for_file: avoid_print, depend_on_referenced_packages
import 'package:convert/convert.dart';
import 'package:expandable/expandable.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:universal_ble/universal_ble.dart';
import 'package:universal_ble_example/data/capabilities.dart';
import 'package:universal_ble_example/peripheral_details/widgets/result_widget.dart';
import 'package:universal_ble_example/peripheral_details/widgets/services_list_widget.dart';
import 'package:universal_ble_example/widgets/platform_button.dart';
import 'package:universal_ble_example/widgets/responsive_buttons_grid.dart';
import 'package:universal_ble_example/widgets/responsive_view.dart';
class PeripheralDetailPage extends StatefulWidget {
final String deviceId;
final String deviceName;
const PeripheralDetailPage(this.deviceId, this.deviceName, {Key? key})
: super(key: key);
@override
State<StatefulWidget> createState() {
return _PeripheralDetailPageState();
}
}
class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
bool isConnected = false;
GlobalKey<FormState> valueFormKey = GlobalKey<FormState>();
List<BleService> discoveredServices = [];
ExpandableController expandableController = ExpandableController();
final List<String> _logs = [];
final binaryCode = TextEditingController();
({
BleService service,
BleCharacteristic characteristic
})? selectedCharacteristic;
@override
void initState() {
super.initState();
UniversalBle.onConnectionChanged = _handleConnectionChange;
UniversalBle.onValueChanged = _handleValueChange;
UniversalBle.onPairingStateChange = _handlePairingStateChange;
}
@override
void dispose() {
super.dispose();
UniversalBle.onConnectionChanged = null;
UniversalBle.onValueChanged = null;
// Disconnect when leaving the page
if (isConnected) UniversalBle.disconnect(widget.deviceId);
}
void _addLog(String type, dynamic data) {
setState(() {
_logs.add('$type: ${data.toString()}');
});
}
void _handleConnectionChange(String deviceId, BleConnectionState state) {
print('_handleConnectionChange $deviceId, ${state.name}');
setState(() {
if (deviceId == widget.deviceId) {
isConnected = (state == BleConnectionState.connected);
}
});
_addLog('Connection', state.name.toUpperCase());
// Auto Discover Services
if (isConnected) {
_discoverServices();
}
}
void _handleValueChange(
String deviceId, String characteristicId, Uint8List value) {
String s = String.fromCharCodes(value);
String data = '$s\nraw : ${value.toString()}';
print('_handleValueChange $deviceId, $characteristicId, $s');
_addLog("Value", data);
}
void _handlePairingStateChange(
String deviceId, bool isPaired, String? error) {
print('OnPairStateChange $deviceId, $isPaired');
if (error != null && error.isNotEmpty) {
_addLog("PairStateChangeError", "(Paired: $isPaired): $error ");
} else {
_addLog("PairStateChange", isPaired);
}
}
Future<void> _discoverServices() async {
var services = await UniversalBle.discoverServices(widget.deviceId);
print('${services.length} services discovered');
discoveredServices.clear();
setState(() {
discoveredServices = services;
});
if (kIsWeb) {
_addLog("DiscoverServices",
'${services.length} services discovered,\nNote: Only services added in WebRequestOptionsBuilder will be discoverd');
}
}
Future<void> _readValue() async {
if (selectedCharacteristic == null) return;
try {
Uint8List value = await UniversalBle.readValue(
widget.deviceId,
selectedCharacteristic!.service.uuid,
selectedCharacteristic!.characteristic.uuid,
);
String s = String.fromCharCodes(value);
String data = '$s\nraw : ${value.toString()}';
_addLog('Read', data);
} catch (e) {
_addLog('ReadError', e);
}
}
Future<void> _writeValue() async {
if (selectedCharacteristic == null ||
!valueFormKey.currentState!.validate() ||
binaryCode.text.isEmpty) {
return;
}
Uint8List value;
try {
value = Uint8List.fromList(hex.decode(binaryCode.text));
} catch (e) {
_addLog('WriteError', "Error parsing hex $e");
return;
}
try {
await UniversalBle.writeValue(
widget.deviceId,
selectedCharacteristic!.service.uuid,
selectedCharacteristic!.characteristic.uuid,
value,
BleOutputProperty.withResponse,
);
_addLog('Write', value);
} catch (e) {
print(e);
_addLog('WriteError', e);
}
}
Future<void> _setBleInputProperty(BleInputProperty inputProperty) async {
if (selectedCharacteristic == null) return;
try {
if (inputProperty != BleInputProperty.disabled) {
List<CharacteristicProperty> properties =
selectedCharacteristic!.characteristic.properties;
if (properties.contains(CharacteristicProperty.notify)) {
inputProperty = BleInputProperty.notification;
} else if (properties.contains(CharacteristicProperty.indicate)) {
inputProperty = BleInputProperty.indication;
} else {
throw 'No notify or indicate property';
}
}
await UniversalBle.setNotifiable(
widget.deviceId,
selectedCharacteristic!.service.uuid,
selectedCharacteristic!.characteristic.uuid,
inputProperty,
);
_addLog('BleInputProperty', inputProperty);
} catch (e) {
_addLog('NotifyError', e);
}
}
bool isValidProperty(List<CharacteristicProperty> properties) {
for (CharacteristicProperty property in properties) {
if (selectedCharacteristic?.characteristic.properties
.contains(property) ??
false) {
return true;
}
}
return false;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("${widget.deviceName} - ${widget.deviceId}"),
elevation: 4,
actions: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
isConnected
? Icons.bluetooth_connected
: Icons.bluetooth_disabled,
color: isConnected ? Colors.greenAccent : Colors.red,
size: 20,
),
)
],
),
body: ResponsiveView(builder: (_, DeviceType deviceType) {
return Row(
children: [
if (deviceType == DeviceType.desktop)
Expanded(
flex: 1,
child: Container(
color: Theme.of(context).secondaryHeaderColor,
child: discoveredServices.isEmpty
? const Center(
child: Text('No Services Discovered'),
)
: ServicesListWidget(
discoveredServices: discoveredServices,
scrollable: true,
onTap: (BleService service,
BleCharacteristic characteristic) {
setState(() {
selectedCharacteristic = (
service: service,
characteristic: characteristic
);
});
},
),
),
),
Expanded(
flex: 3,
child: Align(
alignment: Alignment.topCenter,
child: SingleChildScrollView(
child: Column(
children: [
// Top buttons
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
PlatformButton(
text: 'Connect',
enabled: !isConnected,
onPressed: () async {
try {
await UniversalBle.connect(widget.deviceId);
} catch (e) {
_addLog('ConnectError', e);
}
},
),
PlatformButton(
text: 'Disconnect',
enabled: isConnected,
onPressed: () {
UniversalBle.disconnect(widget.deviceId);
},
),
],
),
),
selectedCharacteristic == null
? Text(discoveredServices.isEmpty
? "Please discover services"
: "Please select a characteristic")
: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
),
child: Card(
child: ListTile(
title: SelectableText(
"Characteristic: ${selectedCharacteristic!.characteristic.uuid}",
),
subtitle: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
SelectableText(
"Service: ${selectedCharacteristic!.service.uuid}",
),
Text(
"Properties: ${selectedCharacteristic!.characteristic.properties.map((e) => e.name)}",
),
],
),
),
),
),
if (isValidProperty([
CharacteristicProperty.write,
CharacteristicProperty.writeWithoutResponse
]))
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Form(
key: valueFormKey,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: TextFormField(
controller: binaryCode,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a value';
}
try {
hex.decode(binaryCode.text);
return null;
} catch (e) {
return 'Please enter a valid hex value ( without spaces or 0x (e.g. F0BB) )';
}
},
decoration: const InputDecoration(
hintText:
"Enter Hex values without spaces or 0x (e.g. F0BB)",
border: OutlineInputBorder(),
),
),
),
),
),
const Divider(),
Padding(
padding: const EdgeInsets.all(8.0),
child: ResponsiveButtonsGrid(
children: [
PlatformButton(
onPressed: () async {
_discoverServices();
},
enabled: isConnected,
text: 'Discover Services',
),
if (Capabilities.supportsRequestMtuApi)
PlatformButton(
enabled: isConnected,
onPressed: () async {
int mtu = await UniversalBle.requestMtu(
widget.deviceId, 247);
_addLog('MTU', mtu);
},
text: 'Request Mtu',
),
PlatformButton(
enabled: isConnected &&
discoveredServices.isNotEmpty &&
isValidProperty([
CharacteristicProperty.read,
]),
onPressed: _readValue,
text: 'Read',
),
PlatformButton(
enabled: isConnected &&
discoveredServices.isNotEmpty &&
isValidProperty([
CharacteristicProperty.write,
CharacteristicProperty.writeWithoutResponse
]),
onPressed: _writeValue,
text: 'Write',
),
PlatformButton(
enabled: isConnected &&
discoveredServices.isNotEmpty &&
isValidProperty([
CharacteristicProperty.notify,
CharacteristicProperty.indicate
]),
onPressed: () => _setBleInputProperty(
BleInputProperty.notification),
text: 'Subscribe',
),
PlatformButton(
enabled: isConnected &&
discoveredServices.isNotEmpty &&
isValidProperty([
CharacteristicProperty.notify,
CharacteristicProperty.indicate
]),
onPressed: () => _setBleInputProperty(
BleInputProperty.disabled),
text: 'Unsubscribe',
),
if (Capabilities.supportsPairingApi)
PlatformButton(
onPressed: () async {
await UniversalBle.pair(widget.deviceId);
},
text: 'Pair',
),
if (Capabilities.supportsPairingApi)
PlatformButton(
onPressed: () async {
bool isPaired = await UniversalBle.isPaired(
widget.deviceId);
_addLog('IsPaired', isPaired);
},
text: 'IsPaired',
),
if (Capabilities.supportsPairingApi)
PlatformButton(
onPressed: () async {
await UniversalBle.unPair(widget.deviceId);
},
text: 'UnPair',
),
],
),
),
// Services
if (deviceType != DeviceType.desktop)
ServicesListWidget(
discoveredServices: discoveredServices,
onTap: (BleService service,
BleCharacteristic characteristic) {
setState(() {
selectedCharacteristic = (
service: service,
characteristic: characteristic
);
});
},
),
const Divider(),
ResultWidget(
results: _logs,
onClearTap: (int? index) {
setState(() {
if (index != null) {
_logs.removeAt(index);
} else {
_logs.clear();
}
});
}),
const SizedBox(height: 20),
],
),
),
),
),
],
);
}),
);
}
}