diff --git a/android/src/main/java/com/boskokg/flutter_blue_plus/FlutterBluePlusPlugin.java b/android/src/main/java/com/boskokg/flutter_blue_plus/FlutterBluePlusPlugin.java index 37adbd4d..277e2fc2 100644 --- a/android/src/main/java/com/boskokg/flutter_blue_plus/FlutterBluePlusPlugin.java +++ b/android/src/main/java/com/boskokg/flutter_blue_plus/FlutterBluePlusPlugin.java @@ -1629,6 +1629,9 @@ private static byte[] hexToBytes(String s) { } private static String bytesToHex(byte[] bytes) { + if (bytes == null) { + return ""; + } StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02x", b)); diff --git a/android/src/main/java/com/boskokg/flutter_blue_plus/MessageMaker.java b/android/src/main/java/com/boskokg/flutter_blue_plus/MessageMaker.java index 21d631b6..66c7ee78 100644 --- a/android/src/main/java/com/boskokg/flutter_blue_plus/MessageMaker.java +++ b/android/src/main/java/com/boskokg/flutter_blue_plus/MessageMaker.java @@ -37,6 +37,9 @@ public class MessageMaker { private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); public static String toHexString(byte[] bytes) { + if (bytes == null) { + return ""; + } char[] hexChars = new char[bytes.length * 2]; for (int j = 0; j < bytes.length; j++) { int v = bytes[j] & 0xFF; @@ -180,9 +183,7 @@ static HashMap bmBluetoothCharacteristic(BluetoothDevice device, map.put("characteristic_uuid", characteristic.getUuid().toString()); map.put("descriptors", descriptors); map.put("properties", bmCharacteristicProperties(characteristic.getProperties())); - if(characteristic.getValue() != null) { - map.put("value", toHexString(characteristic.getValue())); - } + map.put("value", toHexString(characteristic.getValue())); return map; } @@ -192,9 +193,7 @@ static HashMap bmBluetoothDescriptor(BluetoothDevice device, Blu map.put("descriptor_uuid", descriptor.getUuid().toString()); map.put("characteristic_uuid", descriptor.getCharacteristic().getUuid().toString()); map.put("service_uuid", descriptor.getCharacteristic().getService().getUuid().toString()); - if(descriptor.getValue() != null) { - map.put("value", toHexString(descriptor.getValue())); - } + map.put("value", toHexString(descriptor.getValue())); return map; } diff --git a/example/lib/main.dart b/example/lib/main.dart index 8eeef4f2..8474af6f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -50,11 +50,16 @@ class FlutterBlueApp extends StatelessWidget { } } -class BluetoothOffScreen extends StatelessWidget { +class BluetoothOffScreen extends StatefulWidget { const BluetoothOffScreen({Key? key, this.adapterState}) : super(key: key); final BluetoothAdapterState? adapterState; + @override + State createState() => _BluetoothOffScreenState(); +} + +class _BluetoothOffScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( @@ -69,7 +74,7 @@ class BluetoothOffScreen extends StatelessWidget { color: Colors.white54, ), Text( - 'Bluetooth Adapter is ${adapterState != null ? adapterState.toString().split(".").last : 'not available'}.', + 'Bluetooth Adapter is ${widget.adapterState != null ? widget.adapterState.toString().split(".").last : 'not available'}.', style: Theme.of(context).primaryTextTheme.titleSmall?.copyWith(color: Colors.white), ), ElevatedButton( @@ -80,8 +85,10 @@ class BluetoothOffScreen extends StatelessWidget { FlutterBluePlus.instance.turnOn(); } } catch (e) { - final snackBar = SnackBar(content: Text('Error: [turnOn] ${e.toString()}')); - ScaffoldMessenger.of(context).showSnackBar(snackBar); + if (mounted) { + final snackBar = SnackBar(content: Text('Error: [turnOn] ${e.toString()}')); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } } }, ), @@ -92,9 +99,14 @@ class BluetoothOffScreen extends StatelessWidget { } } -class FindDevicesScreen extends StatelessWidget { +class FindDevicesScreen extends StatefulWidget { const FindDevicesScreen({Key? key}) : super(key: key); + @override + State createState() => _FindDevicesScreenState(); +} + +class _FindDevicesScreenState extends State { @override Widget build(BuildContext context) { return Scaffold( @@ -155,8 +167,10 @@ class FindDevicesScreen extends StatelessWidget { result: r, onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) { r.device.connect().catchError((e) { - final snackBar = SnackBar(content: Text('Error: [connect] ${e.toString()}')); - ScaffoldMessenger.of(context).showSnackBar(snackBar); + if (mounted) { + final snackBar = SnackBar(content: Text('Error: [connect] ${e.toString()}')); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } }); return DeviceScreen(device: r.device); })), @@ -177,9 +191,11 @@ class FindDevicesScreen extends StatelessWidget { return FloatingActionButton( child: const Icon(Icons.stop), onPressed: () { - FlutterBluePlus.instance.stopScan().catchError((e){ - final snackBar = SnackBar(content: Text('Error: [stopScan] ${e.toString()}')); - ScaffoldMessenger.of(context).showSnackBar(snackBar); + FlutterBluePlus.instance.stopScan().catchError((e) { + if (mounted) { + final snackBar = SnackBar(content: Text('Error: [stopScan] ${e.toString()}')); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } }); }, backgroundColor: Colors.red, @@ -188,10 +204,13 @@ class FindDevicesScreen extends StatelessWidget { return FloatingActionButton( child: const Icon(Icons.search), onPressed: () { - FlutterBluePlus.instance - .startScan(timeout: const Duration(seconds: 15), androidUsesFineLocation: false).catchError((e){ - final snackBar = SnackBar(content: Text('Error: [startScan] ${e.toString()}')); - ScaffoldMessenger.of(context).showSnackBar(snackBar); + FlutterBluePlus.instance + .startScan(timeout: const Duration(seconds: 15), androidUsesFineLocation: false) + .catchError((e) { + if (mounted) { + final snackBar = SnackBar(content: Text('Error: [startScan] ${e.toString()}')); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } }); }); } @@ -201,11 +220,16 @@ class FindDevicesScreen extends StatelessWidget { } } -class DeviceScreen extends StatelessWidget { +class DeviceScreen extends StatefulWidget { const DeviceScreen({Key? key, required this.device}) : super(key: key); final BluetoothDevice device; + @override + State createState() => _DeviceScreenState(); +} + +class _DeviceScreenState extends State { List _getRandomBytes() { final math = Random(); return [math.nextInt(255), math.nextInt(255), math.nextInt(255), math.nextInt(255)]; @@ -224,8 +248,10 @@ class DeviceScreen extends StatelessWidget { try { await c.read(); } catch (e) { - final snackBar = SnackBar(content: Text('Error: [read] ${e.toString()}')); - ScaffoldMessenger.of(context).showSnackBar(snackBar); + if (mounted) { + final snackBar = SnackBar(content: Text('Error: [read] ${e.toString()}')); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } } }, onWritePressed: () async { @@ -235,8 +261,10 @@ class DeviceScreen extends StatelessWidget { await c.read(); } } catch (e) { - final snackBar = SnackBar(content: Text('Error: [write] ${e.toString()}')); - ScaffoldMessenger.of(context).showSnackBar(snackBar); + if (mounted) { + final snackBar = SnackBar(content: Text('Error: [write] ${e.toString()}')); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } } }, onNotificationPressed: () async { @@ -246,8 +274,10 @@ class DeviceScreen extends StatelessWidget { await c.read(); } } catch (e) { - final snackBar = SnackBar(content: Text('Error: [setNotifyValue] ${e.toString()}')); - ScaffoldMessenger.of(context).showSnackBar(snackBar); + if (mounted) { + final snackBar = SnackBar(content: Text('Error: [setNotifyValue] ${e.toString()}')); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } } }, descriptorTiles: c.descriptors @@ -271,10 +301,10 @@ class DeviceScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(device.localName), + title: Text(widget.device.localName), actions: [ StreamBuilder( - stream: device.connectionState, + stream: widget.device.connectionState, initialData: BluetoothConnectionState.connecting, builder: (c, snapshot) { VoidCallback? onPressed; @@ -283,21 +313,25 @@ class DeviceScreen extends StatelessWidget { case BluetoothConnectionState.connected: onPressed = () async { try { - await device.disconnect(); + await widget.device.disconnect(); } catch (e) { - final snackBar = SnackBar(content: Text('Error: [disconnect] ${e.toString()}')); - ScaffoldMessenger.of(context).showSnackBar(snackBar); + if (mounted) { + final snackBar = SnackBar(content: Text('Error: [disconnect] ${e.toString()}')); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } } }; text = 'DISCONNECT'; break; case BluetoothConnectionState.disconnected: - onPressed = () async { + onPressed = () async { try { - await device.connect(); + await widget.device.connect(); } catch (e) { - final snackBar = SnackBar(content: Text('Error: [connect] ${e.toString()}')); - ScaffoldMessenger.of(context).showSnackBar(snackBar); + if (mounted) { + final snackBar = SnackBar(content: Text('Error: [connect] ${e.toString()}')); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } } }; text = 'CONNECT'; @@ -321,7 +355,7 @@ class DeviceScreen extends StatelessWidget { child: Column( children: [ StreamBuilder( - stream: device.connectionState, + stream: widget.device.connectionState, initialData: BluetoothConnectionState.connecting, builder: (c, snapshot) => ListTile( leading: Column( @@ -341,9 +375,9 @@ class DeviceScreen extends StatelessWidget { ], ), title: Text('Device is ${snapshot.data.toString().split('.')[1]}.'), - subtitle: Text('${device.remoteId}'), + subtitle: Text('${widget.device.remoteId}'), trailing: StreamBuilder( - stream: device.isDiscoveringServices, + stream: widget.device.isDiscoveringServices, initialData: false, builder: (c, snapshot) => IndexedStack( index: snapshot.data! ? 1 : 0, @@ -352,10 +386,12 @@ class DeviceScreen extends StatelessWidget { child: const Text("Discover Services"), onPressed: () async { try { - await device.discoverServices(); + await widget.device.discoverServices(); } catch (e) { - final snackBar = SnackBar(content: Text('Error: [discoverServices] ${e.toString()}')); - ScaffoldMessenger.of(context).showSnackBar(snackBar); + if (mounted) { + final snackBar = SnackBar(content: Text('Error: [discoverServices] ${e.toString()}')); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } } }, ), @@ -375,7 +411,7 @@ class DeviceScreen extends StatelessWidget { ), ), StreamBuilder( - stream: device.mtu, + stream: widget.device.mtu, initialData: 0, builder: (c, snapshot) => ListTile( title: const Text('MTU Size'), @@ -384,16 +420,18 @@ class DeviceScreen extends StatelessWidget { icon: const Icon(Icons.edit), onPressed: () async { try { - await device.requestMtu(223); + await widget.device.requestMtu(223); } catch (e) { - final snackBar = SnackBar(content: Text('Error: [requestMtu] ${e.toString()}')); - ScaffoldMessenger.of(context).showSnackBar(snackBar); + if (mounted) { + final snackBar = SnackBar(content: Text('Error: [requestMtu] ${e.toString()}')); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } } }), ), ), StreamBuilder>( - stream: device.services, + stream: widget.device.services, initialData: const [], builder: (c, snapshot) { return Column( @@ -409,12 +447,12 @@ class DeviceScreen extends StatelessWidget { Stream rssiStream({Duration frequency = const Duration(seconds: 1)}) async* { var isConnected = true; - final subscription = device.connectionState.listen((v) { + final subscription = widget.device.connectionState.listen((v) { isConnected = v == BluetoothConnectionState.connected; }); while (isConnected) { try { - yield await device.readRssi(); + yield await widget.device.readRssi(); } catch (e) { print("Error reading RSSI: $e"); break; diff --git a/example/lib/widgets.dart b/example/lib/widgets.dart index a06951ed..23067dcf 100644 --- a/example/lib/widgets.dart +++ b/example/lib/widgets.dart @@ -168,8 +168,8 @@ class _CharacteristicTileState extends State { return StreamBuilder>( stream: widget.characteristic.lastValueStream, initialData: widget.characteristic.lastValue, - builder: (c, snapshot) { - final value = snapshot.data; + builder: (context, snapshot) { + final List? value = snapshot.data; return ExpansionTile( title: ListTile( title: Row( diff --git a/ios/Classes/FlutterBluePlusPlugin.m b/ios/Classes/FlutterBluePlusPlugin.m index d1738f79..5b5fe692 100644 --- a/ios/Classes/FlutterBluePlusPlugin.m +++ b/ios/Classes/FlutterBluePlusPlugin.m @@ -1099,7 +1099,11 @@ - (void)peripheral:(CBPeripheral *)peripheral ServicePair *pair = [self getServicePair:peripheral characteristic:descriptor.characteristic]; - int value = [descriptor.value intValue]; + NSData* data = nil; + if (descriptor.value) { + int value = [descriptor.value intValue]; + data = [NSData dataWithBytes:&value length:sizeof(value)]; + } // See BmOnDescriptorResponse NSDictionary* result = @{ @@ -1109,7 +1113,7 @@ - (void)peripheral:(CBPeripheral *)peripheral @"secondary_service_uuid": pair.secondary ? [pair.secondary.UUID fullUUIDString] : [NSNull null], @"characteristic_uuid": [descriptor.characteristic.UUID fullUUIDString], @"descriptor_uuid": [descriptor.UUID fullUUIDString], - @"value": [self convertDataToHex:[NSData dataWithBytes:&value length:sizeof(value)]], + @"value": [self convertDataToHex:data], @"success": @(error == nil), @"error_string": error ? [error localizedDescription] : [NSNull null], @"error_code": error ? @(error.code) : [NSNull null], @@ -1215,6 +1219,10 @@ - (void)peripheralIsReadyToSendWriteWithoutResponse:(CBPeripheral *)peripheral - (NSString *)convertDataToHex:(NSData *)data { + if (data == nil) { + return @""; + } + const unsigned char *bytes = (const unsigned char *)[data bytes]; NSMutableString *hexString = [NSMutableString new]; @@ -1375,7 +1383,11 @@ - (NSDictionary*)toCharacteristicProto:(CBPeripheral *)peripheral NSMutableArray *descriptorProtos = [NSMutableArray new]; for (CBDescriptor *d in [characteristic descriptors]) { - int value = [d.value intValue]; + NSData* data = nil; + if (d.value) { + int value = [d.value intValue]; + data = [NSData dataWithBytes:&value length:sizeof(value)]; + } // See: BmBluetoothDescriptor NSDictionary* desc = @{ @@ -1384,7 +1396,7 @@ - (NSDictionary*)toCharacteristicProto:(CBPeripheral *)peripheral @"secondary_service_uuid": [NSNull null], @"characteristic_uuid": [d.characteristic.UUID fullUUIDString], @"descriptor_uuid": [d.UUID fullUUIDString], - @"value": [self convertDataToHex:[NSData dataWithBytes:&value length:sizeof(value)]], + @"value": [self convertDataToHex:data], }; [descriptorProtos addObject:desc]; @@ -1416,7 +1428,7 @@ - (NSDictionary*)toCharacteristicProto:(CBPeripheral *)peripheral @"characteristic_uuid": [characteristic.UUID fullUUIDString], @"descriptors": descriptorProtos, @"properties": propsMap, - @"value": [self convertDataToHex:[characteristic value]], + @"value": [self convertDataToHex:characteristic.value], }; } diff --git a/lib/src/bluetooth_msgs.dart b/lib/src/bluetooth_msgs.dart index 71cb3357..66dae93c 100644 --- a/lib/src/bluetooth_msgs.dart +++ b/lib/src/bluetooth_msgs.dart @@ -278,7 +278,7 @@ class BmBluetoothCharacteristic { characteristicUuid: Guid(json['characteristic_uuid']), descriptors: descs, properties: BmCharacteristicProperties.fromMap(json['properties']), - value: _hexDecode(json['value'] ?? ""), + value: _hexDecode(json['value']), ); } } @@ -305,7 +305,7 @@ class BmBluetoothDescriptor { serviceUuid: Guid(json['service_uuid']), characteristicUuid: Guid(json['characteristic_uuid']), descriptorUuid: Guid(json['descriptor_uuid']), - value: _hexDecode(json['value'] ?? ""), + value: _hexDecode(json['value']), ); } } @@ -434,7 +434,7 @@ class BmOnCharacteristicReceived { serviceUuid: Guid(json['service_uuid']), secondaryServiceUuid: json['secondary_service_uuid'] != null ? Guid(json['secondary_service_uuid']) : null, characteristicUuid: Guid(json['characteristic_uuid']), - value: _hexDecode(json['value'] ?? ""), + value: _hexDecode(json['value']), success: json['success'] != 0, errorCode: json['error_code'], errorString: json['error_string'], @@ -566,18 +566,6 @@ class BmWriteDescriptorRequest { _printDbg("\nBmWriteDescriptorRequest $data"); return data; } - - factory BmWriteDescriptorRequest.fromMap(Map json) { - _printDbg("\nBmWriteDescriptorRequest $json"); - return BmWriteDescriptorRequest( - remoteId: json['remote_id'], - serviceUuid: Guid(json['service_uuid']), - secondaryServiceUuid: json['secondary_service_uuid'] != null ? Guid(json['secondary_service_uuid']) : null, - characteristicUuid: Guid(json['characteristic_uuid']), - descriptorUuid: Guid(json['descriptor_uuid']), - value: _hexDecode(json['value'] ?? ""), - ); - } } enum BmOnDescriptorResponseType { @@ -629,7 +617,7 @@ class BmOnDescriptorResponse { secondaryServiceUuid: json['secondary_service_uuid'] != null ? Guid(json['secondary_service_uuid']) : null, characteristicUuid: Guid(json['characteristic_uuid']), descriptorUuid: Guid(json['descriptor_uuid']), - value: _hexDecode(json['value'] ?? ""), + value: _hexDecode(json['value']), success: json['success'] != 0, errorCode: json['error_code'], errorString: json['error_string'],