| .. | .. |
|---|
| 10 | 10 | import 'package:uuid/uuid.dart'; |
|---|
| 11 | 11 | |
|---|
| 12 | 12 | import '../models/server_config.dart'; |
|---|
| 13 | | -import 'websocket_service.dart' show ConnectionStatus; |
|---|
| 14 | 13 | import 'wol_service.dart'; |
|---|
| 14 | + |
|---|
| 15 | +/// Connection status for the MQTT client. |
|---|
| 16 | +enum ConnectionStatus { |
|---|
| 17 | + disconnected, |
|---|
| 18 | + connecting, |
|---|
| 19 | + connected, |
|---|
| 20 | + reconnecting, |
|---|
| 21 | +} |
|---|
| 15 | 22 | |
|---|
| 16 | 23 | // Debug log to file (survives release builds) |
|---|
| 17 | 24 | Future<void> _mqttLog(String msg) async { |
|---|
| .. | .. |
|---|
| 23 | 30 | } catch (_) {} |
|---|
| 24 | 31 | } |
|---|
| 25 | 32 | |
|---|
| 26 | | -/// MQTT client for PAILot, replacing WebSocketService. |
|---|
| 33 | +/// MQTT client for PAILot. |
|---|
| 27 | 34 | /// |
|---|
| 28 | 35 | /// Connects to the AIBroker daemon's embedded aedes broker. |
|---|
| 29 | 36 | /// Subscribes to all pailot/ topics and dispatches messages |
|---|
| 30 | | -/// through the same callback interface as WebSocketService. |
|---|
| 37 | +/// through the onMessage callback interface. |
|---|
| 31 | 38 | class MqttService with WidgetsBindingObserver { |
|---|
| 32 | 39 | MqttService({required this.config}); |
|---|
| 33 | 40 | |
|---|
| .. | .. |
|---|
| 43 | 50 | final List<String> _seenMsgIdOrder = []; |
|---|
| 44 | 51 | static const int _maxSeenIds = 500; |
|---|
| 45 | 52 | |
|---|
| 46 | | - // Callbacks — same interface as WebSocketService |
|---|
| 53 | + // Callbacks |
|---|
| 47 | 54 | void Function(ConnectionStatus status)? onStatusChanged; |
|---|
| 48 | 55 | void Function(Map<String, dynamic> message)? onMessage; |
|---|
| 49 | 56 | void Function()? onOpen; |
|---|
| .. | .. |
|---|
| 149 | 156 | client.onAutoReconnect = _onAutoReconnect; |
|---|
| 150 | 157 | client.onAutoReconnected = _onAutoReconnected; |
|---|
| 151 | 158 | |
|---|
| 152 | | - // Persistent session: broker queues QoS 1 messages while client is offline |
|---|
| 159 | + // Clean session: we handle offline delivery ourselves via catch_up protocol. |
|---|
| 160 | + // Persistent sessions cause the broker to flood all queued QoS 1 messages |
|---|
| 161 | + // on reconnect, which overwhelms the client with large voice payloads. |
|---|
| 153 | 162 | final connMessage = MqttConnectMessage() |
|---|
| 154 | 163 | .withClientIdentifier(clientId) |
|---|
| 164 | + .startClean() |
|---|
| 155 | 165 | .authenticateAs('pailot', config.mqttToken ?? ''); |
|---|
| 156 | 166 | |
|---|
| 157 | 167 | client.connectionMessage = connMessage; |
|---|
| .. | .. |
|---|
| 268 | 278 | |
|---|
| 269 | 279 | /// Route incoming MQTT messages to the onMessage callback. |
|---|
| 270 | 280 | /// Translates MQTT topic structure into the flat message format |
|---|
| 271 | | - /// that chat_screen expects (same as WebSocket messages). |
|---|
| 281 | + /// that chat_screen expects. |
|---|
| 272 | 282 | void _dispatchMessage(String topic, Map<String, dynamic> json) { |
|---|
| 273 | 283 | final parts = topic.split('/'); |
|---|
| 274 | 284 | |
|---|
| .. | .. |
|---|
| 369 | 379 | } |
|---|
| 370 | 380 | |
|---|
| 371 | 381 | /// Send a message — routes to the appropriate MQTT topic based on content. |
|---|
| 372 | | - /// Accepts the same message format as WebSocketService.send(). |
|---|
| 373 | 382 | void send(Map<String, dynamic> message) { |
|---|
| 374 | 383 | final type = message['type'] as String?; |
|---|
| 375 | 384 | final sessionId = message['sessionId'] as String?; |
|---|