Commit 89759ad
Changed files (7)
app
app/lib/bluetooth/ble_read_cubit.dart
@@ -27,7 +27,7 @@ part 'ble_read_state.dart';
/// 3. If the searched characteristic is found read its value
/// 4. If binary data is read decode it to object
/// 5. Emit decoded object
-class BleReadCubit extends Cubit<BleRead> {
+class BleReadCubit extends Cubit<BleReadState> {
/// Start reading a characteristic from a device.
BleReadCubit(this._device)
: super(BleReadInProgress()) {
app/lib/bluetooth/ble_read_state.dart
@@ -2,16 +2,16 @@ part of 'ble_read_cubit.dart';
/// State of reading a characteristic from a BLE device.
@immutable
-abstract class BleRead {}
+sealed class BleReadState {}
/// The reading has been started.
-class BleReadInProgress extends BleRead {}
+class BleReadInProgress extends BleReadState {}
/// The reading failed unrecoverable for some reason.
-class BleReadFailure extends BleRead {}
+class BleReadFailure extends BleReadState {}
/// Data has been successfully read.
-class BleReadSuccess extends BleRead {
+class BleReadSuccess extends BleReadState {
/// Indicate a successful reading of a ble characteristic.
BleReadSuccess(this.data);
app/lib/bluetooth/bluetooth_state.dart
@@ -2,11 +2,12 @@ part of 'bluetooth_cubit.dart';
/// State of the devices bluetooth module.
@immutable
-abstract class BluetoothState {}
+sealed class BluetoothState {}
/// No information on whether bluetooth is available.
///
-/// Options relating to bluetooth should only be shown where they don't disturb.
+/// Users may show a loading indication but can not assume bluetooth is
+/// available.
class BluetoothInitial extends BluetoothState {}
/// There is no way bluetooth will work (e.g. no sensor).
app/lib/bluetooth/device_scan_cubit.dart
@@ -71,7 +71,7 @@ class DeviceScanCubit extends Cubit<DeviceScanState> {
// characteristic as users have to select their device anyways.
if(state is DeviceSelected) return;
final preferred = devices.firstWhereOrNull((dev) =>
- settings.knownBleDev.contains(dev.device.advName));
+ settings.knownBleDev.contains(dev.device.platformName));
if (preferred != null) {
emit(DeviceSelected(preferred.device));
} else if (devices.isEmpty) {
@@ -99,7 +99,7 @@ class DeviceScanCubit extends Cubit<DeviceScanState> {
assert(!_flutterBluePlus.isScanningNow);
emit(DeviceSelected(device));
final List<String> list = settings.knownBleDev.toList();
- list.add(device.advName);
+ list.add(device.platformName);
settings.knownBleDev = list;
}
app/lib/bluetooth/device_scan_state.dart
@@ -2,7 +2,7 @@ part of 'device_scan_cubit.dart';
/// Search of bluetooth devices that meet some criteria.
@immutable
-abstract class DeviceScanState {}
+sealed class DeviceScanState {}
/// Searching for devices or a reason they are not available.
class DeviceListLoading extends DeviceScanState {}
app/lib/components/bluetooth_input.dart
@@ -0,0 +1,192 @@
+import 'dart:async';
+
+import 'package:blood_pressure_app/bluetooth/ble_read_cubit.dart';
+import 'package:blood_pressure_app/bluetooth/bluetooth_cubit.dart';
+import 'package:blood_pressure_app/bluetooth/device_scan_cubit.dart';
+import 'package:blood_pressure_app/model/storage/storage.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_blue_plus/flutter_blue_plus.dart' show Guid;
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+
+/// Class for inputting measurement through bluetooth.
+class BluetoothInput extends StatefulWidget {
+ /// Create a measurement input through bluetooth.
+ const BluetoothInput({super.key, required this.settings});
+
+ final Settings settings;
+
+ @override
+ State<BluetoothInput> createState() => _BluetoothInputState();
+}
+
+class _BluetoothInputState extends State<BluetoothInput> {
+ /// Whether the user expanded bluetooth input
+ bool _isActive = false;
+
+ final BluetoothCubit _bluetoothCubit = BluetoothCubit();
+ StreamSubscription<BluetoothState>? _bluetoothSubscription;
+
+ void _returnToIdle() {
+ _bluetoothSubscription?.cancel();
+ _bluetoothSubscription = null;
+ if (_isActive) {
+ setState(() {
+ _isActive = false;
+ });
+ }
+ }
+
+ Widget _buildIdle(BuildContext context) => BlocBuilder<BluetoothCubit, BluetoothState>(
+ bloc: _bluetoothCubit,
+ builder: (context, BluetoothState state) => switch(state) {
+ BluetoothInitial() => const Align(
+ alignment: Alignment.topRight,
+ child: CircularProgressIndicator(),
+ ),
+ BluetoothUnfeasible() => const SizedBox.shrink(),
+ BluetoothUnauthorized() => Align(
+ alignment: Alignment.topRight,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(AppLocalizations.of(context)!.errBleNoPerms),
+ const Icon(Icons.bluetooth_disabled),
+ // TODO: tapable
+ ],
+ ),
+ ),
+ BluetoothDisabled() => Align(
+ alignment: Alignment.topRight,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(AppLocalizations.of(context)!.bluetoothDisabled),
+ const Icon(Icons.bluetooth_disabled),
+ // TODO: tapable
+ ],
+ ),
+ ),
+ BluetoothReady() => IconButton(
+ icon: const Icon(Icons.bluetooth),
+ onPressed: () => setState(() => _isActive = true),
+ ),
+ },
+ );
+
+ Widget _buildActive(BuildContext context) {
+ _bluetoothSubscription = _bluetoothCubit.stream.listen((state) {
+ if (state is! BluetoothReady) _returnToIdle();
+ });
+ final deviceScanBloc = DeviceScanCubit(
+ service: Guid('1810'),
+ settings: widget.settings,
+ );
+ return BlocBuilder<DeviceScanCubit, DeviceScanState>(
+ bloc: deviceScanBloc,
+ builder: (context, DeviceScanState state) => switch(state) {
+ DeviceListLoading() => _buildMainCard(context,
+ child: const CircularProgressIndicator()),
+ DeviceListAvailable() => _buildMainCard(context,
+ title: Text(AppLocalizations.of(context)!.selectDevice),
+ child: ListView(
+ children: [
+ for (final d in state.devices)
+ ListTile(
+ // TODO: consider only passing the string
+ title: Text(d.device.platformName),
+ onTap: () => deviceScanBloc.acceptDevice(d.device),
+ ),
+ ],
+ ),
+ ),
+ SingleDeviceAvailable() => _buildMainCard(context,
+ title: Text(AppLocalizations.of(context)!
+ .connectTo(state.device.device.platformName)),
+ child: FilledButton(
+ child: Text(AppLocalizations.of(context)!.connect),
+ onPressed: () => deviceScanBloc.acceptDevice(state.device.device),
+ ),
+ ),
+ DeviceSelected() => BlocBuilder<BleReadCubit, BleReadState>(
+ bloc: BleReadCubit(state.device),
+ builder: (BuildContext context, BleReadState state) => switch (state) {
+ BleReadInProgress() => _buildMainCard(context, child: CircularProgressIndicator()),
+ BleReadFailure() => _buildMainCard(context,
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(Icons.error_outline, color: Colors.red),
+ const SizedBox(height: 8,),
+ Text(AppLocalizations.of(context)!.errMeasurementRead),
+ ],
+ ),
+ ),
+ ),
+ BleReadSuccess() => _buildMainCard(context,
+ child: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(Icons.done, color: Colors.green),
+ const SizedBox(height: 8,),
+ Text(AppLocalizations.of(context)!.measurementSuccess),
+ ],
+ ),
+ ),
+ ),
+
+ },
+ ),
+ },
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (_isActive) return _buildActive(context);
+ return _buildIdle(context);
+ }
+
+
+ Widget _buildMainCard(BuildContext context, {
+ required Widget child,
+ Widget? title,
+ }) => 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,
+ ),
+ if (title != null)
+ Align(
+ alignment: Alignment.topLeft,
+ child: title,
+ ),
+ Align(
+ alignment: Alignment.topRight,
+ child: IconButton(
+ icon: const Icon(Icons.close),
+ onPressed: () => null, // TODO
+ ),
+ ),
+ ],
+ ),
+ );
+}
+
+enum _State {
+ /// Default state of the widget shown when no interaction happened.
+ idle,
+ /// Scanning and selecting devices.
+ deviceScan,
+ measurementRead,
+}
app/lib/l10n/app_en.arb
@@ -505,6 +505,8 @@
"@valueDistribution": {},
"titleInCsv": "Title in CSV",
"@titleInCsv": {},
+ "errBleNoPerms": "No bluetooth permissions",
+ "@errBleNoPerms": {},
"preferredPressureUnit": "Preferred pressure unit",
"@preferredPressureUnit": {},
"scanningDevices": "Scanning for devices...",
@@ -513,14 +515,19 @@
"@errBleCantOpen": {},
"errBleNoDev": "No compatible BLE GATT devices found. Tap to refresh!",
"@errBleNoDev": {},
- "errBleNoPerms": "Permissions error. Please allow all bluetooth permissions. On pre-Android 12 devices the location permission is required.",
- "@errBleNoPerms": {},
- "errBleCouldNotConnect": "Connection to bluetooth device failed. Tap to restart.",
- "@errBleCouldNotConnect": {},
- "bleConnecting": "Connecting to bluetooth device...",
- "@bleConnecting": {},
- "bleConnected": "Connected to device. Waiting for measurement...",
- "@bleConnected": {},
- "bleProcessing": "Processing incoming measurement...",
- "@bleProcessing": {}
+ "bluetoothDisabled": "Bluetooth disabled",
+ "@bluetoothDisabled": {},
+ "selectDevice": "Select device",
+ "@selectDevice": {},
+ "connectTo": "Connect to {deviceName}?",
+ "@connectTo": {
+ "type": "String",
+ "example": "boso medicus CE6674"
+ },
+ "errMeasurementRead": "Error reading measurement",
+ "@errBluetooth": {},
+ "measurementSuccess": "Measurement taken successfully",
+ "@measurementSuccess": {},
+ "connect": "Connect",
+ "@connect": {}
}