Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add bluetooth backend abstraction & support devices with multiple measurements #432

Open
wants to merge 92 commits into
base: main
Choose a base branch
from

Conversation

pimlie
Copy link
Contributor

@pimlie pimlie commented Sep 12, 2024

Resolves #423
Resolves #352

This is a draft pr for now, need to:

  • review it a bit more (in general and Linux compatibility specifically)
  • check my code comments and
  • at minimum add some tests for measurement_multiple.dart
  • To resolve 423 I decided to add an abstraction layer for the bluetooth implementation. I wasn't fully sure whether the bluetooth_low_energy package will be the best (the maintainer is quite responsive tho) so with this abstraction we should be able to quickly switch between backends if needed.
  • I've added a BleReadMultiple read state that will shows a ListView with all returned measurements by the device
    • ⚠️ I've noticed that one of my devices has two reading modes. You either 1) take a measurement after which it enables bluetooth and you receive the one measurement you just did, or 2) you can hold the BT button on the device for 3s after which it turns into download mode. But reading in download mode seems to remove all data from the device memory, this feature seems to exists to sync all data with the app from the manufacturer which would store all your measurements in the cloud. My current implementation will only read one measurements, so that would mean users could loose other measurements. I'm happy to change the current implementation to sync/import all but lets create a ticket that defines the desired behaviour then first and create a new PR for that behaviour.
    • The ListView has no limit at the moment, even though some devices could store up to 100 measurements per user. In those cases then all those 100 measurements would be shown atm, again happy to change this behaviour but let's create a ticket to define the desired behaviour first and create a new PR for that behaviour
    • I did not check what happens when a device supports multiple users, and whether it always downloads all measurements from all users (which I think it does). If so then the 100 measurements above could also be 2 * 100...
  • I've added logging support through the official logging/logging package. This provide much more granular log levels and would also make it easier to support in-app logging etc if we ever decide we want that (e.g. think debug support in beta builds). (I added this because I wanted to be able to see the bluetooth_low_energy logs which also used logging)
  • I implemented the bluetooth abstraction layer with the goal that implementing the happy path of reading bluetooth data is as straight forward as possible. By that I mean I preferred using Future's instead of Stream's, using StreamTransformers if possible and I only implemented what we currently need
  • The FlutterBluePlus backend is still using your FlutterBluePlusMockable class, this means that we can re-use the existing tests and also test most of the new backend logic, even if we use the bluetooth_low_energy package when building the app.
  • Added BLUETOOTH.md so we can maintain a list of supported devices and their features. In a next pr we can expand the list to f.e. also list time support etc

@derdilla
Copy link
Owner

That looks great so far! You kept that code really tidy. I will be happy to properly review this once it's ready.

I noticed your code style is fairly simmilar to what you see in java. Specifically: In dart comments work a bit differently (e.g references to other classes in square brackets, the first line in the comment is a short summary, details come after a blank line). There is a great doc page about it with notes on how to write helpful comments that can act as a general guideline. I just wanted to give you an heads up about it, as this might come unexpected for someone new to dart.

Copy link
Contributor Author

@pimlie pimlie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry for the huge PR, but most changed files are just the new bt backend abstraction, dependency changes and added/fixed tests. Maybe I could have made the PR a bit smaller by not also immediately supporting devices with multiple measurements, but this feature seemed an essential part of the new bluetooth backend and not including that would only have saved 2 files.

I'm sure you are already familiar with Github, but if not please make sure to check Viewed after finishing reviewing a file in the PR :)

Linux is still not yet fully supported due to the missing plugins for storage, but will leave that to a different PR. Reading Bluetooth works fine. Also there is likely an issue with the jsaver dependency as I get warnings in the console about no plugin being available.

Note that I did not test on other platforms then Linux yet. I assume there is a workflow to build test apk's for PR's?

@pimlie pimlie marked this pull request as ready for review September 13, 2024 16:19
app/lib/logging.dart Outdated Show resolved Hide resolved
Copy link
Owner

@derdilla derdilla left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks allot for this PR. I added some thoughts to the first few files and tried to answer your questions. It would be great if you could consider them and apply these lessons to other files.

Don't feel discouraged by the amount or density, I tried to point everything out. You found some really great solutions, that I didn't comment on.

In the meantime I will try to implement apk building in PRs so you can better test this on real devices.

BLUETOOTH.md Show resolved Hide resolved
app/lib/config.dart Outdated Show resolved Hide resolved
app/lib/config.dart Outdated Show resolved Hide resolved
app/lib/features/bluetooth/backend/bluetooth_backend.dart Outdated Show resolved Hide resolved
app/lib/features/input/add_measurement_dialoge.dart Outdated Show resolved Hide resolved
app/lib/logging.dart Outdated Show resolved Hide resolved
@derdilla
Copy link
Owner

As a general note: You can drop the support for the flutter_blue_plus backend. This should drastically simplify the code.

@pimlie
Copy link
Contributor Author

pimlie commented Sep 16, 2024

