diff --git a/lib/events.dart b/lib/events.dart index cf4e4221436aa914a3cde0a8fbbab1aa3d4b15b6..81cbf83aa1713f829476778ffbfccc604494f743 100644 --- a/lib/events.dart +++ b/lib/events.dart @@ -36,6 +36,9 @@ // // buttonData: task -> main // {"time": int} +// +// batteryLevel: task -> main +// {"level": int} enum EventName { requestLocationState, @@ -50,6 +53,7 @@ enum EventName { disconnected, distanceData, buttonData, + batteryLevel, } enum LocationState { @@ -109,6 +113,8 @@ abstract class EventData { return DistanceDataEvent.fromJson(json); case EventName.buttonData: return ButtonDataEvent.fromJson(json); + case EventName.batteryLevel: + return BatteryLevelEvent.fromJson(json); default: throw ArgumentError('Switch fallthrough:Invalid event name'); } @@ -331,3 +337,26 @@ class ButtonDataEvent extends EventData { } } } + +class BatteryLevelEvent extends EventData { + BatteryLevelEvent({required this.level}) + : super(event: EventName.batteryLevel); + + final int level; + + @override + Map<String, dynamic> toJson() => { + 'event': event.toString().split('.').last, + 'level': level, + }; + + factory BatteryLevelEvent.fromJson(Map<String, dynamic> json) { + if (json.containsKey('level')) { + return BatteryLevelEvent( + level: json['level'], + ); + } else { + throw ArgumentError('Invalid JSON data for BatteryLevelEvent'); + } + } +} diff --git a/lib/foreground_task/bluetooth.dart b/lib/foreground_task/bluetooth.dart index 5a9e640e0a522638e8c62c8e88906617d1d8e1ac..a5393e109e6ae62b4c89d2fb2345b31648c0a6d9 100644 --- a/lib/foreground_task/bluetooth.dart +++ b/lib/foreground_task/bluetooth.dart @@ -5,6 +5,9 @@ import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; // The services that should be exposed by the OpenBikeSensor +Guid batteryServiceID = Guid("0000180F-0000-1000-8000-00805F9B34FB"); +Guid batteryCharacteristic = Guid("00002A19-0000-1000-8000-00805F9B34FB"); + Guid deviceServiceID = Guid("0000180A-0000-1000-8000-00805F9B34FB"); Guid firmwareCharacteristic = Guid("00002a26-0000-1000-8000-00805f9b34fb"); Guid nameCharacteristic = Guid("00002a29-0000-1000-8000-00805f9b34fb"); @@ -15,7 +18,7 @@ Guid buttonCharacteristic = Guid("1FE7FAF9-CE63-4236-0004-000000000003"); Guid offsetCharacteristic = Guid("1FE7FAF9-CE63-4236-0004-000000000004"); Guid trackidCharacteristic = Guid("1FE7FAF9-CE63-4236-0004-000000000005"); -enum BluetoothEvent { distance, button, offset, trackId, firmware } +enum BluetoothEvent { distance, button, offset, trackId, firmware, battery } typedef BluetoothCallbackFunction = Function(BluetoothEvent, CallbackResults); typedef DiscoveredDeviceCallbackFunction = Function(List<BluetoothDevice>); @@ -36,6 +39,7 @@ class CallbackResults { int distance1 = 0; int distance2 = 0; int clock = 0; + int batteryLevel = 0; } class Bluetooth { @@ -48,6 +52,7 @@ class Bluetooth { // Subscription to the distance and button characteristics StreamSubscription<List<int>>? _distanceSubscription; StreamSubscription<List<int>>? _buttonSubscription; + StreamSubscription<List<int>>? _batterySubscription; StreamSubscription<OnDiscoveredServicesEvent>? _servicesSubscription; @@ -67,6 +72,7 @@ class Bluetooth { _servicesSubscription?.cancel(); _distanceSubscription?.cancel(); _buttonSubscription?.cancel(); + _batterySubscription?.cancel(); } static Bluetooth? _instance; @@ -276,6 +282,14 @@ class Bluetooth { _notifyCharacteristicCallbacks(BluetoothEvent.trackId, data); } + void _handleBatteryLevelCharacteristic(List<int> valueList) { + if (valueList.isEmpty) { + throw Exception("Invalid battery level characteristic"); + } + final CallbackResults data = CallbackResults()..batteryLevel = valueList[0]; + _notifyCharacteristicCallbacks(BluetoothEvent.battery, data); + } + // Discover all services exposed by the device Future<void> subscribeToCharacteristics( BluetoothCallbackFunction callback) async { @@ -296,6 +310,16 @@ class Bluetooth { firmware.lastValueStream.listen(handleFirmwareCharacteristic); await firmware.read(); + // BatteryService + // https://www.bluetooth.com/wp-content/uploads/Files/Specification/HTML/BAS_v1.1/out/en/index-en.html#UUID-fb902c2f-32bc-833c-8eb6-245fc3a52b81 + final batteryService = services + .firstWhere((element) => element.uuid == batteryServiceID) + .characteristics; + final battery = batteryService + .firstWhere((element) => element.uuid == batteryCharacteristic); + battery.setNotifyValue(true); + battery.lastValueStream.listen(_handleBatteryLevelCharacteristic); + // Handle the obsService final obsCharacteristics = services .firstWhere((element) => element.uuid == obsServiceID) diff --git a/lib/foreground_task/task_handler.dart b/lib/foreground_task/task_handler.dart index 83e97356486f32947a2c107af9f3e4c54d97705c..9d88ab4edf94b8db4e02ee34137c21742efda02c 100644 --- a/lib/foreground_task/task_handler.dart +++ b/lib/foreground_task/task_handler.dart @@ -314,6 +314,11 @@ class ForegroundTaskHandler extends TaskHandler { break; case BluetoothEvent.trackId: break; // unused + case BluetoothEvent.battery: + _sendMessageToUI(BatteryLevelEvent( + level: data.batteryLevel, + )); + default: throw Exception("Unknown event $event"); } diff --git a/lib/main.dart b/lib/main.dart index b6decee90d0ef6b27f3cae6ce1afa950309eb776..921f446ae1b42b83c322f2e2dc37d783f410514c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:app/generated/i18n/app_localizations.dart'; import 'package:app/manager/background_manger.dart'; +import 'package:app/providers/battery_provider.dart'; import 'package:app/providers/bluetooth_provider.dart'; import 'package:app/providers/button_provider.dart'; import 'package:app/providers/location_provider.dart'; @@ -29,6 +30,7 @@ Future<void> main() async { ChangeNotifierProvider(create: (context) => ButtonProvider()), ChangeNotifierProvider(create: (context) => TripProvider()), ChangeNotifierProvider(create: (context) => LocationProvider()), + ChangeNotifierProvider(create: (context) => BatteryProvider()), ], child: const MyApp(), ); diff --git a/lib/manager/background_manger.dart b/lib/manager/background_manger.dart index 80d0781fa1de080674fa58720ae1554539d44bd0..57008003de4c4ab13cd18c7f1e170a90bcd00fd4 100644 --- a/lib/manager/background_manger.dart +++ b/lib/manager/background_manger.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:app/foreground_task/bluetooth.dart'; import 'package:app/foreground_task/task_handler.dart'; +import 'package:app/providers/battery_provider.dart'; import 'package:app/providers/bluetooth_provider.dart'; import 'package:app/providers/button_provider.dart'; import 'package:app/providers/distance_provider.dart'; @@ -129,6 +130,8 @@ class BackgroundManager { Provider.of<ButtonProvider>(_context!, listen: false); final locationProvider = Provider.of<LocationProvider>(_context!, listen: false); + final batteryProvider = + Provider.of<BatteryProvider>(_context!, listen: false); final event = EventData.fromJson(jsonData); @@ -185,6 +188,10 @@ class BackgroundManager { case EventName.buttonData: buttonProvider.setButton(); break; + case EventName.batteryLevel: + final batteryLevel = (event as BatteryLevelEvent).level; + batteryProvider.setBatteryLevel(batteryLevel); + break; default: throw Exception("Unknown event ${event.event}"); } diff --git a/lib/providers/battery_provider.dart b/lib/providers/battery_provider.dart new file mode 100644 index 0000000000000000000000000000000000000000..9aaa5686ca584942a1d92ef913e66bfc05bd01a5 --- /dev/null +++ b/lib/providers/battery_provider.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; + +class BatteryProvider extends ChangeNotifier { + int batteryLevel = -1; // No information about the battery available + + BatteryProvider(); + + void setBatteryLevel(int level) { + batteryLevel = level; + notifyListeners(); + } +} diff --git a/lib/widgets/bluetooth_status.dart b/lib/widgets/bluetooth_status.dart index 9816fdc94359d2d9a78f18bddb441addd16ec705..25d2e3bf925af0f93bd3fe3cdec725fa1353bda9 100644 --- a/lib/widgets/bluetooth_status.dart +++ b/lib/widgets/bluetooth_status.dart @@ -1,4 +1,5 @@ import 'package:app/foreground_task/bluetooth.dart'; +import 'package:app/providers/battery_provider.dart'; import 'package:app/providers/bluetooth_provider.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -19,7 +20,7 @@ class _BluetoothStatusState extends State<BluetoothStatus> { } // return a text widget based on the location status - Widget locationText(BleState state, ConnectionResult? device) { + Widget bluetoothStatusText(BleState state, ConnectionResult? device) { final localizations = AppLocalizations.of(context)!; switch (state) { @@ -43,9 +44,36 @@ class _BluetoothStatusState extends State<BluetoothStatus> { } } + Widget batteryState(int batteryLevel) { + Icon batteryIcon = Icon(Icons.battery_unknown); + if (batteryLevel < 0) { + batteryIcon = Icon(Icons.battery_unknown); + } else if (batteryLevel < 10) { + batteryIcon = Icon(Icons.battery_0_bar); + } else if (batteryLevel < 20) { + batteryIcon = Icon(Icons.battery_2_bar); + } else if (batteryLevel < 40) { + batteryIcon = Icon(Icons.battery_3_bar); + } else if (batteryLevel < 60) { + batteryIcon = Icon(Icons.battery_4_bar); + } else if (batteryLevel < 80) { + batteryIcon = Icon(Icons.battery_5_bar); + } else if (batteryLevel < 100) { + batteryIcon = Icon(Icons.battery_6_bar); + } else if (batteryLevel == 100) { + batteryIcon = Icon(Icons.battery_full); + } + + return Row(children: [ + Icon(Icons.battery_5_bar), + Text(batteryLevel >= 0 ? '$batteryLevel%' : '') + ]); + } + @override Widget build(BuildContext context) { final bluetoothProvider = context.watch<BluetoothProvider>(); + final batteryProvider = context.watch<BatteryProvider>(); return Card( child: ListTile( @@ -56,8 +84,11 @@ class _BluetoothStatusState extends State<BluetoothStatus> { subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - locationText(bluetoothProvider.connectionState, + bluetoothStatusText(bluetoothProvider.connectionState, bluetoothProvider.connectedDevice), + bluetoothProvider.connectionState == BleState.connected + ? batteryState(batteryProvider.batteryLevel) + : SizedBox.shrink() ], ), ),