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
## Installation
flutter build --release
flutter run --release

View File

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

View File

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

View File

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

View File

@ -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 = '';

View File

@ -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,

View File

@ -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;

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_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<DeviceAdvertizinScan> {
Config config = Config();
List<ScanResult> 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<BLEProvider>(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<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(
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<DeviceAdvertizinScan> {
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)))
],
),
);

View File

@ -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<DeviceFlowMap> {
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<ScanResult> scanResults = [];
List<GeoPosition> geoPositions = [];
List<int> 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(() {
restoreLocal().then((value) async {
preferenceSaved = value;
timeTakingPointSecond = preferenceSaved.timeTakingPointSecond;
if (timeTakingPointSecond > 0) {
// set the default time capture if the value is 0
if (timeTakingPointSecond == 0) {
timeTakingPointSecond = config.defaultTimeCapture;
}
int totalUpdates = (timeTakingPointSecond * 1000) ~/ updateInterval;
timer = Timer.periodic(Duration(milliseconds: updateInterval),
(Timer timer) {
(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);
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<DeviceFlowMap> {
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<DeviceFlowMap> {
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<DeviceFlowMap> {
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<DeviceFlowMap> {
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<DeviceFlowMap> {
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<DeviceFlowMap> {
},
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/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<DeviceSelection> {
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<DeviceSelection> {
}
}
/*
* Function to check if the scan is activated
*/
bool checkIsScanning(BuildContext context, bool isScanning) {
if (!isScanning) {
ScaffoldMessenger.of(context).showSnackBar(

View File

@ -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<File> generateCsv(List<GeoPosition> positions) async {
List<List<dynamic>> csvData = [
['latitude', 'longitude', 'altitude', 'spped'] // Header
@ -26,6 +30,9 @@ Future<File> generateCsv(List<GeoPosition> 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<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() {
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(