@@ -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+
1548var errAdvertisementNotStarted = errors .New ("bluetooth: advertisement is not started" )
1649var errAdvertisementAlreadyStarted = errors .New ("bluetooth: advertisement is already started" )
1750var 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 .
361434type 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