| .. | .. |
|---|
| 156 | 156 | } |
|---|
| 157 | 157 | |
|---|
| 158 | 158 | /// Discover AIBroker on local network via Bonjour/mDNS. |
|---|
| 159 | + /// Falls back to subnet scan if Bonjour fails (iOS blocks mDNS on Personal Hotspot). |
|---|
| 159 | 160 | /// Returns the IP address or null if not found within timeout. |
|---|
| 160 | 161 | Future<String?> _discoverViaMdns({Duration timeout = const Duration(seconds: 3)}) async { |
|---|
| 162 | + // Try Bonjour first |
|---|
| 161 | 163 | try { |
|---|
| 162 | 164 | final discovery = BonsoirDiscovery(type: '_mqtt._tcp'); |
|---|
| 163 | 165 | await discovery.initialize(); |
|---|
| .. | .. |
|---|
| 187 | 189 | await sub?.cancel(); |
|---|
| 188 | 190 | await discovery.stop(); |
|---|
| 189 | 191 | |
|---|
| 190 | | - return ip; |
|---|
| 192 | + if (ip != null) return ip; |
|---|
| 191 | 193 | } catch (e) { |
|---|
| 192 | 194 | _mqttLog('MQTT: Bonjour discovery error: $e'); |
|---|
| 195 | + } |
|---|
| 196 | + |
|---|
| 197 | + // Fallback: scan local subnet for MQTT port (handles Personal Hotspot) |
|---|
| 198 | + _mqttLog('MQTT: Bonjour failed, trying subnet scan...'); |
|---|
| 199 | + return _scanSubnetForMqtt(); |
|---|
| 200 | + } |
|---|
| 201 | + |
|---|
| 202 | + /// Scan the local subnet for an MQTT broker by probing the configured port. |
|---|
| 203 | + /// Useful when iOS Personal Hotspot blocks mDNS. |
|---|
| 204 | + Future<String?> _scanSubnetForMqtt() async { |
|---|
| 205 | + try { |
|---|
| 206 | + // Get device's own IP to determine the subnet |
|---|
| 207 | + final interfaces = await NetworkInterface.list(type: InternetAddressType.IPv4); |
|---|
| 208 | + for (final iface in interfaces) { |
|---|
| 209 | + for (final addr in iface.addresses) { |
|---|
| 210 | + final parts = addr.address.split('.'); |
|---|
| 211 | + if (parts.length != 4) continue; |
|---|
| 212 | + // Skip loopback |
|---|
| 213 | + if (parts[0] == '127') continue; |
|---|
| 214 | + // Only scan small subnets (hotspot = /28, max 14 hosts) |
|---|
| 215 | + final subnet = '${parts[0]}.${parts[1]}.${parts[2]}'; |
|---|
| 216 | + _mqttLog('MQTT: scanning $subnet.0/24 on ${iface.name}'); |
|---|
| 217 | + |
|---|
| 218 | + // Probe hosts 1-14 in parallel (covers /28 hotspot subnet) |
|---|
| 219 | + final futures = <Future<String?>>[]; |
|---|
| 220 | + for (int i = 1; i <= 14; i++) { |
|---|
| 221 | + final probe = '$subnet.$i'; |
|---|
| 222 | + if (probe == addr.address) continue; // skip self |
|---|
| 223 | + futures.add(_probeHost(probe, config.port)); |
|---|
| 224 | + } |
|---|
| 225 | + |
|---|
| 226 | + final results = await Future.wait(futures); |
|---|
| 227 | + final found = results.firstWhere((r) => r != null, orElse: () => null); |
|---|
| 228 | + if (found != null) { |
|---|
| 229 | + _mqttLog('MQTT: subnet scan found broker at $found'); |
|---|
| 230 | + return found; |
|---|
| 231 | + } |
|---|
| 232 | + } |
|---|
| 233 | + } |
|---|
| 234 | + } catch (e) { |
|---|
| 235 | + _mqttLog('MQTT: subnet scan error: $e'); |
|---|
| 236 | + } |
|---|
| 237 | + return null; |
|---|
| 238 | + } |
|---|
| 239 | + |
|---|
| 240 | + /// Probe a single host:port with a TCP connection attempt (1s timeout). |
|---|
| 241 | + Future<String?> _probeHost(String host, int port) async { |
|---|
| 242 | + try { |
|---|
| 243 | + final socket = await Socket.connect(host, port, |
|---|
| 244 | + timeout: const Duration(seconds: 1)); |
|---|
| 245 | + await socket.close(); |
|---|
| 246 | + return host; |
|---|
| 247 | + } catch (_) { |
|---|
| 193 | 248 | return null; |
|---|
| 194 | 249 | } |
|---|
| 195 | 250 | } |
|---|