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
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
beb447f
feat: implement logging/logging package
pimlie Sep 12, 2024
a50ef6e
chore: extract some global configuration props
pimlie Sep 12, 2024
38c8aeb
feat: add bluetooth backend abstraction layers
pimlie Sep 12, 2024
ee1f931
feat: add support for bt devices that always return multiple measurem…
pimlie Sep 12, 2024
2670413
feat: implement new bluetooth backend abstraction layer
pimlie Sep 12, 2024
7f7c831
test: fix/update tests for new bluetooth logic
pimlie Sep 12, 2024
7f677f5
chore: remove some debug logs
pimlie Sep 12, 2024
9a8873b
chore: be consistent with Uuid naming
pimlie Sep 13, 2024
88fbcd5
test: add logging util tests
pimlie Sep 13, 2024
4ec6095
chore: abstract device conecct/disconnect logic as its implementation…
pimlie Sep 13, 2024
ccfc3c8
feat: add bluetooth information page
pimlie Sep 13, 2024
3f9ed1a
test: add test for ui/measurement_multiple.dart
pimlie Sep 13, 2024
080490a
chore: fix a code comment
pimlie Sep 13, 2024
7b35dcd
chore: improve bluetooth.md
pimlie Sep 13, 2024
b7870e8
chore: improve bluetooth.md
pimlie Sep 13, 2024
34194a3
chore: improve bluetooth.md
pimlie Sep 13, 2024
1b3b8ef
fix: log message if callback _was_ removed
pimlie Sep 13, 2024
467dc0c
chore: improve/fix code comments
pimlie Sep 13, 2024
30ff048
chore: extract ValueGraph as HomeScreen boolean & fix some code comments
pimlie Sep 13, 2024
1ffbf03
docs: add/change links to official bluetooth specs
pimlie Sep 13, 2024
0aa4418
chore: add blood pressure service specification to BLUETOOTH.md
pimlie Sep 16, 2024
9c16286
fix: apply suggestion
pimlie Sep 16, 2024
9b99c31
fix: improve code comment
pimlie Sep 16, 2024
b128949
chore: remove redundant code comment
pimlie Sep 16, 2024
6d6e7de
fix: cancel connection listener before listening (again)
pimlie Sep 16, 2024
3b895a6
fix: prefer using firstWhereOrNull
pimlie Sep 16, 2024
44e5990
chore: prefer inline if
pimlie Sep 16, 2024
fe14c58
chore: just use final var
pimlie Sep 16, 2024
83c1a22
chore: onDiscoveryError doesnt need to be protected anymore
pimlie Sep 16, 2024
847ee16
chore: cancel any existing discovery subscriptions
pimlie Sep 16, 2024
b849f7b
chore: update code comment
pimlie Sep 16, 2024
529f487
chore: add assert statement
pimlie Sep 16, 2024
cf03f8a
chore: remove todo as there is a ticket on github
pimlie Sep 16, 2024
c91457d
chore: remove automatic connection retry logic
pimlie Sep 16, 2024
bfee729
chore: remove unused class
pimlie Sep 16, 2024
29e2b2b
chore: prefer just using final
pimlie Sep 16, 2024
0e14180
chore: remove deprecated log methods & unneeded stacktrace parser
pimlie Sep 18, 2024
3d9e48f
chore: remove some config vars
pimlie Sep 18, 2024
5d35a69
chore: remove parts & fix test
pimlie Sep 18, 2024
05aaa14
chore: make discovered._devices a Set
pimlie Sep 19, 2024
5ed5054
Merge branch 'main' into feat-use-ble-package
derdilla Sep 20, 2024
ea74071
chore: refactor reading characateristic values + some fixes
pimlie Sep 20, 2024
06d9da5
chore: refactor BluetoothManager instantiation
derdilla Sep 20, 2024
7697c95
chore: migrate .transform to .map
derdilla Sep 20, 2024
c257878
chore(backend): revision code patterns, comments and formatting
derdilla Sep 20, 2024
2f767dc
chore: extract logging
derdilla Sep 20, 2024
aca47b7
chore(ui): revision code patterns, comments and formatting
derdilla Sep 20, 2024
92c8243
chore: remove excess nl localization
derdilla Sep 20, 2024
7cc448e
chore(logic): revision code patterns, comments and formatting
derdilla Sep 20, 2024
8a3d3e2
Update app/lib/features/bluetooth/backend/bluetooth_low_energy/ble_de…
pimlie Sep 22, 2024
2a93308
fix: forceRefresh isnt async anymore
pimlie Sep 22, 2024
c34b551
fix: passthrough state arg
pimlie Sep 22, 2024
193ca76
chore: update code comment
pimlie Sep 22, 2024
e0e21a7
fix: use onClosed instead for measreuemnt_multiple state
pimlie Sep 22, 2024
9ad866a
chore: update lock file
pimlie Sep 23, 2024
692a022
test: fix arg
pimlie Sep 23, 2024
c322eda
Merge branch 'main' into feat-use-ble-package
pimlie Sep 23, 2024
3a672f1
fix: revert change that broke uuidcomparison
pimlie Sep 23, 2024
ce139ea
fix: requesting permissions on android is required
pimlie Sep 23, 2024
e8c4cce
fix: dont implement workflow in library method
pimlie Sep 26, 2024
2c97c7d
Merge branch 'main' into feat-use-ble-package
pimlie Sep 26, 2024
df24861
fix: resolve dependency issues
pimlie Sep 26, 2024
8af76ab
fix: try building android with flutter blue plus
pimlie Sep 26, 2024
e05e26f
fix: merge duplicate logic
pimlie Sep 26, 2024
1325c45
chore: always enable bluetooth when initiating bluetooth_cubit
pimlie Sep 26, 2024
d7e5e6e
chore: revert back to only enabling when not authorized
pimlie Sep 27, 2024
6ef22c7
fix: only resolve future when connect state changed
pimlie Sep 27, 2024
881072d
chore: debug with fbp
pimlie Sep 27, 2024
30c912b
chore: add error message to uuid length assert
pimlie Sep 27, 2024
caa1bb7
fix: fbp doesnt return a 128bit uuid for guid.tostring
pimlie Sep 27, 2024
948c025
chore: always build package for now
pimlie Sep 27, 2024
0a81e1a
chore: always build package for now (2)
pimlie Sep 27, 2024
ca7df73
chore: comment out condition
pimlie Sep 27, 2024
7204563
chore: set ref in code checkout to pr head
pimlie Sep 27, 2024
1a0cc7e
fix: use char uuid for fbo charecteristics
pimlie Sep 27, 2024
012845b
feat: better device state management & implement automatic retry afte…
pimlie Oct 1, 2024
4bff687
chore: increase max retry count
pimlie Oct 1, 2024
74b470e
chore: upgrade gradle to current version
pimlie Oct 16, 2024
0db33ff
chore: update code comments & lock file
pimlie Oct 16, 2024
fe86605
chore: add vscode files
pimlie Oct 16, 2024
c8cc50f
Merge branch 'main' into feat-use-ble-package
pimlie Oct 16, 2024
d9ea3ca
chore: upgrade gradle version
pimlie Oct 16, 2024
41d9aef
chore: return nullable boolean from manager.enable to prevent closed_…
pimlie Oct 16, 2024
3bb86de
revert: changes to pr workflow
pimlie Oct 17, 2024
0796b0e
chore: rework waiting for disconnecting state logic
pimlie Oct 17, 2024
48704eb
chore: reset state to disconnected after connect error
pimlie Oct 17, 2024
4ebcfa7
chore: typo
pimlie Oct 17, 2024
e9b1c73
chore: log error
pimlie Oct 17, 2024
1e35fbd
chore: remove debug code
pimlie Oct 17, 2024
d42883b
test: fix tests
pimlie Oct 17, 2024
89c7ada
test: ensure device comparison works also in tests
pimlie Oct 18, 2024
ebe7274
chore: add code comment about device comparison
pimlie Oct 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions BLUETOOTH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Supported Bluetooth devices

