Commit a4a9959
Changed files (9)
app
lib
bluetooth
components
l10n
test
ui
components
bluetooth_input
app/lib/bluetooth/characteristics/ble_measurement_data.dart
@@ -88,7 +88,7 @@ class BleMeasurementData {
systolic: systolic,
diastolic: diastolic,
meanArterialPressure: meanArterialPressure,
- isMMHG: isMMHG, // TODO: use
+ isMMHG: isMMHG,
pulse: pulse,
userID: userId,
status: status,
app/lib/bluetooth/characteristics/decoding_util.dart
@@ -11,7 +11,8 @@ bool isBitIntByteSet(int byte, int offset) =>
double? readSFloat(List<int> data, int offset) {
if (data.length < offset + 2) return null;
// TODO: special values (NaN, Infinity)
- final mantissa = data[offset] + ((data[offset + 1] & 0x0F) << 8); // TODO: https://github.com/NordicSemiconductor/Kotlin-BLE-Library/blob/6b565e59de21dfa53ef80ff8351ac4a4550e8d58/core/src/main/java/no/nordicsemi/android/kotlin/ble/core/data/util/DataByteArray.kt#L392
+ // If this ever stops working: https://github.com/NordicSemiconductor/Kotlin-BLE-Library/blob/6b565e59de21dfa53ef80ff8351ac4a4550e8d58/core/src/main/java/no/nordicsemi/android/kotlin/ble/core/data/util/DataByteArray.kt#L392
+ final mantissa = data[offset] + ((data[offset + 1] & 0x0F) << 8);
final exponent = data[offset + 1] >> 4;
return (mantissa * (pow(10, exponent))).toDouble();
}
app/lib/bluetooth/ble_read_cubit.dart
@@ -9,7 +9,7 @@ import 'package:flutter_blue_plus/flutter_blue_plus.dart';
part 'ble_read_state.dart';
-/// Logic for reading a characteristic from a device.
+/// Logic for reading a characteristic from a device through a "indication".
///
/// May only be used on devices that are fully connected.
///
@@ -29,11 +29,13 @@ part 'ble_read_state.dart';
/// 5. Emit decoded object
class BleReadCubit extends Cubit<BleReadState> {
/// Start reading a characteristic from a device.
- BleReadCubit(this._device)
+ BleReadCubit(this._device, {
+ required this.serviceUUID,
+ required this.characteristicUUID,
+ })
: super(BleReadInProgress()){
_subscription = _device.connectionState
.listen(_onConnectionStateChanged);
- _device.cancelWhenDisconnected(_device.mtu.listen((int mtu) => Log.trace('BleReadCubit mtu: $mtu'))); // TODO: remove
// timeout
_timeoutTimer = Timer(const Duration(minutes: 2), () {
if (state is BleReadInProgress) {
@@ -44,9 +46,12 @@ class BleReadCubit extends Cubit<BleReadState> {
}
});
}
-
- static const String _kServiceID = '1810';
- static const String _kCharacteristicID = '2A35';
+
+ /// UUID of the service to read.
+ final Guid serviceUUID;
+
+ /// UUID of the characteristic to read.
+ final Guid characteristicUUID;
/// Bluetooth device to connect to.
///
@@ -138,7 +143,7 @@ class BleReadCubit extends Cubit<BleReadState> {
// [Guid.str] trims standard parts from the uuid. 0x1810 is the blood
// pressure uuid. 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
final BluetoothService? service = allServices
- .firstWhereOrNull((BluetoothService s) => s.uuid.str == _kServiceID);
+ .firstWhereOrNull((BluetoothService s) => s.uuid == serviceUUID);
if (service == null) {
Log.err('unsupported service', [_device, allServices]);
emit(BleReadFailure());
@@ -149,7 +154,7 @@ class BleReadCubit extends Cubit<BleReadState> {
final List<BluetoothCharacteristic> allCharacteristics = service.characteristics;
Log.trace('BleReadCubit allCharacteristics: $allCharacteristics');
final BluetoothCharacteristic? characteristic = allCharacteristics
- .firstWhereOrNull((c) => c.uuid == Guid(_kCharacteristicID),);
+ .firstWhereOrNull((c) => c.uuid == characteristicUUID,);
if (characteristic == null) {
Log.err('no characteristic', [_device, allServices, allCharacteristics]);
emit(BleReadFailure());
app/lib/components/bluetooth_input/device_selection.dart
@@ -30,7 +30,7 @@ class DeviceSelection extends StatelessWidget {
Widget build(BuildContext context) {
assert(scanResults.isNotEmpty);
return InputCard(
- //title: Text('Available devices:'), TODO
+ title: Text(AppLocalizations.of(context)!.availableDevices),
child: ListView(
shrinkWrap: true,
children: [
app/lib/components/bluetooth_input/measurement_success.dart
@@ -36,42 +36,42 @@ class MeasurementSuccess extends StatelessWidget {
style: Theme.of(context).textTheme.titleMedium,),
const SizedBox(height: 8,),
ListTile(
- title: Text('Mean arterial pressure'), // TODO: localizations and testing
- subtitle: Text(data.meanArterialPressure.toString()),
+ title: Text(AppLocalizations.of(context)!.meanArterialPressure),
+ subtitle: Text(data.meanArterialPressure.round().toString()),
),
if (data.userID != null)
ListTile(
- title: Text('User ID'),
+ title: Text(AppLocalizations.of(context)!.userID),
subtitle: Text(data.userID!.toString()),
),
if (data.status?.bodyMovementDetected ?? false)
ListTile(
- title: Text('Body movement detected'),
+ title: Text(AppLocalizations.of(context)!.bodyMovementDetected),
leading: Icon(Icons.directions_walk),
),
if (data.status?.cuffTooLose ?? false)
ListTile(
- title: Text('Cuff too loose'),
+ title: Text(AppLocalizations.of(context)!.cuffTooLoose),
leading: Icon(Icons.space_bar),
),
if (data.status?.improperMeasurementPosition ?? false)
ListTile(
- title: Text('Improper measurement position'),
+ title: Text(AppLocalizations.of(context)!.improperMeasurementPosition),
leading: Icon(Icons.emoji_people),
),
if (data.status?.irregularPulseDetected ?? false)
ListTile(
- title: Text('Irregular pulse detected'),
+ title: Text(AppLocalizations.of(context)!.irregularPulseDetected),
leading: Icon(Icons.heart_broken),
),
if (data.status?.pulseRateExceedsUpperLimit ?? false)
ListTile(
- title: Text('Pulse rate exceeds upper limit'),
+ title: Text(AppLocalizations.of(context)!.pulseRateExceedsUpperLimit),
leading: Icon(Icons.monitor_heart),
),
if (data.status?.pulseRateIsLessThenLowerLimit ?? false)
ListTile(
- title: Text('Pulse rate is less than lower limit'),
+ title: Text(AppLocalizations.of(context)!.pulseRateLessThanLowerLimit),
leading: Icon(Icons.monitor_heart),
),
const SizedBox(height: 8,),
app/lib/components/bluetooth_input.dart
@@ -67,6 +67,8 @@ class _BluetoothInputState extends State<BluetoothInput> {
}
Widget _buildActive(BuildContext context) {
+ final Guid serviceUUID = Guid('1810');
+ final Guid characteristicUUID = Guid('1810');
_bluetoothSubscription = _bluetoothCubit.stream.listen((state) {
if (state is! BluetoothReady) {
Log.trace('_BluetoothInputState: _bluetoothSubscription state=$state, calling _returnToIdle');
@@ -74,7 +76,7 @@ class _BluetoothInputState extends State<BluetoothInput> {
}
});
_deviceScanCubit ??= DeviceScanCubit(
- service: Guid('1810'), // TODO one source of truth (w read cubit)
+ service: serviceUUID,
settings: widget.settings,
);
return BlocBuilder<DeviceScanCubit, DeviceScanState>(
@@ -97,7 +99,10 @@ class _BluetoothInputState extends State<BluetoothInput> {
// distinction
DeviceSelected() => BlocConsumer<BleReadCubit, BleReadState>(
bloc: (){
- _deviceReadCubit = BleReadCubit(state.device);
+ _deviceReadCubit = BleReadCubit(state.device,
+ characteristicUUID: characteristicUUID,
+ serviceUUID: serviceUUID,
+ );
return _deviceReadCubit;
}(),
listener: (BuildContext context, BleReadState state) {
@@ -117,7 +122,6 @@ class _BluetoothInputState extends State<BluetoothInput> {
return switch (state) {
BleReadInProgress() => _buildMainCard(context,
child: const CircularProgressIndicator(),
- // TODO: onTap to retry
),
BleReadFailure() => MeasurementFailure(
onTap: _returnToIdle,
app/lib/l10n/app_en.arb
@@ -530,5 +530,23 @@
"scanningForDevices": "Scanning for devices",
"@scanningForDevices": {},
"tapToClose": "Tap to close.",
- "@tapToClose": {}
+ "@tapToClose": {},
+ "meanArterialPressure": "Mean arterial pressure",
+ "@meanArterialPressure": {},
+ "userID": "User ID",
+ "@userID": {},
+ "bodyMovementDetected": "Body movement detected",
+ "@bodyMovementDetected": {},
+ "cuffTooLoose": "Cuff too loose",
+ "@cuffTooLoose": {},
+ "improperMeasurementPosition": "Improper measurement position",
+ "@improperMeasurementPosition": {},
+ "irregularPulseDetected": "Irregular pulse detected",
+ "@irregularPulseDetected": {},
+ "pulseRateExceedsUpperLimit": "Pulse rate exceeds upper limit",
+ "@pulseRateExceedsUpperLimit": {},
+ "pulseRateLessThanLowerLimit": "Pulse rate is less than lower limit",
+ "@pulseRateLessThanLowerLimit": {},
+ "availableDevices": "Available devices",
+ "@availableDevices": {}
}
app/lib/main.dart
@@ -11,7 +11,6 @@ import 'package:blood_pressure_app/model/storage/settings_store.dart';
import 'package:blood_pressure_app/model/storage/update_legacy_settings.dart';
import 'package:blood_pressure_app/screens/home_screen.dart';
import 'package:blood_pressure_app/screens/loading_screen.dart';
-import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:fluttertoast/fluttertoast.dart';
@@ -23,20 +22,8 @@ import 'package:sqflite/sqflite.dart';
late final ConfigDB _database;
late final BloodPressureModel _bloodPressureModel;
-// TODO: remove
-final debugLog = <String>[];
void main() async {
- // TODO: remove
- FlutterError.onError = (details) {
- FlutterError.presentError(details);
- debugLog.add('FLUTTER: {{$details}}');
- };
- PlatformDispatcher.instance.onError = (error, stack) {
- debugLog.add('PLATFORM: {{$error||$stack}}');
- return true;
- };
-
runApp(ConsistentFutureBuilder(
future: _loadApp(),
onWaiting: const LoadingScreen(),
app/test/ui/components/bluetooth_input/measurement_success_test.dart
@@ -18,7 +18,7 @@ void main() {
systolic: 123,
diastolic: 456,
pulse: 67,
- meanArterialPressure: 123,
+ meanArterialPressure: 123456,
isMMHG: true,
userID: 3,
status: BleMeasurementStatus(
@@ -33,13 +33,23 @@ void main() {
timestamp: DateTime.now(),
),
)));
- // TODO
expect(find.byIcon(Icons.done), findsOneWidget);
expect(find.byIcon(Icons.close), findsOneWidget);
final localizations = await AppLocalizations.delegate.load(const Locale('en'));
expect(find.text(localizations.measurementSuccess), findsOneWidget);
+ expect(find.text(localizations.meanArterialPressure), findsOneWidget);
+ expect(find.text('123456'), findsOneWidget);
+
+ expect(find.text(localizations.userID), findsOneWidget);
+ expect(find.text(localizations.bodyMovementDetected), findsOneWidget);
+ expect(find.text(localizations.cuffTooLoose), findsOneWidget);
+ expect(find.text(localizations.improperMeasurementPosition), findsOneWidget);
+ expect(find.text(localizations.irregularPulseDetected), findsOneWidget);
+ expect(find.text(localizations.pulseRateExceedsUpperLimit), findsOneWidget);
+ expect(find.text(localizations.pulseRateLessThanLowerLimit), findsOneWidget);
+
expect(tapCount, 0);
await tester.tap(find.text(localizations.measurementSuccess));
await tester.pump();
@@ -48,5 +58,36 @@ void main() {
await tester.pump();
expect(tapCount, 2);
});
- // TODO: works when not everything shown
+ testWidgets('hides elements correctly', (WidgetTester tester) async {
+ int tapCount = 0;
+ await tester.pumpWidget(materialApp(MeasurementSuccess(
+ onTap: () => tapCount++,
+ data: BleMeasurementData(
+ systolic: 123,
+ diastolic: 456,
+ pulse: null,
+ meanArterialPressure: 123456,
+ isMMHG: true,
+ userID: null,
+ status: null,
+ timestamp: null,
+ ),
+ )));
+
+ expect(find.byIcon(Icons.done), findsOneWidget);
+ expect(find.byIcon(Icons.close), findsOneWidget);
+ final localizations = await AppLocalizations.delegate.load(const Locale('en'));
+ expect(find.text(localizations.measurementSuccess), findsOneWidget);
+
+ expect(find.text(localizations.meanArterialPressure), findsOneWidget);
+ expect(find.text('123456'), findsOneWidget);
+
+ expect(find.text(localizations.userID), findsNothing);
+ expect(find.text(localizations.bodyMovementDetected), findsNothing);
+ expect(find.text(localizations.cuffTooLoose), findsNothing);
+ expect(find.text(localizations.improperMeasurementPosition), findsNothing);
+ expect(find.text(localizations.irregularPulseDetected), findsNothing);
+ expect(find.text(localizations.pulseRateExceedsUpperLimit), findsNothing);
+ expect(find.text(localizations.pulseRateLessThanLowerLimit), findsNothing);
+ });
}