This commit is contained in:
Guillaume David 2024-07-08 23:03:37 +02:00
parent 84a37b9603
commit 5dbac7f8f5
13 changed files with 335 additions and 72 deletions

View File

@ -1,4 +1,4 @@
# Eagle Tr@cker # Eagle Tr@cker
## Installation ## Installation
flutter build --release flutter run --release

View File

@ -23,7 +23,7 @@ if (flutterVersionName == null) {
} }
android { android {
namespace "com.example.gps_map_flowpoint" namespace "com.example.eagletracker"
compileSdkVersion flutter.compileSdkVersion compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion ndkVersion flutter.ndkVersion

View File

@ -18,7 +18,7 @@
<application <application
android:label="gps_map_flowpoint" android:label="eagletracker"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<activity <activity

View File

@ -1,6 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart';
/*
* Class for the BLE provider
*/
class BLEProvider with ChangeNotifier { class BLEProvider with ChangeNotifier {
List<ScanResult> _scanResults = []; List<ScanResult> _scanResults = [];

View File

@ -2,6 +2,9 @@ import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
/*
* Class to save the devices pinned by the user
*/
class DevicesSaved { class DevicesSaved {
String deviceName = ''; String deviceName = '';
String deviceAddress = ''; String deviceAddress = '';

View File

@ -4,6 +4,9 @@ class GeoPosition {
final double altitude; final double altitude;
final double speed; final double speed;
/*
* Class for the geographical position element needed for tracking the bird
*/
GeoPosition({ GeoPosition({
required this.latitude, required this.latitude,
required this.longitude, required this.longitude,

View File

@ -2,6 +2,9 @@ import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
/*
* Class to save the preferences of the user
*/
class PreferenceSaved { class PreferenceSaved {
int timeTakingPointSecond = 0; int timeTakingPointSecond = 0;

20
lib/config.dart Normal file
View File

@ -0,0 +1,20 @@
/*
* Singleton class for configuration
*/
import 'package:flutter/foundation.dart';
class Config {
static final Config _instance = Config._internal();
Config._internal();
factory Config() {
return _instance;
}
String projectName = 'Eagle Tr@cker';
bool debug = kDebugMode; // Set to true to enable debug logs
int manufacturerId = 49406; // Manufacturer ID setted in arduino code
int defaultTimeCapture = 10; // Default time capture in seconds
}

View File

@ -1,9 +1,15 @@
import 'package:eagletracker/config.dart';
import 'package:eagletracker/function.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:flutter_osm_plugin/flutter_osm_plugin.dart';
import 'package:eagletracker/class/BLEProvider.dart'; import 'package:eagletracker/class/BLEProvider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
/*
* Class for the device advertisement scan information screen
*/
class DeviceAdvertizinScan extends StatefulWidget { class DeviceAdvertizinScan extends StatefulWidget {
const DeviceAdvertizinScan( const DeviceAdvertizinScan(
{super.key, {super.key,
@ -20,38 +26,66 @@ class DeviceAdvertizinScan extends StatefulWidget {
} }
class _DeviceAdvertizinScanState extends State<DeviceAdvertizinScan> { class _DeviceAdvertizinScanState extends State<DeviceAdvertizinScan> {
Config config = Config();
List<ScanResult> scanResults = []; List<ScanResult> scanResults = [];
TextEditingController tecTakingPoint = TextEditingController(); double latitude = 0;
MapController mapController = MapController( double longitude = 0;
initPosition: GeoPoint(latitude: 47.4358055, longitude: 8.4737324), double altitude = 0;
); double speed = 0;
String value = 'Aucune valeur reçue'; String value = 'Aucune valeur reçue';
TextEditingController tecTakingPoint = TextEditingController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
//Get the BLE provider and the scan results
final bleProvider = Provider.of<BLEProvider>(context); final bleProvider = Provider.of<BLEProvider>(context);
scanResults = bleProvider.scanResults; scanResults = bleProvider.scanResults;
//Check if the device is in the list of scanned devices, make sure the manufacturer data is not empty and the remoteId is the same as the one selected
if (scanResults if (scanResults
.where((element) => .where((element) =>
element.advertisementData.manufacturerData.isNotEmpty && element.advertisementData.manufacturerData.isNotEmpty &&
element.device.remoteId.toString() == widget.remoteId) element.device.remoteId.toString() == widget.remoteId)
.isNotEmpty) { .isNotEmpty) {
value = scanResults value = scanResults
.where((element) => .where((element) {
element.device.remoteId.toString() == widget.remoteId) if (element.device.remoteId.toString() == widget.remoteId) {
.map((e) => e.advertisementData.toString()) if (element.advertisementData.manufacturerData[49406] != null) {
.first; List<int> data =
element.advertisementData.manufacturerData[49406]!;
// Faites quelque chose avec la variable 'value' ici //Convert the data to hexadecimal
List<String> hexList =
data.map((int value) => value.toRadixString(16)).toList();
if (config.debug) {
print(data);
print(hexList);
}
//Get the values from the advertisement data
List<double> values = getValueFromAdvData(data);
latitude = values[0];
longitude = values[1];
altitude = values[2];
speed = values[3];
setState(() {});
}
}
return element.device.remoteId.toString() == widget.remoteId;
})
.map((e) => e.toString())
.first;
} }
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: title: Text(config.projectName, style: TextStyle(color: Colors.white)),
const Text("Eagle Tr@cker", style: TextStyle(color: Colors.white)),
backgroundColor: Colors.blue, backgroundColor: Colors.blue,
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white), icon: const Icon(Icons.arrow_back, color: Colors.white),
@ -97,6 +131,13 @@ class _DeviceAdvertizinScanState extends State<DeviceAdvertizinScan> {
child: Text(value, child: Text(value,
style: const TextStyle( style: const TextStyle(
color: Colors.black, fontWeight: FontWeight.bold))), color: Colors.black, fontWeight: FontWeight.bold))),
Container(
margin: const EdgeInsets.all(10),
alignment: Alignment.center,
child: Text(
"Valeurs de la longitude ($longitude), latitude ($latitude), altitude ($altitude) et vitesse ($speed)",
style: const TextStyle(
color: Colors.black, fontWeight: FontWeight.bold)))
], ],
), ),
); );

View File

@ -1,7 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:eagletracker/config.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:flutter_osm_plugin/flutter_osm_plugin.dart'; import 'package:flutter_osm_plugin/flutter_osm_plugin.dart';
import 'package:eagletracker/class/BLEProvider.dart'; import 'package:eagletracker/class/BLEProvider.dart';
@ -10,6 +12,9 @@ import 'package:eagletracker/class/preferenceSaved.dart';
import 'package:eagletracker/function.dart'; import 'package:eagletracker/function.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
/*
* Class for the device flow map screen
*/
class DeviceFlowMap extends StatefulWidget { class DeviceFlowMap extends StatefulWidget {
const DeviceFlowMap( const DeviceFlowMap(
{super.key, required this.deviceName, required this.deviceAddress}); {super.key, required this.deviceName, required this.deviceAddress});
@ -22,46 +27,146 @@ class DeviceFlowMap extends StatefulWidget {
} }
class _DeviceFlowMapState extends State<DeviceFlowMap> { class _DeviceFlowMapState extends State<DeviceFlowMap> {
final Config config = Config();
PreferenceSaved preferenceSaved = PreferenceSaved.empty();
MapController mapController = MapController( MapController mapController = MapController(
initPosition: GeoPoint(latitude: 47.4358055, longitude: 8.4737324), initPosition: GeoPoint(latitude: 46.1723, longitude: 7.1841));
);
List<ScanResult> scanResults = []; List<ScanResult> scanResults = [];
List<GeoPosition> geoPositions = []; List<GeoPosition> geoPositions = [];
List<int> advDataLastReception = [];
Timer? timer; Timer? timer;
double progression = 0.0; double progression = 0.0;
int timeTakingPointSecond = 10; int timeTakingPointSecond = 10;
int updateInterval = 500; // Intervalle de mise à jour en millisecondes int updateInterval = 500;
PreferenceSaved preferenceSaved = PreferenceSaved.empty(); double vitesseMax = 0.0;
double altitudeMax = 0.0;
double latitude = 0;
double longitude = 0;
double altitude = 0;
double speed = 0;
bool targetTracking = false;
bool drawRoad = false;
GeoPoint targetTrackingPoint = GeoPoint(latitude: 0, longitude: 0);
GeoPoint targetTrackingPointHistory = GeoPoint(latitude: 0, longitude: 0);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
//Positions de test SystemChrome.setPreferredOrientations([
geoPositions = [ DeviceOrientation.portraitUp,
GeoPosition(latitude: 1.0, longitude: 2.0, altitude: 100.0, speed: 10.0), DeviceOrientation.portraitDown,
GeoPosition(latitude: 3.0, longitude: 4.0, altitude: 200.0, speed: 20.0), DeviceOrientation.landscapeLeft,
]; DeviceOrientation.landscapeRight,
]);
restoreLocal().then((value) { restoreLocal().then((value) async {
setState(() { preferenceSaved = value;
preferenceSaved = value; timeTakingPointSecond = preferenceSaved.timeTakingPointSecond;
timeTakingPointSecond = preferenceSaved.timeTakingPointSecond;
if (timeTakingPointSecond > 0) { // set the default time capture if the value is 0
int totalUpdates = (timeTakingPointSecond * 1000) ~/ updateInterval; if (timeTakingPointSecond == 0) {
timeTakingPointSecond = config.defaultTimeCapture;
}
timer = Timer.periodic(Duration(milliseconds: updateInterval), int totalUpdates = (timeTakingPointSecond * 1000) ~/ updateInterval;
(Timer timer) {
setState(() { timer = Timer.periodic(Duration(milliseconds: updateInterval),
progression = (timer.tick % totalUpdates) / totalUpdates * 100.0; (Timer timer) async {
}); setState(() {
}); progression = (timer.tick % totalUpdates) / totalUpdates * 100.0;
} else { });
print('Erreur: timeTakingPointSecond doit être supérieur à zéro.');
// Check if the manufacturer data is not empty and the last reception is not empty
if (advDataLastReception.isNotEmpty && timer.tick % totalUpdates == 0) {
List<String> hexList = advDataLastReception
.map((int value) => value.toRadixString(16))
.toList();
if (config.debug) {
print(advDataLastReception);
print(hexList);
}
//Get the values from the advertisement data
List<double> values = getValueFromAdvData(advDataLastReception);
latitude = values[0];
longitude = values[1];
altitude = values[2];
speed = values[3];
//Ajout de la position
GeoPosition geoPosition = GeoPosition(
latitude: latitude,
longitude: longitude,
altitude: altitude,
);
geoPositions.add(geoPosition);
//Vitesse max et altitude max
if (speed > vitesseMax) {
vitesseMax = speed;
}
if (altitude > altitudeMax) {
altitudeMax = altitude;
}
targetTrackingPoint = GeoPoint(
latitude: latitude,
longitude: longitude,
);
//Draw the road if the option is enabled
mapController.clearAllRoads();
if (drawRoad) {
List<GeoPoint> linedsPoints = [];
for (var geoPosition in geoPositions) {
GeoPoint geoPoint = GeoPoint(
latitude: geoPosition.latitude,
longitude: geoPosition.longitude,
);
if (!linedsPoints.contains(geoPoint)) {
linedsPoints.add(geoPoint);
}
if (config.debug) {
print(linedsPoints.length);
}
}
if (linedsPoints.length > 1) {
// Dessiner la route
await mapController.drawRoadManually(
linedsPoints,
const RoadOption(roadColor: Colors.red, zoomInto: false),
);
}
}
mapController.removeMarker(targetTrackingPointHistory);
mapController.addMarker(targetTrackingPoint,
markerIcon: const MarkerIcon(
icon: Icon(
Icons.star_outlined,
color: Colors.red,
size: 48,
),
));
targetTrackingPointHistory = targetTrackingPoint;
if (targetTracking) {
mapController.changeLocation(targetTrackingPoint);
}
setState(() {});
} }
}); });
}); });
@ -78,6 +183,16 @@ class _DeviceFlowMapState extends State<DeviceFlowMap> {
final bleProvider = Provider.of<BLEProvider>(context); final bleProvider = Provider.of<BLEProvider>(context);
scanResults = bleProvider.scanResults; scanResults = bleProvider.scanResults;
for (var scanResult in scanResults) {
if (scanResult.device.remoteId.toString() == widget.deviceAddress) {
if (scanResult.advertisementData.manufacturerData[49406] != null) {
advDataLastReception =
scanResult.advertisementData.manufacturerData[49406]!;
break;
}
}
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: title:
@ -86,6 +201,10 @@ class _DeviceFlowMapState extends State<DeviceFlowMap> {
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white), icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () { onPressed: () {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
mapController.dispose(); mapController.dispose();
Navigator.pop(context); Navigator.pop(context);
}, },
@ -96,7 +215,7 @@ class _DeviceFlowMapState extends State<DeviceFlowMap> {
controller: mapController, controller: mapController,
onMapIsReady: (bool value) async { onMapIsReady: (bool value) async {
if (value) { if (value) {
Future.delayed(const Duration(milliseconds: 500), () async { Future.delayed(const Duration(milliseconds: 750), () async {
await mapController.currentLocation(); await mapController.currentLocation();
}); });
} }
@ -149,14 +268,28 @@ class _DeviceFlowMapState extends State<DeviceFlowMap> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Altitude: ${geoPositions.last.altitude.toStringAsFixed(2)} m', 'Altitude: ${altitude.toStringAsFixed(2)} m',
style: const TextStyle( style: const TextStyle(
color: Colors.black, color: Colors.black,
fontSize: 16, fontSize: 16,
), ),
), ),
Text( Text(
'Vitesse: ${geoPositions.last.speed.toStringAsFixed(2)} km/h', 'Vitesse: ${speed.toStringAsFixed(2)} km/h',
style: const TextStyle(
color: Colors.black,
fontSize: 16,
),
),
Text(
'Alt. max: ${altitudeMax.toStringAsFixed(2)} m',
style: const TextStyle(
color: Colors.black,
fontSize: 16,
),
),
Text(
'Vit. max: ${vitesseMax.toStringAsFixed(2)} km/h',
style: const TextStyle( style: const TextStyle(
color: Colors.black, color: Colors.black,
fontSize: 16, fontSize: 16,
@ -191,15 +324,17 @@ class _DeviceFlowMapState extends State<DeviceFlowMap> {
floatingActionButton: floatingActionButton:
Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [
FloatingActionButton( FloatingActionButton(
tooltip: 'Centrer la carte sur ma position actuelle',
heroTag: 'actionFab1', heroTag: 'actionFab1',
backgroundColor: Colors.blue, backgroundColor: Colors.blue,
onPressed: () { onPressed: () {
mapController.currentLocation(); mapController.currentLocation();
}, },
child: const Icon(Icons.map_outlined, color: Colors.white), child: const Icon(Icons.gps_not_fixed_sharp, color: Colors.white),
), ),
FloatingActionButton( FloatingActionButton(
heroTag: 'actionFab2', heroTag: 'actionFab2',
tooltip: 'Réinitialiser les points',
backgroundColor: Colors.blue, backgroundColor: Colors.blue,
onPressed: () { onPressed: () {
showDialog( showDialog(
@ -241,6 +376,29 @@ class _DeviceFlowMapState extends State<DeviceFlowMap> {
child: const Icon(Icons.delete_sharp, color: Colors.white), child: const Icon(Icons.delete_sharp, color: Colors.white),
), ),
FloatingActionButton( FloatingActionButton(
heroTag: 'actionFab4',
tooltip: 'Centrer la carte sur la cible',
backgroundColor: targetTracking ? Colors.green : Colors.blue,
onPressed: () {
setState(() {
targetTracking = !targetTracking;
});
},
child: const Icon(Icons.gps_fixed, color: Colors.white),
),
FloatingActionButton(
tooltip: 'Dessiner la route',
heroTag: 'actionFab6',
backgroundColor: drawRoad ? Colors.green : Colors.blue,
onPressed: () {
setState(() {
drawRoad = !drawRoad;
});
},
child: const Icon(Icons.roundabout_left, color: Colors.white),
),
FloatingActionButton(
tooltip: 'Envoyer les positions par email',
heroTag: 'actionFab3', heroTag: 'actionFab3',
backgroundColor: Colors.blue, backgroundColor: Colors.blue,
onPressed: () async { onPressed: () async {
@ -259,33 +417,6 @@ class _DeviceFlowMapState extends State<DeviceFlowMap> {
}, },
child: const Icon(Icons.mail, color: Colors.white), child: const Icon(Icons.mail, color: Colors.white),
), ),
FloatingActionButton(
heroTag: 'actionFab4',
backgroundColor: Colors.blue,
onPressed: () {
List<GeoPoint> linedsPoints = [];
for (var geoPosition in geoPositions) {
GeoPoint geoPoint = GeoPoint(
latitude: geoPosition.latitude,
longitude: geoPosition.longitude,
);
linedsPoints.add(geoPoint);
mapController.clearAllRoads();
if (linedsPoints.length > 1 &&
linedsPoints.toSet().length == linedsPoints.length) {
mapController.drawRoadManually(
linedsPoints,
const RoadOption(roadColor: Colors.red),
);
}
}
},
child: const Icon(Icons.draw, color: Colors.white),
),
]), ]),
); );
} }

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:eagletracker/class/BLEProvider.dart'; import 'package:eagletracker/class/BLEProvider.dart';
import 'package:eagletracker/class/devicesSaved.dart' as DevicesSaved; import 'package:eagletracker/class/devicesSaved.dart' as DevicesSaved;
@ -7,6 +8,9 @@ import 'package:eagletracker/deviceAdvertizinScan.dart';
import 'package:eagletracker/deviceFlowMap.dart'; import 'package:eagletracker/deviceFlowMap.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
/*
* Class for the device selection screen
*/
class DeviceSelection extends StatefulWidget { class DeviceSelection extends StatefulWidget {
const DeviceSelection({super.key}); const DeviceSelection({super.key});
@ -25,13 +29,17 @@ class _DeviceSelectionState extends State<DeviceSelection> {
TextEditingController tecTimeTakingPointSecond = TextEditingController(); TextEditingController tecTimeTakingPointSecond = TextEditingController();
// Initialize Bluetooth scanning and subscription in initState
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Set the orientation to portrait
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
// Scanning for BLE devices in the default options
isScanning = true; isScanning = true;
// Restore the saved preferences
PreferenceSaved.restoreLocal().then((value) { PreferenceSaved.restoreLocal().then((value) {
setState(() { setState(() {
preferenceSaved = value; preferenceSaved = value;
@ -354,6 +362,9 @@ class _DeviceSelectionState extends State<DeviceSelection> {
} }
} }
/*
* Function to check if the scan is activated
*/
bool checkIsScanning(BuildContext context, bool isScanning) { bool checkIsScanning(BuildContext context, bool isScanning) {
if (!isScanning) { if (!isScanning) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(

View File

@ -1,10 +1,14 @@
import 'dart:io'; import 'dart:io';
import 'dart:typed_data';
import 'package:csv/csv.dart'; import 'package:csv/csv.dart';
import 'package:flutter_email_sender/flutter_email_sender.dart'; import 'package:flutter_email_sender/flutter_email_sender.dart';
import 'package:eagletracker/class/geoPosition.dart'; import 'package:eagletracker/class/geoPosition.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
/*
* Function to generate a CSV file with the positions array
*/
Future<File> generateCsv(List<GeoPosition> positions) async { Future<File> generateCsv(List<GeoPosition> positions) async {
List<List<dynamic>> csvData = [ List<List<dynamic>> csvData = [
['latitude', 'longitude', 'altitude', 'spped'] // Header ['latitude', 'longitude', 'altitude', 'spped'] // Header
@ -26,6 +30,9 @@ Future<File> generateCsv(List<GeoPosition> positions) async {
return csvFile; return csvFile;
} }
/*
* Function to send an email with the CSV file as an attachment
*/
void sendEmailWithAttachment(File csvFile) async { void sendEmailWithAttachment(File csvFile) async {
final Email email = Email( final Email email = Email(
subject: 'Eagle Tr@cker - Positions', subject: 'Eagle Tr@cker - Positions',
@ -40,3 +47,43 @@ void sendEmailWithAttachment(File csvFile) async {
throw 'Failed to send email: $error'; throw 'Failed to send email: $error';
} }
} }
/*
* Function to get the values from the manufacturer data according to the data structure
*/
List<double> getValueFromAdvData(List<int> data) {
double latitude = 0;
double longitude = 0;
double altitude = 0;
double speed = 0;
List<double> values = [];
//Latitude
List<int> dataLatitude = data.sublist(0, 4);
ByteData byteDataLatitude =
ByteData.sublistView(Uint8List.fromList(dataLatitude));
latitude = byteDataLatitude.getFloat32(0, Endian.little);
values.add(latitude);
//Longitude
List<int> dataLongitude = data.sublist(4, 8);
ByteData byteDataLongitude =
ByteData.sublistView(Uint8List.fromList(dataLongitude));
longitude = byteDataLongitude.getFloat32(0, Endian.little);
values.add(longitude);
//Altitude
List<int> dataAltitude = data.sublist(8, 12);
ByteData byteDataAltitude =
ByteData.sublistView(Uint8List.fromList(dataAltitude));
altitude = byteDataAltitude.getFloat32(0, Endian.little);
values.add(altitude);
//Speed
List<int> dataSpeed = data.sublist(12, 16);
ByteData byteDataSpeed = ByteData.sublistView(Uint8List.fromList(dataSpeed));
speed = byteDataSpeed.getFloat32(0, Endian.little);
values.add(speed);
return values;
}

View File

@ -5,6 +5,7 @@ import 'package:provider/provider.dart';
void main() { void main() {
runApp( runApp(
// Add the ChangeNotifierProvider to the root of the widget tree
MultiProvider( MultiProvider(
providers: [ providers: [
ChangeNotifierProvider(create: (_) => BLEProvider()), ChangeNotifierProvider(create: (_) => BLEProvider()),
@ -17,7 +18,6 @@ void main() {
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
const MyApp({super.key}); const MyApp({super.key});
// This widget is the root of your application.
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(