In general any device that supports [`BLE_UUID_BLOOD_PRESSURE_SERVICE`](https://developer.nordicsemi.com/nRF51_SDK/nRF51_SDK_v4.x.x/doc/html/group___u_u_i_d___s_e_r_v_i_c_e_s.html) could be used. The measurement values are stored in the characteristic [`BLE_UUID_BLOOD_PRESSURE_MEASUREMENT_CHAR`](https://developer.nordicsemi.com/nRF51_SDK/nRF51_SDK_v4.x.x/doc/html/group___u_u_i_d___c_h_a_r_a_c_t_e_r_i_s_t_i_c_s.html#ga95fc99c7a99cf9d991c81027e4866936)

## Reading caveats

There are some difference in how devices report their measurements.

Most devices provide 2 ways to retrieve measurements over bluetooth, but there are also difference in how those operate:

1. Immediately after taking a measurement
1. and returns all measurements stored in memory
2. and only returns the latest measurement
2. As a download mode
1. and automatically remove all locally stored measurements after a succesful download
2. and leave measurements untouched, i.e. the user needs to remove the stored measurements themselves

> :warning: At the moment situation 2.i is not well supported. Do not use this unless you are ok with loosing previously stored measurements

## Known working devices

> If your device is not listed please edit this page and add it! :bow:

|Device|Bluetooth name|Read after measurement|Download mode|Automatically disconnects after reading|
pimlie marked this conversation as resolved.
Show resolved Hide resolved
|---|---| :---: | :---: | :---: |
|HealthForYou by Silvercrest (Type SBM 69)|SBM69| :1234: | :white_check_mark: | :white_check_mark: |
|Omron X4 Smart|X4 Smart| :one: | :white_check_mark::wastebasket: | :white_check_mark: |

#### Legenda

|Icon|Description|
| :---: | --- |
| :no_entry_sign: |Not supported / No|
| :white_check_mark: |Supported / Yes|
| :one: | Returns latest measurement|
| :1234: | Returns all measurements|
| :white_check_mark::wastebasket: |Supported and removes all locally stored measurements|
14 changes: 14 additions & 0 deletions app/lib/config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'dart:io';
import 'package:blood_pressure_app/logging.dart';
pimlie marked this conversation as resolved.
Show resolved Hide resolved

/// Default name for global [log] instance (not used when printing messages atm)
pimlie marked this conversation as resolved.
Show resolved Hide resolved
const defaultLoggerName = 'BloodPressureMonitor';

/// Prefix used when printing log messages
const loggerRecordPrefix = 'BPM';

/// Whether bluetooth is supported on this platform by this app
final isPlatformSupportedBluetooth = Platform.isAndroid || Platform.isIOS || Platform.isMacOS || Platform.isLinux;

/// Whether we are running in a test environment
final isTestingEnvironment = Platform.environment['FLUTTER_TEST'] == 'true';
42 changes: 42 additions & 0 deletions app/lib/features/bluetooth/backend/bluetooth_backend.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import 'dart:async';
import 'package:blood_pressure_app/features/bluetooth/backend/bluetooth_low_energy/ble_manager.dart';
import 'package:blood_pressure_app/features/bluetooth/backend/flutter_blue_plus/fbp_manager.dart';
import 'package:blood_pressure_app/features/bluetooth/backend/flutter_blue_plus/flutter_blue_plus_mockable.dart';
import 'package:blood_pressure_app/features/bluetooth/backend/mock/mock_manager.dart';
import 'package:blood_pressure_app/logging.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';

part 'bluetooth_connection.dart';
pimlie marked this conversation as resolved.
Show resolved Hide resolved
part 'bluetooth_device.dart';
part 'bluetooth_discovery.dart';
part 'bluetooth_manager.dart';
part 'bluetooth_service.dart';
part 'bluetooth_state.dart';
part 'bluetooth_utils.dart';

/// Supported bluetooth backend implementations
///
/// Enum values provide a [create] instance method to easily get a new backend instance
enum BluetoothBackend {
pimlie marked this conversation as resolved.
Show resolved Hide resolved
/// Use the package 'bluetooth_low_energy' as backend manager
bluetoothLowEnergy,
/// Use the package 'flutter_blue_plus' as backend manager
flutterBluePlus,
/// Use a (partially) mocked backend manager.
/// Note that for full fledged testing it might be better to use flutter_blue_plus with a
/// custom [FlutterBluePlusMockable] instance
mock;

/// Create the bluetooth backend instance for the current enum value
BluetoothManager create() {
switch(this) {
case BluetoothBackend.bluetoothLowEnergy:
return BluetoothLowEnergyManager();
case BluetoothBackend.flutterBluePlus:
return FlutterBluePlusManager();
case BluetoothBackend.mock:
return MockBluetoothManager();
}
}
}
26 changes: 26 additions & 0 deletions app/lib/features/bluetooth/backend/bluetooth_connection.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
part of 'bluetooth_backend.dart';

/// State of the bluetooth connection of a device
enum BluetoothConnectionState {
pimlie marked this conversation as resolved.
Show resolved Hide resolved
/// Device is connected
connected,
/// Device is disconnect
disconnected;
}

/// Bluetooth connection state parser base class
///
/// This is a separate helper class as factory or static methods cannot be abstract,
/// so even though this class only has one method it's useful to enforce the types
abstract class BluetoothConnectionStateParser<BackendConnectionState> extends StreamDataParser<BackendConnectionState, BluetoothConnectionState> {}

/// Transforms the backend's bluetooth adapter state stream to emit [BluetoothAdapterState]'s
///
/// Can normally be used directly, backends should only inject a customized BluetoothStateParser
class BluetoothConnectionStateStreamTransformer<BackendConnectionState, BASP extends BluetoothConnectionStateParser<BackendConnectionState>>
pimlie marked this conversation as resolved.
Show resolved Hide resolved
extends StreamDataParserTransformer<BackendConnectionState, BluetoothConnectionState, BASP> {
/// Create a BluetoothConnectionStateStreamTransformer
///
/// [stateParser] The [BluetoothConnectionStateParser] that provides the backend logic to convert [BackendConnectionState] to [BluetoothConnectionState]
BluetoothConnectionStateStreamTransformer({ required super.stateParser, super.sync, super.cancelOnError });
}
166 changes: 166 additions & 0 deletions app/lib/features/bluetooth/backend/bluetooth_device.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
part of 'bluetooth_backend.dart';

/// Wrapper class for bluetooth implementations to generically expose needed props
///
/// This class can't be used directly, it should be implemented by a backend
pimlie marked this conversation as resolved.
Show resolved Hide resolved
abstract class BluetoothDevice<BM extends BluetoothManager, BS extends BluetoothService, BC extends BluetoothCharacteristic, BackendDevice> with TypeLogger {
/// constructor
BluetoothDevice(this._manager, this._source) {
logger.finer('init device: $this');
}

/// BluetoothManager this device belongs to
late final BM _manager;
pimlie marked this conversation as resolved.
Show resolved Hide resolved

/// Corresponding BluetoothManager
BM get manager => _manager;

/// Original source device as returned by the backend
late final BackendDevice _source;

/// Original source device as returned by the backend
BackendDevice get source => _source;

/// (Unique?) id of the device
String get deviceId;

/// Name of the device
String get name;

/// Memoized service list for the device
List<BS>? _services;

bool _isConnected = false;
derdilla marked this conversation as resolved.
Show resolved Hide resolved
StreamSubscription<BluetoothConnectionState>? _connectionListener;

/// Whether the device is connected
bool get isConnected => _isConnected;

/// Stream to listen to for connection state changes after connecting to a device
Stream<BluetoothConnectionState> get connectionStream;

/// Backend implementation to connect to the device
Future<void> backendConnect();
/// Backend implementation to disconnect to the device
Future<void> backendDisconnect();

/// Array of disconnect callbacks
///
/// Disconnect callbacks are processed in reverse order, i.e. the latest added callback is executed as first. Callbacks
pimlie marked this conversation as resolved.
Show resolved Hide resolved
/// can return true to indicate they have fully handled the disconnect. This will then also stop executing any remaining
/// callbacks.
@protected
final List<bool Function(bool wasConnected)> disconnectCallbacks = [];
pimlie marked this conversation as resolved.
Show resolved Hide resolved

/// Connect to the device
///
/// Always call disconnect when ready after calling connect
Future<bool> connect({ VoidCallback? onConnect, bool Function(bool wasConnected)? onDisconnect, ValueSetter<Object>? onError, int maxTries = 5 }) {
final completer = Completer<bool>();
int connectTry = 1;

logger.finer('connect: Init');

if (onDisconnect != null) {
disconnectCallbacks.add(onDisconnect);
}

/// Local helper util to only complete the completer when it's not completed yet
pimlie marked this conversation as resolved.
Show resolved Hide resolved
void doComplete(bool res) => !completer.isCompleted ? completer.complete(res) : null;

_connectionListener = connectionStream.listen((BluetoothConnectionState state) {
pimlie marked this conversation as resolved.
Show resolved Hide resolved
logger.finest('connectionStream.listen[isConnected: $_isConnected]: $state, connectTry: $connectTry');

switch (state) {
case BluetoothConnectionState.connected:
connectTry = 0; // reset try count
pimlie marked this conversation as resolved.
Show resolved Hide resolved

onConnect??();
pimlie marked this conversation as resolved.
Show resolved Hide resolved
doComplete(true);
_isConnected = true;
return;
case BluetoothConnectionState.disconnected:
final wasConnected = _isConnected;
// TODO: does this make even sense? I.e. can this listener be called with successive disconnected states?
if (!wasConnected && connectTry < maxTries) {
connectTry++;
backendConnect();
return;
}

for (final fn in disconnectCallbacks.reversed) {
if (fn(wasConnected)) {
// ignore other disconnect callbacks
break;
}
}

disconnectCallbacks.clear();
pimlie marked this conversation as resolved.
Show resolved Hide resolved
doComplete(false);
_isConnected = false;
}
}, onError: onError);

backendConnect();
return completer.future.then((res) {
logger.finer('connect: completer.resolved($res)');
return res;
});
}

/// Disconnect & dispose the device
///
/// Always call [disconnect] after calling [connect] to ensure all resources are disposed
Future<bool> disconnect() async {
await _connectionListener?.cancel();
await backendDisconnect();
return true;
}

/// Discover all available services on the device
///
/// It's recommended to use [getServices] instead
Future<List<BS>?> discoverServices();

/// Return all available services for this device
///
/// Difference with [discoverServices] is that [getServices] memoizes the results
Future<List<BS>?> getServices() async {
_services ??= await discoverServices();
if (_services == null) {
logger.finer('Failed to discoverServices on: $this');
derdilla marked this conversation as resolved.
Show resolved Hide resolved
}
return _services;
}

/// Returns the service with requested [uuid] or null if requested service is not available
Future<BS?> getServiceByUuid(BluetoothUuid uuid) async {
final services = await getServices();
try {
pimlie marked this conversation as resolved.
Show resolved Hide resolved
return services?.firstWhere((service) => service.uuid == uuid);
} on StateError {
return null;
}
}

/// Retrieves the value of [characteristic] from the device, the value is added to [value]
///
/// Note that a [characteristic] could have multiple values, hency why [value] is a list of a list.
/// Also note that [value] is a function arg as the return value is an indication if the read was
/// succesful or not. This function could convert a listener to a Future and reading a characteristic
/// could return muliple value items. So value could contain data even though the read gave an error
/// indicating not all data had been read from the device.
Future<bool> getCharacteristicValueByUuid(BC characteristic, List<Uint8List> value);

@override
String toString() => 'BluetoothDevice{name: $name, deviceId: $deviceId}';
}

/// Bluetooth device parser base class
///
/// This is a separate helper class as factory or static methods cannot be abstract,
/// so even though this class only has one method it's useful to enforce the types
abstract class BluetoothDeviceParser<T> {
pimlie marked this conversation as resolved.
Show resolved Hide resolved
/// Method that converts the raw bluetooth device data to a [BluetoothDevice] instance
BluetoothDevice parse(T rawDevice, BluetoothManager manager);
}
Loading