Thanks for the thorough review, good to see you are quite opinionated! Will go through your remarks and fix them ;)

I'd rather hold of for now on immediately dropping the fbp code unless we know for sure that we have at least one stable release where bluetooth_low_energy is working well. Also we'd loose testing of the bt backed class through your existing tests & mockable class. So dropping fbp would mean more work as we'd need to add more tests.
I was also thinking it could make sense to move the bluetooth logic into a separate library as you did with health data. I'm currently looking into adding characteristic decode/encode implementations that follow the bluetooth spec more closely and all of that could potentially be used by any app instead of just this.

app/lib/config.dart Outdated Show resolved Hide resolved
app/lib/config.dart Outdated Show resolved Hide resolved
app/lib/config.dart Outdated Show resolved Hide resolved
app/lib/features/bluetooth/backend/bluetooth_device.dart Outdated Show resolved Hide resolved
@pimlie
Copy link
Contributor Author

pimlie commented Sep 27, 2024

FWIW: Latest action build is using flutter_blue_plus and is working correctly with one of my devices. It's not yet working with the Omron, not sure why but with fbp that device keeps sending bond requests but after bonding it automatically disconnects which triggers the main BleReadFailure state. Looking into that behaviour still

@derdilla
Copy link
Owner

@pimlie I looked into the ble lines you send and they look like enough, potentially even too much permissions (iirc modern android doesn't need that many location permissions). I only glanced at the rest of their code, but it looks like all permissions are requested at once, which fits the observed behavior of opening the app-settings page.

Open an issue if you want, I think that's something the maintainer should at least be aware of. This is however not blocking for us since we can request the permissions ourself (architecture wise both sides have valid arguments).

In other news: Due to personal reasons I will have less/no time next week, so sorry if you don't get immediate responses.

@pimlie
Copy link
Contributor Author

pimlie commented Oct 8, 2024

@derdilla Hey, just a quick check-in that I have been very busy as well and will try to get back to this later this week, maybe next week.

Last status from last week was that even with using flutter_blue_plus there was still weird disconnect behaviour after trying to read a measurement from the device. I'm almost sure that this isn't an issue in the current code because you just retry a couple of times and are not depending on a disconnect from the device itself to stop reading as the current code always ever reads only one measurement and then the code itselfs triggers a disconnect. Not sure yet how that can be solved best 😞

@derdilla
Copy link
Owner

derdilla commented Oct 14, 2024

@pimlie glad to hear my retrying had at least some use. You probably already thought of this, but I just had the idea of "simply" ignoring disconnects/retrying until at least one measurement is read.

@5FeetUnder
Copy link

Ideally (but not necessarily):

  • someone with another ble device tests this

I just tested 4bff687 with the Beurer BM 64 and it offered me all measurements that were on the device to select from (it is apparently of the variety "send all stored measurements after taking a measurement").

I have ideas about usability, but those are probably better suited as feature requests after this PR got merged, since I don't want to blow its scope up more than necessary.

@pimlie
Copy link
Contributor Author

pimlie commented Oct 16, 2024

@derdilla Pfff, I spent the last week trying to get a proper docker based dev env to work so I could test this PR with hot reloading and I finally got it working I think. Getting some errors about compiling against SDK 34 instead of 35, had to fix/update gradle version, and now it reports warnings about the NDK version (got 23 but it requires 25 and it wants me to update build.gradle). Not sure if these issues are (automatically) fixable by updating the android config, but it seems that setting up an android dev env is not as simple / straight forward as it appears to be from the github workflows?

@5FeetUnder Thanks for testing, and yeah creating a new issue/pr to improve UX should be preferred indeed

@derdilla
Copy link
Owner

@pimlie your description sounds like you are building from a stable flutter, and try to build beta-flutter code.

In that case you can either use beta flutter or use the latest android/ and pubspec. files from main branch.

…bluetooth_input from openening appsettings on non android devices

chore: improve some logs/formatting
@pimlie
Copy link
Contributor Author

pimlie commented Oct 16, 2024

@derdilla Hmm, seems you are right. Thanks for that tip. Can we indicate that correctly in the pubspec maybe? I expected that if we require a minimum version (whether that's stable or beta) that that would be enforced by the pubspec.

As I kept having issues with the Omron device, I checked back with v1.8.1 published on FDroid and that version has the same connecting issue as I mentioned above that it keeps trying to bond/pair the device and never is able to connect (note that in the past I have been able to connect to the Omron from an Android device, just not currently from various devices). Cause also when trying to pair from the Android bluetooth settings dialog it also has 'issues'. So the Omron just seems very finicky, not just on Android as on Linux it does work after I first manually pair the device but it often also takes a couple of tries. Maybe the Omron doesn't like switching between devices not very much, not sure.
Given that the issues also exists on v1.8.1 I think we can ignore them for now.

Just as a note, the SBM69 device basically always works flawlessly especially after you first pair the device using the system dialogs.

That means I believe this PR is ready again for review

@pimlie pimlie requested a review from derdilla October 16, 2024 15:53
Copy link
Owner

@derdilla derdilla left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few things before we can get this merged: Test failures that need to be disabled, and potential bugs you should check again.

I'm fine with keeping the UI stuff, some doc changes, package extraction, and proper testing to other PRs.

.github/workflows/pr.yml Outdated Show resolved Hide resolved
app/android/settings.gradle Show resolved Hide resolved
app/lib/features/bluetooth/backend/bluetooth_device.dart Outdated Show resolved Hide resolved
Comment on lines 78 to 84
Future<void> _chainWaitForDisconnectingStateChange() async {
if (_state == BluetoothDeviceState.disconnecting) {
logger.finest('Waiting because device is still disconnecting');
await Future.delayed(const Duration(milliseconds: 10))
.then((_) => _chainWaitForDisconnectingStateChange());
}
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it could cause stack overflows (and might fail when some implementation instantly jumps to disconnected, and log spam, bu I trust you tested these). Is a loop also fine?

Suggested change
Future<void> _chainWaitForDisconnectingStateChange() async {
if (_state == BluetoothDeviceState.disconnecting) {
logger.finest('Waiting because device is still disconnecting');
await Future.delayed(const Duration(milliseconds: 10))
.then((_) => _chainWaitForDisconnectingStateChange());
}
}
Future<void> _chainWaitForDisconnectingStateChange() async {
while(_state != BluetoothDeviceState.disconnecting) {
logger.finest('Waiting because device is still disconnecting');
await Future.delayed(const Duration(milliseconds: 10));
}
}

yes, technically this implementation isn't a chain. Feel free to change if that bothers you

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm actually a little bit conflicted about the need for this logic to be honest, this was basically added to try to get the Omron to work reliably on Android but I currently believe that the Omron might be just faulty on Android. That said I don't think adding this logic does any harm and does make sense, so will rework this a bit more



void main() {
testWidgets('should show everything and be interactive', (WidgetTester tester) async {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still getting a failure:

══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure was thrown running a test:
Expected: <1>
  Actual: <0>

When the exception was thrown, this was the stack:
#4      main.<anonymous closure> (file:///home/derdilla/Coding/Flutter/blood_pressure_app/app/test/features/bluetooth/ui/measurement_multiple_test.dart:83:5)
<asynchronous suspension>
#5      testWidgets.<anonymous closure>.<anonymous closure> (package:flutter_test/src/widget_tester.dart:189:15)
<asynchronous suspension>
#6      TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:1032:5)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)

This was caught by the test expectation on the following line:
  file:///home/derdilla/Coding/Flutter/blood_pressure_app/app/test/features/bluetooth/ui/measurement_multiple_test.dart line 83
The test description was:
  should show everything and be interactive
════════════════════════════════════════════════════════════════════════════════════════════════════
00:22 +131 -3: /home/derdilla/Coding/Flutter/blood_pressure_app/app/test/features/bluetooth/ui/measurement_multiple_test.dart: should show everything and be interactive [E]                                                                     
  Test failed. See exception logs above.
  The test description was: should show everything and be interactive

If you don't want to learn the test framework that's fine. Just comment this out and I will do appropriate testing when writing the logic out of the UI.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a new error to me, will have a look. The only errors I had locally in the past were related to the golden's

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@derdilla Any idea how I can get mockito to generate a new hashCode for each mock instance?

Issue is that after your suggestion of using a Set for the device discovery list, this test fails because all mock devices have the same hashCode = 0 so adding them to the set only adds the first device

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, the issue might be something else. Creating a simple set with mock classes does actually work. It just doesnt work when sinking them through the stream, looking at this again

Copy link
Contributor Author

@pimlie pimlie Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@derdilla Fixed it for now by ensuring deviceId are also equal when comparing BluetoothDevice's, not sure why hashCode's are the same for the mocks. Have added a todo/code comments for now as the fix is working fine and shouldnt be an issue. Feel free to investigate further if you have the time and wanna spent that on this, but as its working now I'd rather spent my time on improving other things ;)

@derdilla
Copy link
Owner

derdilla commented Oct 16, 2024

Can we indicate that correctly in the pubspec maybe?

@pimlie, that seems reasonable. The CI should probably also run with the oldest supported version.

Sorry for the confusion the version caused, I really should get out of the habit of having undocumented things like this. You are the first major contributor so feedback like this is highly appreciated.

As soon as this is ready for merge: Do you prefer your PR rebased (main-branch gets 90 nicely named conventional-commits) or squashed (one clean commit and we rely on gh if we ever need details)?

@pimlie
Copy link
Contributor Author

pimlie commented Oct 17, 2024

Do you prefer your PR rebased (main-branch gets 90 nicely named conventional-commits) or squashed (one clean commit and we rely on gh if we ever need details)?

I always prefer to keep the master branch as clean as possible, no need to pollute the git log with all the stupid debug commits in this branch. So a single squash should be fine. Thanks for asking :)

@derdilla derdilla mentioned this pull request Nov 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants