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? _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 _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'); } } }