initial commit: push everything
This commit is contained in:
67
lib/utils/device.dart
Normal file
67
lib/utils/device.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class DeviceUtils {
|
||||
static const String _deviceIdKey = 'deviceId';
|
||||
static const String _fcmTokenKey = 'fcmToken';
|
||||
|
||||
static Future<String> ensureDeviceId(SharedPreferences prefs) async {
|
||||
final String? existing = prefs.getString(_deviceIdKey);
|
||||
if (existing != null && existing.isNotEmpty) {
|
||||
if (kDebugMode) print('[DEVICE] Using cached device ID: $existing');
|
||||
return existing;
|
||||
}
|
||||
try {
|
||||
final deviceInfo = DeviceInfoPlugin();
|
||||
final android = await deviceInfo.androidInfo;
|
||||
final String androidId = android.id;
|
||||
if (androidId.isNotEmpty) {
|
||||
await prefs.setString(_deviceIdKey, androidId);
|
||||
return androidId;
|
||||
} else {
|
||||
throw Exception('Android ID is empty');
|
||||
}
|
||||
} on PlatformException catch (e) {
|
||||
throw Exception('Failed to get device ID: ${e.message}');
|
||||
} catch (e) {
|
||||
throw Exception('Failed to get device ID: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<String> ensureFcmToken(SharedPreferences prefs) async {
|
||||
try {
|
||||
final String? existing = prefs.getString(_fcmTokenKey);
|
||||
if (existing != null && existing.isNotEmpty) {
|
||||
return existing;
|
||||
}
|
||||
if (Firebase.apps.isEmpty) {
|
||||
try {
|
||||
await Firebase.initializeApp();
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
final FirebaseMessaging messaging = FirebaseMessaging.instance;
|
||||
final NotificationSettings settings = await messaging.requestPermission(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
if (settings.authorizationStatus == AuthorizationStatus.authorized ||
|
||||
settings.authorizationStatus == AuthorizationStatus.provisional) {
|
||||
final String? token = await messaging.getToken();
|
||||
if (token != null && token.isNotEmpty) {
|
||||
await prefs.setString(_fcmTokenKey, token);
|
||||
return token;
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
231
lib/utils/kalman_filter.dart
Normal file
231
lib/utils/kalman_filter.dart
Normal file
@@ -0,0 +1,231 @@
|
||||
import 'dart:math';
|
||||
|
||||
/// A simple 4D Kalman Filter implementation for GPS smoothing.
|
||||
/// State vector x = [lat, lng, velocity_lat, velocity_lng]
|
||||
class NearleKalmanFilter {
|
||||
late List<double> x; // State estimate
|
||||
late List<List<double>> P; // Covariance matrix
|
||||
late List<List<double>> F; // State transition matrix
|
||||
late List<List<double>> H; // Measurement matrix
|
||||
late List<List<double>> R; // Measurement noise covariance
|
||||
late List<List<double>> Q; // Process noise covariance
|
||||
|
||||
NearleKalmanFilter({
|
||||
required double lat,
|
||||
required double lng,
|
||||
}) {
|
||||
// Initial state: [lat, lng, 0, 0]
|
||||
x = [lat, lng, 0, 0];
|
||||
|
||||
// Initial covariance: High uncertainty for initial velocity
|
||||
P = [
|
||||
[1.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 1.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 1000.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 1000.0],
|
||||
];
|
||||
|
||||
// State transition matrix (assuming dt = 1 for simplicity, will update in predict)
|
||||
F = [
|
||||
[1.0, 0.0, 1.0, 0.0],
|
||||
[0.0, 1.0, 0.0, 1.0],
|
||||
[0.0, 0.0, 1.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
];
|
||||
|
||||
// Measurement matrix: We only measure lat and lng
|
||||
H = [
|
||||
[1.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 1.0, 0.0, 0.0],
|
||||
];
|
||||
|
||||
// Measurement noise: GPS is typically accurate to ~5-10 meters.
|
||||
// In degrees, this is roughly 0.0001
|
||||
R = [
|
||||
[0.00001, 0.0],
|
||||
[0.0, 0.00001],
|
||||
];
|
||||
|
||||
// Process noise: How much we trust our prediction vs measurement
|
||||
Q = [
|
||||
[0.00001, 0.0, 0.0, 0.0],
|
||||
[0.0, 0.00001, 0.0, 0.0],
|
||||
[0.0001, 0.0, 0.0001, 0.0],
|
||||
[0.0, 0.0001, 0.0, 0.0001],
|
||||
];
|
||||
}
|
||||
|
||||
/// Predict the next state
|
||||
void predict(double dt) {
|
||||
// Update F based on dt
|
||||
F[0][2] = dt;
|
||||
F[1][3] = dt;
|
||||
|
||||
// x = F * x
|
||||
final newX = List<double>.filled(4, 0);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
newX[i] += F[i][j] * x[j];
|
||||
}
|
||||
}
|
||||
x = newX;
|
||||
|
||||
// P = F * P * F^T + Q
|
||||
final FP = _multiply4x4(F, P);
|
||||
final F_T = _transpose4x4(F);
|
||||
final FPF_T = _multiply4x4(FP, F_T);
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
P[i][j] = FPF_T[i][j] + Q[i][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the state with a new measurement
|
||||
void update(double measuredLat, double measuredLng) {
|
||||
final z = [measuredLat, measuredLng];
|
||||
|
||||
// y = z - H * x (Innovation)
|
||||
final y = [
|
||||
z[0] - (H[0][0] * x[0] + H[0][1] * x[1] + H[0][2] * x[2] + H[0][3] * x[3]),
|
||||
z[1] - (H[1][0] * x[0] + H[1][1] * x[1] + H[1][2] * x[2] + H[1][3] * x[3]),
|
||||
];
|
||||
|
||||
// S = H * P * H^T + R (Innovation covariance)
|
||||
// H is 2x4, P is 4x4, H^T is 4x2
|
||||
final HP = _multiply2x4_4x4(H, P);
|
||||
final H_T = _transpose2x4(H);
|
||||
final HPH_T = _multiply2x4_4x2(HP, H_T);
|
||||
|
||||
final S = [
|
||||
[HPH_T[0][0] + R[0][0], HPH_T[0][1] + R[0][1]],
|
||||
[HPH_T[1][0] + R[1][0], HPH_T[1][1] + R[1][1]],
|
||||
];
|
||||
|
||||
// K = P * H^T * S^-1 (Kalman gain)
|
||||
final Sinv = _inverse2x2(S);
|
||||
final PH_T = _multiply4x4_4x2(P, H_T);
|
||||
final K = _multiply4x2_2x2(PH_T, Sinv);
|
||||
|
||||
// x = x + K * y
|
||||
for (int i = 0; i < 4; i++) {
|
||||
x[i] += K[i][0] * y[0] + K[i][1] * y[1];
|
||||
}
|
||||
|
||||
// P = (I - K * H) * P
|
||||
final KH = _multiply4x2_2x4(K, H);
|
||||
final I = [
|
||||
[1.0, 0.0, 0.0, 0.0],
|
||||
[0.0, 1.0, 0.0, 0.0],
|
||||
[0.0, 0.0, 1.0, 0.0],
|
||||
[0.0, 0.0, 0.0, 1.0],
|
||||
];
|
||||
|
||||
final I_KH = List.generate(4, (i) => List.generate(4, (j) => I[i][j] - KH[i][j]));
|
||||
P = _multiply4x4(I_KH, P);
|
||||
}
|
||||
|
||||
// --- Helper Math Functions ---
|
||||
|
||||
List<List<double>> _multiply4x4(List<List<double>> A, List<List<double>> B) {
|
||||
final C = List.generate(4, (_) => List<double>.filled(4, 0));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
for (int k = 0; k < 4; k++) {
|
||||
C[i][j] += A[i][k] * B[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
return C;
|
||||
}
|
||||
|
||||
List<List<double>> _transpose4x4(List<List<double>> A) {
|
||||
final C = List.generate(4, (_) => List<double>.filled(4, 0));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
C[i][j] = A[j][i];
|
||||
}
|
||||
}
|
||||
return C;
|
||||
}
|
||||
|
||||
List<List<double>> _multiply2x4_4x4(List<List<double>> A, List<List<double>> B) {
|
||||
final C = List.generate(2, (_) => List<double>.filled(4, 0));
|
||||
for (int i = 0; i < 2; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
for (int k = 0; k < 4; k++) {
|
||||
C[i][j] += A[i][k] * B[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
return C;
|
||||
}
|
||||
|
||||
List<List<double>> _transpose2x4(List<List<double>> A) {
|
||||
final C = List.generate(4, (_) => List<double>.filled(2, 0));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 2; j++) {
|
||||
C[i][j] = A[j][i];
|
||||
}
|
||||
}
|
||||
return C;
|
||||
}
|
||||
|
||||
List<List<double>> _multiply2x4_4x2(List<List<double>> A, List<List<double>> B) {
|
||||
final C = List.generate(2, (_) => List<double>.filled(2, 0));
|
||||
for (int i = 0; i < 2; i++) {
|
||||
for (int j = 0; j < 2; j++) {
|
||||
for (int k = 0; k < 4; k++) {
|
||||
C[i][j] += A[i][k] * B[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
return C;
|
||||
}
|
||||
|
||||
List<List<double>> _inverse2x2(List<List<double>> A) {
|
||||
final det = A[0][0] * A[1][1] - A[0][1] * A[1][0];
|
||||
if (det == 0) return [[1, 0], [0, 1]]; // Should not happen with noise
|
||||
return [
|
||||
[A[1][1] / det, -A[0][1] / det],
|
||||
[-A[1][0] / det, A[0][0] / det],
|
||||
];
|
||||
}
|
||||
|
||||
List<List<double>> _multiply4x4_4x2(List<List<double>> A, List<List<double>> B) {
|
||||
final C = List.generate(4, (_) => List<double>.filled(2, 0));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 2; j++) {
|
||||
for (int k = 0; k < 4; k++) {
|
||||
C[i][j] += A[i][k] * B[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
return C;
|
||||
}
|
||||
|
||||
List<List<double>> _multiply4x2_2x2(List<List<double>> A, List<List<double>> B) {
|
||||
final C = List.generate(4, (_) => List<double>.filled(2, 0));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 2; j++) {
|
||||
for (int k = 0; k < 2; k++) {
|
||||
C[i][j] += A[i][k] * B[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
return C;
|
||||
}
|
||||
|
||||
List<List<double>> _multiply4x2_2x4(List<List<double>> K, List<List<double>> H) {
|
||||
final C = List.generate(4, (_) => List<double>.filled(4, 0));
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
for (int k = 0; k < 2; k++) {
|
||||
C[i][j] += K[i][k] * H[k][j];
|
||||
}
|
||||
}
|
||||
}
|
||||
return C;
|
||||
}
|
||||
}
|
||||
195
lib/utils/mqtt_service.dart
Normal file
195
lib/utils/mqtt_service.dart
Normal file
@@ -0,0 +1,195 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:mqtt_client/mqtt_client.dart';
|
||||
import 'package:mqtt_client/mqtt_server_client.dart';
|
||||
import 'package:nearle/views/helpers/constants/mqtt_constants.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class NearleMqttService {
|
||||
static final NearleMqttService _instance = NearleMqttService._internal();
|
||||
|
||||
factory NearleMqttService() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
NearleMqttService._internal();
|
||||
|
||||
MqttServerClient? _client;
|
||||
bool _isConnected = false;
|
||||
bool _isConnecting = false; // Prevents concurrent connection attempts
|
||||
String? _currentRiderId;
|
||||
|
||||
// Unique per isolate startup — prevents client ID collision between
|
||||
// main isolate and background foreground-service isolate.
|
||||
static final String _sessionSuffix =
|
||||
DateTime.now().millisecondsSinceEpoch.toRadixString(36);
|
||||
|
||||
bool get isConnected => _isConnected;
|
||||
|
||||
Future<void> connect() async {
|
||||
if (_isConnected || _isConnecting) return;
|
||||
_isConnecting = true;
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final riderIdLong = prefs.getInt('userId') ?? prefs.getInt('userid');
|
||||
if (riderIdLong == null || riderIdLong <= 0) {
|
||||
debugPrint('[MQTT] Cannot connect: No rider ID found in preferences.');
|
||||
return;
|
||||
}
|
||||
_currentRiderId = riderIdLong.toString();
|
||||
|
||||
const host = MqttConstants.brokerHost;
|
||||
final clientId = 'rider_${_currentRiderId}_$_sessionSuffix';
|
||||
|
||||
_client = MqttServerClient(host, clientId);
|
||||
_client!.port = MqttConstants.brokerPort;
|
||||
_client!.keepAlivePeriod = 30;
|
||||
_client!.autoReconnect = true;
|
||||
_client!.logging(on: false);
|
||||
|
||||
final lwtTopic =
|
||||
MqttConstants.topicRiderStatus.replaceAll('{riderId}', _currentRiderId!);
|
||||
_client!.onDisconnected = _onDisconnected;
|
||||
_client!.onConnected = _onConnected;
|
||||
_client!.onAutoReconnect = _onAutoReconnect;
|
||||
_client!.onSubscribed = _onSubscribed;
|
||||
|
||||
final connMessage = MqttConnectMessage()
|
||||
.withClientIdentifier(clientId)
|
||||
.authenticateAs(MqttConstants.username, MqttConstants.passwordString)
|
||||
.withWillTopic(lwtTopic)
|
||||
.withWillMessage(MqttConstants.statusOffline)
|
||||
.withWillQos(MqttQos.atLeastOnce)
|
||||
.withWillRetain()
|
||||
.startClean();
|
||||
|
||||
_client!.connectionMessage = connMessage;
|
||||
|
||||
debugPrint('[MQTT] Connecting to $host as $clientId...');
|
||||
await _client!.connect();
|
||||
} catch (e) {
|
||||
debugPrint('[MQTT] Connection failed: $e');
|
||||
_cleanDisconnect();
|
||||
} finally {
|
||||
_isConnecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Cleanly disconnects and resets all state. Call on logout or duty end.
|
||||
void disconnect() {
|
||||
_publishStatus(MqttConstants.statusOffline);
|
||||
_cleanDisconnect();
|
||||
debugPrint('[MQTT] Disconnected and state cleared.');
|
||||
}
|
||||
|
||||
void _cleanDisconnect() {
|
||||
_client?.disconnect();
|
||||
_client = null;
|
||||
_isConnected = false;
|
||||
_currentRiderId = null;
|
||||
}
|
||||
|
||||
void _onConnected() {
|
||||
_isConnected = true;
|
||||
debugPrint('[MQTT] Connected successfully.');
|
||||
_publishStatus(MqttConstants.statusOnline);
|
||||
}
|
||||
|
||||
void _onDisconnected() {
|
||||
_isConnected = false;
|
||||
debugPrint('[MQTT] Disconnected from broker.');
|
||||
}
|
||||
|
||||
void _onAutoReconnect() {
|
||||
debugPrint('[MQTT] Auto-reconnecting...');
|
||||
}
|
||||
|
||||
void _onSubscribed(String topic) {
|
||||
debugPrint('[MQTT] Subscribed to topic: $topic');
|
||||
}
|
||||
|
||||
void _publishStatus(String status) {
|
||||
if (!_isConnected || _currentRiderId == null) return;
|
||||
|
||||
final topic =
|
||||
MqttConstants.topicRiderStatus.replaceAll('{riderId}', _currentRiderId!);
|
||||
final builder = MqttClientPayloadBuilder();
|
||||
builder.addString(status);
|
||||
|
||||
_client!.publishMessage(topic, MqttQos.atLeastOnce, builder.payload!,
|
||||
retain: true);
|
||||
debugPrint('[MQTT] Published status: $status to $topic');
|
||||
}
|
||||
|
||||
// --- PUBLIC API ---
|
||||
|
||||
void updateStatus(String status) {
|
||||
_publishStatus(status);
|
||||
}
|
||||
|
||||
void publishProfile(Map<String, dynamic> profileData) {
|
||||
if (!_isConnected || _currentRiderId == null) return;
|
||||
|
||||
final topic =
|
||||
MqttConstants.topicRiderProfile.replaceAll('{riderId}', _currentRiderId!);
|
||||
final builder = MqttClientPayloadBuilder();
|
||||
builder.addString(jsonEncode(profileData));
|
||||
|
||||
_client!.publishMessage(topic, MqttQos.atLeastOnce, builder.payload!,
|
||||
retain: true);
|
||||
debugPrint('[MQTT] Published profile to $topic');
|
||||
}
|
||||
|
||||
void publishLocation(Map<String, dynamic> locationData) {
|
||||
if (!_isConnected || _currentRiderId == null) return;
|
||||
|
||||
final topic =
|
||||
MqttConstants.topicRiderLocation.replaceAll('{riderId}', _currentRiderId!);
|
||||
final builder = MqttClientPayloadBuilder();
|
||||
builder.addString(jsonEncode(locationData));
|
||||
|
||||
_client!.publishMessage(topic, MqttQos.atMostOnce, builder.payload!);
|
||||
}
|
||||
|
||||
void publishTelemetry(Map<String, dynamic> telemetryData) {
|
||||
if (!_isConnected || _currentRiderId == null) return;
|
||||
|
||||
final topic =
|
||||
MqttConstants.topicRiderTelemetry.replaceAll('{riderId}', _currentRiderId!);
|
||||
final builder = MqttClientPayloadBuilder();
|
||||
builder.addString(jsonEncode(telemetryData));
|
||||
|
||||
_client!.publishMessage(topic, MqttQos.atLeastOnce, builder.payload!);
|
||||
debugPrint('[MQTT] Published telemetry data.');
|
||||
}
|
||||
|
||||
void publishLog(String eventName, Map<String, dynamic> data) {
|
||||
if (!_isConnected || _currentRiderId == null) return;
|
||||
|
||||
final topic =
|
||||
'${MqttConstants.topicRiderLogs.replaceAll('{riderId}', _currentRiderId!)}/$eventName';
|
||||
final builder = MqttClientPayloadBuilder();
|
||||
builder.addString(jsonEncode(data));
|
||||
|
||||
_client!.publishMessage(topic, MqttQos.atLeastOnce, builder.payload!);
|
||||
debugPrint('[MQTT] Published log: $eventName');
|
||||
}
|
||||
|
||||
void publish(String subTopic, dynamic data) {
|
||||
if (!_isConnected || _currentRiderId == null) return;
|
||||
|
||||
final topic = 'nearle/riders/$_currentRiderId/$subTopic';
|
||||
final builder = MqttClientPayloadBuilder();
|
||||
if (data is String) {
|
||||
builder.addString(data);
|
||||
} else {
|
||||
builder.addString(jsonEncode(data));
|
||||
}
|
||||
|
||||
_client!.publishMessage(topic, MqttQos.atLeastOnce, builder.payload!);
|
||||
debugPrint('[MQTT] Published to $topic');
|
||||
}
|
||||
}
|
||||
1
lib/utils/no_internet.json
Normal file
1
lib/utils/no_internet.json
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user