first commit
This commit is contained in:
246
lib/background/live_tracking_service.dart
Normal file
246
lib/background/live_tracking_service.dart
Normal file
@@ -0,0 +1,246 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:miler/utils/kalman_filter.dart';
|
||||
import 'package:miler/utils/mqtt_service.dart';
|
||||
import 'package:battery_plus/battery_plus.dart';
|
||||
|
||||
class LiveTrackingService {
|
||||
static final LiveTrackingService _instance = LiveTrackingService._internal();
|
||||
|
||||
factory LiveTrackingService() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
LiveTrackingService._internal();
|
||||
|
||||
StreamSubscription<Position>? _positionStreamSubscription;
|
||||
bool _isTracking = false;
|
||||
MilerKalmanFilter? _kf;
|
||||
DateTime? _lastUpdateTime;
|
||||
|
||||
void startTracking() async {
|
||||
if (_isTracking) return;
|
||||
|
||||
// Check permissions before starting
|
||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
debugPrint('[LIVE TRACKING] Location services are disabled.');
|
||||
return;
|
||||
}
|
||||
|
||||
LocationPermission permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied ||
|
||||
permission == LocationPermission.deniedForever) {
|
||||
debugPrint('[LIVE TRACKING] Location permission denied.');
|
||||
return;
|
||||
}
|
||||
|
||||
_isTracking = true;
|
||||
debugPrint('[LIVE TRACKING] Started high-frequency live tracking...');
|
||||
|
||||
final LocationSettings locationSettings = const LocationSettings(
|
||||
accuracy: LocationAccuracy
|
||||
.bestForNavigation, // High precision required for Kalman
|
||||
distanceFilter: 0, // Request updates regardless of distance moved
|
||||
);
|
||||
|
||||
// Some Android devices might drop points if we don't have a foreground service,
|
||||
// but the app already uses flutter_foreground_task elsewhere.
|
||||
_positionStreamSubscription =
|
||||
Geolocator.getPositionStream(
|
||||
locationSettings: locationSettings,
|
||||
).listen((Position? position) async {
|
||||
if (position != null) {
|
||||
// Implement your own custom debounce if it happens too fast, e.g limit to 2-3 secs
|
||||
_sendToKalmanBackend(position);
|
||||
}
|
||||
});
|
||||
|
||||
_positionStreamSubscription?.onError((error) {
|
||||
debugPrint('[LIVE TRACKING] Stream error: $error');
|
||||
});
|
||||
}
|
||||
|
||||
void stopTracking() {
|
||||
if (!_isTracking) return;
|
||||
debugPrint('[LIVE TRACKING] Stopped live tracking.');
|
||||
_positionStreamSubscription?.cancel();
|
||||
_positionStreamSubscription = null;
|
||||
_kf = null; // Reset filter state
|
||||
_lastUpdateTime = null;
|
||||
_isTracking = false;
|
||||
}
|
||||
|
||||
// Rate limiter details
|
||||
DateTime? _lastSentTime;
|
||||
|
||||
Future<void> _sendToKalmanBackend(Position position) async {
|
||||
final now = DateTime.now();
|
||||
// Only send if it has been at least 3 seconds
|
||||
if (_lastSentTime != null && now.difference(_lastSentTime!).inSeconds < 3) {
|
||||
return;
|
||||
}
|
||||
_lastSentTime = now;
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final userid = prefs.getInt('userId') ?? prefs.getInt('userid');
|
||||
final currentOrderId = prefs.getString('current_riding_order_id') ?? '';
|
||||
|
||||
// Decompose velocity for logging/backend reference
|
||||
final double headingRadians = position.heading * (math.pi / 180.0);
|
||||
final double velocityLng = position.speed * math.sin(headingRadians);
|
||||
final double velocityLat = position.speed * math.cos(headingRadians);
|
||||
|
||||
// --- APPLY LOCAL KALMAN FILTER ---
|
||||
double displayLat = position.latitude;
|
||||
double displayLng = position.longitude;
|
||||
|
||||
if (_kf == null) {
|
||||
// Initialize filter with first measurement
|
||||
_kf = MilerKalmanFilter(
|
||||
lat: position.latitude,
|
||||
lng: position.longitude,
|
||||
);
|
||||
} else {
|
||||
// Calculate dt (delta time) since last update
|
||||
final double dt = _lastUpdateTime != null
|
||||
? now.difference(_lastUpdateTime!).inMilliseconds / 1000.0
|
||||
: 3.0; // Default to 3s if unknown
|
||||
|
||||
_kf!.predict(dt);
|
||||
_kf!.update(position.latitude, position.longitude);
|
||||
|
||||
// Extract smoothed coordinates from state vector [lat, lng, v_lat, v_lng]
|
||||
displayLat = _kf!.x[0];
|
||||
displayLng = _kf!.x[1];
|
||||
}
|
||||
_lastUpdateTime = now;
|
||||
|
||||
final payload = {
|
||||
'userid': userid,
|
||||
'orderid': currentOrderId,
|
||||
'lat': displayLat, // Smoothed Latitude
|
||||
'lng': displayLng, // Smoothed Longitude
|
||||
'raw_lat':
|
||||
position.latitude, // Kept for server-side verification if needed
|
||||
'raw_lng': position.longitude,
|
||||
'speed': position.speed,
|
||||
'heading': position.heading,
|
||||
'velocity_lat': velocityLat,
|
||||
'velocity_lng': velocityLng,
|
||||
'timestamp': now.toIso8601String(),
|
||||
};
|
||||
|
||||
// --- TRACK ACTIVE PICKUPS DISTANCE ---
|
||||
final activePickupIds =
|
||||
prefs.getStringList('active_tracking_pickup_ids') ?? [];
|
||||
|
||||
for (String dId in activePickupIds) {
|
||||
try {
|
||||
final lastLatStr =
|
||||
prefs.getString('pickup_tracking_${dId}_lastLat') ?? '';
|
||||
final lastLngStr =
|
||||
prefs.getString('pickup_tracking_${dId}_lastLng') ?? '';
|
||||
final currentCumulativeKm =
|
||||
double.tryParse(
|
||||
prefs.getString('pickup_tracking_${dId}_cumulativeKm') ?? '0',
|
||||
) ??
|
||||
0.0;
|
||||
|
||||
if (lastLatStr.isNotEmpty && lastLngStr.isNotEmpty) {
|
||||
final lastLat = double.tryParse(lastLatStr) ?? 0.0;
|
||||
final lastLng = double.tryParse(lastLngStr) ?? 0.0;
|
||||
|
||||
if (lastLat != 0 &&
|
||||
lastLng != 0 &&
|
||||
displayLat != 0 &&
|
||||
displayLng != 0) {
|
||||
final distanceMeters = Geolocator.distanceBetween(
|
||||
lastLat,
|
||||
lastLng,
|
||||
displayLat,
|
||||
displayLng,
|
||||
);
|
||||
|
||||
if (distanceMeters >= 5.0) {
|
||||
// minimum 5 meters to reduce drift accumulation
|
||||
final distanceKm = distanceMeters / 1000.0;
|
||||
final newCumulativeKm = currentCumulativeKm + distanceKm;
|
||||
|
||||
await prefs.setString(
|
||||
'pickup_tracking_${dId}_cumulativeKm',
|
||||
newCumulativeKm.toStringAsFixed(4),
|
||||
);
|
||||
await prefs.setString(
|
||||
'pickup_tracking_${dId}_lastLat',
|
||||
displayLat.toString(),
|
||||
);
|
||||
await prefs.setString(
|
||||
'pickup_tracking_${dId}_lastLng',
|
||||
displayLng.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await prefs.setString(
|
||||
'pickup_tracking_${dId}_lastLat',
|
||||
displayLat.toString(),
|
||||
);
|
||||
await prefs.setString(
|
||||
'pickup_tracking_${dId}_lastLng',
|
||||
displayLng.toString(),
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// OPTION 1: MQTT PUBLICATION (Integrated)
|
||||
// -------------------------------------------------------------
|
||||
final mqttService = MilerMqttService();
|
||||
if (mqttService.isConnected) {
|
||||
mqttService.publishLocation(payload);
|
||||
debugPrint(
|
||||
'[LIVE TRACKING] Dispatched to MQTT: ${payload['lat']}, ${payload['lng']}',
|
||||
);
|
||||
} else {
|
||||
// Fallback or attempt reconnect if on-duty
|
||||
debugPrint(
|
||||
'[LIVE TRACKING] MQTT not connected. Skipping MQTT dispatch.',
|
||||
);
|
||||
mqttService.connect(); // Attempt to restore connection
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// OPTION 2: HTTP POST PLACEHOLDER (Wait for further instruction)
|
||||
// -------------------------------------------------------------
|
||||
// final url = Uri.parse('https://your-python-backend.com/kalman/track');
|
||||
// http.post(url, headers: {'Content-Type': 'application/json'}, body: jsonEncode(payload));
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// OPTION 3: TELEMETRY (Battery/System logs)
|
||||
// -------------------------------------------------------------
|
||||
if (now.minute % 5 == 0 && now.second < 5) {
|
||||
// Log every 5 mins
|
||||
final battery = Battery();
|
||||
final batteryLevel = await battery.batteryLevel;
|
||||
mqttService.publishTelemetry({
|
||||
'battery_level': batteryLevel,
|
||||
'gps_accuracy': position.accuracy,
|
||||
'mocked': position.isMocked,
|
||||
'timestamp': now.toIso8601String(),
|
||||
});
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'[LIVE TRACKING] Kalman processed: ${payload['lat']}, ${payload['lng']} (speed: ${position.speed})',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('[LIVE TRACKING] Error sending to backend: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user