Skip to content

Commit 8455a42

Browse files
*: detecting abrupt disconnections in Linux
This PR implements persistent D-Bus signal monitoring at the adapter level to detect abrupt disconnections. The `connectHandler` callback was not being invoked when Bluetooth devices disconnected abruptly (e.g., device powered off, out of range, battery died) on Linux. It only worked for planned disconnections via `Device.Disconnect()`. This created an inconsistency with the Darwin implementation, which correctly detects all disconnections through CoreBluetooth's `DidDisconnectPeripheral` delegate method.
1 parent ec428dc commit 8455a42

File tree

1 file changed

+86
-1
lines changed

1 file changed

+86
-1
lines changed

adapter_linux.go

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ type Adapter struct {
2323
address string
2424
defaultAdvertisement *Advertisement
2525

26-
connectHandler func(device Device, connected bool)
26+
connectHandler func(device Device, connected bool)
27+
connectionMonitorChan chan *dbus.Signal
28+
monitoringConnections bool
29+
stopMonitorChan chan struct{}
2730
}
2831

2932
// NewAdapter creates a new Adapter with the given ID.
@@ -61,6 +64,11 @@ func (a *Adapter) Enable() (err error) {
6164
}
6265
addr.Store(&a.address)
6366

67+
// Start connection monitoring if connectHandler is set
68+
if a.connectHandler != nil {
69+
a.startConnectionMonitoring()
70+
}
71+
6472
return nil
6573
}
6674

@@ -74,3 +82,80 @@ func (a *Adapter) Address() (MACAddress, error) {
7482
}
7583
return MACAddress{MAC: mac}, nil
7684
}
85+
86+
func (a *Adapter) startConnectionMonitoring() error {
87+
if a.monitoringConnections {
88+
return nil // already monitoring
89+
}
90+
91+
a.connectionMonitorChan = make(chan *dbus.Signal, 10)
92+
a.stopMonitorChan = make(chan struct{})
93+
a.bus.Signal(a.connectionMonitorChan)
94+
95+
if err := a.bus.AddMatchSignal(matchOptionsPropertiesChanged...); err != nil {
96+
return fmt.Errorf("bluetooth: add dbus match signal: %w", err)
97+
}
98+
99+
a.monitoringConnections = true
100+
go a.handleConnectionSignals()
101+
102+
return nil
103+
}
104+
105+
func (a *Adapter) handleConnectionSignals() {
106+
for {
107+
select {
108+
case <-a.stopMonitorChan:
109+
return
110+
case sig, ok := <-a.connectionMonitorChan:
111+
if !ok {
112+
return // channel was closed
113+
}
114+
if sig.Name != dbusSignalPropertiesChanged {
115+
continue
116+
}
117+
interfaceName, ok := sig.Body[0].(string)
118+
if !ok || interfaceName != bluezDevice1Interface {
119+
continue
120+
}
121+
changes, ok := sig.Body[1].(map[string]dbus.Variant)
122+
if !ok {
123+
continue
124+
}
125+
if connectedVariant, ok := changes["Connected"]; ok {
126+
connected, ok := connectedVariant.Value().(bool)
127+
if !ok {
128+
continue
129+
}
130+
device := Device{
131+
device: a.bus.Object("org.bluez", sig.Path),
132+
adapter: a,
133+
}
134+
var props map[string]dbus.Variant
135+
if err := device.device.Call("org.freedesktop.DBus.Properties.GetAll",
136+
0, bluezDevice1Interface).Store(&props); err != nil {
137+
continue
138+
}
139+
if err := device.parseProperties(&props); err != nil {
140+
continue
141+
}
142+
a.connectHandler(device, connected)
143+
}
144+
}
145+
}
146+
}
147+
148+
func (a *Adapter) StopConnectionMonitoring() error {
149+
if !a.monitoringConnections {
150+
return nil
151+
}
152+
// Unregister the signal channel from D-Bus before closing
153+
a.bus.RemoveSignal(a.connectionMonitorChan)
154+
if err := a.bus.RemoveMatchSignal(matchOptionsPropertiesChanged...); err != nil {
155+
return fmt.Errorf("bluetooth: remove dbus match signal: %w", err)
156+
}
157+
close(a.stopMonitorChan)
158+
close(a.connectionMonitorChan)
159+
a.monitoringConnections = false
160+
return nil
161+
}

0 commit comments

Comments
 (0)