Add support for scanning for devices
There are some limitations, but it basically works (on both Linux and nrf).
This commit is contained in:
parent
93550127da
commit
7a11ef8562
7 changed files with 443 additions and 11 deletions
|
@ -8,9 +8,10 @@ import (
|
|||
)
|
||||
|
||||
type Adapter struct {
|
||||
adapter *adapter.Adapter1
|
||||
id string
|
||||
handler func(Event)
|
||||
adapter *adapter.Adapter1
|
||||
id string
|
||||
handler func(Event)
|
||||
cancelScan func()
|
||||
}
|
||||
|
||||
// DefaultAdapter returns the default adapter on the current system. On Linux,
|
||||
|
|
|
@ -58,6 +58,7 @@ func init() {
|
|||
// SoftDevice on the chip.
|
||||
type Adapter struct {
|
||||
isDefault bool
|
||||
scanning bool
|
||||
handler func(Event)
|
||||
charWriteHandlers []charWriteHandler
|
||||
}
|
||||
|
@ -117,21 +118,34 @@ func handleEvent() {
|
|||
id := eventBuf.header.evt_id
|
||||
switch {
|
||||
case id >= C.BLE_GAP_EVT_BASE && id <= C.BLE_GAP_EVT_LAST:
|
||||
connHandle := eventBuf.evt.unionfield_gap_evt().conn_handle
|
||||
gapEvent := GAPEvent{
|
||||
Connection: Connection(connHandle),
|
||||
}
|
||||
gapEvent := eventBuf.evt.unionfield_gap_evt()
|
||||
switch id {
|
||||
case C.BLE_GAP_EVT_CONNECTED:
|
||||
handler := defaultAdapter.handler
|
||||
if handler != nil {
|
||||
handler(&ConnectEvent{GAPEvent: gapEvent})
|
||||
handler(&ConnectEvent{GAPEvent: GAPEvent{Connection(gapEvent.conn_handle)}})
|
||||
}
|
||||
case C.BLE_GAP_EVT_DISCONNECTED:
|
||||
handler := defaultAdapter.handler
|
||||
if handler != nil {
|
||||
handler(&DisconnectEvent{GAPEvent: gapEvent})
|
||||
handler(&DisconnectEvent{GAPEvent: GAPEvent{Connection(gapEvent.conn_handle)}})
|
||||
}
|
||||
case C.BLE_GAP_EVT_ADV_REPORT:
|
||||
advReport := gapEvent.params.unionfield_adv_report()
|
||||
if debug && &scanReportBuffer.data[0] != advReport.data.p_data {
|
||||
// Sanity check.
|
||||
panic("scanReportBuffer != advReport.p_data")
|
||||
}
|
||||
// Prepare the globalScanResult, which will be passed to the
|
||||
// callback.
|
||||
scanReportBuffer.len = byte(advReport.data.len)
|
||||
globalScanResult.RSSI = int16(advReport.rssi)
|
||||
globalScanResult.Address = advReport.peer_addr.addr
|
||||
globalScanResult.AdvertisementPayload = &scanReportBuffer
|
||||
// Signal to the main thread that there was a scan report.
|
||||
// Scanning will be resumed (from the main thread) once the scan
|
||||
// report has been processed.
|
||||
gotScanReport.Set(1)
|
||||
case C.BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST:
|
||||
// Respond with the default PPCP connection parameters by passing
|
||||
// nil:
|
||||
|
@ -140,11 +154,11 @@ func handleEvent() {
|
|||
// > NULL is provided on a central role and in response to a
|
||||
// > BLE_GAP_EVT_CONN_PARAM_UPDATE_REQUEST, the peripheral request
|
||||
// > will be rejected
|
||||
C.sd_ble_gap_conn_param_update(connHandle, nil)
|
||||
C.sd_ble_gap_conn_param_update(gapEvent.conn_handle, nil)
|
||||
case C.BLE_GAP_EVT_DATA_LENGTH_UPDATE_REQUEST:
|
||||
// We need to respond with sd_ble_gap_data_length_update. Setting
|
||||
// both parameters to nil will make sure we send the default values.
|
||||
C.sd_ble_gap_data_length_update(connHandle, nil, nil)
|
||||
C.sd_ble_gap_data_length_update(gapEvent.conn_handle, nil, nil)
|
||||
default:
|
||||
if debug {
|
||||
println("unknown GAP event:", id)
|
||||
|
|
25
examples/scanner/main.go
Normal file
25
examples/scanner/main.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/aykevl/go-bluetooth"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Enable BLE interface.
|
||||
adapter, err := bluetooth.DefaultAdapter()
|
||||
must("get default adapter", err)
|
||||
must("enable adapter", adapter.Enable())
|
||||
|
||||
// Start scanning.
|
||||
println("scanning...")
|
||||
err = adapter.Scan(func(adapter *bluetooth.Adapter, device bluetooth.ScanResult) {
|
||||
println("found device:", device.Address.String(), device.RSSI, device.LocalName())
|
||||
})
|
||||
must("start scan", err)
|
||||
}
|
||||
|
||||
func must(action string, err error) {
|
||||
if err != nil {
|
||||
panic("failed to " + action + ": " + err.Error())
|
||||
}
|
||||
}
|
114
gap.go
114
gap.go
|
@ -1,5 +1,13 @@
|
|||
package bluetooth
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
errScanning = errors.New("bluetooth: a scan is already in progress")
|
||||
errNotScanning = errors.New("bluetooth: there is no scan in progress")
|
||||
errMalformedAdvertisementPayload = errors.New("bluetooth: malformed advertisement packet")
|
||||
)
|
||||
|
||||
// AdvertiseOptions configures everything related to BLE advertisements.
|
||||
type AdvertiseOptions struct {
|
||||
Interval AdvertiseInterval
|
||||
|
@ -32,3 +40,109 @@ type ConnectEvent struct {
|
|||
type DisconnectEvent struct {
|
||||
GAPEvent
|
||||
}
|
||||
|
||||
// ScanResult contains information from when an advertisement packet was
|
||||
// received. It is passed as a parameter to the callback of the Scan method.
|
||||
type ScanResult struct {
|
||||
// MAC address of the scanned device.
|
||||
Address MAC
|
||||
|
||||
// RSSI the last time a packet from this device has been received.
|
||||
RSSI int16
|
||||
|
||||
// The data obtained from the advertisement data, which may contain many
|
||||
// different properties.
|
||||
// Warning: this data may only stay valid until the next event arrives. If
|
||||
// you need any of the fields to stay alive until after the callback
|
||||
// returns, copy them.
|
||||
AdvertisementPayload
|
||||
}
|
||||
|
||||
// AdvertisementPayload contains information obtained during a scan (see
|
||||
// ScanResult). It is provided as an interface as there are two possible
|
||||
// implementations: an implementation that works with raw data (usually on
|
||||
// low-level BLE stacks) and an implementation that works with structured data.
|
||||
type AdvertisementPayload interface {
|
||||
// LocalName is the (complete or shortened) local name of the device.
|
||||
// Please note that many devices do not broadcast a local name, but may
|
||||
// broadcast other data (e.g. manufacturer data or service UUIDs) with which
|
||||
// they may be identified.
|
||||
LocalName() string
|
||||
|
||||
// Bytes returns the raw advertisement packet, if available. It returns nil
|
||||
// if this data is not available.
|
||||
Bytes() []byte
|
||||
}
|
||||
|
||||
// AdvertisementFields contains advertisement fields in structured form.
|
||||
type AdvertisementFields struct {
|
||||
// The LocalName part of the advertisement (either the complete local name
|
||||
// or the shortened local name).
|
||||
LocalName string
|
||||
}
|
||||
|
||||
// advertisementFields wraps AdvertisementFields to implement the
|
||||
// AdvertisementPayload interface. The methods to implement the interface (such
|
||||
// as LocalName) cannot be implemented on AdvertisementFields because they would
|
||||
// conflict with field names.
|
||||
type advertisementFields struct {
|
||||
AdvertisementFields
|
||||
}
|
||||
|
||||
// LocalName returns the underlying LocalName field.
|
||||
func (p *advertisementFields) LocalName() string {
|
||||
return p.AdvertisementFields.LocalName
|
||||
}
|
||||
|
||||
// Bytes returns nil, as structured advertisement data does not have the
|
||||
// original raw advertisement data available.
|
||||
func (p *advertisementFields) Bytes() []byte {
|
||||
return nil
|
||||
}
|
||||
|
||||
// rawAdvertisementPayload encapsulates a raw advertisement packet. Methods to
|
||||
// get the data (such as LocalName()) will parse just the needed field. Scanning
|
||||
// the data should be fast as most advertisement packets only have a very small
|
||||
// (3 or so) amount of fields.
|
||||
type rawAdvertisementPayload struct {
|
||||
data [31]byte
|
||||
len uint8
|
||||
}
|
||||
|
||||
// Bytes returns the raw advertisement packet as a byte slice.
|
||||
func (buf *rawAdvertisementPayload) Bytes() []byte {
|
||||
return buf.data[:buf.len]
|
||||
}
|
||||
|
||||
// findField returns the data of a specific field in the advertisement packet.
|
||||
func (buf *rawAdvertisementPayload) findField(fieldType byte) []byte {
|
||||
data := buf.Bytes()
|
||||
for len(data) >= 2 {
|
||||
fieldLength := data[0]
|
||||
if int(fieldLength)+1 > len(data) {
|
||||
// Invalid field length.
|
||||
return nil
|
||||
}
|
||||
if fieldType == data[1] {
|
||||
return data[2 : fieldLength+1]
|
||||
}
|
||||
data = data[fieldLength+1:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LocalName returns the local name (complete or shortened) in the advertisement
|
||||
// payload.
|
||||
func (buf *rawAdvertisementPayload) LocalName() string {
|
||||
b := buf.findField(9) // Complete Local Name
|
||||
if len(b) != 0 {
|
||||
println("complete")
|
||||
return string(b)
|
||||
}
|
||||
b = buf.findField(8) // Shortened Local Name
|
||||
if len(b) != 0 {
|
||||
println("shortened")
|
||||
return string(b)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
132
gap_linux.go
132
gap_linux.go
|
@ -6,7 +6,9 @@ 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 (
|
||||
|
@ -78,3 +80,133 @@ func (a *Advertisement) Start() error {
|
|||
}
|
||||
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))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
74
gap_sd.go
74
gap_sd.go
|
@ -2,6 +2,11 @@
|
|||
|
||||
package bluetooth
|
||||
|
||||
import (
|
||||
"device/arm"
|
||||
"runtime/volatile"
|
||||
)
|
||||
|
||||
/*
|
||||
// Define SoftDevice functions as regular function declarations (not inline
|
||||
// static functions).
|
||||
|
@ -11,6 +16,13 @@ package bluetooth
|
|||
*/
|
||||
import "C"
|
||||
|
||||
// Memory buffers needed by sd_ble_gap_scan_start.
|
||||
var (
|
||||
scanReportBuffer rawAdvertisementPayload
|
||||
gotScanReport volatile.Register8
|
||||
globalScanResult ScanResult
|
||||
)
|
||||
|
||||
// Advertisement encapsulates a single advertisement instance.
|
||||
type Advertisement struct {
|
||||
handle uint8
|
||||
|
@ -54,3 +66,65 @@ func (a *Advertisement) Start() error {
|
|||
errCode := C.sd_ble_gap_adv_start(a.handle, C.BLE_CONN_CFG_TAG_DEFAULT)
|
||||
return makeError(errCode)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) error {
|
||||
if a.scanning {
|
||||
// There is a possible race condition here if Scan() is called from a
|
||||
// different goroutine, but that is not allowed (and will likely result
|
||||
// in an error below anyway).
|
||||
return errScanning
|
||||
}
|
||||
a.scanning = true
|
||||
|
||||
scanParams := C.ble_gap_scan_params_t{}
|
||||
scanParams.set_bitfield_extended(0)
|
||||
scanParams.set_bitfield_active(0)
|
||||
scanParams.interval = 100 * 1000 / 625 // 100ms in 625µs units
|
||||
scanParams.window = 100 * 1000 / 625 // 100ms in 625µs units
|
||||
scanParams.timeout = C.BLE_GAP_SCAN_TIMEOUT_UNLIMITED
|
||||
scanReportBufferInfo := C.ble_data_t{
|
||||
p_data: &scanReportBuffer.data[0],
|
||||
len: uint16(len(scanReportBuffer.data)),
|
||||
}
|
||||
errCode := C.sd_ble_gap_scan_start(&scanParams, &scanReportBufferInfo)
|
||||
if errCode != 0 {
|
||||
return Error(errCode)
|
||||
}
|
||||
|
||||
// Wait for received scan reports.
|
||||
for a.scanning {
|
||||
// Wait for the next advertisement packet to arrive.
|
||||
// TODO: use some sort of condition variable once the scheduler supports
|
||||
// them.
|
||||
arm.Asm("wfe")
|
||||
if gotScanReport.Get() == 0 {
|
||||
// Spurious event. Continue waiting.
|
||||
continue
|
||||
}
|
||||
gotScanReport.Set(0)
|
||||
|
||||
// Call the callback with the scan result.
|
||||
callback(a, globalScanResult)
|
||||
|
||||
// Restart the advertisement. This is needed, because advertisements are
|
||||
// automatically stopped when the first packet arrives.
|
||||
errCode := C.sd_ble_gap_scan_start(nil, &scanReportBufferInfo)
|
||||
if errCode != 0 {
|
||||
return Error(errCode)
|
||||
}
|
||||
}
|
||||
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.scanning {
|
||||
return errNotScanning
|
||||
}
|
||||
a.scanning = false
|
||||
return nil
|
||||
}
|
||||
|
|
72
mac.go
Normal file
72
mac.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package bluetooth
|
||||
|
||||
import "errors"
|
||||
|
||||
// MAC represents a MAC address, in little endian format.
|
||||
type MAC [6]byte
|
||||
|
||||
var errInvalidMAC = errors.New("bluetooth: failed to parse MAC address")
|
||||
|
||||
// ParseMAC parses the given MAC address, which must be in 11:22:33:AA:BB:CC
|
||||
// format. If it cannot be parsed, an error is returned.
|
||||
func ParseMAC(s string) (mac MAC, err error) {
|
||||
macIndex := 11
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if c == ':' {
|
||||
continue
|
||||
}
|
||||
var nibble byte
|
||||
if c >= '0' && c <= '9' {
|
||||
nibble = c - '0' + 0x0
|
||||
} else if c >= 'A' && c <= 'F' {
|
||||
nibble = c - 'A' + 0xA
|
||||
} else {
|
||||
err = errInvalidMAC
|
||||
return
|
||||
}
|
||||
if macIndex < 0 {
|
||||
err = errInvalidMAC
|
||||
return
|
||||
}
|
||||
if macIndex%2 == 0 {
|
||||
mac[macIndex/2] |= nibble
|
||||
} else {
|
||||
mac[macIndex/2] |= nibble << 4
|
||||
}
|
||||
macIndex--
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// String returns a human-readable version of this MAC address, such as
|
||||
// 11:22:33:AA:BB:CC.
|
||||
func (mac MAC) String() string {
|
||||
// TODO: make this more efficient.
|
||||
s := ""
|
||||
for i := 5; i >= 0; i-- {
|
||||
c := mac[i]
|
||||
// Insert a hyphen at the correct locations.
|
||||
if i != 5 {
|
||||
s += ":"
|
||||
}
|
||||
|
||||
// First nibble.
|
||||
nibble := c >> 4
|
||||
if nibble <= 9 {
|
||||
s += string(nibble + '0')
|
||||
} else {
|
||||
s += string(nibble + 'A' - 10)
|
||||
}
|
||||
|
||||
// Second nibble.
|
||||
nibble = c & 0x0f
|
||||
if nibble <= 9 {
|
||||
s += string(nibble + '0')
|
||||
} else {
|
||||
s += string(nibble + 'A' - 10)
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
Loading…
Reference in a new issue