diff --git a/core/internal/server/bluez/manager.go b/core/internal/server/bluez/manager.go index 542895c5b..8964c21b2 100644 --- a/core/internal/server/bluez/manager.go +++ b/core/internal/server/bluez/manager.go @@ -43,10 +43,10 @@ func NewManager() (*Manager, error) { broker := NewSubscriptionBroker(m.broadcastPairingPrompt) m.promptBroker = broker - adapter, err := m.findAdapter() + adapter, err := findAdapter(conn) if err != nil { conn.Close() - return nil, fmt.Errorf("no bluetooth adapter found: %w", err) + return nil, err } m.adapterPath = adapter @@ -74,12 +74,12 @@ func NewManager() (*Manager, error) { return m, nil } -func (m *Manager) findAdapter() (dbus.ObjectPath, error) { - obj := m.dbusConn.Object(bluezService, dbus.ObjectPath("/")) +func findAdapter(conn *dbus.Conn) (dbus.ObjectPath, error) { + obj := conn.Object(bluezService, dbus.ObjectPath("/")) var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant if err := obj.Call(objectMgrIface+".GetManagedObjects", 0).Store(&objects); err != nil { - return "", err + return "", fmt.Errorf("%w: %v", ErrNoAdapter, err) } for path, interfaces := range objects { @@ -89,7 +89,7 @@ func (m *Manager) findAdapter() (dbus.ObjectPath, error) { } } - return "", fmt.Errorf("no adapter found") + return "", ErrNoAdapter } func (m *Manager) initialize() error { @@ -487,6 +487,11 @@ func (m *Manager) StopDiscovery() error { } func (m *Manager) SetPowered(powered bool) error { + if powered { + if err := rfkillUnblockBluetooth(); err != nil { + log.Debugf("[BluezManager] rfkill unblock failed: %v", err) + } + } obj := m.dbusConn.Object(bluezService, m.adapterPath) return obj.Call(propertiesIface+".Set", 0, adapter1Iface, "Powered", dbus.MakeVariant(powered)).Err } diff --git a/core/internal/server/bluez/rfkill_freebsd.go b/core/internal/server/bluez/rfkill_freebsd.go new file mode 100644 index 000000000..a3f200feb --- /dev/null +++ b/core/internal/server/bluez/rfkill_freebsd.go @@ -0,0 +1,5 @@ +package bluez + +func rfkillUnblockBluetooth() error { + return nil +} diff --git a/core/internal/server/bluez/rfkill_linux.go b/core/internal/server/bluez/rfkill_linux.go new file mode 100644 index 000000000..9f7764893 --- /dev/null +++ b/core/internal/server/bluez/rfkill_linux.go @@ -0,0 +1,19 @@ +package bluez + +import "os" + +// linux/rfkill.h: struct rfkill_event { __u32 idx; __u8 type; __u8 op; __u8 soft; __u8 hard; }, +// RFKILL_TYPE_BLUETOOTH=2, RFKILL_OP_CHANGE_ALL=3 +func rfkillUnblockBluetooth() error { + f, err := os.OpenFile("/dev/rfkill", os.O_WRONLY, 0) + if err != nil { + return err + } + defer f.Close() + + var event [8]byte + event[4] = 2 + event[5] = 3 + _, err = f.Write(event[:]) + return err +} diff --git a/core/internal/server/bluez/watcher.go b/core/internal/server/bluez/watcher.go new file mode 100644 index 000000000..4e86e3054 --- /dev/null +++ b/core/internal/server/bluez/watcher.go @@ -0,0 +1,46 @@ +package bluez + +import ( + "errors" + "fmt" + + "github.com/godbus/dbus/v5" +) + +var ErrNoAdapter = errors.New("no bluetooth adapter found") + +func WaitForAdapter() error { + conn, err := dbus.ConnectSystemBus() + if err != nil { + return err + } + defer conn.Close() + + if err := conn.AddMatchSignal( + dbus.WithMatchInterface(objectMgrIface), + dbus.WithMatchMember("InterfacesAdded"), + ); err != nil { + return err + } + + signals := make(chan *dbus.Signal, 64) + conn.Signal(signals) + + if _, err := findAdapter(conn); err == nil { + return nil + } + + for sig := range signals { + if sig == nil || sig.Name != objectMgrIface+".InterfacesAdded" || len(sig.Body) < 2 { + continue + } + ifaces, ok := sig.Body[1].(map[string]map[string]dbus.Variant) + if !ok { + continue + } + if _, ok := ifaces[adapter1Iface]; ok { + return nil + } + } + return fmt.Errorf("dbus signal stream closed") +} diff --git a/core/internal/server/server.go b/core/internal/server/server.go index 8b845f6ca..69405d06c 100644 --- a/core/internal/server/server.go +++ b/core/internal/server/server.go @@ -1668,10 +1668,21 @@ func (s *Server) Serve(printDocs bool) error { }() go func() { - if err := InitializeBluezManager(); err != nil { + for { + err := InitializeBluezManager() + if err == nil { + notifyCapabilityChange() + return + } log.Warnf("Bluez manager unavailable: %v", err) - } else { - notifyCapabilityChange() + if !errors.Is(err, bluez.ErrNoAdapter) { + return + } + if err := bluez.WaitForAdapter(); err != nil { + log.Warnf("Bluetooth adapter watch failed: %v", err) + return + } + log.Info("Bluetooth adapter appeared, initializing bluez manager") } }() diff --git a/quickshell/Services/DMSService.qml b/quickshell/Services/DMSService.qml index a159fa96c..339ab4b22 100644 --- a/quickshell/Services/DMSService.qml +++ b/quickshell/Services/DMSService.qml @@ -335,6 +335,7 @@ Singleton { const data = response.result.data; if (service === "server") { + const prevCapabilities = capabilities; apiVersion = data.apiVersion || 0; cliVersion = data.cliVersion || ""; capabilities = data.capabilities || []; @@ -346,6 +347,12 @@ Singleton { } capabilitiesReceived(); + + const capabilitiesChanged = prevCapabilities.length !== capabilities.length || capabilities.some(c => !prevCapabilities.includes(c)); + if (prevCapabilities.length > 0 && capabilitiesChanged) { + log.info("Capabilities changed, resubscribing"); + subscribe(activeSubscriptions); + } } else if (service === "network") { networkStateUpdate(data); } else if (service === "network.credentials") {