From 3d11df04536c740c41858e692919e93928acebca Mon Sep 17 00:00:00 2001 From: Italo Matos Date: Thu, 29 Aug 2024 17:09:27 -0300 Subject: [PATCH] add bluetooth screen --- android/app/proguard-rules.pro | 1 + android/app/src/main/AndroidManifest.xml | 153 +++-- lib/main.dart | 1 - .../bluetooth/BackgroundCollectedPage.dart | 145 +++++ .../bluetooth/BackgroundCollectingTask.dart | 124 ++++ .../bluetooth/BluetoothDeviceListEntry.dart | 74 +++ lib/modules/bluetooth/ChatPage.dart | 239 +++++++ lib/modules/bluetooth/DiscoveryPage.dart | 163 +++++ lib/modules/bluetooth/MainPage.dart | 354 ++++++++++ .../bluetooth/SelectBondedDevicePage.dart | 146 +++++ lib/modules/bluetooth/bluetooth_screen.dart | 20 + lib/modules/bluetooth/helpers/LineChart.dart | 605 ++++++++++++++++++ lib/modules/bluetooth/helpers/PaintStyle.dart | 263 ++++++++ lib/modules/home_page/screen/home_screen.dart | 12 +- pubspec.lock | 196 +++--- pubspec.yaml | 8 +- 16 files changed, 2339 insertions(+), 165 deletions(-) create mode 100644 android/app/proguard-rules.pro create mode 100644 lib/modules/bluetooth/BackgroundCollectedPage.dart create mode 100644 lib/modules/bluetooth/BackgroundCollectingTask.dart create mode 100644 lib/modules/bluetooth/BluetoothDeviceListEntry.dart create mode 100644 lib/modules/bluetooth/ChatPage.dart create mode 100644 lib/modules/bluetooth/DiscoveryPage.dart create mode 100644 lib/modules/bluetooth/MainPage.dart create mode 100644 lib/modules/bluetooth/SelectBondedDevicePage.dart create mode 100644 lib/modules/bluetooth/bluetooth_screen.dart create mode 100644 lib/modules/bluetooth/helpers/LineChart.dart create mode 100644 lib/modules/bluetooth/helpers/PaintStyle.dart diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..ad4f552 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1 @@ +-keep class com.lib.flutter_blue_plus.* { *; } \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 5e74459..9b63373 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,68 +1,93 @@ - - - + package="com.zenithaerospace.monitor"> - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/main.dart b/lib/main.dart index 651d02c..fdc23fa 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,7 +13,6 @@ import 'package:zenith_monitor/modules/terminal/bloc/terminal_bloc.dart'; import 'package:zenith_monitor/modules/terminal/screen/terminal_screen.dart'; import 'package:zenith_monitor/utils/services/usb/usb.dart'; import 'package:zenith_monitor/core/pipelines/data_pipeline/data_bloc.dart'; -import 'package:zenith_monitor/modules/forget_password/screen/forgot_my_password_screen.dart'; import 'package:zenith_monitor/modules/login/bloc/login_bloc.dart'; import 'package:zenith_monitor/modules/map/screen/map_screen.dart'; import 'package:zenith_monitor/modules/signup/screen/sign_up_screen.dart'; diff --git a/lib/modules/bluetooth/BackgroundCollectedPage.dart b/lib/modules/bluetooth/BackgroundCollectedPage.dart new file mode 100644 index 0000000..298cc16 --- /dev/null +++ b/lib/modules/bluetooth/BackgroundCollectedPage.dart @@ -0,0 +1,145 @@ +import 'package:flutter/material.dart'; + +import './BackgroundCollectingTask.dart'; +import './helpers/LineChart.dart'; +import './helpers/PaintStyle.dart'; + +class BackgroundCollectedPage extends StatelessWidget { + @override + Widget build(BuildContext context) { + final BackgroundCollectingTask task = + BackgroundCollectingTask.of(context, rebuildOnChange: true); + + // Arguments shift is needed for timestamps as miliseconds in double could loose precision. + final int argumentsShift = + task.samples.first.timestamp.millisecondsSinceEpoch; + + final Duration showDuration = + Duration(hours: 2); // @TODO . show duration should be configurable + final Iterable lastSamples = task.getLastOf(showDuration); + + final Iterable arguments = lastSamples.map((sample) { + return (sample.timestamp.millisecondsSinceEpoch - argumentsShift) + .toDouble(); + }); + + // Step for argument labels + final Duration argumentsStep = + Duration(minutes: 15); // @TODO . step duration should be configurable + + // Find first timestamp floored to step before + final DateTime beginningArguments = lastSamples.first.timestamp; + DateTime beginningArgumentsStep = DateTime(beginningArguments.year, + beginningArguments.month, beginningArguments.day); + while (beginningArgumentsStep.isBefore(beginningArguments)) { + beginningArgumentsStep = beginningArgumentsStep.add(argumentsStep); + } + beginningArgumentsStep = beginningArgumentsStep.subtract(argumentsStep); + final DateTime endingArguments = lastSamples.last.timestamp; + + // Generate list of timestamps of labels + final Iterable argumentsLabelsTimestamps = () sync* { + DateTime timestamp = beginningArgumentsStep; + yield timestamp; + while (timestamp.isBefore(endingArguments)) { + timestamp = timestamp.add(argumentsStep); + yield timestamp; + } + }(); + + // Map strings for labels + final Iterable argumentsLabels = + argumentsLabelsTimestamps.map((timestamp) { + return LabelEntry( + (timestamp.millisecondsSinceEpoch - argumentsShift).toDouble(), + ((timestamp.hour <= 9 ? '0' : '') + + timestamp.hour.toString() + + ':' + + (timestamp.minute <= 9 ? '0' : '') + + timestamp.minute.toString())); + }); + + return Scaffold( + appBar: AppBar( + title: Text('Collected data'), + actions: [ + // Progress circle + (task.inProgress + ? FittedBox( + child: Container( + margin: new EdgeInsets.all(16.0), + child: CircularProgressIndicator( + valueColor: + AlwaysStoppedAnimation(Colors.white)))) + : Container(/* Dummy */)), + // Start/stop buttons + (task.inProgress + ? IconButton(icon: Icon(Icons.pause), onPressed: task.pause) + : IconButton( + icon: Icon(Icons.play_arrow), onPressed: task.reasume)), + ], + ), + body: ListView( + children: [ + Divider(), + ListTile( + leading: const Icon(Icons.brightness_7), + title: const Text('Temperatures'), + subtitle: const Text('In Celsius'), + ), + LineChart( + constraints: const BoxConstraints.expand(height: 350), + arguments: arguments, + argumentsLabels: argumentsLabels, + values: [ + lastSamples.map((sample) => sample.temperature1), + lastSamples.map((sample) => sample.temperature2), + ], + verticalLinesStyle: const PaintStyle(color: Colors.grey), + additionalMinimalHorizontalLabelsInterval: 0, + additionalMinimalVerticalLablesInterval: 0, + seriesPointsStyles: [ + null, + null, + //const PaintStyle(style: PaintingStyle.stroke, strokeWidth: 1.7*3, color: Colors.indigo, strokeCap: StrokeCap.round), + ], + seriesLinesStyles: [ + const PaintStyle( + style: PaintingStyle.stroke, + strokeWidth: 1.7, + color: Colors.indigoAccent), + const PaintStyle( + style: PaintingStyle.stroke, + strokeWidth: 1.7, + color: Colors.redAccent), + ], + ), + Divider(), + ListTile( + leading: const Icon(Icons.filter_vintage), + title: const Text('Water pH level'), + ), + LineChart( + constraints: const BoxConstraints.expand(height: 200), + arguments: arguments, + argumentsLabels: argumentsLabels, + values: [ + lastSamples.map((sample) => sample.waterpHlevel), + ], + verticalLinesStyle: const PaintStyle(color: Colors.grey), + additionalMinimalHorizontalLabelsInterval: 0, + additionalMinimalVerticalLablesInterval: 0, + seriesPointsStyles: [ + null, + ], + seriesLinesStyles: [ + const PaintStyle( + style: PaintingStyle.stroke, + strokeWidth: 1.7, + color: Colors.greenAccent), + ], + ), + ], + )); + } +} diff --git a/lib/modules/bluetooth/BackgroundCollectingTask.dart b/lib/modules/bluetooth/BackgroundCollectingTask.dart new file mode 100644 index 0000000..502423a --- /dev/null +++ b/lib/modules/bluetooth/BackgroundCollectingTask.dart @@ -0,0 +1,124 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; +import 'package:scoped_model/scoped_model.dart'; + +class DataSample { + double temperature1; + double temperature2; + double waterpHlevel; + DateTime timestamp; + + DataSample({ + required this.temperature1, + required this.temperature2, + required this.waterpHlevel, + required this.timestamp, + }); +} + +class BackgroundCollectingTask extends Model { + static BackgroundCollectingTask of( + BuildContext context, { + bool rebuildOnChange = false, + }) => + ScopedModel.of( + context, + rebuildOnChange: rebuildOnChange, + ); + + final BluetoothConnection _connection; + List _buffer = List.empty(growable: true); + + // @TODO , Such sample collection in real code should be delegated + // (via `Stream` preferably) and then saved for later + // displaying on chart (or even stright prepare for displaying). + // @TODO ? should be shrinked at some point, endless colleting data would cause memory shortage. + List samples = List.empty(growable: true); + + bool inProgress = false; + + BackgroundCollectingTask._fromConnection(this._connection) { + _connection.input!.listen((data) { + _buffer += data; + + while (true) { + // If there is a sample, and it is full sent + int index = _buffer.indexOf('t'.codeUnitAt(0)); + if (index >= 0 && _buffer.length - index >= 7) { + final DataSample sample = DataSample( + temperature1: (_buffer[index + 1] + _buffer[index + 2] / 100), + temperature2: (_buffer[index + 3] + _buffer[index + 4] / 100), + waterpHlevel: (_buffer[index + 5] + _buffer[index + 6] / 100), + timestamp: DateTime.now()); + _buffer.removeRange(0, index + 7); + + samples.add(sample); + notifyListeners(); // Note: It shouldn't be invoked very often - in this example data comes at every second, but if there would be more data, it should update (including repaint of graphs) in some fixed interval instead of after every sample. + //print("${sample.timestamp.toString()} -> ${sample.temperature1} / ${sample.temperature2}"); + } + // Otherwise break + else { + break; + } + } + }).onDone(() { + inProgress = false; + notifyListeners(); + }); + } + + static Future connect( + BluetoothDevice server) async { + final BluetoothConnection connection = + await BluetoothConnection.toAddress(server.address); + return BackgroundCollectingTask._fromConnection(connection); + } + + void dispose() { + _connection.dispose(); + } + + Future start() async { + inProgress = true; + _buffer.clear(); + samples.clear(); + notifyListeners(); + _connection.output.add(ascii.encode('start')); + await _connection.output.allSent; + } + + Future cancel() async { + inProgress = false; + notifyListeners(); + _connection.output.add(ascii.encode('stop')); + await _connection.finish(); + } + + Future pause() async { + inProgress = false; + notifyListeners(); + _connection.output.add(ascii.encode('stop')); + await _connection.output.allSent; + } + + Future reasume() async { + inProgress = true; + notifyListeners(); + _connection.output.add(ascii.encode('start')); + await _connection.output.allSent; + } + + Iterable getLastOf(Duration duration) { + DateTime startingTime = DateTime.now().subtract(duration); + int i = samples.length; + do { + i -= 1; + if (i <= 0) { + break; + } + } while (samples[i].timestamp.isAfter(startingTime)); + return samples.getRange(i, samples.length); + } +} diff --git a/lib/modules/bluetooth/BluetoothDeviceListEntry.dart b/lib/modules/bluetooth/BluetoothDeviceListEntry.dart new file mode 100644 index 0000000..3915353 --- /dev/null +++ b/lib/modules/bluetooth/BluetoothDeviceListEntry.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; + +class BluetoothDeviceListEntry extends ListTile { + BluetoothDeviceListEntry({ + required BluetoothDevice device, + int? rssi, + GestureTapCallback? onTap, + GestureLongPressCallback? onLongPress, + bool enabled = true, + }) : super( + onTap: onTap, + onLongPress: onLongPress, + enabled: enabled, + leading: const Icon( + Icons.devices), // @TODO . !BluetoothClass! class aware icon + title: Text(device.name ?? ""), + subtitle: Text(device.address.toString()), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + rssi != null + ? Container( + margin: const EdgeInsets.all(8.0), + child: DefaultTextStyle( + style: _computeTextStyle(rssi), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(rssi.toString()), + const Text('dBm'), + ], + ), + ), + ) + : Container(width: 0, height: 0), + device.isConnected + ? const Icon(Icons.import_export) + : Container(width: 0, height: 0), + device.isBonded + ? const Icon(Icons.link) + : Container(width: 0, height: 0), + ], + ), + ); + + static TextStyle _computeTextStyle(int rssi) { + /**/ if (rssi >= -35) { + return TextStyle(color: Colors.greenAccent[700]); + } else if (rssi >= -45) { + return TextStyle( + color: Color.lerp( + Colors.greenAccent[700], Colors.lightGreen, -(rssi + 35) / 10)); + } else if (rssi >= -55) { + return TextStyle( + color: Color.lerp( + Colors.lightGreen, Colors.lime[600], -(rssi + 45) / 10)); + } else if (rssi >= -65) { + return TextStyle( + color: Color.lerp(Colors.lime[600], Colors.amber, -(rssi + 55) / 10)); + } else if (rssi >= -75) { + return TextStyle( + color: Color.lerp( + Colors.amber, Colors.deepOrangeAccent, -(rssi + 65) / 10)); + } else if (rssi >= -85) { + return TextStyle( + color: Color.lerp( + Colors.deepOrangeAccent, Colors.redAccent, -(rssi + 75) / 10)); + } else { + /*code symmetry*/ + return const TextStyle(color: Colors.redAccent); + } + } +} diff --git a/lib/modules/bluetooth/ChatPage.dart b/lib/modules/bluetooth/ChatPage.dart new file mode 100644 index 0000000..67af307 --- /dev/null +++ b/lib/modules/bluetooth/ChatPage.dart @@ -0,0 +1,239 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; + +class ChatPage extends StatefulWidget { + final BluetoothDevice server; + + const ChatPage({required this.server}); + + @override + _ChatPage createState() => new _ChatPage(); +} + +class _Message { + int whom; + String text; + + _Message(this.whom, this.text); +} + +class _ChatPage extends State { + static final clientID = 0; + BluetoothConnection? connection; + + List<_Message> messages = List<_Message>.empty(growable: true); + String _messageBuffer = ''; + + final TextEditingController textEditingController = + new TextEditingController(); + final ScrollController listScrollController = new ScrollController(); + + bool isConnecting = true; + bool get isConnected => (connection?.isConnected ?? false); + + bool isDisconnecting = false; + + @override + void initState() { + super.initState(); + + BluetoothConnection.toAddress(widget.server.address).then((_connection) { + print('Connected to the device'); + connection = _connection; + setState(() { + isConnecting = false; + isDisconnecting = false; + }); + + connection!.input!.listen(_onDataReceived).onDone(() { + // Example: Detect which side closed the connection + // There should be `isDisconnecting` flag to show are we are (locally) + // in middle of disconnecting process, should be set before calling + // `dispose`, `finish` or `close`, which all causes to disconnect. + // If we except the disconnection, `onDone` should be fired as result. + // If we didn't except this (no flag set), it means closing by remote. + if (isDisconnecting) { + print('Disconnecting locally!'); + } else { + print('Disconnected remotely!'); + } + if (this.mounted) { + setState(() {}); + } + }); + }).catchError((error) { + print('Cannot connect, exception occured'); + print(error); + }); + } + + @override + void dispose() { + // Avoid memory leak (`setState` after dispose) and disconnect + if (isConnected) { + isDisconnecting = true; + connection?.dispose(); + connection = null; + } + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final List list = messages.map((_message) { + return Row( + children: [ + Container( + child: Text( + (text) { + return text == '/shrug' ? '¯\\_(ツ)_/¯' : text; + }(_message.text.trim()), + style: TextStyle(color: Colors.white)), + padding: EdgeInsets.all(12.0), + margin: EdgeInsets.only(bottom: 8.0, left: 8.0, right: 8.0), + width: 222.0, + decoration: BoxDecoration( + color: + _message.whom == clientID ? Colors.blueAccent : Colors.grey, + borderRadius: BorderRadius.circular(7.0)), + ), + ], + mainAxisAlignment: _message.whom == clientID + ? MainAxisAlignment.end + : MainAxisAlignment.start, + ); + }).toList(); + + final serverName = widget.server.name ?? "Unknown"; + return Scaffold( + appBar: AppBar( + title: (isConnecting + ? Text('Connecting chat to ' + serverName + '...') + : isConnected + ? Text('Live chat with ' + serverName) + : Text('Chat log with ' + serverName))), + body: SafeArea( + child: Column( + children: [ + Flexible( + child: ListView( + padding: const EdgeInsets.all(12.0), + controller: listScrollController, + children: list), + ), + Row( + children: [ + Flexible( + child: Container( + margin: const EdgeInsets.only(left: 16.0), + child: TextField( + style: const TextStyle(fontSize: 15.0), + controller: textEditingController, + decoration: InputDecoration.collapsed( + hintText: isConnecting + ? 'Wait until connected...' + : isConnected + ? 'Type your message...' + : 'Chat got disconnected', + hintStyle: const TextStyle(color: Colors.grey), + ), + enabled: isConnected, + ), + ), + ), + Container( + margin: const EdgeInsets.all(8.0), + child: IconButton( + icon: const Icon(Icons.send), + onPressed: isConnected + ? () => _sendMessage(textEditingController.text) + : null), + ), + ], + ) + ], + ), + ), + ); + } + + void _onDataReceived(Uint8List data) { + // Allocate buffer for parsed data + int backspacesCounter = 0; + data.forEach((byte) { + if (byte == 8 || byte == 127) { + backspacesCounter++; + } + }); + Uint8List buffer = Uint8List(data.length - backspacesCounter); + int bufferIndex = buffer.length; + + // Apply backspace control character + backspacesCounter = 0; + for (int i = data.length - 1; i >= 0; i--) { + if (data[i] == 8 || data[i] == 127) { + backspacesCounter++; + } else { + if (backspacesCounter > 0) { + backspacesCounter--; + } else { + buffer[--bufferIndex] = data[i]; + } + } + } + + // Create message if there is new line character + String dataString = String.fromCharCodes(buffer); + int index = buffer.indexOf(13); + if (~index != 0) { + setState(() { + messages.add( + _Message( + 1, + backspacesCounter > 0 + ? _messageBuffer.substring( + 0, _messageBuffer.length - backspacesCounter) + : _messageBuffer + dataString.substring(0, index), + ), + ); + _messageBuffer = dataString.substring(index); + }); + } else { + _messageBuffer = (backspacesCounter > 0 + ? _messageBuffer.substring( + 0, _messageBuffer.length - backspacesCounter) + : _messageBuffer + dataString); + } + } + + void _sendMessage(String text) async { + text = text.trim(); + textEditingController.clear(); + + if (text.length > 0) { + try { + connection!.output.add(Uint8List.fromList(utf8.encode(text + "\r\n"))); + await connection!.output.allSent; + + setState(() { + messages.add(_Message(clientID, text)); + }); + + Future.delayed(Duration(milliseconds: 333)).then((_) { + listScrollController.animateTo( + listScrollController.position.maxScrollExtent, + duration: Duration(milliseconds: 333), + curve: Curves.easeOut); + }); + } catch (e) { + // Ignore error, but notify state + setState(() {}); + } + } + } +} diff --git a/lib/modules/bluetooth/DiscoveryPage.dart b/lib/modules/bluetooth/DiscoveryPage.dart new file mode 100644 index 0000000..200dc0d --- /dev/null +++ b/lib/modules/bluetooth/DiscoveryPage.dart @@ -0,0 +1,163 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; + +import './BluetoothDeviceListEntry.dart'; + +class DiscoveryPage extends StatefulWidget { + /// If true, discovery starts on page start, otherwise user must press action button. + final bool start; + + const DiscoveryPage({this.start = true}); + + @override + DiscoveryPageState createState() => DiscoveryPageState(); +} + +class DiscoveryPageState extends State { + StreamSubscription? _streamSubscription; + List results = + List.empty(growable: true); + bool isDiscovering = false; + + DiscoveryPageState(); + + @override + void initState() { + super.initState(); + + isDiscovering = widget.start; + if (isDiscovering) { + _startDiscovery(); + } + } + + void _restartDiscovery() { + setState(() { + results.clear(); + isDiscovering = true; + }); + + _startDiscovery(); + } + + void _startDiscovery() { + _streamSubscription = + FlutterBluetoothSerial.instance.startDiscovery().listen((r) { + setState(() { + final existingIndex = results.indexWhere( + (element) => element.device.address == r.device.address); + if (existingIndex >= 0) { + results[existingIndex] = r; + } else { + results.add(r); + } + }); + }); + + _streamSubscription!.onDone(() { + setState(() { + isDiscovering = false; + }); + }); + } + + // @TODO . One day there should be `_pairDevice` on long tap on something... ;) + + @override + void dispose() { + // Avoid memory leak (`setState` after dispose) and cancel discovery + _streamSubscription?.cancel(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: isDiscovering + ? const Text('Discovering devices') + : const Text('Discovered devices'), + actions: [ + isDiscovering + ? FittedBox( + child: Container( + margin: const EdgeInsets.all(16.0), + child: const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ), + ) + : IconButton( + icon: const Icon(Icons.replay), + onPressed: _restartDiscovery, + ) + ], + ), + body: ListView.builder( + itemCount: results.length, + itemBuilder: (BuildContext context, index) { + BluetoothDiscoveryResult result = results[index]; + final device = result.device; + final address = device.address; + return BluetoothDeviceListEntry( + device: device, + rssi: result.rssi, + onTap: () { + Navigator.of(context).pop(result.device); + }, + onLongPress: () async { + try { + bool bonded = false; + if (device.isBonded) { + print('Unbonding from ${device.address}...'); + await FlutterBluetoothSerial.instance + .removeDeviceBondWithAddress(address); + print('Unbonding from ${device.address} has succed'); + } else { + print('Bonding with ${device.address}...'); + bonded = (await FlutterBluetoothSerial.instance + .bondDeviceAtAddress(address))!; + print( + 'Bonding with ${device.address} has ${bonded ? 'succed' : 'failed'}.'); + } + setState(() { + results[results.indexOf(result)] = BluetoothDiscoveryResult( + device: BluetoothDevice( + name: device.name ?? '', + address: address, + type: device.type, + bondState: bonded + ? BluetoothBondState.bonded + : BluetoothBondState.none, + ), + rssi: result.rssi); + }); + } catch (ex) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Error occured while bonding'), + content: Text("${ex.toString()}"), + actions: [ + new TextButton( + child: const Text("Close"), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + }, + ); + }, + ), + ); + } +} diff --git a/lib/modules/bluetooth/MainPage.dart b/lib/modules/bluetooth/MainPage.dart new file mode 100644 index 0000000..6030d8f --- /dev/null +++ b/lib/modules/bluetooth/MainPage.dart @@ -0,0 +1,354 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; +import 'package:scoped_model/scoped_model.dart'; + +import './BackgroundCollectedPage.dart'; +import './BackgroundCollectingTask.dart'; +import './ChatPage.dart'; +import './DiscoveryPage.dart'; +import './SelectBondedDevicePage.dart'; + +// import './helpers/LineChart.dart'; + +class MainPage extends StatefulWidget { + @override + MainPageState createState() => MainPageState(); +} + +class MainPageState extends State { + BluetoothState _bluetoothState = BluetoothState.UNKNOWN; + + String _address = "..."; + String _name = "..."; + + Timer? _discoverableTimeoutTimer; + int _discoverableTimeoutSecondsLeft = 0; + + BackgroundCollectingTask? _collectingTask; + + bool _autoAcceptPairingRequests = false; + + @override + void initState() { + super.initState(); + + // Get current state + FlutterBluetoothSerial.instance.state.then((state) { + setState(() { + _bluetoothState = state; + }); + }); + + Future.doWhile(() async { + // Wait if adapter not enabled + if ((await FlutterBluetoothSerial.instance.isEnabled) ?? false) { + return false; + } + await Future.delayed(const Duration(milliseconds: 0xDD)); + return true; + }).then((_) { + // Update the address field + FlutterBluetoothSerial.instance.address.then((address) { + setState(() { + _address = address!; + }); + }); + }); + + FlutterBluetoothSerial.instance.name.then((name) { + setState(() { + _name = name!; + }); + }); + + // Listen for futher state changes + FlutterBluetoothSerial.instance + .onStateChanged() + .listen((BluetoothState state) { + setState(() { + _bluetoothState = state; + + // Discoverable mode is disabled when Bluetooth gets disabled + _discoverableTimeoutTimer = null; + _discoverableTimeoutSecondsLeft = 0; + }); + }); + } + + @override + void dispose() { + FlutterBluetoothSerial.instance.setPairingRequestHandler(null); + _collectingTask?.dispose(); + _discoverableTimeoutTimer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Flutter Bluetooth Serial'), + ), + body: ListView( + children: [ + const Divider(), + const ListTile(title: Text('General')), + SwitchListTile( + title: const Text('Enable Bluetooth'), + value: _bluetoothState.isEnabled, + onChanged: (bool value) { + // Do the request and update with the true value then + future() async { + // async lambda seems to not working + if (value) { + await FlutterBluetoothSerial.instance.requestEnable(); + } else { + await FlutterBluetoothSerial.instance.requestDisable(); + } + } + + future().then((_) { + setState(() {}); + }); + }, + ), + ListTile( + title: const Text('Bluetooth status'), + subtitle: Text(_bluetoothState.toString()), + trailing: ElevatedButton( + child: const Text('Settings'), + onPressed: () { + FlutterBluetoothSerial.instance.openSettings(); + }, + ), + ), + ListTile( + title: const Text('Local adapter address'), + subtitle: Text(_address), + ), + ListTile( + title: const Text('Local adapter name'), + subtitle: Text(_name), + onLongPress: null, + ), + ListTile( + title: _discoverableTimeoutSecondsLeft == 0 + ? const Text("Discoverable") + : Text("Discoverable for ${_discoverableTimeoutSecondsLeft}s"), + subtitle: const Text("PsychoX-Luna"), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Checkbox( + value: _discoverableTimeoutSecondsLeft != 0, + onChanged: null, + ), + const IconButton( + icon: Icon(Icons.edit), + onPressed: null, + ), + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () async { + print('Discoverable requested'); + final int timeout = (await FlutterBluetoothSerial.instance + .requestDiscoverable(60))!; + if (timeout < 0) { + print('Discoverable mode denied'); + } else { + print('Discoverable mode acquired for $timeout seconds'); + } + setState(() { + _discoverableTimeoutTimer?.cancel(); + _discoverableTimeoutSecondsLeft = timeout; + _discoverableTimeoutTimer = Timer.periodic( + const Duration(seconds: 1), (Timer timer) { + setState(() { + if (_discoverableTimeoutSecondsLeft < 0) { + FlutterBluetoothSerial.instance.isDiscoverable + .then((isDiscoverable) { + if (isDiscoverable ?? false) { + print( + "Discoverable after timeout... might be infinity timeout :F"); + _discoverableTimeoutSecondsLeft += 1; + } + }); + timer.cancel(); + _discoverableTimeoutSecondsLeft = 0; + } else { + _discoverableTimeoutSecondsLeft -= 1; + } + }); + }); + }); + }, + ) + ], + ), + ), + const Divider(), + const ListTile(title: Text('Devices discovery and connection')), + SwitchListTile( + title: const Text('Auto-try specific pin when pairing'), + subtitle: const Text('Pin 1234'), + value: _autoAcceptPairingRequests, + onChanged: (bool value) { + setState(() { + _autoAcceptPairingRequests = value; + }); + if (value) { + FlutterBluetoothSerial.instance.setPairingRequestHandler( + (BluetoothPairingRequest request) { + print("Trying to auto-pair with Pin 1234"); + if (request.pairingVariant == PairingVariant.Pin) { + return Future.value("1234"); + } + return Future.value(null); + }); + } else { + FlutterBluetoothSerial.instance.setPairingRequestHandler(null); + } + }, + ), + ListTile( + title: ElevatedButton( + child: const Text('Explore discovered devices'), + onPressed: () async { + final BluetoothDevice? selectedDevice = + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) { + return const DiscoveryPage(); + }, + ), + ); + + if (selectedDevice != null) { + print('Discovery -> selected ${selectedDevice.address}'); + } else { + print('Discovery -> no device selected'); + } + }), + ), + ListTile( + title: ElevatedButton( + child: const Text('Connect to paired device to chat'), + onPressed: () async { + final BluetoothDevice? selectedDevice = + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) { + return const SelectBondedDevicePage( + checkAvailability: false); + }, + ), + ); + + if (selectedDevice != null) { + print('Connect -> selected ${selectedDevice.address}'); + _startChat(context, selectedDevice); + } else { + print('Connect -> no device selected'); + } + }, + ), + ), + const Divider(), + const ListTile(title: Text('Multiple connections example')), + ListTile( + title: ElevatedButton( + child: ((_collectingTask?.inProgress ?? false) + ? const Text('Disconnect and stop background collecting') + : const Text('Connect to start background collecting')), + onPressed: () async { + if (_collectingTask?.inProgress ?? false) { + await _collectingTask!.cancel(); + setState(() { + /* Update for `_collectingTask.inProgress` */ + }); + } else { + final BluetoothDevice? selectedDevice = + await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) { + return const SelectBondedDevicePage( + checkAvailability: false); + }, + ), + ); + + if (selectedDevice != null) { + await _startBackgroundTask(context, selectedDevice); + setState(() { + /* Update for `_collectingTask.inProgress` */ + }); + } + } + }, + ), + ), + ListTile( + title: ElevatedButton( + onPressed: (_collectingTask != null) + ? () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) { + return ScopedModel( + model: _collectingTask!, + child: BackgroundCollectedPage(), + ); + }, + ), + ); + } + : null, + child: const Text('View background collected data'), + ), + ), + ], + ), + ); + } + + void _startChat(BuildContext context, BluetoothDevice server) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) { + return ChatPage(server: server); + }, + ), + ); + } + + Future _startBackgroundTask( + BuildContext context, + BluetoothDevice server, + ) async { + try { + _collectingTask = await BackgroundCollectingTask.connect(server); + await _collectingTask!.start(); + } catch (ex) { + _collectingTask?.cancel(); + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Error occured while connecting'), + content: Text("${ex.toString()}"), + actions: [ + new TextButton( + child: const Text("Close"), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } + } +} diff --git a/lib/modules/bluetooth/SelectBondedDevicePage.dart b/lib/modules/bluetooth/SelectBondedDevicePage.dart new file mode 100644 index 0000000..01a26fa --- /dev/null +++ b/lib/modules/bluetooth/SelectBondedDevicePage.dart @@ -0,0 +1,146 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart'; + +import './BluetoothDeviceListEntry.dart'; + +class SelectBondedDevicePage extends StatefulWidget { + /// If true, on page start there is performed discovery upon the bonded devices. + /// Then, if they are not avaliable, they would be disabled from the selection. + final bool checkAvailability; + + const SelectBondedDevicePage({this.checkAvailability = true}); + + @override + SelectBondedDevicePageState createState() => SelectBondedDevicePageState(); +} + +enum DeviceAvailability { + no, + maybe, + yes, +} + +class DeviceWithAvailability { + BluetoothDevice device; + DeviceAvailability availability; + int? rssi; + + DeviceWithAvailability(this.device, this.availability, [this.rssi]); +} + +class SelectBondedDevicePageState extends State { + List devices = + List.empty(growable: true); + + // Availability + StreamSubscription? _discoveryStreamSubscription; + bool _isDiscovering = false; + + SelectBondedDevicePageState(); + + @override + void initState() { + super.initState(); + + _isDiscovering = widget.checkAvailability; + + if (_isDiscovering) { + _startDiscovery(); + } + + // Setup a list of the bonded devices + FlutterBluetoothSerial.instance + .getBondedDevices() + .then((List bondedDevices) { + setState(() { + devices = bondedDevices + .map( + (device) => DeviceWithAvailability( + device, + widget.checkAvailability + ? DeviceAvailability.maybe + : DeviceAvailability.yes, + ), + ) + .toList(); + }); + }); + } + + void _restartDiscovery() { + setState(() { + _isDiscovering = true; + }); + + _startDiscovery(); + } + + void _startDiscovery() { + _discoveryStreamSubscription = + FlutterBluetoothSerial.instance.startDiscovery().listen((r) { + setState(() { + Iterator i = devices.iterator; + while (i.moveNext()) { + var device = i.current; + if (device.device == r.device) { + device.availability = DeviceAvailability.yes; + device.rssi = r.rssi; + } + } + }); + }); + + _discoveryStreamSubscription?.onDone(() { + setState(() { + _isDiscovering = false; + }); + }); + } + + @override + void dispose() { + // Avoid memory leak (`setState` after dispose) and cancel discovery + _discoveryStreamSubscription?.cancel(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + List list = devices + .map((device) => BluetoothDeviceListEntry( + device: device.device, + rssi: device.rssi, + enabled: device.availability == DeviceAvailability.yes, + onTap: () { + Navigator.of(context).pop(device.device); + }, + )) + .toList(); + return Scaffold( + appBar: AppBar( + title: const Text('Select device'), + actions: [ + _isDiscovering + ? FittedBox( + child: Container( + margin: const EdgeInsets.all(16.0), + child: const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation( + Colors.white, + ), + ), + ), + ) + : IconButton( + icon: const Icon(Icons.replay), + onPressed: _restartDiscovery, + ) + ], + ), + body: ListView(children: list), + ); + } +} diff --git a/lib/modules/bluetooth/bluetooth_screen.dart b/lib/modules/bluetooth/bluetooth_screen.dart new file mode 100644 index 0000000..89359fc --- /dev/null +++ b/lib/modules/bluetooth/bluetooth_screen.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +// import 'package:flutter_blue_plus/flutter_blue_plus.dart'; +// import 'package:zenith_monitor/modules/bluetooth/device_caracteristics_screen.dart'; +import './MainPage.dart'; + +class BluetoothScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + home: ExampleApplication(), + ); + } +} + +class ExampleApplication extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp(home: MainPage()); + } +} diff --git a/lib/modules/bluetooth/helpers/LineChart.dart b/lib/modules/bluetooth/helpers/LineChart.dart new file mode 100644 index 0000000..e29ba97 --- /dev/null +++ b/lib/modules/bluetooth/helpers/LineChart.dart @@ -0,0 +1,605 @@ +/// @name LineChart +/// @version 0.0.5 +/// @description Simple line chart widget +/// @author Patryk "PsychoX" Ludwikowski +/// @license MIT License (see https://mit-license.org/) +import 'dart:math' as math show min, max; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; + +import './PaintStyle.dart'; + +class LabelEntry { + final double value; + final String label; + + LabelEntry(this.value, this.label); +} + +/// Widget that allows to show data on line chart. +/// +/// All arguments, values and labels data should be sorted! +/// Since both the arguments and the values must be `double` type, +/// be aware of the precision. +class LineChart extends StatelessWidget { + /// Constraints for the line chart. + final BoxConstraints constraints; + + // @TODO ? Both `_LineChartPainter` and `LineChart` have most the same fields. + // `LineChart` is just mainly passing them to the painter. Shouldn't there be + // only one class containing these data? Some `LineChartData` forged inside here + // and then passed and used by the painter? :thinking: + + /// Padding around main drawng area. Necessary for displaying labels (around the chart). + final EdgeInsets padding; + + /* Arguments */ + /// Collection of doubles as arguments. + final Iterable arguments; + + /// Mappings of strings for doubles arguments, which allow to specify custom + /// strings as labels for certain arguments. + final Iterable argumentsLabels; + + /* Values */ + /// Collection of data series as collections of next values on corresponding arguments. + final Iterable> values; + + /// Mappings of string for doubles values, which allow to specify custom + /// string as labels for certain values. + final Iterable? valuesLabels; + + /* Labels & lines styles */ + /// Style of horizontal lines labels + final TextStyle? horizontalLabelsTextStyle; + + /// Style of vertical lines labels + final TextStyle? verticalLabelsTextStyle; + + /// Defines style of horizontal lines. Might be null in order to prevent lines from drawing. + final Paint? horizontalLinesPaint; + + /// Defines style of vertical lines. Might be null in order to prevent lines from drawing. + final Paint? verticalLinesPaint; + + // @TODO . expose it + final bool snapToLeftLabel = false; + final bool snapToTopLabel = true; + final bool snapToRightLabel = false; + final bool snapToBottomLabel = true; + + /* Series points & lines styles */ + /// List of paint styles for series values points. + /// + /// On whole list null would use predefined set of styles. + /// On list entry null there will be no points for certain series. + final List seriesPointsPaints; + + /// List of paint styles for lines between next series points. + /// + /// On null there will be no lines. + final List? seriesLinesPaints; + + final double additionalMinimalHorizontalLabelsInterval; + final double additionalMinimalVerticalLablesInterval; + + LineChart({ + required this.constraints, + this.padding = const EdgeInsets.fromLTRB(32, 12, 20, 28), + required this.arguments, + required this.argumentsLabels, + required this.values, + this.valuesLabels, + this.horizontalLabelsTextStyle, + this.verticalLabelsTextStyle, + PaintStyle horizontalLinesStyle = const PaintStyle(color: Colors.grey), + PaintStyle? verticalLinesStyle, // null for default + + this.additionalMinimalHorizontalLabelsInterval = 8, + this.additionalMinimalVerticalLablesInterval = 8, + Iterable? + seriesPointsStyles, // null would use predefined set of styles + Iterable? seriesLinesStyles, // null for default + }) : horizontalLinesPaint = horizontalLinesStyle.toPaint(), + verticalLinesPaint = verticalLinesStyle?.toPaint(), + seriesPointsPaints = _prepareSeriesPointsPaints(seriesPointsStyles), + seriesLinesPaints = _prepareSeriesLinesPaints(seriesLinesStyles) { + if ((seriesPointsStyles?.length ?? values.length) < values.length && + 12 /* default paints */ < values.length) { + throw "Too few `seriesPointsPaintStyle`s! Try define more or limit number of displayed series"; + } + if ((seriesLinesStyles?.length ?? values.length) < values.length) { + throw "Too few `seriesLinesStyles`s! Try define more or limit number of displayed series"; + } + } + + static List _prepareSeriesPointsPaints( + Iterable? seriesPointsStyles) { + if (seriesPointsStyles == null) { + // Default paint for points + return List.unmodifiable([ + PaintStyle(strokeWidth: 1.7, color: Colors.blue).toPaint(), + PaintStyle(strokeWidth: 1.7, color: Colors.red).toPaint(), + PaintStyle(strokeWidth: 1.7, color: Colors.yellow).toPaint(), + PaintStyle(strokeWidth: 1.7, color: Colors.green).toPaint(), + + PaintStyle(strokeWidth: 1.7, color: Colors.purple).toPaint(), + PaintStyle(strokeWidth: 1.7, color: Colors.deepOrange).toPaint(), + PaintStyle(strokeWidth: 1.7, color: Colors.brown).toPaint(), + PaintStyle(strokeWidth: 1.7, color: Colors.lime).toPaint(), + + PaintStyle(strokeWidth: 1.7, color: Colors.indigo).toPaint(), + PaintStyle(strokeWidth: 1.7, color: Colors.pink).toPaint(), + PaintStyle(strokeWidth: 1.7, color: Colors.amber).toPaint(), + PaintStyle(strokeWidth: 1.7, color: Colors.teal).toPaint(), + + // For more, user should specify them :F + ]); + } else { + return seriesPointsStyles.map((style) => style?.toPaint()).toList(); + } + } + + static List? _prepareSeriesLinesPaints( + Iterable? seriesLinesStyles) { + if (seriesLinesStyles == null) { + return null; + } else { + return seriesLinesStyles.map((style) => style.toPaint()).toList(); + } + } + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: this.constraints, + child: CustomPaint( + painter: _LineChartPainter( + padding: padding, + arguments: arguments, + argumentsLabels: argumentsLabels, + values: values, + valuesLabels: valuesLabels, + horizontalLabelsTextStyle: horizontalLabelsTextStyle ?? + Theme.of(context).textTheme.bodySmall, + verticalLabelsTextStyle: + verticalLabelsTextStyle ?? Theme.of(context).textTheme.bodySmall, + horizontalLinesPaint: horizontalLinesPaint, + verticalLinesPaint: verticalLinesPaint, + additionalMinimalHorizontalLabelsInterval: + additionalMinimalHorizontalLabelsInterval, + additionalMinimalVerticalLablesInterval: + additionalMinimalVerticalLablesInterval, + seriesPointsPaints: seriesPointsPaints, + seriesLinesPaints: seriesLinesPaints, + ))); + } +} + +class _LineChartPainter extends CustomPainter { + /// Padding around main drawng area. Necessary for displaying labels (around the chart). + final EdgeInsets padding; + + /* Arguments */ + /// Collection of doubles as arguments. + final Iterable arguments; + + /// Mappings of strings for doubles arguments, which allow to specify custom + /// strings as labels for certain arguments. + final Iterable? argumentsLabels; + + /* Values */ + /// Collection of data series as collections of next values on corresponding arguments. + final Iterable> values; + + /// Mappings of string for doubles values, which allow to specify custom + /// string as labels for certain values. + final Iterable? valuesLabels; + + /* Labels & lines styles */ + /// Style of horizontal lines labels + final TextStyle? horizontalLabelsTextStyle; + + /// Style of vertical lines labels + final TextStyle? verticalLabelsTextStyle; + + /// Defines style of horizontal lines. Might be null in order to prevent lines from drawing. + final Paint? horizontalLinesPaint; + + /// Defines style of vertical lines. Might be null in order to prevent lines from drawing. + final Paint? verticalLinesPaint; + + // @TODO . expose it + final bool snapToLeftLabel = false; + final bool snapToTopLabel = true; + final bool snapToRightLabel = false; + final bool snapToBottomLabel = true; + + /* Series points & lines styles */ + /// Collection of paint styles for series values points. + /// + /// On whole argument null would use predefined set of styles. + /// On collection entry null there will be no points for certain series. + final Iterable seriesPointsPaints; + + /// Collection of paint styles for lines between next series points. + /// + /// On null there will be no lines. + final Iterable? seriesLinesPaints; + + /* Runtime */ + /// Minimal allowed interval between horizontal lines. Calculated from font size. + final double minimalHorizontalLabelsInterval; + + /// Maximal value of all data series values + double maxValue = -double.maxFinite; + + /// Minimal value of all data series values + double minValue = double.maxFinite; + + double _minimalHorizontalRatio = 0; + double _minimalVerticalRatio = 0; + + /// Creates `_LineChartPainter` (`CustomPainter`) with given data and styling. + _LineChartPainter({ + this.padding = const EdgeInsets.fromLTRB(40, 8, 8, 32), + required this.arguments, + required this.argumentsLabels, + required this.values, + required this.valuesLabels, + required this.horizontalLabelsTextStyle, + required this.verticalLabelsTextStyle, + required this.horizontalLinesPaint, + required this.verticalLinesPaint, + double additionalMinimalHorizontalLabelsInterval = 8, + double additionalMinimalVerticalLablesInterval = 8, + required this.seriesPointsPaints, + required this.seriesLinesPaints, + }) : this.minimalHorizontalLabelsInterval = + (horizontalLabelsTextStyle?.fontSize ?? 12) + + additionalMinimalHorizontalLabelsInterval { + // Find max & min values of data to be show + for (Iterable series in values) { + for (double value in series) { + if (value > maxValue) { + maxValue = value; + } else if (value < minValue) { + minValue = value; + } + } + } + + if (valuesLabels != null) { + // Find minimal vertical ratio to fit all provided values labels + Iterator entry = valuesLabels!.iterator; + entry.moveNext(); + double lastValue = entry.current.value; + + while (entry.moveNext()) { + final double goodRatio = + minimalHorizontalLabelsInterval / (entry.current.value - lastValue); + if (goodRatio > _minimalVerticalRatio) { + _minimalVerticalRatio = goodRatio; + } + + lastValue = entry.current.value; + } + } + + if (argumentsLabels != null) { + // Find minimal horizontal ratio to fit all provided arguments labels + Iterator entry = argumentsLabels!.iterator; + entry.moveNext(); + double lastValue = entry.current.value; + double lastWidth = + _getLabelTextPainter(entry.current.label, verticalLabelsTextStyle) + .width; + + while (entry.moveNext()) { + final double nextValue = entry.current.value; + final double nextWidth = + _getLabelTextPainter(entry.current.label, verticalLabelsTextStyle) + .width; + + final double goodRatio = ((lastWidth + nextWidth) / 2 + + additionalMinimalVerticalLablesInterval) / + (nextValue - lastValue); + if (goodRatio > _minimalHorizontalRatio) { + _minimalHorizontalRatio = goodRatio; + } + + lastValue = nextValue; + lastWidth = nextWidth; + } + } + } + + @override + void paint(Canvas canvas, Size size) { + final double width = size.width - padding.left - padding.right; + final double height = size.height - padding.top - padding.bottom; + + /* Horizontal lines with labels */ + double valuesOffset = 0; // @TODO ? could be used in future for scrolling + double verticalRatio; + + { + Iterable labels; + + // If no labels provided - generate them! + if (valuesLabels == null) { + final double optimalStepValue = + _calculateOptimalStepValue(maxValue - minValue, height); + int stepsNumber = 1; + + // Find bottom line value + double bottomValue = 0; + if (minValue > 0) { + while (bottomValue < minValue) { + bottomValue += optimalStepValue; + } + bottomValue -= optimalStepValue; + } else { + while (bottomValue > minValue) { + bottomValue -= optimalStepValue; + } + } + valuesOffset = bottomValue; + + // Find top line value + double topValue = bottomValue; + while (topValue < maxValue) { + topValue += optimalStepValue; + stepsNumber += 1; + } + + // Set labels iterable from prepared generator + Iterable generator(double optimalStepValue, int stepsNumber, + [double value = 0.0]) sync* { + //double value = _bottomValue; + for (int i = 0; i < stepsNumber; i++) { + yield LabelEntry( + value, + value + .toString()); // @TODO , choose better precision based on optimal step value while parsing to string + value += optimalStepValue; + } + } + + labels = generator(optimalStepValue, stepsNumber, bottomValue); + + if (!snapToTopLabel) { + topValue = maxValue; + } + if (!snapToBottomLabel) { + bottomValue = valuesOffset = minValue; + } + + // Calculate vertical ratio of pixels per value + // Note: There is no empty space already + verticalRatio = height / (topValue - bottomValue); + } + // If labels provided - use them + else { + // Set labels iterable as the provided list + labels = valuesLabels!; + + // Use minimal visible value as offset. + // Note: `minValue` is calculated in constructor and includes miniaml labels values. + valuesOffset = minValue; + + // Calculate vertical ratio of pixels per value + // Note: `_minimalVerticalRatio` is calculated in constructor + final double topValue = + snapToTopLabel ? math.max(maxValue, labels.last.value) : maxValue; + final double bottomValue = snapToBottomLabel + ? math.min(minValue, labels.first.value) + : minValue; + final double noEmptySpaceRatio = height / (topValue - bottomValue); + verticalRatio = math.max(_minimalVerticalRatio, noEmptySpaceRatio); + } + + // Draw the horizontal lines and labels + for (LabelEntry tuple in labels) { + if (tuple.value < valuesOffset) continue; + final double yOffset = (size.height - + padding.bottom - + (tuple.value - valuesOffset) * verticalRatio); + if (yOffset < padding.top) break; + + // Draw line + if (horizontalLinesPaint != null) { + canvas.drawLine( + Offset(padding.left, yOffset), + Offset(size.width - padding.right, yOffset), + horizontalLinesPaint!); + } + + // Draw label + TextPainter( + text: TextSpan(text: tuple.label, style: horizontalLabelsTextStyle), + textAlign: TextAlign.right, + textDirection: TextDirection.ltr) + ..layout(minWidth: padding.left - 4) + ..paint( + canvas, + Offset( + 0, + yOffset - + (horizontalLabelsTextStyle?.fontSize ?? 12) / 2 - + 1)); + } + } + + /* Vertical lines with labels */ + double argumentsOffset = 0; + final double xOffsetLimit = size.width - padding.right; + double horizontalRatio; + + { + Iterable labels; + + // If no labels provided - generate them! + if (argumentsLabels == null) { + throw "not implemented"; + // @TODO . after few hot days of thinking about the problem for 1-2 hour a day, I just gave up. + // The hardest in the problem is that there must be trade-off between space for labels and max lines, + // but keep in mind that the label values should be in some human-readable steps (0.5, 10, 0.02...). + } + // If labels provided - use them + else { + // Set labels iterable as the provided list + labels = argumentsLabels!; + + // Use first visible argument as arguments offset + argumentsOffset = labels.first.value; + + if (!snapToLeftLabel) { + argumentsOffset = arguments.first; + } + + // Calculate vertical ratio of pixels per value + // Note: `_minimalHorizontalRatio` is calculated in constructor + final double leftMost = snapToLeftLabel + ? math.min(arguments.first, labels.first.value) + : arguments.first; + final double rightMost = snapToRightLabel + ? math.max(arguments.last, labels.last.value) + : arguments.last; + final double noEmptySpaceRatio = width / (rightMost - leftMost); + horizontalRatio = math.max(_minimalHorizontalRatio, noEmptySpaceRatio); + } + + // Draw the vertical lines and labels + for (LabelEntry tuple in labels) { + if (tuple.value < argumentsOffset) continue; + final double xOffset = + padding.left + (tuple.value - argumentsOffset) * horizontalRatio; + if (xOffset > xOffsetLimit) break; + + // Draw line + if (verticalLinesPaint != null) { + canvas.drawLine( + Offset(xOffset, padding.top), + Offset(xOffset, size.height - padding.bottom), + verticalLinesPaint!); + } + + // Draw label + final TextPainter textPainter = TextPainter( + text: TextSpan(text: tuple.label, style: verticalLabelsTextStyle), + textDirection: TextDirection.ltr) + ..layout(); + textPainter.paint( + canvas, + Offset(xOffset - textPainter.width / 2, + size.height - (verticalLabelsTextStyle?.fontSize ?? 12) - 8)); + } + } + + /* Points and lines between subsequent */ + Iterator> series = values.iterator; + Iterator linesPaints = seriesLinesPaints == null + ? [].iterator + : (seriesLinesPaints ?? []).iterator; + Iterator pointsPaints = seriesPointsPaints.iterator; + while (series.moveNext()) { + List points = []; + Iterator value = series.current.iterator; + Iterator argument = arguments.iterator; + while (value.moveNext()) { + argument.moveNext(); + if (value.current == null || value.current == double.nan) continue; + + if (argument.current < argumentsOffset) continue; + final double xOffset = padding.left + + (argument.current - argumentsOffset) * horizontalRatio; + if (xOffset > xOffsetLimit) break; + + if (value.current! < valuesOffset) continue; + final double yOffset = size.height - + padding.bottom - + (value.current! - valuesOffset) * verticalRatio; + if (yOffset < padding.top) continue; + + points.add(Offset(xOffset, yOffset)); + } + + // Lines + if (linesPaints.moveNext() && linesPaints.current != null) { + canvas.drawPath( + Path()..addPolygon(points, false), linesPaints.current!); + } + + // Points + if (pointsPaints.moveNext() && pointsPaints.current != null) { + canvas.drawPoints(ui.PointMode.points, points, pointsPaints.current!); + } + } + } + + @override + bool shouldRepaint(_LineChartPainter old) => + (this.arguments != old.arguments || + this.values != old.values || + this.argumentsLabels != old.argumentsLabels || + this.valuesLabels != old.valuesLabels || + this.seriesPointsPaints != old.seriesPointsPaints || + this.seriesLinesPaints != old.seriesLinesPaints || + this.horizontalLabelsTextStyle != old.horizontalLabelsTextStyle || + this.verticalLabelsTextStyle != old.verticalLabelsTextStyle || + this.padding != old.padding // + ); + + // ..., 0.01, 0.02, 0.05, 0.1, [0.125], 0.2, [0.25], 0.5, 1, 2, 5, 10, 20, 50, 100, 200, 500, ... + double _calculateOptimalStepValue(double valueRange, double height) { + final int maxSteps = height ~/ minimalHorizontalLabelsInterval; + if (maxSteps <= 0) { + throw "invalid max lines!"; + } + double interval = valueRange / maxSteps; + if (interval > 1) { + int zeros = 0; + while (interval >= 10) { + interval = interval / 10; + zeros += 1; + } + /**/ if (interval <= 1) { + interval = 1; + } else if (interval <= 2) { + interval = 2; + } else if (interval <= 5) { + interval = 5; + } + for (; zeros-- != 0;) { + interval *= 10; + } + } else { + // @TODO ! not working at all for lower :C + int zeros = 0; + while (interval < 0) { + interval = interval * 10; + zeros += 1; + } + /**/ if (interval <= 1) { + interval = 1; + } else if (interval <= 2) { + interval = 2; + } else if (interval <= 5) { + interval = 5; + } + for (; zeros-- != 0;) { + interval /= 10; + } + } + return interval; + } + + TextPainter _getLabelTextPainter(String text, TextStyle? style) { + return TextPainter( + text: TextSpan(text: text, style: style), + textDirection: TextDirection.ltr) + ..layout(); + } +} diff --git a/lib/modules/bluetooth/helpers/PaintStyle.dart b/lib/modules/bluetooth/helpers/PaintStyle.dart new file mode 100644 index 0000000..6479cb7 --- /dev/null +++ b/lib/modules/bluetooth/helpers/PaintStyle.dart @@ -0,0 +1,263 @@ +import 'dart:ui'; + +/// A description of the style to use when drawing on a [Canvas]. +/// +/// Most APIs on [Canvas] take a [Paint] object to describe the style +/// to use for that operation. [PaintStyle] allows to be const +/// constructed and later in runtime forged into the [Paint] object. +class PaintStyle { + /// Whether to apply anti-aliasing to lines and images drawn on the + /// canvas. + /// + /// Defaults to true. + final bool isAntiAlias; + + // Must be kept in sync with the default in paint.cc. + static const int _kColorDefault = 0xFF000000; + + /// The color to use when stroking or filling a shape. + /// + /// Defaults to opaque black. + /// + /// See also: + /// + /// * [style], which controls whether to stroke or fill (or both). + /// * [colorFilter], which overrides [color]. + /// * [shader], which overrides [color] with more elaborate effects. + /// + /// This color is not used when compositing. To colorize a layer, use + /// [colorFilter]. + final Color? color; + + // Must be kept in sync with the default in paint.cc. + static final int _kBlendModeDefault = BlendMode.srcOver.index; + + /// A blend mode to apply when a shape is drawn or a layer is composited. + /// + /// The source colors are from the shape being drawn (e.g. from + /// [Canvas.drawPath]) or layer being composited (the graphics that were drawn + /// between the [Canvas.saveLayer] and [Canvas.restore] calls), after applying + /// the [colorFilter], if any. + /// + /// The destination colors are from the background onto which the shape or + /// layer is being composited. + /// + /// Defaults to [BlendMode.srcOver]. + /// + /// See also: + /// + /// * [Canvas.saveLayer], which uses its [Paint]'s [blendMode] to composite + /// the layer when [restore] is called. + /// * [BlendMode], which discusses the user of [saveLayer] with [blendMode]. + final BlendMode blendMode; + + /// Whether to paint inside shapes, the edges of shapes, or both. + /// + /// Defaults to [PaintingStyle.fill]. + final PaintingStyle style; + + /// How wide to make edges drawn when [style] is set to + /// [PaintingStyle.stroke]. The width is given in logical pixels measured in + /// the direction orthogonal to the direction of the path. + /// + /// Defaults to 0.0, which correspond to a hairline width. + final double strokeWidth; + + /// The kind of finish to place on the end of lines drawn when + /// [style] is set to [PaintingStyle.stroke]. + /// + /// Defaults to [StrokeCap.butt], i.e. no caps. + final StrokeCap strokeCap; + + /// The kind of finish to place on the joins between segments. + /// + /// This applies to paths drawn when [style] is set to [PaintingStyle.stroke], + /// It does not apply to points drawn as lines with [Canvas.drawPoints]. + /// + /// Defaults to [StrokeJoin.miter], i.e. sharp corners. + /// + /// Some examples of joins: + /// + /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/miter_4_join.mp4} + /// + /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/round_join.mp4} + /// + /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/bevel_join.mp4} + /// + /// The centers of the line segments are colored in the diagrams above to + /// highlight the joins, but in normal usage the join is the same color as the + /// line. + /// + /// See also: + /// + /// * [strokeMiterLimit] to control when miters are replaced by bevels when + /// this is set to [StrokeJoin.miter]. + /// * [strokeCap] to control what is drawn at the ends of the stroke. + /// * [StrokeJoin] for the definitive list of stroke joins. + final StrokeJoin strokeJoin; + + // Must be kept in sync with the default in paint.cc. + static const double _kStrokeMiterLimitDefault = 4.0; + + /// The limit for miters to be drawn on segments when the join is set to + /// [StrokeJoin.miter] and the [style] is set to [PaintingStyle.stroke]. If + /// this limit is exceeded, then a [StrokeJoin.bevel] join will be drawn + /// instead. This may cause some 'popping' of the corners of a path if the + /// angle between line segments is animated, as seen in the diagrams below. + /// + /// This limit is expressed as a limit on the length of the miter. + /// + /// Defaults to 4.0. Using zero as a limit will cause a [StrokeJoin.bevel] + /// join to be used all the time. + /// + /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/miter_0_join.mp4} + /// + /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/miter_4_join.mp4} + /// + /// {@animation 300 300 https://flutter.github.io/assets-for-api-docs/assets/dart-ui/miter_6_join.mp4} + /// + /// The centers of the line segments are colored in the diagrams above to + /// highlight the joins, but in normal usage the join is the same color as the + /// line. + /// + /// See also: + /// + /// * [strokeJoin] to control the kind of finish to place on the joins + /// between segments. + /// * [strokeCap] to control what is drawn at the ends of the stroke. + final double strokeMiterLimit; + + /// A mask filter (for example, a blur) to apply to a shape after it has been + /// drawn but before it has been composited into the image. + /// + /// See [MaskFilter] for details. + final MaskFilter? maskFilter; + + /// Controls the performance vs quality trade-off to use when applying + /// filters, such as [maskFilter], or when drawing images, as with + /// [Canvas.drawImageRect] or [Canvas.drawImageNine]. + /// + /// Defaults to [FilterQuality.none]. + // TODO(ianh): verify that the image drawing methods actually respect this + final FilterQuality filterQuality; + + /// The shader to use when stroking or filling a shape. + /// + /// When this is null, the [color] is used instead. + /// + /// See also: + /// + /// * [Gradient], a shader that paints a color gradient. + /// * [ImageShader], a shader that tiles an [Image]. + /// * [colorFilter], which overrides [shader]. + /// * [color], which is used if [shader] and [colorFilter] are null. + final Shader? shader; + + /// A color filter to apply when a shape is drawn or when a layer is + /// composited. + /// + /// See [ColorFilter] for details. + /// + /// When a shape is being drawn, [colorFilter] overrides [color] and [shader]. + final ColorFilter? colorFilter; + + /// Whether the colors of the image are inverted when drawn. + /// + /// Inverting the colors of an image applies a new color filter that will + /// be composed with any user provided color filters. This is primarily + /// used for implementing smart invert on iOS. + final bool invertColors; + + const PaintStyle({ + this.isAntiAlias = true, + this.color = const Color(_kColorDefault), + this.blendMode = BlendMode.srcOver, + this.style = PaintingStyle.fill, + this.strokeWidth = 0.0, + this.strokeCap = StrokeCap.butt, + this.strokeJoin = StrokeJoin.miter, + this.strokeMiterLimit = 4.0, + this.maskFilter, // null + this.filterQuality = FilterQuality.none, + this.shader, // null + this.colorFilter, // null + this.invertColors = false, + }); + + @override + String toString() { + final StringBuffer result = StringBuffer(); + String semicolon = ''; + result.write('PaintStyle('); + if (style == PaintingStyle.stroke) { + result.write('$style'); + if (strokeWidth != 0.0) + result.write(' ${strokeWidth.toStringAsFixed(1)}'); + else + result.write(' hairline'); + if (strokeCap != StrokeCap.butt) result.write(' $strokeCap'); + if (strokeJoin == StrokeJoin.miter) { + if (strokeMiterLimit != _kStrokeMiterLimitDefault) + result.write( + ' $strokeJoin up to ${strokeMiterLimit.toStringAsFixed(1)}'); + } else { + result.write(' $strokeJoin'); + } + semicolon = '; '; + } + if (isAntiAlias != true) { + result.write('${semicolon}antialias off'); + semicolon = '; '; + } + if (color != const Color(_kColorDefault)) { + if (color != null) + result.write('$semicolon$color'); + else + result.write('${semicolon}no color'); + semicolon = '; '; + } + if (blendMode.index != _kBlendModeDefault) { + result.write('$semicolon$blendMode'); + semicolon = '; '; + } + if (colorFilter != null) { + result.write('${semicolon}colorFilter: $colorFilter'); + semicolon = '; '; + } + if (maskFilter != null) { + result.write('${semicolon}maskFilter: $maskFilter'); + semicolon = '; '; + } + if (filterQuality != FilterQuality.none) { + result.write('${semicolon}filterQuality: $filterQuality'); + semicolon = '; '; + } + if (shader != null) { + result.write('${semicolon}shader: $shader'); + semicolon = '; '; + } + if (invertColors) result.write('${semicolon}invert: $invertColors'); + result.write(')'); + return result.toString(); + } + + Paint toPaint() { + Paint paint = Paint(); + if (this.isAntiAlias != true) paint.isAntiAlias = this.isAntiAlias; + if (this.color != const Color(_kColorDefault)) paint.color = this.color!; + if (this.blendMode != BlendMode.srcOver) paint.blendMode = this.blendMode; + if (this.style != PaintingStyle.fill) paint.style = this.style; + if (this.strokeWidth != 0.0) paint.strokeWidth = this.strokeWidth; + if (this.strokeCap != StrokeCap.butt) paint.strokeCap = this.strokeCap; + if (this.strokeJoin != StrokeJoin.miter) paint.strokeJoin = this.strokeJoin; + if (this.strokeMiterLimit != 4.0) + paint.strokeMiterLimit = this.strokeMiterLimit; + if (this.maskFilter != null) paint.maskFilter = this.maskFilter; + if (this.filterQuality != FilterQuality.none) + paint.filterQuality = this.filterQuality; + if (this.shader != null) paint.shader = this.shader; + if (this.colorFilter != null) paint.colorFilter = this.colorFilter; + if (this.invertColors != false) paint.invertColors = this.invertColors; + return paint; + } +} diff --git a/lib/modules/home_page/screen/home_screen.dart b/lib/modules/home_page/screen/home_screen.dart index 8262465..ef52383 100644 --- a/lib/modules/home_page/screen/home_screen.dart +++ b/lib/modules/home_page/screen/home_screen.dart @@ -1,15 +1,15 @@ import 'package:flutter/material.dart'; +import 'package:zenith_monitor/modules/bluetooth/bluetooth_screen.dart'; class HomeScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Home Screen'), - ), - body: const Center( - child: Text('Welcome to the Home Screen!'), + return MaterialApp( + title: 'Flutter Bluetooth App', + theme: ThemeData( + primarySwatch: Colors.blue, ), + home: BluetoothScreen(), // Defina a tela inicial como BluetoothScreen ); } } \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index d572dff..0c78173 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: b1595874fbc8f7a50da90f5d8f327bb0bfd6a95dc906c390efe991540c3b54aa + sha256: "9371d13b8ee442e3bfc08a24e3a1b3742c839abbfaf5eef11b79c4b862c89bf7" url: "https://pub.dev" source: hosted - version: "1.3.40" + version: "1.3.41" args: dependency: transitive description: @@ -61,26 +61,26 @@ packages: dependency: "direct main" description: name: cloud_firestore - sha256: "240c1c3598e62ad58ee665b6df9c65172d2fbe4742770c21ed060e013cb8e037" + sha256: "77ac40d66a0fd585ca1e80ef3cbfd9df34462103bd4c1cf5950cb7d1f4c2e188" url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.3.0" cloud_firestore_platform_interface: dependency: "direct main" description: name: cloud_firestore_platform_interface - sha256: "5b5a9c2b5a85bf995f12e7447c4197d7ad659533d642d3d904ccbb509f83d62a" + sha256: da0fa07284bc845b4a517449365eac0346a084676b3ed399f75a0ed25c3ec4c8 url: "https://pub.dev" source: hosted - version: "6.2.9" + version: "6.3.2" cloud_firestore_web: dependency: transitive description: name: cloud_firestore_web - sha256: "898e9f65548df65ca7b2cff9f32cf233424ceccff1d870b8819ea2b8050d5b39" + sha256: "98dca6f864141cd3f79cf42a036f45233b8e2a3940b0011e6a370dd2283c45db" url: "https://pub.dev" source: hosted - version: "4.0.3" + version: "4.1.2" collection: dependency: transitive description: @@ -93,26 +93,26 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: db7a4e143dc72cc3cb2044ef9b052a7ebfe729513e6a82943bc3526f784365b8 + sha256: "2056db5241f96cdc0126bd94459fc4cdc13876753768fc7a31c425e50a7177d0" url: "https://pub.dev" source: hosted - version: "6.0.3" + version: "6.0.5" connectivity_plus_platform_interface: dependency: transitive description: name: connectivity_plus_platform_interface - sha256: b6a56efe1e6675be240de39107281d4034b64ac23438026355b4234042a35adb + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" cross_file: dependency: transitive description: name: cross_file - sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "0.3.4+1" + version: "0.3.4+2" csslib: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: facebook_auth_desktop - sha256: "0e4f147a57de8fdb8eaaee4836e6b9859482921143af0c350ffbf2a9bbd531a0" + sha256: "223e04414e078271682dadd2a694da1f4d329634137414b30217b2f478457fdb" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" fake_async: dependency: transitive description: @@ -165,10 +165,10 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" file_selector_linux: dependency: transitive description: @@ -197,42 +197,42 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69" url: "https://pub.dev" source: hosted - version: "0.9.3+1" + version: "0.9.3+2" firebase_auth: dependency: "direct main" description: name: firebase_auth - sha256: "2457ac6cbc152fa464aad3fb35f98039b0c4ab8e9bedf476672508b291bdbc3a" + sha256: "6f5792bdc208416bfdfbfe3363b78ce01667b6ebc4c5cb47cfa891f2fca45ab7" url: "https://pub.dev" source: hosted - version: "5.1.4" + version: "5.2.0" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface - sha256: "0408e2ed74b1afa0490a93aa041fe90d7573af7ffc59a641edc6c5b5c1b8d2a4" + sha256: "80237bb8a92bb0a5e3b40de1c8dbc80254e49ac9e3907b4b47b8e95ac3dd3fad" url: "https://pub.dev" source: hosted - version: "7.4.3" + version: "7.4.4" firebase_auth_web: dependency: transitive description: name: firebase_auth_web - sha256: "7e0c6d0fa8c5c1b2ae126a78f2d1a206a77a913f78d20f155487bf746162dccc" + sha256: "9d315491a6be65ea83511cb0e078544a309c39dd54c0ee355c51dbd6d8c03cc8" url: "https://pub.dev" source: hosted - version: "5.12.5" + version: "5.12.6" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "3187f4f8e49968573fd7403011dca67ba95aae419bc0d8131500fae160d94f92" + sha256: "06537da27db981947fa535bb91ca120b4e9cb59cb87278dbdde718558cafc9ff" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "3.4.0" firebase_core_platform_interface: dependency: "direct overridden" description: @@ -245,34 +245,34 @@ packages: dependency: transitive description: name: firebase_core_web - sha256: e8d1e22de72cb21cdcfc5eed7acddab3e99cd83f3b317f54f7a96c32f25fd11e + sha256: "362e52457ed2b7b180964769c1e04d1e0ea0259fdf7025fdfedd019d4ae2bd88" url: "https://pub.dev" source: hosted - version: "2.17.4" + version: "2.17.5" firebase_storage: dependency: "direct main" description: name: firebase_storage - sha256: "472e8532dfdcffd00ed8cc1ff57fda76af6f0a8a26547908bc20f25079922408" + sha256: dfc06d783dbc0b6200a4b936d8cdbd826bd1571c959854d14a70259188d96e85 url: "https://pub.dev" source: hosted - version: "12.1.1" + version: "12.2.0" firebase_storage_platform_interface: dependency: transitive description: name: firebase_storage_platform_interface - sha256: f07b5dbf0efcaab0f7d38910851791f7509b8864d491bb11c079f67339f31b27 + sha256: "3da511301b77514dee5370281923fbbc6d5725c2a0b96004c5c45415e067f234" url: "https://pub.dev" source: hosted - version: "5.1.26" + version: "5.1.28" firebase_storage_web: dependency: transitive description: name: firebase_storage_web - sha256: ef9d27afcd68a1c2b90d0cad2b02cf6333a053655988f4a2f6cba2796134cae5 + sha256: "7ad67b1c1c46c995a6bd4f225d240fc9a5fb277fade583631ae38750ffd9be17" url: "https://pub.dev" source: hosted - version: "3.9.11" + version: "3.9.13" flutter: dependency: "direct main" description: flutter @@ -286,6 +286,22 @@ packages: url: "https://pub.dev" source: hosted version: "8.1.6" + flutter_blue_plus: + dependency: "direct main" + description: + name: flutter_blue_plus + sha256: "55d37e9339765fef9439f3759a2bddaf9c8db1f90d46ba5a5d834fb28e0ab809" + url: "https://pub.dev" + source: hosted + version: "1.32.12" + flutter_bluetooth_serial: + dependency: "direct main" + description: + name: flutter_bluetooth_serial + sha256: "85ae82c4099b2b1facdc54e75e1bcfa88dc7f719e55dc886bb0b648cb16636b1" + url: "https://pub.dev" + source: hosted + version: "0.4.0" flutter_dotenv: dependency: "direct main" description: @@ -298,26 +314,26 @@ packages: dependency: "direct main" description: name: flutter_facebook_auth - sha256: "48aab06f3b1d3494a33b0c10c11ecbc226fdcf0bf37b478e91d9de57cce840f5" + sha256: faa92fb7626c230837f30d02de7d92b6af334b77ba744867c63545d4ad7e171f url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.1.0" flutter_facebook_auth_platform_interface: dependency: transitive description: name: flutter_facebook_auth_platform_interface - sha256: dc9d621dd45c4f0b341173a16e94f4b77155fa9c0f4326743f1251f2f445ba38 + sha256: a416f89de881ba98188942060aecf17b434b06677d4557731f19e859343df54c url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.1.0" flutter_facebook_auth_web: dependency: transitive description: name: flutter_facebook_auth_web - sha256: "947d93fc5a7cc5db1ce0274505254bb3b619cdd98176954f125f742964696804" + sha256: "8a6890a98522604169ca9f958d7189c9f579dbf05ba4d3d7adf26eca4c4c4c93" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.1.1" flutter_lints: dependency: "direct dev" description: @@ -330,10 +346,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e + sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda" url: "https://pub.dev" source: hosted - version: "2.0.20" + version: "2.0.22" flutter_polyline_points: dependency: "direct main" description: @@ -412,58 +428,58 @@ packages: dependency: transitive description: name: google_identity_services_web - sha256: "9482364c9f8b7bd36902572ebc3a7c2b5c8ee57a9c93e6eb5099c1a9ec5265d8" + sha256: "5be191523702ba8d7a01ca97c17fca096822ccf246b0a9f11923a6ded06199b6" url: "https://pub.dev" source: hosted - version: "0.3.1+1" + version: "0.3.1+4" google_maps: dependency: transitive description: name: google_maps - sha256: "47eef3836b49bb030d5cb3afc60b8451408bf34cf753e571b645d6529eb4251a" + sha256: "463b38e5a92a05cde41220a11fd5eef3847031fef3e8cf295ac76ec453246907" url: "https://pub.dev" source: hosted - version: "7.1.0" + version: "8.0.0" google_maps_flutter: dependency: "direct main" description: name: google_maps_flutter - sha256: acf0ec482d86b2ac55ade80597ce7f797a47971f5210ebfd030f0d58130e0a94 + sha256: "2e302fa3aaf4e2a297f0342d83ebc5e8e9f826e9a716aef473fe7f404ec630a7" url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.9.0" google_maps_flutter_android: dependency: transitive description: name: google_maps_flutter_android - sha256: f6306d83edddba7aa017ca6f547d6f36a1443f90ed49d91d48ef70d7aa86e2e1 + sha256: "60a005bf1ba8d178144e442f6e2d734b0ffc2cc800a05415388472f934ad6d6a" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.14.4" google_maps_flutter_ios: dependency: transitive description: name: google_maps_flutter_ios - sha256: a6e3c6ecdda6c985053f944be13a0645ebb919da2ef0f5bc579c5e1670a5b2a8 + sha256: "3a484846fc56f15e47e3de1f5ea80a7ff2b31721d2faa88f390f3b3cf580c953" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.13.0" google_maps_flutter_platform_interface: dependency: transitive description: name: google_maps_flutter_platform_interface - sha256: bd60ca330e3c7763b95b477054adec338a522d982af73ecc520b232474063ac5 + sha256: "4f6930fd668bf5d40feb2695d5695dbc0c35e5542b557a34ad35be491686d2ba" url: "https://pub.dev" source: hosted - version: "2.8.0" + version: "2.9.0" google_maps_flutter_web: dependency: transitive description: name: google_maps_flutter_web - sha256: f3155c12119d8a5c2732fdf39ceb5cc095bc662059a03b4ea23294ecebe1d199 + sha256: ff39211bd25d7fad125d19f757eba85bd154460907cd4d135e07e3d0f98a4130 url: "https://pub.dev" source: hosted - version: "0.5.8" + version: "0.5.10" google_sign_in: dependency: "direct main" description: @@ -476,18 +492,18 @@ packages: dependency: transitive description: name: google_sign_in_android - sha256: d30fb34b659679ea74397e9748b4ab5d720720d57dcc79538f1b3c4a68654cb3 + sha256: "5a47ebec9af97daf0822e800e4f101c3340b5ebc3f6898cf860c1a71b53cf077" url: "https://pub.dev" source: hosted - version: "6.1.27" + version: "6.1.28" google_sign_in_ios: dependency: transitive description: name: google_sign_in_ios - sha256: a058c9880be456f21e2e8571c1126eaacd570bdc5b6c6d9d15aea4bdf22ca9fe + sha256: "4898410f55440049e1ba8f15411612d9f89299d89c61cd9baf7e02d56ff81ac7" url: "https://pub.dev" source: hosted - version: "5.7.6" + version: "5.7.7" google_sign_in_platform_interface: dependency: transitive description: @@ -500,18 +516,18 @@ packages: dependency: transitive description: name: google_sign_in_web - sha256: d606264c7a1a526a3aa79d938b85a601d8589731a478bd4a3dcbdeb14a572228 + sha256: "042805a21127a85b0dc46bba98a37926f17d2439720e8a459d27045d8ef68055" url: "https://pub.dev" source: hosted - version: "0.12.4+1" + version: "0.12.4+2" graphs: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" html: dependency: transitive description: @@ -572,18 +588,18 @@ packages: dependency: transitive description: name: image_picker_android - sha256: ff39a10ab4f48f4ac70776d0494a97bf073cd2570892cd46bc8a5cac162c25db + sha256: "8c5abf0dcc24fe6e8e0b4a5c0b51a5cf30cefdf6407a3213dae61edc75a70f56" url: "https://pub.dev" source: hosted - version: "0.8.12+4" + version: "0.8.12+12" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "5d6eb13048cd47b60dbf1a5495424dea226c5faf3950e20bf8120a58efb5b5f3" + sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.0.5" image_picker_ios: dependency: transitive description: @@ -632,14 +648,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" - js_wrapping: - dependency: transitive - description: - name: js_wrapping - sha256: e385980f7c76a8c1c9a560dfb623b890975841542471eade630b2871d243851c - url: "https://pub.dev" - source: hosted - version: "0.7.4" leak_tracker: dependency: transitive description: @@ -724,10 +732,10 @@ packages: dependency: transitive description: name: mime - sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.6" nested: dependency: transitive description: @@ -756,18 +764,18 @@ packages: dependency: "direct main" description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "30c5aa827a6ae95ce2853cdc5fe3971daaac00f6f081c419c013f7f57bff2f5e" + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" url: "https://pub.dev" source: hosted - version: "2.2.7" + version: "2.2.10" path_provider_foundation: dependency: transitive description: @@ -836,18 +844,18 @@ packages: dependency: "direct main" description: name: rive - sha256: "3c0047e636ebe8e4044087e239dffdd026cf839fe9aecf55d53431b255668bcf" + sha256: daa5394a7d064b4997b39e9afa02f6882c479c38b19fa0dd60f052b99c105400 url: "https://pub.dev" source: hosted - version: "0.13.9" + version: "0.13.13" rive_common: dependency: transitive description: name: rive_common - sha256: "3fe76ba4680787741688ee393e47b63417e8643816795e4eac01021683af1d84" + sha256: c7bf0781b1621629361579c300ac2f8aa1a238227a242202a596e82becc244d7 url: "https://pub.dev" source: hosted - version: "0.4.9" + version: "0.4.11" sanitize_html: dependency: transitive description: @@ -856,6 +864,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + scoped_model: + dependency: "direct main" + description: + name: scoped_model + sha256: "8dacc77cb5de78d5e159d54cab883847491a73dfaa3d28c52f4ec8b0be32645b" + url: "https://pub.dev" + source: hosted + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -961,10 +977,10 @@ packages: dependency: transitive description: name: win32 - sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" url: "https://pub.dev" source: hosted - version: "5.5.1" + version: "5.5.4" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 615161f..2c20fd8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,10 +21,9 @@ environment: sdk: ">=2.12.0 <3.0.0" dependencies: - flutter: + flutter: sdk: flutter get: ^4.3.8 - # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -51,6 +50,9 @@ dependencies: flutter_dotenv: ^5.1.0 cloud_firestore_platform_interface: ^6.2.9 equatable: ^2.0.5 + flutter_blue_plus: ^1.32.12 + flutter_bluetooth_serial: ^0.4.0 + scoped_model: ^2.0.0 dependency_overrides: firebase_core_platform_interface: 5.1.0 @@ -70,7 +72,6 @@ flutter: # the material Icons class. uses-material-design: true - assets: - assets/images/ - assets/maps/ @@ -109,4 +110,3 @@ flutter: - asset: assets/fonts/DM_Sans/DMSans-Regular.ttf # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages -