bluetooth/gap_linux.go
Ayke van Laethem c034fbca54
all: change NewAdvertisement to DefaultAdvertisement
Instead of attempting to allocate multiple advertisement instances, only
use one by default. If needed, a NewAdvertisement method could be added
in the future for devices that actually do support multiple
advertisements at a time.

The motivation for this change is fix an inconsistency with the nrf51
(which already had the behavior of DefaultAdvertisement) and the
discovery that nrf52 devices also don't seem to support more than one
advertisement instance, even though their API does allow for multiple
instances. But the primary motivation is that for consistency with
hosted systems, it would be best if the nrf port would automatically
re-enable advertisement when a connection is lost (or made).

While BlueZ does support more than one instance, it is implemented by
simply iterating through the active advertisement instances so could
also be implemented by doing that manually. I haven't checked the
behavior of Windows and MacOS - but as always, the API is not yet stable
and can be changed if needed.
2020-06-01 18:09:44 +02:00

188 lines
5.5 KiB
Go

// +build !baremetal
package bluetooth
import (
"github.com/muka/go-bluetooth/api"
"github.com/muka/go-bluetooth/bluez/profile/adapter"
"github.com/muka/go-bluetooth/bluez/profile/advertising"
"github.com/muka/go-bluetooth/bluez/profile/device"
)
// Advertisement encapsulates a single advertisement instance.
type Advertisement struct {
adapter *Adapter
advertisement *api.Advertisement
properties *advertising.LEAdvertisement1Properties
}
// DefaultAdvertisement returns the default advertisement instance but does not
// configure it.
func (a *Adapter) DefaultAdvertisement() *Advertisement {
if a.defaultAdvertisement == nil {
a.defaultAdvertisement = &Advertisement{
adapter: a,
}
}
return a.defaultAdvertisement
}
// Configure this advertisement.
//
// On Linux with BlueZ, it is not possible to set the advertisement interval.
func (a *Advertisement) Configure(options AdvertisementOptions) error {
if a.advertisement != nil {
panic("todo: configure advertisement a second time")
}
a.properties = &advertising.LEAdvertisement1Properties{
Type: advertising.AdvertisementTypeBroadcast,
Timeout: 1<<16 - 1,
LocalName: options.LocalName,
}
return nil
}
// Start advertisement. May only be called after it has been configured.
func (a *Advertisement) Start() error {
if a.advertisement != nil {
panic("todo: start advertisement a second time")
}
_, err := api.ExposeAdvertisement(a.adapter.id, a.properties, uint32(a.properties.Timeout))
if err != nil {
return err
}
return nil
}
// Scan starts a BLE scan. It is stopped by a call to StopScan. A common pattern
// is to cancel the scan when a particular device has been found.
//
// On Linux with BlueZ, incoming packets cannot be observed directly. Instead,
// existing devices are watched for property changes. This closely simulates the
// behavior as if the actual packets were observed, but it has flaws: it is
// possible some events are missed and perhaps even possible that some events
// are duplicated.
func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
if a.cancelScan != nil {
return errScanning
}
// This appears to be necessary to receive any BLE discovery results at all.
defer a.adapter.SetDiscoveryFilter(nil)
err := a.adapter.SetDiscoveryFilter(map[string]interface{}{
"Transport": "le",
})
if err != nil {
return err
}
// Instruct BlueZ to start discovering.
err = a.adapter.StartDiscovery()
if err != nil {
return err
}
// Listen for newly found devices.
discoveryChan, cancelChan, err := a.adapter.OnDeviceDiscovered()
if err != nil {
return err
}
a.cancelScan = cancelChan
// Obtain a list of cached devices to watch.
// BlueZ won't show advertisement data as it is discovered. Instead, it
// caches all the data and only produces events for changes. Worse: it
// doesn't seem to remove cached devices for a long time (3 minutes?) so
// simply reading the list of cached devices won't tell you what devices are
// actually around right now.
// Luckily, there is a workaround. When any value changes, you can be sure a
// new advertisement packet has been received. The RSSI value changes almost
// every time it seems so just watching property changes is enough to get a
// near-accurate view of the current state of the world around the listening
// device.
devices, err := a.adapter.GetDevices()
if err != nil {
return err
}
for _, dev := range devices {
a.startWatchingDevice(dev, callback)
}
// Iterate through new devices as they become visible.
for result := range discoveryChan {
if result.Type != adapter.DeviceAdded {
continue
}
// We only got a DBus object path, so turn that into a Device1 object.
dev, err := device.NewDevice1(result.Path)
if err != nil || dev == nil {
continue
}
// Signal to the API client that a new device has been found.
callback(a, makeScanResult(dev))
// Start watching this new device for when there are property changes.
a.startWatchingDevice(dev, callback)
}
return nil
}
// StopScan stops any in-progress scan. It can be called from within a Scan
// callback to stop the current scan. If no scan is in progress, an error will
// be returned.
func (a *Adapter) StopScan() error {
if a.cancelScan == nil {
return errNotScanning
}
a.adapter.StopDiscovery()
cancel := a.cancelScan
a.cancelScan = nil
cancel()
return nil
}
// makeScanResult creates a ScanResult from a Device1 object.
func makeScanResult(dev *device.Device1) ScanResult {
// Assume the Address property is well-formed.
addr, _ := ParseMAC(dev.Properties.Address)
return ScanResult{
RSSI: dev.Properties.RSSI,
Address: addr,
AdvertisementPayload: &advertisementFields{
AdvertisementFields{
LocalName: dev.Properties.Name,
},
},
}
}
// startWatchingDevice starts watching for property changes in the device.
// Errors are ignored (for example, if watching the device failed).
// The dev object will be owned by the function and will be modified as
// properties change.
func (a *Adapter) startWatchingDevice(dev *device.Device1, callback func(*Adapter, ScanResult)) {
ch, err := dev.WatchProperties()
if err != nil {
// Assume the device has disappeared or something.
return
}
go func() {
for change := range ch {
// Update the device with the changed property.
props, _ := dev.Properties.ToMap()
props[change.Name] = change.Value
dev.Properties, _ = dev.Properties.FromMap(props)
// Signal to the API client that a property changed, as if this was
// an incoming BLE advertisement packet.
callback(a, makeScanResult(dev))
}
}()
}