Commit 4723a55
Changed files (7)
app
lib
app/lib/bluetooth/device_scan_cubit.dart
@@ -62,8 +62,9 @@ class DeviceScanCubit extends Cubit<DeviceScanState> {
await _flutterBluePlus.startScan(
// no timeout, the user knows best how long scanning is needed
withServices: [service],
- // Might not find the device (https://pub.dev/packages/flutter_blue_plus#scanning-does-not-find-my-device)
- // TODO: Make decision on whether to support these devices
+ // Not all devices are found using this configuration (https://pub.dev/packages/flutter_blue_plus#scanning-does-not-find-my-device).
+ // As long as no significant issues arise from this these devices are
+ // considered unsupported.
);
} catch (e) {
app/lib/components/ble_input/ble_input._dart
@@ -1,157 +0,0 @@
-import 'package:blood_pressure_app/components/ble_input/ble_input_bloc.dart';
-import 'package:blood_pressure_app/components/ble_input/ble_input_events.dart';
-import 'package:blood_pressure_app/components/ble_input/ble_input_state.dart';
-import 'package:blood_pressure_app/main.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
-
-/// An interactive way to add measurements over bluetooth.
-class BleInput extends StatefulWidget {
- /// Create an interactive bluetooth measurement adder.
- const BleInput({super.key, this.bloc});
-
- /// Logic implementation of this input form.
- final BleInputBloc? bloc; // Configurable for testing.
-
- @override
- State<BleInput> createState() => _BleInputState();
-}
-
-class _BleInputState extends State<BleInput> {
-
- late final BleInputBloc bloc;
-
- @override
- void initState() {
- super.initState();
- bloc = widget.bloc ?? BleInputBloc();
- }
-
- @override
- Widget build(BuildContext context) => SizeChangedLayoutNotifier(
- child: BlocBuilder<BleInputBloc, BleInputState>(
- bloc: bloc,
- builder: (BuildContext context, BleInputState state) {
- debugLog.add('${DateTime.now()} - STATE:${state.runtimeType}');
- final localizations = AppLocalizations.of(context)!;
- return switch (state) {
- BleInputClosed() => IconButton(
- icon: const Icon(Icons.bluetooth),
- onPressed: () => bloc.add(OpenBleInput()),
- ),
- BleInputLoadInProgress() => _buildTwoElementCard(context,
- const CircularProgressIndicator(),
- Text(localizations.scanningDevices),
- ),
- BleInputLoadFailure() => _buildTwoElementCard(context,
- const Icon(Icons.bluetooth_disabled),
- Text(localizations.errBleCantOpen),
- onTap: () => bloc.add(OpenBleInput()),
- ),
- BleInputLoadSuccess() => _buildLoadSuccess(state),
- BleInputPermissionFailure() => _buildTwoElementCard(context,
- const Icon(Icons.bluetooth_disabled),
- Text(localizations.errBleNoPerms),
- onTap: () => bloc.add(OpenBleInput()),
- ),
- BleConnectInProgress() => _buildTwoElementCard(context,
- const CircularProgressIndicator(),
- Text(localizations.bleConnecting),
- ),
- BleConnectFailed() => _buildTwoElementCard(context,
- const Icon(Icons.bluetooth_disabled),
- Text(localizations.errBleCouldNotConnect),
- onTap: () => bloc.add(OpenBleInput()),
- ),
- BleConnectSuccess() => _buildTwoElementCard(context,
- const Icon(Icons.bluetooth_connected),
- Text(localizations.bleConnected),
- ),
- BleMeasurementInProgress() => _buildTwoElementCard(context,
- const CircularProgressIndicator(),
- Text(localizations.bleProcessing),
- ),
- BleMeasurementSuccess() => _buildTwoElementCard(context,
- const Icon(Icons.done, color: Colors.lightGreen,),
- Text('Received measurement:' // TODO: rework this process
- '\n${state.record}'
- '\nCuff loose: ${state.cuffLoose}'
- '\nIrregular pulse: ${state.irregularPulse}'
- '\nBody moved: ${state.bodyMoved}'
- '\nWrong measurement position: ${state.improperMeasurementPosition}'
- '\nMeasurement status: ${state.measurementStatus}'
- ),
- ),
- };
- },
- ),
- );
-
- Widget _buildLoadSuccess(BleInputLoadSuccess state) {
- debugLog.add('BleInputLoadSuccess:${state.availableDevices}');
- // List of available ble devices
- final localizations = AppLocalizations.of(context)!;
- if (state.availableDevices.isEmpty) {
- return _buildTwoElementCard(context,
- const Icon(Icons.info),
- Text(localizations.errBleNoDev),
- onTap: () => bloc.add(OpenBleInput()),
- );
- }
- return SizedBox(
- height: 250,
- child: _buildMainCard(context, ListView.builder(
- itemCount: state.availableDevices.length,
- itemBuilder: (context, idx) => ListTile(
- title: Text(state.availableDevices[idx].name),
- trailing: state.availableDevices[idx].connectable == Connectable.available
- ? const Icon(Icons.bluetooth_audio)
- : const Icon(Icons.bluetooth_disabled),
- onTap: () => bloc.add(BleInputDeviceSelected(state.availableDevices[idx])),
- ),
- ),),
- );
- }
-
- Widget _buildMainCard(BuildContext context, Widget child) => Card.outlined(
- color: Theme.of(context).cardColor,
- // borderRadius: BorderRadius.circular(24),
- // width: MediaQuery.of(context).size.width,
- // height: MediaQuery.of(context).size.width,
- // padding: const EdgeInsets.all(24),
- margin: const EdgeInsets.all(8),
- child: Stack(
- children: [
- Padding( // content
- padding: const EdgeInsets.all(24),
- child: child,
- ),
- Align(
- alignment: Alignment.topRight,
- child: IconButton(
- icon: const Icon(Icons.close),
- onPressed: () => bloc.add(CloseBleInput()),
- ),
- ),
- ],
- ),
- );
-
- /// Builds the full card but with two centered elements.
- Widget _buildTwoElementCard(
- BuildContext context,
- Widget top,
- Widget bottom, {
- void Function()? onTap,
- }) => InkWell(
- onTap: onTap,
- child: _buildMainCard(context, Center(
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- children: [top, const SizedBox(height: 8,), bottom,],
- ),
- ),),
- );
-}
app/lib/components/ble_input/ble_input_bloc._dart
@@ -1,146 +0,0 @@
-import 'dart:async';
-
-import 'package:blood_pressure_app/components/ble_input/ble_input_events.dart';
-import 'package:blood_pressure_app/components/ble_input/ble_input_state.dart';
-import 'package:blood_pressure_app/components/ble_input/measurement_characteristic.dart';
-import 'package:blood_pressure_app/model/blood_pressure/record.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
-import 'package:permission_handler/permission_handler.dart';
-
-import '../../main.dart';
-
-/// Logic for bluetooth measurement input.
-class BleInputBloc extends Bloc<BleInputEvent, BleInputState> {
- /// Create logic component for bluetooth measurement input.
- BleInputBloc(): super(BleInputClosed()) {
- on<OpenBleInput>(_onOpenBleInput);
- on<CloseBleInput>(_onCloseBleInput);
- on<BleInputDeviceSelected>(_onBleInputDeviceSelected);
- // TODO: figure out exhaustive approach
-
- // TODO: show capabilities during testing:
- // _ble.getDiscoveredServices()
-
- // Interesting available characteristics:
- // - Battery Health Information 0x2BEB (and other battery ...)
- // - Blood Pressure Feature 0x2A49
- // - Device Name 0x2A00
- // - Enhanced Blood Pressure Measurement 0x2B34
- // - Live Health Observations 0x2B8B
-
- }
-
- final _ble = FlutterReactiveBle();
-
- final Set<DiscoveredDevice> _availableDevices = {};
-
- StreamSubscription<DiscoveredDevice>? _deviceStreamSubscribtion;
- StreamSubscription<ConnectionStateUpdate>? _connectionUpdateStreamSubscribtion;
-
- final _requiredServices = [
- Uuid.parse('1810'),
- ];
-
-
- Future<void> _onOpenBleInput(OpenBleInput event, Emitter<BleInputState> emit) async {
- emit(BleInputLoadInProgress());
- if (await Permission.bluetoothConnect.isDenied) {
- emit(BleInputPermissionFailure());
- await Permission.bluetoothConnect.request();
- return;
- }
- emit(BleInputLoadInProgress());
- if (await Permission.bluetoothScan.isDenied) {
- emit(BleInputPermissionFailure());
- await Permission.bluetoothScan.request();
- return;
- }
- emit(BleInputLoadInProgress());
-
- try {
- await _ble.initialize();
- final deviceStream = _ble.scanForDevices(withServices: _requiredServices,);
- await _deviceStreamSubscribtion?.cancel();
- _deviceStreamSubscribtion = deviceStream.listen((device) {
- if (!_availableDevices.any((e) => e.name == device.name)) {
- _availableDevices.add(device);
- emit(BleInputLoadSuccess(_availableDevices.toList()));
- }
- });
- await _deviceStreamSubscribtion!.asFuture();
- } catch (e) {
- emit(BleInputLoadFailure());
- }
- }
-
- Future<void> _onCloseBleInput(CloseBleInput event, Emitter<BleInputState> emit) async {
- await _deviceStreamSubscribtion?.cancel();
- await _connectionUpdateStreamSubscribtion?.cancel();
- await _ble.deinitialize();
- emit(BleInputClosed());
- // TODO: cleanup
- }
-
- Future<void> _onBleInputDeviceSelected(BleInputDeviceSelected event, Emitter<BleInputState> emit) async {
- await _deviceStreamSubscribtion?.cancel();
- emit(BleConnectInProgress());
- try {
- await _connectionUpdateStreamSubscribtion?.cancel();
- _connectionUpdateStreamSubscribtion = _ble.connectToAdvertisingDevice(
- id: event.device.id,
- prescanDuration: const Duration(seconds: 5),
- withServices: _requiredServices,
- connectionTimeout: const Duration(minutes: 2),
- ).listen((update) {
- if (update.failure != null) {
- emit(BleConnectFailed());
- } else if (update.connectionState == DeviceConnectionState.connected) {
- // characteristics IDs (https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/Assigned_Numbers/out/en/Assigned_Numbers.pdf?v=1711151578821):
- // - Blood Pressure Measurement: 0x2A35 (https://bitbucket.org/bluetooth-SIG/public/src/main/gss/org.bluetooth.characteristic.blood_pressure_measurement.yaml)
- // - Blood Pressure Records: 0x2A36 (https://bitbucket.org/bluetooth-SIG/public/src/main/gss/org.bluetooth.characteristic.blood_pressure_record.yaml)
- //
- // A record represents a stored measurement, so in theory we should
- // search for a measurement.
- // Definition: https://www.bluetooth.com/specifications/bls-1-1-1/
- final characteristic = QualifiedCharacteristic(
- characteristicId: Uuid.parse('2A35'),
- serviceId: Uuid.parse('1810'),
- deviceId: event.device.id,
- );
- // TODO: extract subscription
- _ble.subscribeToCharacteristic(characteristic).listen((List<int> data) async {
- await _deviceStreamSubscribtion?.cancel();
- await _connectionUpdateStreamSubscribtion?.cancel();
- emit(BleMeasurementInProgress());
- debugLog.add('BLE MESSAGE: $data');
- final decoded = BPMeasurementCharacteristic.parse(data);
- final record = BloodPressureRecord(
- decoded.time ?? DateTime.now(),
- // TODO: unit conversions
- decoded.sys.toInt(),
- decoded.dia.toInt(),
- decoded.pul?.toInt(),
- '',
- );
- emit(BleMeasurementSuccess(record,
- bodyMoved: decoded.bodyMoved,
- cuffLoose: decoded.cuffLoose,
- irregularPulse: decoded.irregularPulse,
- improperMeasurementPosition: decoded.improperMeasurementPosition,
- measurementStatus: decoded.measurementStatus,
- ),);
- });
- emit(BleConnectSuccess());
- } else if (update.connectionState == DeviceConnectionState.connecting) {
- emit(BleConnectInProgress());
- } else {
- emit(BleConnectFailed());
- }
- });
- await _connectionUpdateStreamSubscribtion!.asFuture();
- } on TimeoutException {
- emit(BleConnectFailed());
- }
- }
-}
app/lib/components/ble_input/ble_input_events._dart
@@ -1,19 +0,0 @@
-import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
-
-/// Bluetooth measurement event.
-sealed class BleInputEvent {}
-
-/// Request expanding the input field.
-class OpenBleInput extends BleInputEvent {}
-
-/// Request closing the input field.
-class CloseBleInput extends BleInputEvent {}
-
-/// Connection with a device has been requested.
-class BleInputDeviceSelected extends BleInputEvent {
- /// Request connection with a device.
- BleInputDeviceSelected(this.device);
-
- /// The device to connect with.
- final DiscoveredDevice device;
-}
app/lib/components/ble_input/ble_input_state._dart
@@ -1,77 +0,0 @@
-import 'package:blood_pressure_app/components/ble_input/measurement_characteristic.dart';
-import 'package:blood_pressure_app/model/blood_pressure/record.dart';
-import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
-
-/// State of a component for inputting measurements through ble devices
-sealed class BleInputState {}
-
-/// The ble input field is inactive (not opened).
-class BleInputClosed extends BleInputState {}
-
-/// Doesn't have permission for bluetooth access.
-///
-/// The UI should show a warning to allow bluetooth and potentially location
-/// permissions.
-class BleInputPermissionFailure extends BleInputState {}
-
-/// Scanning for devices.
-class BleInputLoadInProgress extends BleInputState {}
-/// Could not start bluetooth search.
-///
-/// Most permissions errors should be covered by [BleInputPermissionFailure] so
-/// this might not be actionable by the user.
-class BleInputLoadFailure extends BleInputState {}
-/// Devices have been found and need selecting.
-class BleInputLoadSuccess extends BleInputState {
- /// Devices have been found and need selecting.
- BleInputLoadSuccess(this.availableDevices);
-
- /// List of all unique devices reported by the ble lib.
- final List<DiscoveredDevice> availableDevices;
-
- @override
- String toString() => 'BleInputLoadSuccess{availableDevices: $availableDevices}';
-}
-
-/// Connecting to device.
-class BleConnectInProgress extends BleInputState {}
-/// Couldn't connect to device or closed connection.
-class BleConnectFailed extends BleInputState {}
-/// Is connected with device.
-class BleConnectSuccess extends BleInputState {}
-
-/// Received information about an blood pressure measurement.
-class BleMeasurementInProgress extends BleInputState {}
-
-/// A measurement was taken through the bluetooth device.
-class BleMeasurementSuccess extends BleInputState {
- /// A measurement that was taken through the bluetooth device.
- BleMeasurementSuccess(this.record, {
- this.bodyMoved,
- this.cuffLoose,
- this.irregularPulse,
- this.measurementStatus,
- this.improperMeasurementPosition,
- });
-
- /// Measured blood pressure data.
- final BloodPressureRecord record;
-
- /// Whether body movement was detected during measurement.
- bool? bodyMoved;
-
- /// Whether the cuff was too loose during measurement.
- bool? cuffLoose;
-
- /// Whether irregular pulse was detected.
- bool? irregularPulse;
-
- /// The range the pulse rate was in.
- MeasurementStatus? measurementStatus;
-
- /// Whether the measurement was taken at an improper position.
- bool? improperMeasurementPosition;
-
- @override
- String toString() => 'BleMeasurementSuccess{record: $record, bodyMoved: $bodyMoved, cuffLoose: $cuffLoose, irregularPulse: $irregularPulse, measurementStatus: $measurementStatus, improperMeasurementPosition: $improperMeasurementPosition}';
-}
app/lib/components/ble_input/tmp
@@ -1,107 +0,0 @@
-import 'dart:async';
-
-import 'package:flutter_blue_plus/flutter_blue_plus.dart';
-
-class BeforeScanning {
- late StreamSubscription<BluetoothAdapterState> _adapterStateStateSubscription;
-
- // Must be: `on`
- BluetoothAdapterState _adapterState = BluetoothAdapterState.unknown;
-
- init() {
- _adapterStateStateSubscription = FlutterBluePlus.adapterState.listen((state) {
- setState(() {
- _adapterState = state;
- });
- });
- }
-
- dispose() {
- _adapterStateStateSubscription.cancel();
- }
-
- enableBluetooth() {
- try {
- if (Platform.isAndroid) {
- await FlutterBluePlus.turnOn();
- }
- } catch (e) {
- Snackbar.show(ABC.a, prettyException("Error Turning On:", e), success: false);
- }
- }
-}
-
-class ScanningPhase {
- List<BluetoothDevice> _systemDevices = [];
- List<ScanResult> _scanResults = [];
- bool _isScanning = false;
- late StreamSubscription<List<ScanResult>> _scanResultsSubscription;
- late StreamSubscription<bool> _isScanningSubscription;
-
- initState() {
- super.initState();
-
- _scanResultsSubscription = FlutterBluePlus.scanResults.listen((results) {
- _scanResults = results;
- }, onError: (e) {
- Snackbar.show(ABC.b, prettyException("Scan Error:", e), success: false);
- });
-
- _isScanningSubscription = FlutterBluePlus.isScanning.listen((state) {
- _isScanning = state;
- });
- }
-
- dispose() {
- _scanResultsSubscription.cancel();
- _isScanningSubscription.cancel();
- super.dispose();
- }
-
- onScanPressed() async {
- try {
- _systemDevices = await FlutterBluePlus.systemDevices;
- } catch (e) {
- Snackbar.show(ABC.b, prettyException("System Devices Error:", e), success: false);
- }
- try {
- await FlutterBluePlus.startScan(timeout: const Duration(seconds: 15));
- } catch (e) {
- Snackbar.show(ABC.b, prettyException("Start Scan Error:", e), success: false);
- }
- }
-
- onStopPressed() async {
- try {
- FlutterBluePlus.stopScan();
- } catch (e) {
- Snackbar.show(ABC.b, prettyException("Stop Scan Error:", e), success: false);
- }
- }
-}
-
-class DeviceInfo {
-
- userInfo() {
- widget.result._device.platformName.isNotEmpty
- ? widget.result._device.platformName
- : widget.result._device.remoteId.str
- }
-
- bool get isConnectable => widget.result.advertisementData.connectable;
-
- connect() {
- _device.connectAndUpdateStream().catchError((e) {
- Snackbar.show(ABC.c, prettyException("Connect Error:", e), success: false);
- });
- }
-}
-
-class DeviceConnection {
- late StreamSubscription<BluetoothConnectionState> _connectionStateSubscription;
- late StreamSubscription<bool> _isConnectingSubscription;
- late StreamSubscription<bool> _isDisconnectingSubscription;
- late StreamSubscription<int> _mtuSubscription;
-
- // https://github.com/boskokg/flutter_blue_plus/blob/master/example/lib/screens/device_screen.dart
-}
\ No newline at end of file
app/lib/components/bluetooth_input.dart
@@ -42,17 +42,18 @@ class _BluetoothInputState extends State<BluetoothInput> {
DeviceScanCubit? _deviceScanCubit;
@override
- void dispose() {
- _bluetoothSubscription?.cancel();
- _bluetoothCubit.close();
- _deviceScanCubit?.close();
+ void dispose() async {
+ await _bluetoothSubscription?.cancel();
+ await _bluetoothCubit.close();
+ await _deviceScanCubit?.close();
super.dispose();
}
- void _returnToIdle() {
- _bluetoothSubscription?.cancel();
+ void _returnToIdle() async {
+ await _bluetoothSubscription?.cancel();
_bluetoothSubscription = null;
- _deviceScanCubit?.close().then((_) => _deviceScanCubit = null);
+ await _deviceScanCubit?.close();
+ _deviceScanCubit = null;
if (_isActive) {
setState(() {
_isActive = false;
@@ -60,8 +61,6 @@ class _BluetoothInputState extends State<BluetoothInput> {
}
}
- // TODO: bloc dispose
-
Widget _buildActive(BuildContext context) {
_bluetoothSubscription = _bluetoothCubit.stream.listen((state) {
if (state is! BluetoothReady) _returnToIdle();
@@ -130,8 +129,6 @@ class _BluetoothInputState extends State<BluetoothInput> {
},
);
}
- // TODO: scanning devices info
-
Widget _buildMainCard(BuildContext context, {
required Widget child,