From 9e5953ced9c02f883203c8e49b2c7a78333ef34b Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Wed, 25 Mar 2026 09:44:01 +0100
Subject: [PATCH] feat: subnet scan fallback when Bonjour fails (handles iOS Personal Hotspot)

---
 lib/services/mqtt_service.dart |   57 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 56 insertions(+), 1 deletions(-)

diff --git a/lib/services/mqtt_service.dart b/lib/services/mqtt_service.dart
index 200ba3c..3fddb0f 100644
--- a/lib/services/mqtt_service.dart
+++ b/lib/services/mqtt_service.dart
@@ -156,8 +156,10 @@
   }
 
   /// Discover AIBroker on local network via Bonjour/mDNS.
+  /// Falls back to subnet scan if Bonjour fails (iOS blocks mDNS on Personal Hotspot).
   /// Returns the IP address or null if not found within timeout.
   Future<String?> _discoverViaMdns({Duration timeout = const Duration(seconds: 3)}) async {
+    // Try Bonjour first
     try {
       final discovery = BonsoirDiscovery(type: '_mqtt._tcp');
       await discovery.initialize();
@@ -187,9 +189,62 @@
       await sub?.cancel();
       await discovery.stop();
 
-      return ip;
+      if (ip != null) return ip;
     } catch (e) {
       _mqttLog('MQTT: Bonjour discovery error: $e');
+    }
+
+    // Fallback: scan local subnet for MQTT port (handles Personal Hotspot)
+    _mqttLog('MQTT: Bonjour failed, trying subnet scan...');
+    return _scanSubnetForMqtt();
+  }
+
+  /// Scan the local subnet for an MQTT broker by probing the configured port.
+  /// Useful when iOS Personal Hotspot blocks mDNS.
+  Future<String?> _scanSubnetForMqtt() async {
+    try {
+      // Get device's own IP to determine the subnet
+      final interfaces = await NetworkInterface.list(type: InternetAddressType.IPv4);
+      for (final iface in interfaces) {
+        for (final addr in iface.addresses) {
+          final parts = addr.address.split('.');
+          if (parts.length != 4) continue;
+          // Skip loopback
+          if (parts[0] == '127') continue;
+          // Only scan small subnets (hotspot = /28, max 14 hosts)
+          final subnet = '${parts[0]}.${parts[1]}.${parts[2]}';
+          _mqttLog('MQTT: scanning $subnet.0/24 on ${iface.name}');
+
+          // Probe hosts 1-14 in parallel (covers /28 hotspot subnet)
+          final futures = <Future<String?>>[];
+          for (int i = 1; i <= 14; i++) {
+            final probe = '$subnet.$i';
+            if (probe == addr.address) continue; // skip self
+            futures.add(_probeHost(probe, config.port));
+          }
+
+          final results = await Future.wait(futures);
+          final found = results.firstWhere((r) => r != null, orElse: () => null);
+          if (found != null) {
+            _mqttLog('MQTT: subnet scan found broker at $found');
+            return found;
+          }
+        }
+      }
+    } catch (e) {
+      _mqttLog('MQTT: subnet scan error: $e');
+    }
+    return null;
+  }
+
+  /// Probe a single host:port with a TCP connection attempt (1s timeout).
+  Future<String?> _probeHost(String host, int port) async {
+    try {
+      final socket = await Socket.connect(host, port,
+          timeout: const Duration(seconds: 1));
+      await socket.close();
+      return host;
+    } catch (_) {
       return null;
     }
   }

--
Gitblit v1.3.1