From 96c8bb5db1a2e0ced999a366e3cf28f9895ec39f Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Tue, 24 Mar 2026 21:51:35 +0100
Subject: [PATCH] feat: Bonjour auto-discovery + VPN IP field in connection flow

---
 lib/services/mqtt_service.dart |   95 ++++++++++++++++++++++++++++++++++++-----------
 1 files changed, 73 insertions(+), 22 deletions(-)

diff --git a/lib/services/mqtt_service.dart b/lib/services/mqtt_service.dart
index 87eafc9..200ba3c 100644
--- a/lib/services/mqtt_service.dart
+++ b/lib/services/mqtt_service.dart
@@ -2,6 +2,7 @@
 import 'dart:convert';
 import 'dart:io';
 
+import 'package:bonsoir/bonsoir.dart';
 import 'package:flutter/widgets.dart';
 import 'package:path_provider/path_provider.dart' as pp;
 import 'package:mqtt_client/mqtt_client.dart';
@@ -102,29 +103,50 @@
     }
 
     final clientId = await _getClientId();
-    final hosts = _getHosts();
-    _mqttLog('MQTT: hosts=${hosts.join(", ")} port=${config.port}');
 
-    for (final host in hosts) {
+    // Connection order: local → Bonjour → VPN → remote
+    final attempts = <MapEntry<String, int>>[];  // host → timeout ms
+    if (config.localHost != null && config.localHost!.isNotEmpty) {
+      attempts.add(MapEntry(config.localHost!, 2500));
+    }
+    // Bonjour placeholder — inserted dynamically below
+    if (config.vpnHost != null && config.vpnHost!.isNotEmpty) {
+      attempts.add(MapEntry(config.vpnHost!, 3000));
+    }
+    if (config.host.isNotEmpty) {
+      attempts.add(MapEntry(config.host, 5000));
+    }
+    _mqttLog('MQTT: attempts=${attempts.map((e) => e.key).join(", ")} port=${config.port}');
+
+    // Try configured local host first
+    for (final attempt in attempts) {
       if (_intentionalClose) return;
-
-      _mqttLog('MQTT: trying $host:${config.port}');
+      _mqttLog('MQTT: trying ${attempt.key}:${config.port}');
       try {
-        final connected = await _tryConnect(
-          host,
-          clientId,
-          timeout: host == hosts.first && hosts.length > 1 ? 2500 : 5000,
-        );
-        _mqttLog('MQTT: $host result=$connected');
-        if (connected) return;
+        if (await _tryConnect(attempt.key, clientId, timeout: attempt.value)) return;
       } catch (e) {
-        _mqttLog('MQTT: $host error=$e');
-        continue;
+        _mqttLog('MQTT: ${attempt.key} error=$e');
+      }
+
+      // After local host fails, try Bonjour discovery before VPN/remote
+      if (attempt.key == config.localHost && !_intentionalClose) {
+        _mqttLog('MQTT: trying Bonjour discovery...');
+        final bonjourHost = await _discoverViaMdns();
+        if (bonjourHost != null && !_intentionalClose) {
+          _mqttLog('MQTT: Bonjour found $bonjourHost');
+          try {
+            if (await _tryConnect(bonjourHost, clientId, timeout: 3000)) return;
+          } catch (e) {
+            _mqttLog('MQTT: Bonjour host $bonjourHost error=$e');
+          }
+        } else {
+          _mqttLog('MQTT: Bonjour discovery returned nothing');
+        }
       }
     }
 
     // All hosts failed — retry after delay
-    _mqttLog('MQTT: all hosts failed, retrying in 5s');
+    _mqttLog('MQTT: all attempts failed, retrying in 5s');
     _setStatus(ConnectionStatus.reconnecting);
     Future.delayed(const Duration(seconds: 5), () {
       if (!_intentionalClose && _status != ConnectionStatus.connected) {
@@ -133,14 +155,43 @@
     });
   }
 
-  /// Returns [localHost, remoteHost] for dual-connect attempts.
-  List<String> _getHosts() {
-    if (config.localHost != null &&
-        config.localHost!.isNotEmpty &&
-        config.localHost != config.host) {
-      return [config.localHost!, config.host];
+  /// Discover AIBroker on local network via Bonjour/mDNS.
+  /// Returns the IP address or null if not found within timeout.
+  Future<String?> _discoverViaMdns({Duration timeout = const Duration(seconds: 3)}) async {
+    try {
+      final discovery = BonsoirDiscovery(type: '_mqtt._tcp');
+      await discovery.initialize();
+
+      final completer = Completer<String?>();
+      StreamSubscription? sub;
+
+      sub = discovery.eventStream?.listen((event) {
+        switch (event) {
+          case BonsoirDiscoveryServiceResolvedEvent():
+            final ip = event.service.host;
+            _mqttLog('MQTT: Bonjour resolved: ${event.service.name} at $ip:${event.service.port}');
+            if (ip != null && ip.isNotEmpty && !completer.isCompleted) {
+              completer.complete(ip);
+            }
+          case BonsoirDiscoveryServiceFoundEvent():
+            _mqttLog('MQTT: Bonjour found: ${event.service.name}');
+          default:
+            break;
+        }
+      });
+
+      await discovery.start();
+
+      final ip = await completer.future.timeout(timeout, onTimeout: () => null);
+
+      await sub?.cancel();
+      await discovery.stop();
+
+      return ip;
+    } catch (e) {
+      _mqttLog('MQTT: Bonjour discovery error: $e');
+      return null;
     }
-    return [config.host];
   }
 
   Future<bool> _tryConnect(String host, String clientId, {int timeout = 5000}) async {

--
Gitblit v1.3.1