diff --git a/README.md b/README.md index 442441d..dc3a047 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ # Eagle Tr@cker ## Installation - flutter build --release \ No newline at end of file + flutter run --release \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index f63dd39..44c9fb1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -23,7 +23,7 @@ if (flutterVersionName == null) { } android { - namespace "com.example.gps_map_flowpoint" + namespace "com.example.eagletracker" compileSdkVersion flutter.compileSdkVersion ndkVersion flutter.ndkVersion diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e799dce..99cdcc1 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ _scanResults = []; diff --git a/lib/class/devicesSaved.dart b/lib/class/devicesSaved.dart index e079ffa..47df929 100644 --- a/lib/class/devicesSaved.dart +++ b/lib/class/devicesSaved.dart @@ -2,6 +2,9 @@ import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; +/* +* Class to save the devices pinned by the user +*/ class DevicesSaved { String deviceName = ''; String deviceAddress = ''; diff --git a/lib/class/geoPosition.dart b/lib/class/geoPosition.dart index 0fa63ac..3e06f5a 100644 --- a/lib/class/geoPosition.dart +++ b/lib/class/geoPosition.dart @@ -4,6 +4,9 @@ class GeoPosition { final double altitude; final double speed; + /* + * Class for the geographical position element needed for tracking the bird + */ GeoPosition({ required this.latitude, required this.longitude, diff --git a/lib/class/preferenceSaved.dart b/lib/class/preferenceSaved.dart index 6c0e5bb..80342fd 100644 --- a/lib/class/preferenceSaved.dart +++ b/lib/class/preferenceSaved.dart @@ -2,6 +2,9 @@ import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; +/* +* Class to save the preferences of the user +*/ class PreferenceSaved { int timeTakingPointSecond = 0; diff --git a/lib/config.dart b/lib/config.dart new file mode 100644 index 0000000..9f805c9 --- /dev/null +++ b/lib/config.dart @@ -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 +} diff --git a/lib/deviceAdvertizinScan.dart b/lib/deviceAdvertizinScan.dart index 19c86e7..91106f2 100644 --- a/lib/deviceAdvertizinScan.dart +++ b/lib/deviceAdvertizinScan.dart @@ -1,9 +1,15 @@ +import 'package:eagletracker/config.dart'; +import 'package:eagletracker/function.dart'; + import 'package:flutter/material.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:provider/provider.dart'; +/* +* Class for the device advertisement scan information screen +*/ class DeviceAdvertizinScan extends StatefulWidget { const DeviceAdvertizinScan( {super.key, @@ -20,38 +26,66 @@ class DeviceAdvertizinScan extends StatefulWidget { } class _DeviceAdvertizinScanState extends State { + Config config = Config(); List scanResults = []; - TextEditingController tecTakingPoint = TextEditingController(); - MapController mapController = MapController( - initPosition: GeoPoint(latitude: 47.4358055, longitude: 8.4737324), - ); + double latitude = 0; + double longitude = 0; + double altitude = 0; + double speed = 0; String value = 'Aucune valeur reçue'; + TextEditingController tecTakingPoint = TextEditingController(); + @override Widget build(BuildContext context) { + //Get the BLE provider and the scan results final bleProvider = Provider.of(context); 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 .where((element) => element.advertisementData.manufacturerData.isNotEmpty && element.device.remoteId.toString() == widget.remoteId) .isNotEmpty) { value = scanResults - .where((element) => - element.device.remoteId.toString() == widget.remoteId) - .map((e) => e.advertisementData.toString()) - .first; + .where((element) { + if (element.device.remoteId.toString() == widget.remoteId) { + if (element.advertisementData.manufacturerData[49406] != null) { + List data = + element.advertisementData.manufacturerData[49406]!; - // Faites quelque chose avec la variable 'value' ici + //Convert the data to hexadecimal + List hexList = + data.map((int value) => value.toRadixString(16)).toList(); + + if (config.debug) { + print(data); + print(hexList); + } + + //Get the values from the advertisement data + List 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( appBar: AppBar( - title: - const Text("Eagle Tr@cker", style: TextStyle(color: Colors.white)), + title: Text(config.projectName, style: TextStyle(color: Colors.white)), backgroundColor: Colors.blue, leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), @@ -97,6 +131,13 @@ class _DeviceAdvertizinScanState extends State { child: Text(value, style: const TextStyle( 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))) ], ), ); diff --git a/lib/deviceFlowMap.dart b/lib/deviceFlowMap.dart index 3569aea..4f9b23d 100644 --- a/lib/deviceFlowMap.dart +++ b/lib/deviceFlowMap.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'dart:io'; - +import 'dart:typed_data'; +import 'package:eagletracker/config.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.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'; @@ -10,6 +12,9 @@ import 'package:eagletracker/class/preferenceSaved.dart'; import 'package:eagletracker/function.dart'; import 'package:provider/provider.dart'; +/* +* Class for the device flow map screen +*/ class DeviceFlowMap extends StatefulWidget { const DeviceFlowMap( {super.key, required this.deviceName, required this.deviceAddress}); @@ -22,46 +27,146 @@ class DeviceFlowMap extends StatefulWidget { } class _DeviceFlowMapState extends State { + final Config config = Config(); + PreferenceSaved preferenceSaved = PreferenceSaved.empty(); + MapController mapController = MapController( - initPosition: GeoPoint(latitude: 47.4358055, longitude: 8.4737324), - ); + initPosition: GeoPoint(latitude: 46.1723, longitude: 7.1841)); List scanResults = []; List geoPositions = []; + List advDataLastReception = []; Timer? timer; double progression = 0.0; 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 void initState() { super.initState(); - //Positions de test - geoPositions = [ - GeoPosition(latitude: 1.0, longitude: 2.0, altitude: 100.0, speed: 10.0), - GeoPosition(latitude: 3.0, longitude: 4.0, altitude: 200.0, speed: 20.0), - ]; + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); - restoreLocal().then((value) { - setState(() { - preferenceSaved = value; - timeTakingPointSecond = preferenceSaved.timeTakingPointSecond; + restoreLocal().then((value) async { + preferenceSaved = value; + timeTakingPointSecond = preferenceSaved.timeTakingPointSecond; - if (timeTakingPointSecond > 0) { - int totalUpdates = (timeTakingPointSecond * 1000) ~/ updateInterval; + // set the default time capture if the value is 0 + if (timeTakingPointSecond == 0) { + timeTakingPointSecond = config.defaultTimeCapture; + } - timer = Timer.periodic(Duration(milliseconds: updateInterval), - (Timer timer) { - setState(() { - progression = (timer.tick % totalUpdates) / totalUpdates * 100.0; - }); - }); - } else { - print('Erreur: timeTakingPointSecond doit être supérieur à zéro.'); + int totalUpdates = (timeTakingPointSecond * 1000) ~/ updateInterval; + + timer = Timer.periodic(Duration(milliseconds: updateInterval), + (Timer timer) async { + setState(() { + progression = (timer.tick % totalUpdates) / totalUpdates * 100.0; + }); + + // Check if the manufacturer data is not empty and the last reception is not empty + if (advDataLastReception.isNotEmpty && timer.tick % totalUpdates == 0) { + List hexList = advDataLastReception + .map((int value) => value.toRadixString(16)) + .toList(); + + if (config.debug) { + print(advDataLastReception); + print(hexList); + } + + //Get the values from the advertisement data + List 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 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 { final bleProvider = Provider.of(context); 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( appBar: AppBar( title: @@ -86,6 +201,10 @@ class _DeviceFlowMapState extends State { leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), onPressed: () { + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); mapController.dispose(); Navigator.pop(context); }, @@ -96,7 +215,7 @@ class _DeviceFlowMapState extends State { controller: mapController, onMapIsReady: (bool value) async { if (value) { - Future.delayed(const Duration(milliseconds: 500), () async { + Future.delayed(const Duration(milliseconds: 750), () async { await mapController.currentLocation(); }); } @@ -149,14 +268,28 @@ class _DeviceFlowMapState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Altitude: ${geoPositions.last.altitude.toStringAsFixed(2)} m', + 'Altitude: ${altitude.toStringAsFixed(2)} m', style: const TextStyle( color: Colors.black, fontSize: 16, ), ), 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( color: Colors.black, fontSize: 16, @@ -191,15 +324,17 @@ class _DeviceFlowMapState extends State { floatingActionButton: Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ FloatingActionButton( + tooltip: 'Centrer la carte sur ma position actuelle', heroTag: 'actionFab1', backgroundColor: Colors.blue, onPressed: () { mapController.currentLocation(); }, - child: const Icon(Icons.map_outlined, color: Colors.white), + child: const Icon(Icons.gps_not_fixed_sharp, color: Colors.white), ), FloatingActionButton( heroTag: 'actionFab2', + tooltip: 'Réinitialiser les points', backgroundColor: Colors.blue, onPressed: () { showDialog( @@ -241,6 +376,29 @@ class _DeviceFlowMapState extends State { child: const Icon(Icons.delete_sharp, color: Colors.white), ), 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', backgroundColor: Colors.blue, onPressed: () async { @@ -259,33 +417,6 @@ class _DeviceFlowMapState extends State { }, child: const Icon(Icons.mail, color: Colors.white), ), - FloatingActionButton( - heroTag: 'actionFab4', - backgroundColor: Colors.blue, - onPressed: () { - List 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), - ), ]), ); } diff --git a/lib/deviceSelection.dart b/lib/deviceSelection.dart index 75e5524..2b69f99 100644 --- a/lib/deviceSelection.dart +++ b/lib/deviceSelection.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:eagletracker/class/BLEProvider.dart'; import 'package:eagletracker/class/devicesSaved.dart' as DevicesSaved; @@ -7,6 +8,9 @@ import 'package:eagletracker/deviceAdvertizinScan.dart'; import 'package:eagletracker/deviceFlowMap.dart'; import 'package:provider/provider.dart'; +/* +* Class for the device selection screen +*/ class DeviceSelection extends StatefulWidget { const DeviceSelection({super.key}); @@ -25,13 +29,17 @@ class _DeviceSelectionState extends State { TextEditingController tecTimeTakingPointSecond = TextEditingController(); - // Initialize Bluetooth scanning and subscription in initState @override void initState() { super.initState(); + // Set the orientation to portrait + SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); + + // Scanning for BLE devices in the default options isScanning = true; + // Restore the saved preferences PreferenceSaved.restoreLocal().then((value) { setState(() { preferenceSaved = value; @@ -354,6 +362,9 @@ class _DeviceSelectionState extends State { } } +/* +* Function to check if the scan is activated +*/ bool checkIsScanning(BuildContext context, bool isScanning) { if (!isScanning) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/function.dart b/lib/function.dart index f1fcb48..9fe3fef 100644 --- a/lib/function.dart +++ b/lib/function.dart @@ -1,10 +1,14 @@ import 'dart:io'; +import 'dart:typed_data'; import 'package:csv/csv.dart'; import 'package:flutter_email_sender/flutter_email_sender.dart'; import 'package:eagletracker/class/geoPosition.dart'; import 'package:path_provider/path_provider.dart'; +/* +* Function to generate a CSV file with the positions array +*/ Future generateCsv(List positions) async { List> csvData = [ ['latitude', 'longitude', 'altitude', 'spped'] // Header @@ -26,6 +30,9 @@ Future generateCsv(List positions) async { return csvFile; } +/* +* Function to send an email with the CSV file as an attachment +*/ void sendEmailWithAttachment(File csvFile) async { final Email email = Email( subject: 'Eagle Tr@cker - Positions', @@ -40,3 +47,43 @@ void sendEmailWithAttachment(File csvFile) async { throw 'Failed to send email: $error'; } } + +/* +* Function to get the values from the manufacturer data according to the data structure +*/ +List getValueFromAdvData(List data) { + double latitude = 0; + double longitude = 0; + double altitude = 0; + double speed = 0; + + List values = []; + //Latitude + List dataLatitude = data.sublist(0, 4); + ByteData byteDataLatitude = + ByteData.sublistView(Uint8List.fromList(dataLatitude)); + latitude = byteDataLatitude.getFloat32(0, Endian.little); + values.add(latitude); + + //Longitude + List dataLongitude = data.sublist(4, 8); + ByteData byteDataLongitude = + ByteData.sublistView(Uint8List.fromList(dataLongitude)); + longitude = byteDataLongitude.getFloat32(0, Endian.little); + values.add(longitude); + + //Altitude + List dataAltitude = data.sublist(8, 12); + ByteData byteDataAltitude = + ByteData.sublistView(Uint8List.fromList(dataAltitude)); + altitude = byteDataAltitude.getFloat32(0, Endian.little); + values.add(altitude); + + //Speed + List dataSpeed = data.sublist(12, 16); + ByteData byteDataSpeed = ByteData.sublistView(Uint8List.fromList(dataSpeed)); + speed = byteDataSpeed.getFloat32(0, Endian.little); + values.add(speed); + + return values; +} diff --git a/lib/main.dart b/lib/main.dart index e31ff83..d8ff1a2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:provider/provider.dart'; void main() { runApp( + // Add the ChangeNotifierProvider to the root of the widget tree MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => BLEProvider()), @@ -17,7 +18,6 @@ void main() { class MyApp extends StatelessWidget { const MyApp({super.key}); - // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp(