| .. | .. |
|---|
| 2 | 2 | import 'dart:convert'; |
|---|
| 3 | 3 | import 'dart:io'; |
|---|
| 4 | 4 | |
|---|
| 5 | +import 'package:bonsoir/bonsoir.dart'; |
|---|
| 5 | 6 | import 'package:flutter/widgets.dart'; |
|---|
| 6 | 7 | import 'package:path_provider/path_provider.dart' as pp; |
|---|
| 7 | 8 | import 'package:mqtt_client/mqtt_client.dart'; |
|---|
| .. | .. |
|---|
| 102 | 103 | } |
|---|
| 103 | 104 | |
|---|
| 104 | 105 | final clientId = await _getClientId(); |
|---|
| 105 | | - final hosts = _getHosts(); |
|---|
| 106 | | - _mqttLog('MQTT: hosts=${hosts.join(", ")} port=${config.port}'); |
|---|
| 107 | 106 | |
|---|
| 108 | | - for (final host in hosts) { |
|---|
| 107 | + // Connection order: local → Bonjour → VPN → remote |
|---|
| 108 | + final attempts = <MapEntry<String, int>>[]; // host → timeout ms |
|---|
| 109 | + if (config.localHost != null && config.localHost!.isNotEmpty) { |
|---|
| 110 | + attempts.add(MapEntry(config.localHost!, 2500)); |
|---|
| 111 | + } |
|---|
| 112 | + // Bonjour placeholder — inserted dynamically below |
|---|
| 113 | + if (config.vpnHost != null && config.vpnHost!.isNotEmpty) { |
|---|
| 114 | + attempts.add(MapEntry(config.vpnHost!, 3000)); |
|---|
| 115 | + } |
|---|
| 116 | + if (config.host.isNotEmpty) { |
|---|
| 117 | + attempts.add(MapEntry(config.host, 5000)); |
|---|
| 118 | + } |
|---|
| 119 | + _mqttLog('MQTT: attempts=${attempts.map((e) => e.key).join(", ")} port=${config.port}'); |
|---|
| 120 | + |
|---|
| 121 | + // Try configured local host first |
|---|
| 122 | + for (final attempt in attempts) { |
|---|
| 109 | 123 | if (_intentionalClose) return; |
|---|
| 110 | | - |
|---|
| 111 | | - _mqttLog('MQTT: trying $host:${config.port}'); |
|---|
| 124 | + _mqttLog('MQTT: trying ${attempt.key}:${config.port}'); |
|---|
| 112 | 125 | try { |
|---|
| 113 | | - final connected = await _tryConnect( |
|---|
| 114 | | - host, |
|---|
| 115 | | - clientId, |
|---|
| 116 | | - timeout: host == hosts.first && hosts.length > 1 ? 2500 : 5000, |
|---|
| 117 | | - ); |
|---|
| 118 | | - _mqttLog('MQTT: $host result=$connected'); |
|---|
| 119 | | - if (connected) return; |
|---|
| 126 | + if (await _tryConnect(attempt.key, clientId, timeout: attempt.value)) return; |
|---|
| 120 | 127 | } catch (e) { |
|---|
| 121 | | - _mqttLog('MQTT: $host error=$e'); |
|---|
| 122 | | - continue; |
|---|
| 128 | + _mqttLog('MQTT: ${attempt.key} error=$e'); |
|---|
| 129 | + } |
|---|
| 130 | + |
|---|
| 131 | + // After local host fails, try Bonjour discovery before VPN/remote |
|---|
| 132 | + if (attempt.key == config.localHost && !_intentionalClose) { |
|---|
| 133 | + _mqttLog('MQTT: trying Bonjour discovery...'); |
|---|
| 134 | + final bonjourHost = await _discoverViaMdns(); |
|---|
| 135 | + if (bonjourHost != null && !_intentionalClose) { |
|---|
| 136 | + _mqttLog('MQTT: Bonjour found $bonjourHost'); |
|---|
| 137 | + try { |
|---|
| 138 | + if (await _tryConnect(bonjourHost, clientId, timeout: 3000)) return; |
|---|
| 139 | + } catch (e) { |
|---|
| 140 | + _mqttLog('MQTT: Bonjour host $bonjourHost error=$e'); |
|---|
| 141 | + } |
|---|
| 142 | + } else { |
|---|
| 143 | + _mqttLog('MQTT: Bonjour discovery returned nothing'); |
|---|
| 144 | + } |
|---|
| 123 | 145 | } |
|---|
| 124 | 146 | } |
|---|
| 125 | 147 | |
|---|
| 126 | 148 | // All hosts failed — retry after delay |
|---|
| 127 | | - _mqttLog('MQTT: all hosts failed, retrying in 5s'); |
|---|
| 149 | + _mqttLog('MQTT: all attempts failed, retrying in 5s'); |
|---|
| 128 | 150 | _setStatus(ConnectionStatus.reconnecting); |
|---|
| 129 | 151 | Future.delayed(const Duration(seconds: 5), () { |
|---|
| 130 | 152 | if (!_intentionalClose && _status != ConnectionStatus.connected) { |
|---|
| .. | .. |
|---|
| 133 | 155 | }); |
|---|
| 134 | 156 | } |
|---|
| 135 | 157 | |
|---|
| 136 | | - /// Returns [localHost, remoteHost] for dual-connect attempts. |
|---|
| 137 | | - List<String> _getHosts() { |
|---|
| 138 | | - if (config.localHost != null && |
|---|
| 139 | | - config.localHost!.isNotEmpty && |
|---|
| 140 | | - config.localHost != config.host) { |
|---|
| 141 | | - return [config.localHost!, config.host]; |
|---|
| 158 | + /// Discover AIBroker on local network via Bonjour/mDNS. |
|---|
| 159 | + /// Returns the IP address or null if not found within timeout. |
|---|
| 160 | + Future<String?> _discoverViaMdns({Duration timeout = const Duration(seconds: 3)}) async { |
|---|
| 161 | + try { |
|---|
| 162 | + final discovery = BonsoirDiscovery(type: '_mqtt._tcp'); |
|---|
| 163 | + await discovery.initialize(); |
|---|
| 164 | + |
|---|
| 165 | + final completer = Completer<String?>(); |
|---|
| 166 | + StreamSubscription? sub; |
|---|
| 167 | + |
|---|
| 168 | + sub = discovery.eventStream?.listen((event) { |
|---|
| 169 | + switch (event) { |
|---|
| 170 | + case BonsoirDiscoveryServiceResolvedEvent(): |
|---|
| 171 | + final ip = event.service.host; |
|---|
| 172 | + _mqttLog('MQTT: Bonjour resolved: ${event.service.name} at $ip:${event.service.port}'); |
|---|
| 173 | + if (ip != null && ip.isNotEmpty && !completer.isCompleted) { |
|---|
| 174 | + completer.complete(ip); |
|---|
| 175 | + } |
|---|
| 176 | + case BonsoirDiscoveryServiceFoundEvent(): |
|---|
| 177 | + _mqttLog('MQTT: Bonjour found: ${event.service.name}'); |
|---|
| 178 | + default: |
|---|
| 179 | + break; |
|---|
| 180 | + } |
|---|
| 181 | + }); |
|---|
| 182 | + |
|---|
| 183 | + await discovery.start(); |
|---|
| 184 | + |
|---|
| 185 | + final ip = await completer.future.timeout(timeout, onTimeout: () => null); |
|---|
| 186 | + |
|---|
| 187 | + await sub?.cancel(); |
|---|
| 188 | + await discovery.stop(); |
|---|
| 189 | + |
|---|
| 190 | + return ip; |
|---|
| 191 | + } catch (e) { |
|---|
| 192 | + _mqttLog('MQTT: Bonjour discovery error: $e'); |
|---|
| 193 | + return null; |
|---|
| 142 | 194 | } |
|---|
| 143 | | - return [config.host]; |
|---|
| 144 | 195 | } |
|---|
| 145 | 196 | |
|---|
| 146 | 197 | Future<bool> _tryConnect(String host, String clientId, {int timeout = 5000}) async { |
|---|