Closed iKK001 closed 1 year ago
I finally found a solution !
I guess my knowledge of dart Streams
was limited. After reading a bit into the topic, I found the following solution:
Instead of trying to get your device out of the listener FlutterBluePlus.scanResults.listen((results) { ... }
(inside the initState), a much better approach is to work with a StreamBuilder
directly in you build() method :
@override
Widget build(BuildContext context) {
return StreamBuilder<List<ScanResult>>(
stream: FlutterBluePlus.scanResults,
builder: (_, snapshot) {
if (snapshot.hasData) {
final filteredResult = snapshot.data!.where((element) => element.device.advName == widget.myKnownId);
return Container( ...whatever you want to do with filteredResult...);
} else {
return const Center(
child: CircularProgressIndicator(),
);
}
},
);
}
This way, I can work with live-changes on filteredResult
as long as Scan is happening.
This works in detail-Screen even tough the scan has been started in parent-screen.
Thank good I understand Streams
a bit better now ;)
I was too early with my joy ! The same thing happened again: after removeIfGone-Duration has passed, then the filteredResult
gets empty.
I am totally sure that it worked two times after the refactor. And now I am back to the misery again. No idea why ?
I am on the simulator running on an actual device.
This thing is turning me crazy !!!!!!!!
Any idea why this happens ?
when stopScan is called (or scan timeout) the scan results are cleared. if you re-listen after stopscan, it will be empty.
if you dont want that behavior, you should put the results in your own cache somewhere. use a global variable. then just always use your cache.
I think you misunderstood the case. Please read carefully:
I do not call stopScan
anywhere in my app.
The device dissappears after removeIfGone-duration - and NOT after timeout. The timeout is not reached yet !!!!!!!!!!!!!
AND in addition, only the one particular device dissappears from the scanResults-list that I test with .where((element) => element.device.advName == widget.myKnownId);
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
I do use startScan with the removeIfGone like so (inside the parent-Screen)
await FlutterBluePlus.startScan(
timeout: const Duration(seconds: 180),
continuousUpdates: true,
removeIfGone: const Duration(seconds: 5), // after this time, the device dissaspears from scanResults-list
continuousDivisor: divisor,
);
Then I navigate to the detail-Screen after let's say one or two seconds.
After 5 seconds from start, the device is gone from the list - even tough timeout is not reached.
But why does ONLY ONE DEVICE DISSAPPEAR AFTER THIS removeIfGone duration ??????? The rest of the devices still are available in the list after the removeIfGone-duration has passed. ONLY the one particular device I apply the .where
dissappears from the scanResults-list. AND it only does so if I apply the .where
filter inside a detail-screen.
And again, it does not dissappear after the timeout - it dissappears after the removeIfGone-duration !
It reappears only if I reboot the physical device.
Therefore your library does not re-start the advertisement reading for this particular device with the .where
filter.
And since timeout is not reached yet, the removal from the list should not take place at all.
It reappears only if I reboot the physical device.
It sounds like your device is misbehaving and has stopped advertising. Can other BLE scanner apps find it when this happens?
yes they can find it no problem. It has nothing to do with the actual device.
In the meantime I found out that if I do the following, then I can at least get the detail-screen working:
--> If I completely eliminate any FlutterBluePlus.onScanResults.listen((results) { ... }
activity on the parent-Screen, then it works !! i.e. then the detail-screen keeps the myKnownID-device in the list even after the removeIfGone-duration.
So it seems that if there are two .onScanResults
-listeners (or StreamBuilders) at work, (one in the parent-Screen and one in the detail-Screen), the the problem occurs !
I thought that one could listen to the .onScanResults
anywhere and in as many screens as needed (since it is a stream that never closes).
But it seems that I need to cancel the parent-listener maybe at the moment of navigation to the detail-screen ??
Again, it is ultra-strange that only the knownDeviceId gets missing after the removeIfGone-duration on which the parent-screen applies the .where
filter inside the listener (or StreamBuilder).
hhhmmmm, I think you still have a point with the device as being the issue: indeed, if I use another bluetooth-scanner I can see that the device also dissappears from the list of found devices. Precisely after this removeIfGone-duration. And only my device with knownId.
Only if the parent-screen and the detail-screen both do their .onScanResults
-listener with the .where
filter, this happens. Not otherwise.
Could two listeners somehow not obey some BLE rules ?
Maybe there is another way I can observe my device inside parent- and detail-screen each ?? (i.e. other than using two .onScanResults
-listeners or StreamBuilders ?
In the meantime I try my idea to cancel the parent-listner when navigating and vice-versa.
fyi: scanResultsSubscription.cancel() of the parent-listener at the moment of navigation does not help, unfortunately !
I thought that one could listen to the .onScanResults anywhere and in as many screens as needed (since it is a stream that never closes)
yes that is how it should work!
hhhmmmm, I think you still have a point with the device as being the issue: indeed, if I use another bluetooth-scanner
yes i still think device issue. I think removeIfGone is a coincidence.
what happens if you make the removeIfGone duration larger? like 60s? does it go away after 60s?
nope - then the error happens after 60s.
It is cleartly entirely bound to this removeIfGone delay !
In the meantime I rewrote the entire parent- and detail-listeners to be inside a Riverpod StreamProvider.
Same story: BLE-device gets kicked out of scanResults-list !!!!!!!
I can only get it to work if I stop calling the listener inside the parent-screen. Then the detail-screen keeps the device inside the scanResults-list. Otherwise, as soon as there is a parent-listener as well, then the two listener bite each other.
I am not sure what happens in case of Riverpod. Since I assumed that there, I would have at least only one instance of the scanResutl-list for the entire App. So I need to keep investigating what is going on.
Fact is that the parent-listener bothers the detail-listener !
For a reference, here is the entrire Riverpod code:
Note:
The important method is:
AsyncValue<ScanResult> getScanResultOfId(String deviceId) {
return state.whenData((results) {
return results.firstWhere((result) => result.device.advName == deviceId);
});
}
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:myApp_flutter/bluetooth/extra.dart';
import 'package:myApp_flutter/constants/constants.dart' as constants;
part 'bluetooth_provider.g.dart';
@riverpod
class MyAppBLE extends _$MyAppBLE {
StreamSubscription<BluetoothAdapterState>? adapterStateStateSubscription;
StreamSubscription<bool>? isScanningSubscription;
StreamSubscription<List<ScanResult>>? scanResultsSubscription;
bool isScanning = false;
BluetoothAdapterState adapterState = BluetoothAdapterState.unknown;
BluetoothDevice? myAppDevice;
List<BluetoothService> _services = [];
@override
Stream<List<ScanResult>> build() async* {
yield [];
}
Future<void> startScan() async {
try {
// android is slow when asking for all advertisments,
// so instead we only ask for 1/8 of them
int divisor = Platform.isAndroid ? 8 : 1;
await FlutterBluePlus.startScan(
timeout: const Duration(seconds: constants.BLUETOOTH_SCAN_DURATION),
continuousUpdates: true,
removeIfGone: const Duration(seconds: 5),
continuousDivisor: divisor,
);
} catch (e) {
debugPrint("Start Scan Error: $e");
}
}
void stopScan() {
FlutterBluePlus.stopScan();
}
void listenForScanningState() =>
isScanningSubscription = FlutterBluePlus.isScanning.listen(
(scanningState) {
isScanning = scanningState;
},
);
void listenForAdapterState() =>
adapterStateStateSubscription = FlutterBluePlus.adapterState.listen(
(adaState) {
adapterState = adaState;
},
);
void listenForScanResults() =>
scanResultsSubscription = FlutterBluePlus.onScanResults.listen(
(results) {
state = AsyncData(results);
},
);
void cancelAdapterStateSubscription() =>
adapterStateStateSubscription?.cancel();
void cancelScanResultsSubscription() => scanResultsSubscription?.cancel();
void cancelScanningStateSubscription() => isScanningSubscription?.cancel();
AsyncValue<ScanResult> getScanResultOfId(String deviceId) {
return state.whenData((results) {
return results.firstWhere((result) => result.device.advName == deviceId);
});
}
void connectDevice(BluetoothDevice device) {
device.connectAndUpdateStream().catchError((e) {
debugPrint("Error connecting: $e");
});
}
void disconnectDevice() {
myAppDevice?.disconnect();
}
Future<BluetoothCharacteristic?> treatServices(
String serviceUuid, String characteristicUuid) async {
try {
if (myAppDevice == null) {
debugPrint("No BLE device connected.");
return null;
} else {
// Waiting for BLE device to finish connecting...
int bailOutCounter = 0;
while (myAppDevice?.isConnected == false && bailOutCounter < 10) {
await Future.delayed(const Duration(milliseconds: 300));
bailOutCounter++;
}
// if connected
debugPrint("BLE device Connected");
if (myAppDevice?.isConnected ?? false) {
_services = await myAppDevice!.discoverServices();
Iterable<BluetoothService> myAppService =
_services.where((s) => s.uuid == Guid(serviceUuid));
if (myAppService.isNotEmpty) {
Iterable<BluetoothCharacteristic> myAppCharacteristic = myAppService
.first.characteristics
.where((c) => c.uuid == Guid(characteristicUuid));
if (myAppCharacteristic.isNotEmpty) {
return myAppCharacteristic.first;
}
}
}
}
return null;
} catch (e) {
debugPrint("Discover Services Error: $e");
return null;
}
}
}
Here is the parent-Screen
class MyAppParent extends ConsumerStatefulWidget {
const MyAppParent({
super.key,
required this.items,
});
final List<MyList> items;
@override
ConsumerState<MyAppParent> createState() => _MyAppParentBluetoothState();
}
class _MyAppParentBluetoothState extends ConsumerState<MyAppParent> {
@override
void initState() {
ref.read(myAppBLEProvider.notifier).listenForAdapterState();
ref.read(myAppBLEProvider.notifier).listenForScanResults();
ref.read(myAppBLEProvider.notifier).listenForScanningState();
super.initState();
}
@override
Widget build(BuildContext context) {
final blues = ref.watch(myAppBLEProvider);
return GridView.builder(
itemCount: ref.read(someOtherProvider.notifier).numberOfProducts(),
shrinkWrap: true,
physics: const ScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
childAspectRatio: childAspectRatio,
crossAxisCount: crossAxisCount,
mainAxisSpacing: 20.h,
crossAxisSpacing: 20.w,
),
itemBuilder: (context, index) {
if (widget.items.isEmpty) {
return const Center(
child: Text("No Products found"),
);
} else {
return ref
.read(myAppBLEProvider.notifier)
.getScanResultOfId(widget.items[index].id)
.when(
data: (item) =>
// in cases of item != null, means scanResult is available
_gridTile(
_showLoading,
widget.items,
index,
item,
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, st) =>
// in case of error, means no scanResult yet...
_gridTile(
_showLoading,
widget.items,
index,
null,
),
);
}
},
);
}
}
question: What about when the device is connected
- does the scanResults
list then kick out the device ??
I have a semi-working solution now (improving the Riverpod Provider). However as soon as the device connects, then the scanResults
list kicks out the device precisely after the removeIfGone
duration.
this is because your device stops advertising when connected
this is in the readme!
btw, you dont have to use removeIf gone.
you can reimplement it yourself using your own stream controllers, etc. see the fbp code for inspiration.
Before all, I would like to thank you for your support. And also for keeping up this great BLE library.
And I also appreciate very much your effort to keep up the readme. (...that I should have read more thoroughly ;)).
I have a working solution now !
Here a summary of the mistakes I did and the misleadings I followed for too long:
connect
in my detail-screen (that sat there without me recognizing this fact - I did it too fast I guess).. This connect
made the scanResult
-list loose the known-device for a very long debug-time.1b. I have to say, it still appears very very strange to me that the dissappearing of this known-device after connect
happens precisely after the removeIfGone
-duration :/ (but this must be a funny coincidence I guess). The coincidence was reason for me to hunt for the ghost that does not exist.
scanResult
-references all the way to the detail-screen. A much better approach is to use State-handling (Riverpod in my case) to make a single source of truth for the parent- and the detail-screen.2b. A helpful method inside my new BLE Riverpod StreamProvider were the following two methods:
The first one allows to get a single source of truth for the scanResult
-list of known-devices (during advertisement - not during connection)
AsyncValue<ScanResult> getScanResultOfId(String deviceId) {
return state.whenData((results) {
return results.firstWhere((result) => result.device.advName == deviceId);
});
}
And the second one allows to retrieve the one ScanResult for the given known deviceId: (also only during avertisement and not connection)
ScanResult? getScanResultOfIdOrNull(String deviceId) {
if (state.value == []) {
return null;
}
if (state.value?.where((result) => result.device.advName == deviceId) ==
null) {
return null;
} else if (state.value!
.where((result) => result.device.advName == deviceId)
.isEmpty) {
return null;
} else {
return state.value
?.where((result) => result.device.advName == deviceId)
.first;
}
}
void disconnectDevice(String teddyId) {
ScanResult? scanResult = getScanResultOfIdOrNull(teddyId);
if (scanResult != null) {
debugPrint("BLE device Disconnected");
_bleDevice = null;
scanResult.device.disconnect();
} else {
if (_bleDevice != null) {
debugPrint("BLE device Disconnected");
_bleDevice?.disconnect();
_bleDevice = null;
}
}
}
You can now close this ticket from my side.
Again many thanks for your patience and support. We are all learning ;)
glad you figured it out
1b. I have to say, it still appears very very strange to me that the dissappearing of this known-device after connect happens precisely after the removeIfGone-duration :/ (but this must be a funny coincidence I guess). The coincidence was reason for me to hunt for the ghost that does not exist.
this is how removeIfGone works. It removes the device from the list after it stops advertising for X duration. Your device stops advertising when it is connected, so after X duration it is removed from the list.
Requirements
Have you checked this problem on the example app?
No
FlutterBluePlus Version
1.28.7
Flutter Version
3.13.9
What OS?
iOS
OS Version
17.1.1
Bluetooth Module
ESP32-S3 Wroom 1U
What is your problem?
On a first Flutter Screen I start scanning BLE devices.
The app does this in order to show present BLE-devices of known advNames.
Everything works - I can power-On devices and they get recognized from the App- and I can power-off devices and they dissappear form the
FlutterBluePlus.scanResults
list as expected. Everything seems to work inside my parent-Screen.Note that I use
removeIfGone: const Duration(seconds: 5),
when calling startScan (to be able to detect the dissapearing of devices). So far so good.This removeIfGone is important since when setting removeIfGone to null, then I do NOT get the error.
Here the error behaviour explained:
From this first parent-Screen, I can click on one device to navigate to a second Detail-screen where I would like to show advertisement-status again for this particular device.
I use
Navigator.pushNamed(...)
to do the navigation !--> on this second Screen, the
FlutterBluePlus.scanResults
-list suddenly does no longer carry my device (with known advName). This loss of my device inside this list happens precisely after the removeIfGone-Duration has passed !!!The way I detect this problem in the detail-Screen is as follows:
==> entering detail-Screen: the log prints "yes", "yes", "yes".... ==> but precisely after removeIfGone-Duration has passed since Scan-start in parent-Screen then I get logs "no", "no", "no"....
Why this strange behaviour ???
Further observations:
If I do the very same code inside the initState of the parent-Screen, then I never get this device-loss in the result-List. (i.e. it prints "yes", "yes" "yes" all the time - even after removeIfGone-Duration has passed.). EXCEPT if I navigate to the detail-screen!!!
The problem only happens if I do this hasMyDevice-test after Navigation to the detail-screen. And it happens no matter where I log (i.e. inside parent- or inside detail-Screen, both turn to "no").
Only AFTER navigation to detail-screen, the
hasMyDevice
check turns to "no", precisely after the removeIfGone-Duration, no matter on which screen I test it.If I remain on the parent-Screen, I never get the issue!
==> And now the strangest observation:
If I reset the physical device being on detail-Screen. Then the
hasMyDevice
-check gives back "yes" again forever (or of course also if I turn off the physical device - but this is not the point here).The point is that this
FlutterBluePlus.scanResults.listen((results) { ... }
routine somehow looses my known-device from its list after the navigation. And it finds it again, when power-off/power-on the physical BLE-device.There must be a problem with this removeIfGone-duration mechano in your libary. Or maybe with the advName-caching - I have no idea.
Very important: WHEN I GET THE myKnown-device LOSS - ALL OTHER DEVICES STILL ARE SHON IN THE scanResults.listener. ONLY THE DEVICE I WAS CHECKING ITS PRESENCE DISSAPPEARS !!!!
(i.e. only the one device is "lost" from the
scanResults
-list, only the one I use the routine to get thishasMyDevice
information:What is wrong with this
hasMyDevice
check ??? And why does it only affect if I navigate to the detail-Screen.I am completely lost with this.
Any idea why ????
Logs
P.S. The number of "yes" or "no"'s of course depends on the number of other BLE-devices in the room. I expect "yes" all the time for my knownId-device.
P.P.S. The problem also exists if I do a
showModalBottomSheet
naviagtion instead of aNavigator.pushNamed(...)
....