From ef90e5d33785b2bc0a8f9ac25b31807974b14b0c Mon Sep 17 00:00:00 2001 From: Ron Evans Date: Sun, 13 Sep 2020 20:21:38 +0200 Subject: [PATCH] gattc: use GetUUID() to allow for bare metal use of short UUID. (#14) * gattc: use UUID() to allow for bare metal to permit clean use of short UUIDs Signed-off-by: deadprogram * gattc/macos: correct usage of UUID wrapper type alias Signed-off-by: Ron Evans * gattc/sd: correct usage of UUID wrapper type alias Signed-off-by: Ron Evans * gattc/sd, uuid/sd: changes intended to reduce memory allocations for service and characteristic discovery Signed-off-by: deadprogram * gattc/sd: partial improvements to DiscoverServices/DiscoverCharacteristics Signed-off-by: deadprogram * gattc/sd: mostly getting uuid back for services in DiscoverServices Signed-off-by: deadprogram * uuid/sd: correct way to calculate UUID from shortUUID Signed-off-by: deadprogram * gattc/sd: able to discover services and characteristics Signed-off-by: deadprogram * examples: updated discover example that can run with OS or bare metal Signed-off-by: deadprogram * gattc/sd: ensure safe casts for length of returned struct when converting short UUID Signed-off-by: deadprogram --- Makefile | 2 + adapter_nrf528xx.go | 4 + examples/discover/main.go | 47 +++++--- examples/discover/mcu.go | 21 ++++ examples/discover/os.go | 22 ++++ gattc_darwin.go | 30 +++-- gattc_linux.go | 22 +++- gattc_sd.go | 234 +++++++++++++++++++++++++------------- uuid_sd.go | 23 ++++ 9 files changed, 301 insertions(+), 104 deletions(-) create mode 100644 examples/discover/mcu.go create mode 100644 examples/discover/os.go diff --git a/Makefile b/Makefile index ef3b38a..56baa1f 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,7 @@ smoketest-linux: GOOS=linux go build -o /tmp/go-build-discard ./examples/heartrate GOOS=linux go build -o /tmp/go-build-discard ./examples/nusserver GOOS=linux go build -o /tmp/go-build-discard ./examples/scanner + GOOS=linux go build -o /tmp/go-build-discard ./examples/discover smoketest-windows: # Test on Windows. @@ -41,3 +42,4 @@ smoketest-windows: smoketest-macos: # Test on macos. GOOS=darwin CGO_ENABLED=1 go build -o /tmp/go-build-discard ./examples/scanner + GOOS=darwin CGO_ENABLED=1 go build -o /tmp/go-build-discard ./examples/discover diff --git a/adapter_nrf528xx.go b/adapter_nrf528xx.go index 5ff341e..2327996 100644 --- a/adapter_nrf528xx.go +++ b/adapter_nrf528xx.go @@ -177,6 +177,7 @@ func handleEvent() { // one discovered service. Use the first as a sensible fallback. discoveringService.startHandle.Set(discoveryEvent.services[0].handle_range.start_handle) discoveringService.endHandle.Set(discoveryEvent.services[0].handle_range.end_handle) + discoveringService.uuid = discoveryEvent.services[0].uuid } else { // No service found. discoveringService.startHandle.Set(0) @@ -192,6 +193,9 @@ func handleEvent() { discoveringCharacteristic.handle_value.Set(discoveryEvent.chars[0].handle_value) discoveringCharacteristic.char_props = discoveryEvent.chars[0].char_props discoveringCharacteristic.uuid = discoveryEvent.chars[0].uuid + } else { + // zero indicates we received no characteristic, set handle_value to last + discoveringCharacteristic.handle_value.Set(0xffff) } case C.BLE_GATTC_EVT_DESC_DISC_RSP: discoveryEvent := gattcEvent.params.unionfield_desc_disc_rsp() diff --git a/examples/discover/main.go b/examples/discover/main.go index 986f202..c24e852 100644 --- a/examples/discover/main.go +++ b/examples/discover/main.go @@ -1,7 +1,23 @@ +// This example scans and then connects to a specific Bluetooth peripheral +// and then displays all of the services and characteristics. +// +// To run this on a desktop system: +// +// go run ./examples/discover EE:74:7D:C9:2A:68 +// +// To run this on a microcontroller, change the constant value in the file +// "mcu.go" to set the MAC address of the device you want to discover. +// Then, flash to the microcontroller board like this: +// +// tinygo flash -o circuitplay-bluefruit ./examples/discover +// +// Once the program is flashed to the board, connect to the USB port +// via serial to view the output. +// package main import ( - "os" + "time" "tinygo.org/x/bluetooth" ) @@ -9,13 +25,9 @@ import ( var adapter = bluetooth.DefaultAdapter func main() { - if len(os.Args) < 2 { - println("usage: discover [local name]") - os.Exit(1) - } + time.Sleep(3 * time.Second) - // look for device with specific name - name := os.Args[1] + println("enabling") // Enable BLE interface. must("enable BLE stack", adapter.Enable()) @@ -26,7 +38,7 @@ func main() { println("scanning...") err := adapter.Scan(func(adapter *bluetooth.Adapter, result bluetooth.ScanResult) { println("found device:", result.Address.String(), result.RSSI, result.LocalName()) - if result.LocalName() == name { + if result.Address.String() == connectAddress() { adapter.StopScan() ch <- result } @@ -38,25 +50,30 @@ func main() { device, err = adapter.Connect(result.Address, bluetooth.ConnectionParams{}) if err != nil { println(err.Error()) - os.Exit(1) + return } - println("connected to ", result.LocalName()) + println("connected to ", result.Address.String()) } // get services println("discovering services/characteristics") srvcs, err := device.DiscoverServices(nil) - for _, srvc := range srvcs { - println("- service", srvc.UUID.String()) + must("discover services", err) - chars, _ := srvc.DiscoverCharacteristics(nil) + for _, srvc := range srvcs { + println("- service", srvc.UUID().String()) + + chars, err := srvc.DiscoverCharacteristics(nil) + if err != nil { + println(err) + } for _, char := range chars { - println("-- characteristic", char.UUID.String()) + println("-- characteristic", char.UUID().String()) } } - must("start scan", err) + done() } func must(action string, err error) { diff --git a/examples/discover/mcu.go b/examples/discover/mcu.go new file mode 100644 index 0000000..e12c479 --- /dev/null +++ b/examples/discover/mcu.go @@ -0,0 +1,21 @@ +// +build baremetal + +package main + +import ( + "time" +) + +// replace this with the MAC address of the Bluetooth peripheral you want to connect to. +const deviceAddress = "E4:B7:F4:11:8D:33" + +func connectAddress() string { + return deviceAddress +} + +// done just blocks forever, allows USB CDC reset for flashing new software. +func done() { + println("Done.") + + time.Sleep(1 * time.Hour) +} diff --git a/examples/discover/os.go b/examples/discover/os.go new file mode 100644 index 0000000..413ec32 --- /dev/null +++ b/examples/discover/os.go @@ -0,0 +1,22 @@ +// +build !baremetal + +package main + +import "os" + +func connectAddress() string { + if len(os.Args) < 2 { + println("usage: discover [address]") + os.Exit(1) + } + + // look for device with specific name + address := os.Args[1] + + return address +} + +// done just prints a message and allows program to exit. +func done() { + println("Done.") +} diff --git a/gattc_darwin.go b/gattc_darwin.go index eb4f2fb..91cc06b 100644 --- a/gattc_darwin.go +++ b/gattc_darwin.go @@ -33,12 +33,12 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { for _, dsvc := range d.prph.Services() { uuid, _ := ParseUUID(dsvc.UUID().String()) svc := DeviceService{ - UUID: uuid, - device: d, - service: dsvc, + uuidWrapper: uuid, + device: d, + service: dsvc, } svcs = append(svcs, svc) - d.services[svc.UUID] = &svc + d.services[svc.uuidWrapper] = &svc } return svcs, nil case <-time.NewTimer(10 * time.Second).C: @@ -46,15 +46,24 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { } } +// uuidWrapper is a type alias for UUID so we ensure no conflicts with +// struct method of the same name. +type uuidWrapper = UUID + // DeviceService is a BLE service on a connected peripheral device. type DeviceService struct { - UUID + uuidWrapper device *Device service cbgo.Service } +// UUID returns the UUID for this DeviceService. +func (s *DeviceService) UUID() UUID { + return s.uuidWrapper +} + // DiscoverCharacteristics discovers characteristics in this service. Pass a // list of characteristic UUIDs you are interested in to this function. Either a // list of all requested services is returned, or if some services could not be @@ -83,12 +92,12 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter for _, dchar := range s.service.Characteristics() { uuid, _ := ParseUUID(dchar.UUID().String()) char := DeviceCharacteristic{ - UUID: uuid, + uuidWrapper: uuid, service: s, characteristic: dchar, } chars = append(chars, char) - s.device.characteristics[char.UUID] = &char + s.device.characteristics[char.uuidWrapper] = &char } return chars, nil case <-time.NewTimer(10 * time.Second).C: @@ -99,7 +108,7 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter // DeviceCharacteristic is a BLE characteristic on a connected peripheral // device. type DeviceCharacteristic struct { - UUID + uuidWrapper service *DeviceService @@ -107,6 +116,11 @@ type DeviceCharacteristic struct { callback func(buf []byte) } +// UUID returns the UUID for this DeviceCharacteristic. +func (c *DeviceCharacteristic) UUID() UUID { + return c.uuidWrapper +} + // WriteWithoutResponse replaces the characteristic value with a new value. The // call will return before all data has been written. A limited number of such // writes can be in flight at any given time. This call is also known as a diff --git a/gattc_linux.go b/gattc_linux.go index fa48f2f..9b1558d 100644 --- a/gattc_linux.go +++ b/gattc_linux.go @@ -11,13 +11,22 @@ import ( "github.com/muka/go-bluetooth/bluez/profile/gatt" ) +// UUIDWrapper is a type alias for UUID so we ensure no conflicts with +// struct method of the same name. +type uuidWrapper = UUID + // DeviceService is a BLE service on a connected peripheral device. type DeviceService struct { - UUID + uuidWrapper service *gatt.GattService1 } +// UUID returns the UUID for this DeviceService. +func (s *DeviceService) UUID() UUID { + return s.uuidWrapper +} + // DiscoverServices starts a service discovery procedure. Pass a list of service // UUIDs you are interested in to this function. Either a slice of all services // is returned (of the same length as the requested UUIDs and in the same @@ -89,7 +98,7 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { } uuid, _ := ParseUUID(service.Properties.UUID) - ds := DeviceService{UUID: uuid, + ds := DeviceService{uuidWrapper: uuid, service: service, } @@ -108,11 +117,16 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { // DeviceCharacteristic is a BLE characteristic on a connected peripheral // device. type DeviceCharacteristic struct { - UUID + uuidWrapper characteristic *gatt.GattCharacteristic1 } +// UUID returns the UUID for this DeviceCharacteristic. +func (c *DeviceCharacteristic) UUID() UUID { + return c.uuidWrapper +} + // DiscoverCharacteristics discovers characteristics in this service. Pass a // list of characteristic UUIDs you are interested in to this function. Either a // list of all requested services is returned, or if some services could not be @@ -171,7 +185,7 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter } uuid, _ := ParseUUID(char.Properties.UUID) - dc := DeviceCharacteristic{UUID: uuid, + dc := DeviceCharacteristic{uuidWrapper: uuid, characteristic: char, } diff --git a/gattc_sd.go b/gattc_sd.go index 1388d83..716ba86 100644 --- a/gattc_sd.go +++ b/gattc_sd.go @@ -17,6 +17,11 @@ import ( "runtime/volatile" ) +const ( + maxDefaultServicesToDiscover = 6 + maxDefaultCharacteristicsToDiscover = 8 +) + var ( errAlreadyDiscovering = errors.New("bluetooth: already discovering a service or characteristic") errNotFound = errors.New("bluetooth: not found") @@ -29,23 +34,30 @@ var discoveringService struct { state volatile.Register8 // 0 means nothing happening, 1 means in progress, 2 means found something startHandle volatile.Register16 endHandle volatile.Register16 + uuid C.ble_uuid_t } // DeviceService is a BLE service on a connected peripheral device. It is only // valid as long as the device remains connected. type DeviceService struct { + uuid shortUUID + connectionHandle uint16 startHandle uint16 endHandle uint16 } +// UUID returns the UUID for this DeviceService. +func (s *DeviceService) UUID() UUID { + return s.uuid.UUID() +} + // DiscoverServices starts a service discovery procedure. Pass a list of service // UUIDs you are interested in to this function. Either a slice of all services // is returned (of the same length as the requested UUIDs and in the same // order), or if some services could not be discovered an error is returned. // -// Passing a nil slice of UUIDs will currently result in zero services being -// returned, but this may be changed in the future to return a complete list of +// Passing a nil slice of UUIDs will return a complete list of // services. // // On the Nordic SoftDevice, only one service discovery procedure may be done at @@ -56,15 +68,46 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { return nil, errAlreadyDiscovering } - services := make([]DeviceService, len(uuids)) - for i, uuid := range uuids { - // Start discovery of this service. - shortUUID, errCode := uuid.shortUUID() - if errCode != 0 { - return nil, Error(errCode) + sz := maxDefaultServicesToDiscover + if len(uuids) > 0 { + sz = len(uuids) + } + services := make([]DeviceService, 0, sz) + + var shortUUIDs []C.ble_uuid_t + + // Make a map of UUIDs in SoftDevice short form, for easier comparing. + if len(uuids) > 0 { + shortUUIDs = make([]C.ble_uuid_t, sz) + for i, uuid := range uuids { + var errCode uint32 + shortUUIDs[i], errCode = uuid.shortUUID() + if errCode != 0 { + return nil, Error(errCode) + } } + } + + numFound := 0 + + var startHandle uint16 = 1 + + for i := 0; i < sz; i++ { + var suuid C.ble_uuid_t + if len(uuids) > 0 { + suuid = shortUUIDs[i] + } + + // Start discovery of this service. discoveringService.state.Set(1) - errCode = C.sd_ble_gattc_primary_services_discover(d.connectionHandle, 0, &shortUUID) + var errCode uint32 + if len(uuids) > 0 { + errCode = C.sd_ble_gattc_primary_services_discover(d.connectionHandle, startHandle, &suuid) + } else { + // calling with nil searches for all primary services. + // TODO: need a way to set suuid from the returned data + errCode = C.sd_ble_gattc_primary_services_discover(d.connectionHandle, startHandle, nil) + } if errCode != 0 { discoveringService.state.Set(0) return nil, Error(errCode) @@ -78,8 +121,9 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { arm.Asm("wfe") } // Retrieve values, and mark the global as unused. - startHandle := discoveringService.startHandle.Get() + startHandle = discoveringService.startHandle.Get() endHandle := discoveringService.endHandle.Get() + suuid = discoveringService.uuid discoveringService.state.Set(0) if startHandle == 0 { @@ -89,11 +133,26 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { } // Store the discovered service. - services[i] = DeviceService{ + svc := DeviceService{ + uuid: suuid, connectionHandle: d.connectionHandle, startHandle: startHandle, endHandle: endHandle, } + services = append(services, svc) + + numFound++ + if numFound >= sz { + break + } + + // last entry + if endHandle == 0xffff { + break + } + + // start with the next handle + startHandle = endHandle + 1 } return services, nil @@ -102,12 +161,19 @@ func (d *Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { // DeviceCharacteristic is a BLE characteristic on a connected peripheral // device. It is only valid as long as the device remains connected. type DeviceCharacteristic struct { + uuid shortUUID + connectionHandle uint16 valueHandle uint16 cccdHandle uint16 permissions CharacteristicPermissions } +// UUID returns the UUID for this DeviceCharacteristic. +func (c *DeviceCharacteristic) UUID() UUID { + return c.uuid.UUID() +} + // A global used to pass information from the event handler back to the // DiscoverCharacteristics function below. var discoveringCharacteristic struct { @@ -123,40 +189,44 @@ var discoveringCharacteristic struct { // slice has the same length as the UUID slice with characteristics in the same // order in the slice as in the requested UUID list. // -// Passing a nil slice of UUIDs will currently result in zero characteristics -// being returned, but this may be changed in the future to return a complete +// Passing a nil slice of UUIDs will return a complete // list of characteristics. func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) { - if len(uuids) == 0 { - // Nothing to do. This behavior might change in the future (if a nil - // uuids slice is passed). - return nil, nil - } - if discoveringCharacteristic.handle_value.Get() != 0 { return nil, errAlreadyDiscovering } - // Make a list of UUIDs in SoftDevice short form, for easier comparing. - shortUUIDs := make([]C.ble_uuid_t, len(uuids)) - for i, uuid := range uuids { - var errCode uint32 - shortUUIDs[i], errCode = uuid.shortUUID() - if errCode != 0 { - return nil, Error(errCode) + sz := maxDefaultCharacteristicsToDiscover + if len(uuids) > 0 { + sz = len(uuids) + } + characteristics := make([]DeviceCharacteristic, 0, sz) + + var shortUUIDs []C.ble_uuid_t + + // Make a map of UUIDs in SoftDevice short form, for easier comparing. + if len(uuids) > 0 { + shortUUIDs = make([]C.ble_uuid_t, sz) + for i, uuid := range uuids { + var errCode uint32 + shortUUIDs[i], errCode = uuid.shortUUID() + if errCode != 0 { + return nil, Error(errCode) + } } } // Request characteristics one by one, until all are found. numFound := 0 - characteristics := make([]DeviceCharacteristic, len(uuids)) startHandle := s.startHandle - for numFound < len(uuids) && startHandle < s.endHandle { + + for startHandle < s.endHandle { // Discover the next characteristic in this service. handles := C.ble_gattc_handle_range_t{ start_handle: startHandle, - end_handle: s.endHandle, + end_handle: startHandle + 1, } + errCode := C.sd_ble_gattc_characteristics_discover(s.connectionHandle, &handles) if errCode != 0 { return nil, Error(errCode) @@ -171,64 +241,74 @@ func (s *DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacter foundCharacteristicHandle := discoveringCharacteristic.handle_value.Get() discoveringCharacteristic.handle_value.Set(0) + // was it last characteristic? + if foundCharacteristicHandle == 0xffff { + break + } + // Start the next request from the handle right after this one. startHandle = foundCharacteristicHandle + 1 - // Look whether we found a requested handle. - for i, shortUUID := range shortUUIDs { - if discoveringCharacteristic.uuid == shortUUID { - // Found a characteristic. - permissions := CharacteristicPermissions(0) - rawPermissions := discoveringCharacteristic.char_props - if rawPermissions.bitfield_broadcast() != 0 { - permissions |= CharacteristicBroadcastPermission - } - if rawPermissions.bitfield_read() != 0 { - permissions |= CharacteristicReadPermission - } - if rawPermissions.bitfield_write_wo_resp() != 0 { - permissions |= CharacteristicWriteWithoutResponsePermission - } - if rawPermissions.bitfield_write() != 0 { - permissions |= CharacteristicWritePermission - } - if rawPermissions.bitfield_notify() != 0 { - permissions |= CharacteristicNotifyPermission - } - if rawPermissions.bitfield_indicate() != 0 { - permissions |= CharacteristicIndicatePermission - } - characteristics[i].permissions = permissions - characteristics[i].valueHandle = foundCharacteristicHandle + // not one of the characteristics we are looking for + if len(shortUUIDs) > 0 && !shortUUID(discoveringCharacteristic.uuid).IsIn(shortUUIDs) { + continue + } - if permissions&CharacteristicNotifyPermission != 0 { - // This characteristic has the notify permission, so most - // likely it also supports notifications. - errCode := C.sd_ble_gattc_descriptors_discover(s.connectionHandle, &C.ble_gattc_handle_range_t{ - start_handle: startHandle, - end_handle: s.endHandle, - }) - if errCode != 0 { - return nil, Error(errCode) - } + // Found a characteristic. + permissions := CharacteristicPermissions(0) + rawPermissions := discoveringCharacteristic.char_props + if rawPermissions.bitfield_broadcast() != 0 { + permissions |= CharacteristicBroadcastPermission + } + if rawPermissions.bitfield_read() != 0 { + permissions |= CharacteristicReadPermission + } + if rawPermissions.bitfield_write_wo_resp() != 0 { + permissions |= CharacteristicWriteWithoutResponsePermission + } + if rawPermissions.bitfield_write() != 0 { + permissions |= CharacteristicWritePermission + } + if rawPermissions.bitfield_notify() != 0 { + permissions |= CharacteristicNotifyPermission + } + if rawPermissions.bitfield_indicate() != 0 { + permissions |= CharacteristicIndicatePermission + } - // Wait until the descriptor handle is found. - for discoveringCharacteristic.handle_value.Get() == 0 { - arm.Asm("wfe") - } - foundDescriptorHandle := discoveringCharacteristic.handle_value.Get() - discoveringCharacteristic.handle_value.Set(0) + dc := DeviceCharacteristic{uuid: discoveringCharacteristic.uuid} + dc.permissions = permissions + dc.valueHandle = foundCharacteristicHandle - characteristics[i].cccdHandle = foundDescriptorHandle - } - - numFound++ - break + if permissions&CharacteristicNotifyPermission != 0 { + // This characteristic has the notify permission, so most + // likely it also supports notifications. + errCode := C.sd_ble_gattc_descriptors_discover(s.connectionHandle, &C.ble_gattc_handle_range_t{ + start_handle: startHandle, + end_handle: startHandle + 1, + }) + if errCode != 0 { + return nil, Error(errCode) } + + // Wait until the descriptor handle is found. + for discoveringCharacteristic.handle_value.Get() == 0 { + arm.Asm("wfe") + } + foundDescriptorHandle := discoveringCharacteristic.handle_value.Get() + discoveringCharacteristic.handle_value.Set(0) + + dc.cccdHandle = foundDescriptorHandle + } + + characteristics = append(characteristics, dc) + numFound++ + if numFound >= sz { + break } } - if numFound != len(uuids) { + if len(uuids) > 0 && numFound != len(uuids) { return nil, errNotFound } diff --git a/uuid_sd.go b/uuid_sd.go index 0ef137a..abe93ad 100644 --- a/uuid_sd.go +++ b/uuid_sd.go @@ -12,6 +12,8 @@ package bluetooth import "C" import "unsafe" +type shortUUID C.ble_uuid_t + func (uuid UUID) shortUUID() (C.ble_uuid_t, uint32) { var short C.ble_uuid_t short.uuid = uint16(uuid[3]) @@ -22,3 +24,24 @@ func (uuid UUID) shortUUID() (C.ble_uuid_t, uint32) { errCode := C.sd_ble_uuid_vs_add((*C.ble_uuid128_t)(unsafe.Pointer(&uuid[0])), &short._type) return short, errCode } + +// UUID returns the full length UUID for this short UUID. +func (s shortUUID) UUID() UUID { + if s._type == C.BLE_UUID_TYPE_BLE { + return New16BitUUID(s.uuid) + } + var outLen C.uint8_t + var outUUID UUID + C.sd_ble_uuid_encode(((*C.ble_uuid_t)(unsafe.Pointer(&s))), &outLen, ((*C.uint8_t)(unsafe.Pointer(&outUUID)))) + return outUUID +} + +// IsIn checks the passed in slice of short UUIDs to see if this uuid is in it. +func (s shortUUID) IsIn(uuids []C.ble_uuid_t) bool { + for _, u := range uuids { + if u == s { + return true + } + } + return false +}