// +build !baremetal package bluetooth import ( "errors" "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" ) var ( ErrMalformedAdvertisement = errors.New("bluetooth: malformed advertisement packet") ) // Advertisement encapsulates a single advertisement instance. type Advertisement struct { adapter *Adapter advertisement *api.Advertisement properties *advertising.LEAdvertisement1Properties } // NewAdvertisement creates a new advertisement instance but does not configure // it. func (a *Adapter) NewAdvertisement() *Advertisement { return &Advertisement{ adapter: a, } } // Configure this advertisement. func (a *Advertisement) Configure(broadcastData, scanResponseData []byte, options *AdvertiseOptions) error { if a.advertisement != nil { panic("todo: configure advertisement a second time") } if scanResponseData != nil { panic("todo: scan response data") } // Quick-and-dirty advertisement packet parser. a.properties = &advertising.LEAdvertisement1Properties{ Type: advertising.AdvertisementTypeBroadcast, Timeout: 1<<16 - 1, } for len(broadcastData) != 0 { if len(broadcastData) < 2 { return ErrMalformedAdvertisement } fieldLength := broadcastData[0] fieldType := broadcastData[1] fieldValue := broadcastData[2 : fieldLength+1] if int(fieldLength) > len(broadcastData) { return ErrMalformedAdvertisement } switch fieldType { case 1: // BLE advertisement flags. Ignore. case 9: // Complete Local Name. a.properties.LocalName = string(fieldValue) default: return ErrMalformedAdvertisement } broadcastData = broadcastData[fieldLength+1:] } 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)) } }() }