mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-17 07:56:28 +10:00
Implement sparse location logging feature and update related services
This commit is contained in:
@@ -2,6 +2,8 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:crypto/crypto.dart' as crypto;
|
||||
import 'package:geolocator_platform_interface/src/models/position.dart';
|
||||
import 'package:meshcore_open/services/sparse_location_logger.dart';
|
||||
import 'package:pointycastle/export.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
@@ -130,6 +132,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
PathHistoryService? _pathHistoryService;
|
||||
AppSettingsService? _appSettingsService;
|
||||
BackgroundService? _backgroundService;
|
||||
SparseLocationLogger? _sparseLocationLogger;
|
||||
final NotificationService _notificationService = NotificationService();
|
||||
BleDebugLogService? _bleDebugLogService;
|
||||
AppDebugLogService? _appDebugLogService;
|
||||
@@ -502,6 +505,7 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
BleDebugLogService? bleDebugLogService,
|
||||
AppDebugLogService? appDebugLogService,
|
||||
BackgroundService? backgroundService,
|
||||
SparseLocationLogger? sparseLocationLogger,
|
||||
}) {
|
||||
_retryService = retryService;
|
||||
_pathHistoryService = pathHistoryService;
|
||||
@@ -509,11 +513,14 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
_bleDebugLogService = bleDebugLogService;
|
||||
_appDebugLogService = appDebugLogService;
|
||||
_backgroundService = backgroundService;
|
||||
_sparseLocationLogger = sparseLocationLogger;
|
||||
|
||||
// Initialize notification service
|
||||
_notificationService.initialize();
|
||||
_loadChannelOrder();
|
||||
|
||||
_sparseLocationLogger?.initialize(_updateLocationandAdvert);
|
||||
|
||||
// Initialize retry service callbacks
|
||||
_retryService?.initialize(
|
||||
sendMessageCallback: _sendMessageDirect,
|
||||
@@ -828,6 +835,8 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
return result;
|
||||
}
|
||||
|
||||
SparseLocationLogger? get sparseLocationLogger => _sparseLocationLogger;
|
||||
|
||||
bool get _shouldAutoReconnect => !_manualDisconnect && _lastDeviceId != null;
|
||||
|
||||
void _cancelReconnectTimer() {
|
||||
@@ -3285,6 +3294,23 @@ class MeshCoreConnector extends ChangeNotifier {
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_updateLocationandAdvert(Position position) async {
|
||||
double lat = position.latitude;
|
||||
double lon = position.longitude;
|
||||
|
||||
if (lat == 0.0 && lon == 0.0) {
|
||||
// Invalid location
|
||||
return;
|
||||
}
|
||||
lat = double.parse(lat.toStringAsFixed(3)) - 0.00015;
|
||||
lon = double.parse(lon.toStringAsFixed(3)) - 0.00015;
|
||||
print('Updating location to lat: $lat, lon: $lon');
|
||||
await sendFrame(buildSetOtherParamsFrame(true, 0, 1, 0));
|
||||
await setNodeLocation(lat: lat, lon: lon);
|
||||
await sendSelfAdvert(flood: true);
|
||||
await sendFrame(buildDeviceQueryFrame());
|
||||
}
|
||||
}
|
||||
|
||||
const int _phRouteMask = 0x03;
|
||||
|
||||
@@ -151,6 +151,7 @@ const int cmdGetContactByKey = 30;
|
||||
const int cmdGetChannel = 31;
|
||||
const int cmdSetChannel = 32;
|
||||
const int cmdSendTracePath = 36;
|
||||
const int cmdSetOtherParams = 38;
|
||||
const int cmdGetRadioSettings = 57;
|
||||
const int cmdGetTelemetryReq = 39;
|
||||
const int cmdGetCustomVar = 40;
|
||||
@@ -777,3 +778,22 @@ Uint8List buildZeroHopContact(Uint8List pubKey) {
|
||||
writer.writeBytes(pubKey);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
// Build CMD_SET_OTHER_PARAMS frame
|
||||
// Format: [cmd][allowAutoAddContacts][allowTelemetryFlags][advert_loc_policy][multi_acks]
|
||||
Uint8List buildSetOtherParamsFrame(
|
||||
bool allowAutoAddContacts,
|
||||
int allowTelemetryFlags,
|
||||
int advert_loc_policy,
|
||||
int multi_acks,
|
||||
) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSetOtherParams);
|
||||
writer.writeByte(
|
||||
allowAutoAddContacts ? 0x01 : 0x00,
|
||||
); // Allow Auto Add Contacts
|
||||
writer.writeByte(allowTelemetryFlags); // Allow Telemetry Flags
|
||||
writer.writeByte(advert_loc_policy); // Advertisement Location Policy
|
||||
writer.writeByte(multi_acks); // Multi Acknowledgements
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
+6
-1
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:meshcore_open/services/sparse_location_logger.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -33,7 +34,7 @@ void main() async {
|
||||
final appDebugLogService = AppDebugLogService();
|
||||
final backgroundService = BackgroundService();
|
||||
final mapTileCacheService = MapTileCacheService();
|
||||
|
||||
final sparseLocationLogger = SparseLocationLogger();
|
||||
// Load settings
|
||||
await appSettingsService.loadSettings();
|
||||
|
||||
@@ -56,6 +57,7 @@ void main() async {
|
||||
bleDebugLogService: bleDebugLogService,
|
||||
appDebugLogService: appDebugLogService,
|
||||
backgroundService: backgroundService,
|
||||
sparseLocationLogger: sparseLocationLogger,
|
||||
);
|
||||
|
||||
await connector.loadContactCache();
|
||||
@@ -76,6 +78,7 @@ void main() async {
|
||||
bleDebugLogService: bleDebugLogService,
|
||||
appDebugLogService: appDebugLogService,
|
||||
mapTileCacheService: mapTileCacheService,
|
||||
sparseLocationLogger: sparseLocationLogger,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -89,6 +92,7 @@ class MeshCoreApp extends StatelessWidget {
|
||||
final BleDebugLogService bleDebugLogService;
|
||||
final AppDebugLogService appDebugLogService;
|
||||
final MapTileCacheService mapTileCacheService;
|
||||
final SparseLocationLogger sparseLocationLogger;
|
||||
|
||||
const MeshCoreApp({
|
||||
super.key,
|
||||
@@ -100,6 +104,7 @@ class MeshCoreApp extends StatelessWidget {
|
||||
required this.bleDebugLogService,
|
||||
required this.appDebugLogService,
|
||||
required this.mapTileCacheService,
|
||||
required this.sparseLocationLogger,
|
||||
});
|
||||
|
||||
@override
|
||||
|
||||
@@ -357,8 +357,8 @@ class _MapScreenState extends State<MapScreen> {
|
||||
connector.selfLatitude!,
|
||||
connector.selfLongitude!,
|
||||
),
|
||||
width: 35,
|
||||
height: 35,
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:meshcore_open/utils/gpx_export.dart';
|
||||
import 'package:meshcore_open/widgets/elements_ui.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:meshcore_open/services/sparse_location_logger.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
@@ -490,6 +491,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final latController = TextEditingController();
|
||||
final lonController = TextEditingController();
|
||||
final intervalController = TextEditingController();
|
||||
bool isLogging = connector.sparseLocationLogger?.isLogging() ?? false;
|
||||
|
||||
latController.text = connector.selfLatitude?.toStringAsFixed(6) ?? '';
|
||||
lonController.text = connector.selfLongitude?.toStringAsFixed(6) ?? '';
|
||||
|
||||
@@ -534,6 +537,20 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
signed: true,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FeatureToggleRow(
|
||||
title: "GPS Logging",
|
||||
subtitle: "Enable GPS logging on the device",
|
||||
value: isLogging,
|
||||
onChanged: (value) async {
|
||||
setDialogState(() => isLogging = value);
|
||||
if (value) {
|
||||
await connector.sparseLocationLogger?.startLogging();
|
||||
} else {
|
||||
await connector.sparseLocationLogger?.stopLogging();
|
||||
}
|
||||
},
|
||||
),
|
||||
if (hasGPS) ...[
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:gpx/gpx.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class SparseLocationLogger {
|
||||
static const double distanceThresholdMiles = 0.25;
|
||||
static const double distanceThresholdMeters =
|
||||
distanceThresholdMiles * 1609.34;
|
||||
static const double headingChangeThresholdDeg = 35.0;
|
||||
static const double minSpeedForTurnKmh = 8.0;
|
||||
static const double minTime = 120.0; // seconds
|
||||
|
||||
Position? _lastLoggedPosition;
|
||||
double? _lastHeading;
|
||||
DateTime? _lastLoggedTime;
|
||||
StreamSubscription<Position>? _positionStream;
|
||||
Timer? _timer;
|
||||
Function(Position position)? _onNewLogPoint;
|
||||
// GPX structures
|
||||
final Gpx _gpx = Gpx();
|
||||
Trkseg _currentSegment = Trkseg(); // one segment for the whole session
|
||||
|
||||
File? _gpxFile;
|
||||
|
||||
bool _isInitialized = false;
|
||||
|
||||
void initialize(Function(Position position) onNewLogPoint) {
|
||||
_onNewLogPoint = onNewLogPoint;
|
||||
}
|
||||
|
||||
Future<void> startLogging() async {
|
||||
// Permissions & service check (same as before)
|
||||
var status = await Permission.location.request();
|
||||
if (!status.isGranted) {
|
||||
print('Location permission denied');
|
||||
return;
|
||||
}
|
||||
|
||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
print('Location services disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare files
|
||||
final directory = await getApplicationDocumentsDirectory();
|
||||
final timestamp = DateTime.now()
|
||||
.toIso8601String()
|
||||
.replaceAll(':', '-')
|
||||
.split('.')
|
||||
.first;
|
||||
_gpxFile = File('${directory.path}/track_$timestamp.gpx');
|
||||
|
||||
// Init GPX metadata
|
||||
_gpx.metadata = Metadata(
|
||||
name: 'Sparse Track ${DateTime.now().toString().split(' ').first}',
|
||||
desc: 'Sparse GPS log: ~every 1.5 mi or significant turns',
|
||||
time: DateTime.now(),
|
||||
);
|
||||
|
||||
// Add one track with one segment
|
||||
final track = Trk(name: 'Main Track');
|
||||
_currentSegment = Trkseg();
|
||||
track.trksegs.add(_currentSegment);
|
||||
_gpx.trks.add(track);
|
||||
|
||||
_isInitialized = true;
|
||||
|
||||
// Start location stream
|
||||
_positionStream = Geolocator.getPositionStream(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
distanceFilter: 152, // meters (~0.16 mi) - helps battery
|
||||
),
|
||||
).listen(_onPositionReceived);
|
||||
|
||||
// Also poll via timer as fallback
|
||||
_timer = Timer.periodic(Duration(seconds: (minTime / 2).toInt()), (
|
||||
_,
|
||||
) async {
|
||||
final position = await Geolocator.getCurrentPosition();
|
||||
await _onPositionReceived(position);
|
||||
});
|
||||
|
||||
_lastLoggedPosition = null;
|
||||
_lastHeading = null;
|
||||
_lastLoggedTime = null;
|
||||
|
||||
print('Sparse GPX logging started → ${_gpxFile?.path}');
|
||||
}
|
||||
|
||||
Future<void> stopLogging() async {
|
||||
await _positionStream?.cancel();
|
||||
_positionStream = null;
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
|
||||
if (_isInitialized && _currentSegment.trkpts.isNotEmpty) {
|
||||
// Write GPX file on stop
|
||||
final xmlString = GpxWriter().asString(_gpx, pretty: true);
|
||||
|
||||
await _gpxFile?.writeAsString(xmlString);
|
||||
|
||||
final result = await SharePlus.instance.share(
|
||||
ShareParams(
|
||||
text: 'Sparse GPS track',
|
||||
subject: 'Sparse GPS track',
|
||||
files: [XFile(_gpxFile?.path ?? '')],
|
||||
),
|
||||
);
|
||||
|
||||
await _gpxFile?.delete();
|
||||
}
|
||||
|
||||
print('Logging stopped');
|
||||
}
|
||||
|
||||
Future<void> _onPositionReceived(Position position) async {
|
||||
final now = DateTime.now();
|
||||
final speedKmh = position.speed * 3.6;
|
||||
final heading = position.heading;
|
||||
|
||||
bool shouldLog = false;
|
||||
String reason = '';
|
||||
|
||||
if (_lastLoggedPosition == null) {
|
||||
shouldLog = true;
|
||||
reason = 'start';
|
||||
} else {
|
||||
final distanceMeters = Geolocator.distanceBetween(
|
||||
_lastLoggedPosition!.latitude,
|
||||
_lastLoggedPosition!.longitude,
|
||||
position.latitude,
|
||||
position.longitude,
|
||||
);
|
||||
|
||||
if (distanceMeters >= distanceThresholdMeters) {
|
||||
shouldLog = true;
|
||||
reason =
|
||||
'distance (${(distanceMeters / 1609.34).toStringAsFixed(2)} mi)';
|
||||
} else if (speedKmh > minSpeedForTurnKmh && _lastHeading != null) {
|
||||
double delta = (heading - _lastHeading!).abs();
|
||||
delta = math.min(delta, 360 - delta);
|
||||
if (delta > headingChangeThresholdDeg) {
|
||||
shouldLog = true;
|
||||
reason = 'turn (${delta.toStringAsFixed(1)}°)';
|
||||
}
|
||||
} else if (_lastLoggedTime != null) {
|
||||
final elapsed = now.difference(_lastLoggedTime!).inSeconds;
|
||||
if (elapsed >= minTime && distanceMeters >= distanceThresholdMeters) {
|
||||
shouldLog = true;
|
||||
reason = 'time (${elapsed}s)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldLog) {
|
||||
// Create GPX Waypoint (trkpt)
|
||||
final pt = Wpt(
|
||||
lat: position.latitude,
|
||||
lon: position.longitude,
|
||||
ele: position.altitude, // if available
|
||||
time: now,
|
||||
extensions: {
|
||||
"course": ?heading.isFinite ? heading : null,
|
||||
"speed": ?speedKmh > 0 ? speedKmh / 3.6 : null, // GPX speed in m/s
|
||||
},
|
||||
// You can add hdop, vdop, etc. from position if desired
|
||||
);
|
||||
|
||||
_currentSegment.trkpts.add(pt);
|
||||
_onNewLogPoint?.call(position);
|
||||
print('Logged point: ${pt.lat}, ${pt.lon} ($reason)');
|
||||
|
||||
_lastLoggedPosition = position;
|
||||
_lastHeading = heading;
|
||||
_lastLoggedTime = now;
|
||||
} else {
|
||||
print('Skipped point: ${position.latitude}, ${position.longitude}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> getGpxFilePath() async => _gpxFile?.path ?? 'Not started';
|
||||
bool isLogging() => _positionStream != null;
|
||||
int getPointCount() => _currentSegment.trkpts.length;
|
||||
}
|
||||
Reference in New Issue
Block a user