Skip to content

Commit dc9fc3b

Browse files
authored
Linux: Fix: connectHandler after adv.Start (#369)
* Linux: Fix: connectHandler after adv.Start In Linux the Adapter.connectHandler is never called after Advertisement.Start. This fix preserves the existing interfaces and adds D-Bus signals to handle connect/disconnect events once the Peripheral is advertising. - Fix Linux Advertise.Start to call the connectHandler, if set - Update inline documentation for Adapter.SetConnectHandler - Update README usage example Signed-off-by: Nick Ross <[email protected]> * Revert README examples to use println Signed-off-by: Nick Ross <[email protected]> * Add resource cleanup in example * Advertising, signal handling, and README corrections - Add alias update on adveristing - Extend property handling for signals - Correct README example Signed-off-by: Nick Ross <[email protected]> * Add connectHandler to advertisement example Signed-off-by: Nick Ross <[email protected]> * Invoke handler on device initial connection Signed-off-by: Nick Ross <[email protected]> --------- Signed-off-by: Nick Ross <[email protected]>
1 parent 38847b0 commit dc9fc3b

File tree

4 files changed

+199
-9
lines changed

4 files changed

+199
-9
lines changed

README.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ This example shows a peripheral that advertises itself as being available for co
5656
package main
5757

5858
import (
59+
"context"
5960
"time"
6061

6162
"tinygo.org/x/bluetooth"
@@ -67,6 +68,17 @@ func main() {
6768
// Enable BLE interface.
6869
must("enable BLE stack", adapter.Enable())
6970

71+
ctx, cancel := context.WithCancel(context.Background())
72+
adapter.SetConnectHandler(func(device bluetooth.Device, connected bool) {
73+
if connected {
74+
println("device connected:", device.Address.String())
75+
return
76+
}
77+
78+
println("device disconnected:", device.Address.String())
79+
cancel()
80+
})
81+
7082
// Define the peripheral device info.
7183
adv := adapter.DefaultAdvertisement()
7284
must("config adv", adv.Configure(bluetooth.AdvertisementOptions{
@@ -75,12 +87,12 @@ func main() {
7587

7688
// Start advertising
7789
must("start adv", adv.Start())
90+
91+
// Stop advertising to release resources
92+
defer adv.Stop()
7893

7994
println("advertising...")
80-
for {
81-
// Sleep forever.
82-
time.Sleep(time.Hour)
83-
}
95+
<- ctx.Done()
8496
}
8597

8698
func must(action string, err error) {

adapter.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package bluetooth
22

33
// SetConnectHandler sets a handler function to be called whenever the adaptor connects
4-
// or disconnects. You must call this before you call adaptor.Connect() for centrals
5-
// or adaptor.Start() for peripherals in order for it to work.
4+
// or disconnects. You must call this before you call adapter.Connect() for centrals
5+
// or advertisement.Start() for peripherals in order for it to work.
66
func (a *Adapter) SetConnectHandler(c func(device Device, connected bool)) {
77
a.connectHandler = c
88
}

examples/advertisement/main.go

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"context"
45
"time"
56

67
"tinygo.org/x/bluetooth"
@@ -10,6 +11,18 @@ var adapter = bluetooth.DefaultAdapter
1011

1112
func main() {
1213
must("enable BLE stack", adapter.Enable())
14+
15+
ctx, cancel := context.WithCancel(context.Background())
16+
adapter.SetConnectHandler(func(device bluetooth.Device, connected bool) {
17+
if connected {
18+
println("device connected:", device.Address.String())
19+
return
20+
}
21+
22+
println("device disconnected:", device.Address.String())
23+
cancel()
24+
})
25+
1326
adv := adapter.DefaultAdvertisement()
1427
must("config adv", adv.Configure(bluetooth.AdvertisementOptions{
1528
LocalName: "Go Bluetooth",
@@ -18,12 +31,17 @@ func main() {
1831
},
1932
}))
2033
must("start adv", adv.Start())
34+
defer adv.Stop()
2135

2236
println("advertising...")
2337
address, _ := adapter.Address()
2438
for {
25-
println("Go Bluetooth /", address.MAC.String())
26-
time.Sleep(time.Second)
39+
select {
40+
case <-time.After(1 * time.Second):
41+
println("Go Bluetooth /", address.MAC.String())
42+
case <-ctx.Done():
43+
return
44+
}
2745
}
2846
}
2947

gap_linux.go

Lines changed: 161 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,39 @@ import (
1212
"github.com/godbus/dbus/v5/prop"
1313
)
1414

15+
const (
16+
// Match rule constants for D-Bus signals.
17+
//
18+
// See [DBusPropertiesLink] for more information.
19+
20+
dbusPropertiesChangedInterfaceName = 0
21+
dbusPropertiesChangedDictionary = 1
22+
dbusPropertiesChangedInvalidated = 2
23+
24+
dbusInterfacesAddedDictionary = 1
25+
26+
dbusSignalInterfacesAdded = "org.freedesktop.DBus.ObjectManager.InterfacesAdded"
27+
dbusSignalPropertiesChanged = "org.freedesktop.DBus.Properties.PropertiesChanged"
28+
29+
bluezDevice1Interface = "org.bluez.Device1"
30+
bluezDevice1Address = "Address"
31+
bluezDevice1Connected = "Connected"
32+
)
33+
34+
var (
35+
// See [DBusPropertiesLink] for more information.
36+
matchOptionsPropertiesChanged = []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
37+
dbus.WithMatchMember("PropertiesChanged"),
38+
dbus.WithMatchArg(dbusPropertiesChangedInterfaceName, "org.bluez.Device1")}
39+
40+
// See [DBusObjectManagerLink] for more information.
41+
matchOptionsInterfacesAdded = []dbus.MatchOption{dbus.WithMatchInterface("org.freedesktop.DBus.ObjectManager"),
42+
dbus.WithMatchMember("InterfacesAdded")}
43+
)
44+
45+
// [DBusPropertiesLink]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-properties
46+
// [DBusObjectManagerLink]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-objectmanager
47+
1548
var errAdvertisementNotStarted = errors.New("bluetooth: advertisement is not started")
1649
var errAdvertisementAlreadyStarted = errors.New("bluetooth: advertisement is already started")
1750
var errAdaptorNotPowered = errors.New("bluetooth: adaptor is not powered")
@@ -30,6 +63,9 @@ type Advertisement struct {
3063
properties *prop.Properties
3164
path dbus.ObjectPath
3265
started bool
66+
67+
// D-Bus Signals
68+
sigCh chan *dbus.Signal
3369
}
3470

3571
// DefaultAdvertisement returns the default advertisement instance but does not
@@ -93,6 +129,17 @@ func (a *Advertisement) Configure(options AdvertisementOptions) error {
93129
}
94130
a.properties = props
95131

132+
if options.LocalName != "" {
133+
// In BlueZ AdvertisementOptions.LocalName will be sent in Extended
134+
// Advertising Data and it will not change the Adapter alias. Setting
135+
// this property will update the name in the initial advertising data.
136+
call := a.adapter.adapter.Call("org.freedesktop.DBus.Properties.Set", 0,
137+
"org.bluez.Adapter1", "Alias", dbus.MakeVariant(options.LocalName))
138+
if call.Err != nil {
139+
return fmt.Errorf("set adapter alias: %w", call.Err)
140+
}
141+
}
142+
96143
return nil
97144
}
98145

@@ -107,6 +154,21 @@ func (a *Advertisement) Start() error {
107154
return fmt.Errorf("bluetooth: could not start advertisement: %w", err)
108155
}
109156

157+
if a.adapter.connectHandler != nil {
158+
a.sigCh = make(chan *dbus.Signal)
159+
a.adapter.bus.Signal(a.sigCh)
160+
161+
if err := a.adapter.bus.AddMatchSignal(matchOptionsPropertiesChanged...); err != nil {
162+
return fmt.Errorf("bluetooth: add dbus match signal: PropertiesChanged: %w", err)
163+
}
164+
165+
if err := a.adapter.bus.AddMatchSignal(matchOptionsInterfacesAdded...); err != nil {
166+
return fmt.Errorf("bluetooth: add dbus match signal: InterfacesAdded: %w", err)
167+
}
168+
169+
go a.handleDBusSignals()
170+
}
171+
110172
// Make us discoverable.
111173
err = a.adapter.adapter.SetProperty("org.bluez.Adapter1.Discoverable", dbus.MakeVariant(true))
112174
if err != nil {
@@ -126,6 +188,17 @@ func (a *Advertisement) Stop() error {
126188
return fmt.Errorf("bluetooth: could not stop advertisement: %w", err)
127189
}
128190
a.started = false
191+
192+
if a.sigCh != nil {
193+
defer close(a.sigCh)
194+
if err := a.adapter.bus.RemoveMatchSignal(matchOptionsPropertiesChanged...); err != nil {
195+
return fmt.Errorf("bluetooth: remove dbus match signal: PropertiesChanged: %w", err)
196+
}
197+
if err := a.adapter.bus.RemoveMatchSignal(matchOptionsInterfacesAdded...); err != nil {
198+
return fmt.Errorf("bluetooth: remove dbus match signal: InterfacesAdded: %w", err)
199+
}
200+
a.adapter.bus.RemoveSignal(a.sigCh)
201+
}
129202
return nil
130203
}
131204

@@ -357,7 +430,7 @@ func makeScanResult(props map[string]dbus.Variant) ScanResult {
357430
}
358431
}
359432

360-
// Device is a connection to a remote peripheral.
433+
// Device is a connection to a remote bluetooth device.
361434
type Device struct {
362435
Address Address // the MAC address of the device
363436

@@ -496,3 +569,90 @@ func (a *Adapter) SetRandomAddress(mac MAC) error {
496569

497570
return nil
498571
}
572+
573+
func (a *Advertisement) handleDBusSignals() {
574+
for {
575+
select {
576+
case sig, ok := <-a.sigCh:
577+
if !ok {
578+
return // channel closed
579+
}
580+
581+
device := Device{
582+
device: a.adapter.bus.Object("org.bluez", sig.Path),
583+
adapter: a.adapter,
584+
}
585+
586+
switch sig.Name {
587+
case dbusSignalInterfacesAdded:
588+
interfaces := sig.Body[dbusInterfacesAddedDictionary].(map[string]map[string]dbus.Variant)
589+
590+
// InterfacesAdded signal also contains all known properties so
591+
// so we do not need to call org.freedesktop.DBus.Properties.GetAll
592+
props, ok := interfaces[bluezDevice1Interface]
593+
if !ok {
594+
continue
595+
}
596+
597+
if err := device.parseProperties(&props); err != nil {
598+
continue
599+
}
600+
601+
if connected, ok := props[bluezDevice1Connected].Value().(bool); ok {
602+
a.adapter.connectHandler(device, connected)
603+
}
604+
case dbusSignalPropertiesChanged:
605+
// Skip any signals that are not the Device1 interface.
606+
if interfaceName, ok := sig.Body[dbusPropertiesChangedInterfaceName].(string); !ok || interfaceName != bluezDevice1Interface {
607+
continue
608+
}
609+
610+
// Get all changed properties and skip any signals that are not
611+
// compliant with the Device1 interface.
612+
changes, ok := sig.Body[dbusPropertiesChangedDictionary].(map[string]dbus.Variant)
613+
if !ok {
614+
continue
615+
}
616+
617+
// Call the connect handler if the Connected property has changed.
618+
if connected, ok := changes[bluezDevice1Connected].Value().(bool); ok {
619+
// The only property received is the changed property "Connected",
620+
// so we have to get the other properties from D-Bus.
621+
var props map[string]dbus.Variant
622+
if err := device.device.Call("org.freedesktop.DBus.Properties.GetAll",
623+
0,
624+
bluezDevice1Interface).Store(&props); err != nil {
625+
continue
626+
}
627+
628+
if err := device.parseProperties(&props); err != nil {
629+
continue
630+
}
631+
632+
a.adapter.connectHandler(device, connected)
633+
}
634+
}
635+
}
636+
}
637+
}
638+
639+
// parseProperties will set fields from provided properties
640+
//
641+
// For all possible properties see:
642+
// https://github.com/luetzel/bluez/blob/master/doc/device-api.txt
643+
func (d *Device) parseProperties(props *map[string]dbus.Variant) error {
644+
for prop, v := range *props {
645+
switch prop {
646+
case bluezDevice1Address:
647+
if addrStr, ok := v.Value().(string); ok {
648+
mac, err := ParseMAC(addrStr)
649+
if err != nil {
650+
return fmt.Errorf("ParseMAC: %w", err)
651+
}
652+
d.Address = Address{MACAddress: MACAddress{MAC: mac}}
653+
}
654+
}
655+
}
656+
657+
return nil
658+
}

0 commit comments

Comments
 (0)