Matthias Nott
2026-03-25 9e5953ced9c02f883203c8e49b2c7a78333ef34b
feat: subnet scan fallback when Bonjour fails (handles iOS Personal Hotspot)
1 files modified
changed files
lib/services/mqtt_service.dart patch | view | blame | history
lib/services/mqtt_service.dart
....@@ -156,8 +156,10 @@
156156 }
157157
158158 /// Discover AIBroker on local network via Bonjour/mDNS.
159
+ /// Falls back to subnet scan if Bonjour fails (iOS blocks mDNS on Personal Hotspot).
159160 /// Returns the IP address or null if not found within timeout.
160161 Future<String?> _discoverViaMdns({Duration timeout = const Duration(seconds: 3)}) async {
162
+ // Try Bonjour first
161163 try {
162164 final discovery = BonsoirDiscovery(type: '_mqtt._tcp');
163165 await discovery.initialize();
....@@ -187,9 +189,62 @@
187189 await sub?.cancel();
188190 await discovery.stop();
189191
190
- return ip;
192
+ if (ip != null) return ip;
191193 } catch (e) {
192194 _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 (_) {
193248 return null;
194249 }
195250 }