Design system - MilerSurface ladder (canvas → working → raised → floating) with MilerPanel as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1. - Visible vocabulary applied across Home, Deliveries, Activity, Account and the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x), canvas wells for anything that opens, small filled tags for shelf labels, demoted placeholders. Recorded in DESIGN_SYSTEM.md §6. - One icon family: 222 Material glyphs migrated to Lucide; none left outside lib/xpress. - Colour semantics corrected: amber only for what is genuinely owed, brand red reserved for the live stop, disabled primaries go neutral rather than pale. Data and lifecycle - lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart makes admin sequence the single ordering authority; service_day.dart, and stop_area.dart rewritten against live Coimbatore addresses (digit-token stripping, city stoplist, street suffixes, stammer collapse). - countLabel states the load once, in bags. Testing - 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity, sheets and verify, with test/failures/ now gitignored (diff debris). - New pins: home_gutter_test, stop_area_test, plus updated structural bounds. Note: this commit also carries pre-existing working-tree deletions that were present before this work (API_SPEC.md, README.md, demo test fixtures). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
214 lines
6.3 KiB
Dart
214 lines
6.3 KiB
Dart
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:miler/xpress/views/helpers/constants/mqtt_constants.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
class DoormileMqttService {
|
|
static final DoormileMqttService _instance = DoormileMqttService._internal();
|
|
|
|
factory DoormileMqttService() {
|
|
return _instance;
|
|
}
|
|
|
|
DoormileMqttService._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 = 'doormile/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');
|
|
}
|
|
}
|