From 0d10c0280b478a4cd02242f21dd70f2ec0ba5e27 Mon Sep 17 00:00:00 2001 From: sophisticode Date: Fri, 20 Aug 2021 15:52:56 +0200 Subject: [PATCH] - Read the RSSI for a connected device - Show RSSI of connected device in example app (below Bluetooth symbol) --- .../flutter_blue/FlutterBluePlugin.java | 24 ++++++ example/ios/Podfile | 77 ++++--------------- example/ios/Podfile.lock | 12 +-- example/lib/main.dart | 19 ++++- ios/Classes/FlutterBluePlugin.m | 16 ++++ ios/gen/Flutterblue.pbobjc.h | 15 ++++ ios/gen/Flutterblue.pbobjc.m | 56 ++++++++++++++ lib/gen/flutterblue.pb.dart | 61 +++++++++++++++ lib/gen/flutterblue.pbjson.dart | 11 +++ lib/src/bluetooth_device.dart | 18 +++++ protos/flutterblue.proto | 5 ++ 11 files changed, 239 insertions(+), 75 deletions(-) diff --git a/android/src/main/java/com/pauldemarco/flutter_blue/FlutterBluePlugin.java b/android/src/main/java/com/pauldemarco/flutter_blue/FlutterBluePlugin.java index f3ffbdb9..4977627c 100644 --- a/android/src/main/java/com/pauldemarco/flutter_blue/FlutterBluePlugin.java +++ b/android/src/main/java/com/pauldemarco/flutter_blue/FlutterBluePlugin.java @@ -627,6 +627,24 @@ public void onMethodCall(MethodCall call, Result result) { break; } + case "readRssi": + { + String remoteId = (String)call.arguments; + BluetoothGatt gatt; + try { + gatt = locateGatt(remoteId); + if(gatt.readRemoteRssi()) { + result.success(null); + } else { + result.error("readRssi", "gatt.readRemoteRssi returned false", null); + } + } catch(Exception e) { + result.error("readRssi", e.getMessage(), e); + } + + break; + } + default: { result.notImplemented(); @@ -964,6 +982,12 @@ public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { @Override public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { log(LogLevel.DEBUG, "[onReadRemoteRssi] rssi: " + rssi + " status: " + status); + if(status == BluetoothGatt.GATT_SUCCESS) { + Protos.ReadRssiResult.Builder p = Protos.ReadRssiResult.newBuilder(); + p.setRemoteId(gatt.getDevice().getAddress()); + p.setRssi(rssi); + invokeMethodUIThread("ReadRssiResult", p.build().toByteArray()); + } } @Override diff --git a/example/ios/Podfile b/example/ios/Podfile index 98a90b8a..f7d6a5e6 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -10,78 +10,29 @@ project 'Runner', { 'Release' => :release, } -def parse_KV_file(file, separator='=') - file_abs_path = File.expand_path(file) - if !File.exists? file_abs_path - return []; +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end - generated_key_values = {} - skip_line_start_symbols = ["#", "/"] - File.foreach(file_abs_path) do |line| - next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ } - plugin = line.split(pattern=separator) - if plugin.length == 2 - podname = plugin[0].strip() - path = plugin[1].strip() - podpath = File.expand_path("#{path}", file_abs_path) - generated_key_values[podname] = podpath - else - puts "Invalid plugin specification: #{line}" - end - end - generated_key_values -end - -target 'Runner' do - # Flutter Pod - - copied_flutter_dir = File.join(__dir__, 'Flutter') - copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework') - copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec') - unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path) - # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet. - # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration. - # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist. - generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig') - unless File.exist?(generated_xcode_build_settings_path) - raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path) - cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR']; - - unless File.exist?(copied_framework_path) - FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir) - end - unless File.exist?(copied_podspec_path) - FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir) - end + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end - # Keep pod path relative so it can be checked into Podfile.lock. - pod 'Flutter', :path => 'Flutter' +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - # Plugin Pods +flutter_ios_podfile_setup - # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock - # referring to absolute paths on developers' machines. - system('rm -rf .symlinks') - system('mkdir -p .symlinks/plugins') - plugin_pods = parse_KV_file('../.flutter-plugins') - plugin_pods.each do |name, path| - symlink = File.join('.symlinks', 'plugins', name) - File.symlink(path, symlink) - pod name, :path => File.join(symlink, 'ios') - end +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end -# Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system. -install! 'cocoapods', :disable_input_output_paths => true - post_install do |installer| installer.pods_project.targets.each do |target| - target.build_configurations.each do |config| - config.build_settings['ENABLE_BITCODE'] = 'NO' - end + flutter_additional_ios_build_settings(target) end end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 0fb4b650..6d25a6b6 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,6 +1,4 @@ PODS: - - e2e (0.0.1): - - Flutter - Flutter (1.0.0) - flutter_blue (0.0.1): - Flutter @@ -11,7 +9,6 @@ PODS: - Protobuf (3.11.4) DEPENDENCIES: - - e2e (from `.symlinks/plugins/e2e/ios`) - Flutter (from `Flutter`) - flutter_blue (from `.symlinks/plugins/flutter_blue/ios`) @@ -20,19 +17,16 @@ SPEC REPOS: - Protobuf EXTERNAL SOURCES: - e2e: - :path: ".symlinks/plugins/e2e/ios" Flutter: :path: Flutter flutter_blue: :path: ".symlinks/plugins/flutter_blue/ios" SPEC CHECKSUMS: - e2e: 967b9b1fc533b7636a3b7a719f840c27f301fe1f - Flutter: 0e3d915762c693b495b44d77113d4970485de6ec + Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c flutter_blue: eeb381dc4727a0954dede73515f683865494b370 Protobuf: 176220c526ad8bd09ab1fb40a978eac3fef665f7 -PODFILE CHECKSUM: 3dbe063e9c90a5d7c9e4e76e70a821b9e2c1d271 +PODFILE CHECKSUM: 8e679eca47255a8ca8067c4c67aab20e64cb974d -COCOAPODS: 1.9.1 +COCOAPODS: 1.10.1 diff --git a/example/lib/main.dart b/example/lib/main.dart index e4a55a1a..94790016 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -247,9 +247,22 @@ class DeviceScreen extends StatelessWidget { stream: device.state, initialData: BluetoothDeviceState.connecting, builder: (c, snapshot) => ListTile( - leading: (snapshot.data == BluetoothDeviceState.connected) - ? Icon(Icons.bluetooth_connected) - : Icon(Icons.bluetooth_disabled), + leading: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Icon(snapshot.data == BluetoothDeviceState.connected + ? Icons.bluetooth_connected + : Icons.bluetooth_disabled), + snapshot.data == BluetoothDeviceState.connected + ? FutureBuilder( + future: device.readRssi(), + builder: (context, snapshot) { + return Text(snapshot.hasData ? '${snapshot.data}' : '', + style: Theme.of(context).textTheme.caption); + }) + : Text('', style: Theme.of(context).textTheme.caption), + ], + ), title: Text( 'Device is ${snapshot.data.toString().split('.')[1]}.'), subtitle: Text('${device.id}'), diff --git a/ios/Classes/FlutterBluePlugin.m b/ios/Classes/FlutterBluePlugin.m index 65a73b6d..a1c66572 100644 --- a/ios/Classes/FlutterBluePlugin.m +++ b/ios/Classes/FlutterBluePlugin.m @@ -253,6 +253,15 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { } } else if([@"requestMtu" isEqualToString:call.method]) { result([FlutterError errorWithCode:@"requestMtu" message:@"iOS does not allow mtu requests to the peripheral" details:NULL]); + } else if([@"readRssi" isEqualToString:call.method]) { + NSString *remoteId = [call arguments]; + @try { + CBPeripheral *peripheral = [self findPeripheral:remoteId]; + [peripheral readRSSI]; + result(nil); + } @catch(FlutterError *e) { + result(e); + } } else { result(FlutterMethodNotImplemented); } @@ -538,6 +547,13 @@ - (void)peripheral:(CBPeripheral *)peripheral didWriteValueForDescriptor:(CBDesc [_channel invokeMethod:@"WriteDescriptorResponse" arguments:[self toFlutterData:result]]; } +- (void)peripheral:(CBPeripheral *)peripheral didReadRSSI:(NSNumber *)rssi error:(NSError *)error { + ProtosReadRssiResult *result = [[ProtosReadRssiResult alloc] init]; + [result setRemoteId:[peripheral.identifier UUIDString]]; + [result setRssi:[rssi intValue]]; + [_channel invokeMethod:@"ReadRssiResult" arguments:[self toFlutterData:result]]; +} + // // Proto Helper methods // diff --git a/ios/gen/Flutterblue.pbobjc.h b/ios/gen/Flutterblue.pbobjc.h index 747d17a9..597959fe 100644 --- a/ios/gen/Flutterblue.pbobjc.h +++ b/ios/gen/Flutterblue.pbobjc.h @@ -777,6 +777,21 @@ typedef GPB_ENUM(ProtosMtuSizeResponse_FieldNumber) { @end +#pragma mark - ProtosReadRssiResult + +typedef GPB_ENUM(ProtosReadRssiResult_FieldNumber) { + ProtosReadRssiResult_FieldNumber_RemoteId = 1, + ProtosReadRssiResult_FieldNumber_Rssi = 2, +}; + +@interface ProtosReadRssiResult : GPBMessage + +@property(nonatomic, readwrite, copy, null_resettable) NSString *remoteId; + +@property(nonatomic, readwrite) int32_t rssi; + +@end + NS_ASSUME_NONNULL_END CF_EXTERN_C_END diff --git a/ios/gen/Flutterblue.pbobjc.m b/ios/gen/Flutterblue.pbobjc.m index 4f9622d0..3f060bca 100644 --- a/ios/gen/Flutterblue.pbobjc.m +++ b/ios/gen/Flutterblue.pbobjc.m @@ -2197,6 +2197,62 @@ + (GPBDescriptor *)descriptor { @end +#pragma mark - ProtosReadRssiResult + +@implementation ProtosReadRssiResult + +@dynamic remoteId; +@dynamic rssi; + +typedef struct ProtosReadRssiResult__storage_ { + uint32_t _has_storage_[1]; + int32_t rssi; + NSString *remoteId; +} ProtosReadRssiResult__storage_; + +// This method is threadsafe because it is initially called +// in +initialize for each subclass. ++ (GPBDescriptor *)descriptor { + static GPBDescriptor *descriptor = nil; + if (!descriptor) { + static GPBMessageFieldDescription fields[] = { + { + .name = "remoteId", + .dataTypeSpecific.className = NULL, + .number = ProtosReadRssiResult_FieldNumber_RemoteId, + .hasIndex = 0, + .offset = (uint32_t)offsetof(ProtosReadRssiResult__storage_, remoteId), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeString, + }, + { + .name = "rssi", + .dataTypeSpecific.className = NULL, + .number = ProtosReadRssiResult_FieldNumber_Rssi, + .hasIndex = 1, + .offset = (uint32_t)offsetof(ProtosReadRssiResult__storage_, rssi), + .flags = GPBFieldOptional, + .dataType = GPBDataTypeInt32, + }, + }; + GPBDescriptor *localDescriptor = + [GPBDescriptor allocDescriptorForClass:[ProtosReadRssiResult class] + rootClass:[ProtosFlutterblueRoot class] + file:ProtosFlutterblueRoot_FileDescriptor() + fields:fields + fieldCount:(uint32_t)(sizeof(fields) / sizeof(GPBMessageFieldDescription)) + storageSize:sizeof(ProtosReadRssiResult__storage_) + flags:GPBDescriptorInitializationFlag_None]; + #if defined(DEBUG) && DEBUG + NSAssert(descriptor == nil, @"Startup recursed!"); + #endif // DEBUG + descriptor = localDescriptor; + } + return descriptor; +} + +@end + #pragma clang diagnostic pop diff --git a/lib/gen/flutterblue.pb.dart b/lib/gen/flutterblue.pb.dart index ec4a022a..791b3692 100644 --- a/lib/gen/flutterblue.pb.dart +++ b/lib/gen/flutterblue.pb.dart @@ -2186,3 +2186,64 @@ class MtuSizeResponse extends $pb.GeneratedMessage { void clearMtu() => clearField(2); } +class ReadRssiResult extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'ReadRssiResult', createEmptyInstance: create) + ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'remoteId') + ..a<$core.int>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'rssi', $pb.PbFieldType.O3) + ..hasRequiredFields = false + ; + + ReadRssiResult._() : super(); + factory ReadRssiResult({ + $core.String? remoteId, + $core.int? rssi, + }) { + final _result = create(); + if (remoteId != null) { + _result.remoteId = remoteId; + } + if (rssi != null) { + _result.rssi = rssi; + } + return _result; + } + factory ReadRssiResult.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ReadRssiResult.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + ReadRssiResult clone() => ReadRssiResult()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + ReadRssiResult copyWith(void Function(ReadRssiResult) updates) => super.copyWith((message) => updates(message as ReadRssiResult)) as ReadRssiResult; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ReadRssiResult create() => ReadRssiResult._(); + ReadRssiResult createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static ReadRssiResult getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ReadRssiResult? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get remoteId => $_getSZ(0); + @$pb.TagNumber(1) + set remoteId($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasRemoteId() => $_has(0); + @$pb.TagNumber(1) + void clearRemoteId() => clearField(1); + + @$pb.TagNumber(2) + $core.int get rssi => $_getIZ(1); + @$pb.TagNumber(2) + set rssi($core.int v) { $_setSignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasRssi() => $_has(1); + @$pb.TagNumber(2) + void clearRssi() => clearField(2); +} + diff --git a/lib/gen/flutterblue.pbjson.dart b/lib/gen/flutterblue.pbjson.dart index cd5ed708..1e131907 100644 --- a/lib/gen/flutterblue.pbjson.dart +++ b/lib/gen/flutterblue.pbjson.dart @@ -415,3 +415,14 @@ const MtuSizeResponse$json = const { /// Descriptor for `MtuSizeResponse`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List mtuSizeResponseDescriptor = $convert.base64Decode('Cg9NdHVTaXplUmVzcG9uc2USGwoJcmVtb3RlX2lkGAEgASgJUghyZW1vdGVJZBIQCgNtdHUYAiABKA1SA210dQ=='); +@$core.Deprecated('Use readRssiResultDescriptor instead') +const ReadRssiResult$json = const { + '1': 'ReadRssiResult', + '2': const [ + const {'1': 'remote_id', '3': 1, '4': 1, '5': 9, '10': 'remoteId'}, + const {'1': 'rssi', '3': 2, '4': 1, '5': 5, '10': 'rssi'}, + ], +}; + +/// Descriptor for `ReadRssiResult`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List readRssiResultDescriptor = $convert.base64Decode('Cg5SZWFkUnNzaVJlc3VsdBIbCglyZW1vdGVfaWQYASABKAlSCHJlbW90ZUlkEhIKBHJzc2kYAiABKAVSBHJzc2k='); diff --git a/lib/src/bluetooth_device.dart b/lib/src/bluetooth_device.dart index ca3bb9ba..0d798ef5 100644 --- a/lib/src/bluetooth_device.dart +++ b/lib/src/bluetooth_device.dart @@ -136,6 +136,24 @@ class BluetoothDevice { Future get canSendWriteWithoutResponse => new Future.error(new UnimplementedError()); + /// Read the RSSI for a connected remote device + Future readRssi() async { + final remoteId = id.toString(); + await FlutterBlue.instance._channel + .invokeMethod('readRssi', remoteId); + + return FlutterBlue.instance._methodStream + .where((m) => m.method == "ReadRssiResult") + .map((m) => m.arguments) + .map((buffer) => protos.ReadRssiResult.fromBuffer(buffer)) + .where((p) => + (p.remoteId == remoteId)) + .first + .then((c) { + return (c.rssi); + }); + } + @override bool operator ==(Object other) => identical(this, other) || diff --git a/protos/flutterblue.proto b/protos/flutterblue.proto index a30cb7a0..90caad69 100644 --- a/protos/flutterblue.proto +++ b/protos/flutterblue.proto @@ -211,4 +211,9 @@ message MtuSizeRequest { message MtuSizeResponse { string remote_id = 1; uint32 mtu = 2; +} + +message ReadRssiResult { + string remote_id = 1; + int32 rssi = 2; } \ No newline at end of file