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