From d74f6a1009ac7d65dac1f52e2d520af46aeecb77 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Mon, 25 Dec 2023 15:29:49 +0100 Subject: [PATCH] all: add RequestConnectionParams to request new connection parameters This allows changing the connection latency, slave latency, and connection timeout of an active connection - whether in the central or peripheral role. This is especially helpful on battery operated BLE devices that don't have a lot of power and need to lower the connection latency for improved speed. It might also be useful for devices that need high speed, as the defaults might be too low. --- Makefile | 3 ++ examples/connparams/main.go | 84 +++++++++++++++++++++++++++++++++++++ gap.go | 8 +++- gap_darwin.go | 12 ++++++ gap_linux.go | 11 +++++ gap_ninafw.go | 10 +++++ gap_nrf528xx-central.go | 31 ++++++++++++++ gap_windows.go | 12 ++++++ 8 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 examples/connparams/main.go diff --git a/Makefile b/Makefile index efa73b2..28da815 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,8 @@ smoketest-tinygo: @md5sum test.hex $(TINYGO) build -o test.uf2 -size=short -target=circuitplay-bluefruit ./examples/circuitplay @md5sum test.hex + $(TINYGO) build -o test.hex -size=short -target=circuitplay-bluefruit ./examples/connparams + @md5sum test.hex $(TINYGO) build -o test.uf2 -size=short -target=circuitplay-bluefruit ./examples/discover @md5sum test.hex $(TINYGO) build -o test.hex -size=short -target=pca10040-s132v6 ./examples/heartrate @@ -42,6 +44,7 @@ smoketest-tinygo: smoketest-linux: # Test on Linux. GOOS=linux go build -o /tmp/go-build-discard ./examples/advertisement + GOOS=linux go build -o /tmp/go-build-discard ./examples/connparams GOOS=linux go build -o /tmp/go-build-discard ./examples/heartrate GOOS=linux go build -o /tmp/go-build-discard ./examples/heartrate-monitor GOOS=linux go build -o /tmp/go-build-discard ./examples/nusserver diff --git a/examples/connparams/main.go b/examples/connparams/main.go new file mode 100644 index 0000000..762cbb9 --- /dev/null +++ b/examples/connparams/main.go @@ -0,0 +1,84 @@ +// Test for setting connection parameters. +// +// To test this feature, run this either on a desktop OS or by flashing it to a +// device with TinyGo. Then connect to it from a BLE connection debugger, for +// example nRF Connect on Android. After a second, you should see in the log of +// the BLE app that the connection latency has been updated. It might look +// something like this: +// +// Connection parameters updated (interval: 510.0ms, latency: 0, timeout: 10000ms) +package main + +import ( + "time" + + "tinygo.org/x/bluetooth" +) + +var ( + adapter = bluetooth.DefaultAdapter + newDevice chan bluetooth.Device +) + +func main() { + must("enable BLE stack", adapter.Enable()) + + newDevice = make(chan bluetooth.Device, 1) + adapter.SetConnectHandler(func(device bluetooth.Device, connected bool) { + // If this is a new device, signal it to the separate goroutine. + if connected { + select { + case newDevice <- device: + default: + } + } + }) + + // Start advertising, so we can be found. + const name = "Go BLE test" + adv := adapter.DefaultAdvertisement() + adv.Configure(bluetooth.AdvertisementOptions{ + LocalName: name, + }) + adv.Start() + println("advertising:", name) + + for device := range newDevice { + println("connection from device:", device.Address.String()) + + // Discover services and characteristics. + svcs, err := device.DiscoverServices(nil) + if err != nil { + println(" failed to resolve services:", err) + } + for _, svc := range svcs { + println(" service:", svc.UUID().String()) + chars, err := svc.DiscoverCharacteristics(nil) + if err != nil { + println(" failed to resolve characteristics:", err) + } + for _, char := range chars { + println(" characteristic:", char.UUID().String()) + } + } + + // Update connection parameters (as a test). + time.Sleep(time.Second) + err = device.RequestConnectionParams(bluetooth.ConnectionParams{ + MinInterval: bluetooth.NewDuration(495 * time.Millisecond), + MaxInterval: bluetooth.NewDuration(510 * time.Millisecond), + Timeout: bluetooth.NewDuration(10 * time.Second), + }) + if err != nil { + println(" failed to update connection parameters:", err) + continue + } + println(" updated connection parameters") + } +} + +func must(action string, err error) { + if err != nil { + panic("failed to " + action + ": " + err.Error()) + } +} diff --git a/gap.go b/gap.go index 17dddfd..f6a418c 100644 --- a/gap.go +++ b/gap.go @@ -391,7 +391,8 @@ func (buf *rawAdvertisementPayload) addServiceUUID(uuid UUID) (ok bool) { } } -// ConnectionParams are used when connecting to a peripherals. +// ConnectionParams are used when connecting to a peripherals or when changing +// the parameters of an active connection. type ConnectionParams struct { // The timeout for the connection attempt. Not used during the rest of the // connection. If no duration is specified, a default timeout will be used. @@ -403,4 +404,9 @@ type ConnectionParams struct { // will be used. MinInterval Duration MaxInterval Duration + + // Connection Supervision Timeout. After this time has passed with no + // communication, the connection is considered lost. If no timeout is + // specified, the timeout will be unchanged. + Timeout Duration } diff --git a/gap_darwin.go b/gap_darwin.go index c87db05..542535c 100644 --- a/gap_darwin.go +++ b/gap_darwin.go @@ -176,6 +176,18 @@ func (d Device) Disconnect() error { return nil } +// RequestConnectionParams requests a different connection latency and timeout +// of the given device connection. Fields that are unset will be left alone. +// Whether or not the device will actually honor this, depends on the device and +// on the specific parameters. +// +// This call has not yet been implemented on macOS. +func (d Device) RequestConnectionParams(params ConnectionParams) error { + // TODO: implement this using setDesiredConnectionLatency, see: + // https://developer.apple.com/documentation/corebluetooth/cbperipheralmanager/1393277-setdesiredconnectionlatency + return nil +} + // Peripheral delegate functions type peripheralDelegate struct { diff --git a/gap_linux.go b/gap_linux.go index 2c77c36..80ecf1a 100644 --- a/gap_linux.go +++ b/gap_linux.go @@ -366,3 +366,14 @@ func (d Device) Disconnect() error { // property change in `watchForConnect` and cancel things then return d.device.Call("org.bluez.Device1.Disconnect", 0).Err } + +// RequestConnectionParams requests a different connection latency and timeout +// of the given device connection. Fields that are unset will be left alone. +// Whether or not the device will actually honor this, depends on the device and +// on the specific parameters. +// +// On Linux, this call doesn't do anything because BlueZ doesn't support +// changing the connection latency. +func (d Device) RequestConnectionParams(params ConnectionParams) error { + return nil +} diff --git a/gap_ninafw.go b/gap_ninafw.go index 51fbbf5..5cf7840 100644 --- a/gap_ninafw.go +++ b/gap_ninafw.go @@ -230,6 +230,16 @@ func (d Device) Disconnect() error { return nil } +// RequestConnectionParams requests a different connection latency and timeout +// of the given device connection. Fields that are unset will be left alone. +// Whether or not the device will actually honor this, depends on the device and +// on the specific parameters. +// +// On NINA, this call hasn't been implemented yet. +func (d Device) RequestConnectionParams(params ConnectionParams) error { + return nil +} + func (d Device) findNotificationRegistration(handle uint16) *notificationRegistration { for _, n := range d.notificationRegistrations { if n.handle == handle { diff --git a/gap_nrf528xx-central.go b/gap_nrf528xx-central.go index b451573..085e7d0 100644 --- a/gap_nrf528xx-central.go +++ b/gap_nrf528xx-central.go @@ -188,3 +188,34 @@ func (d Device) Disconnect() error { return nil } + +// RequestConnectionParams requests a different connection latency and timeout +// of the given device connection. Fields that are unset will be left alone. +// Whether or not the device will actually honor this, depends on the device and +// on the specific parameters. +// +// On the Nordic SoftDevice, this call will also set the slave latency to 0. +func (d Device) RequestConnectionParams(params ConnectionParams) error { + // The default parameters if no specific parameters are picked. + connParams := C.ble_gap_conn_params_t{ + min_conn_interval: C.BLE_GAP_CP_MIN_CONN_INTVL_NONE, + max_conn_interval: C.BLE_GAP_CP_MAX_CONN_INTVL_NONE, + slave_latency: 0, + conn_sup_timeout: C.BLE_GAP_CP_CONN_SUP_TIMEOUT_NONE, + } + + // Use specified parameters if available. + if params.MinInterval != 0 { + connParams.min_conn_interval = C.uint16_t(params.MinInterval) / 2 + } + if params.MaxInterval != 0 { + connParams.max_conn_interval = C.uint16_t(params.MaxInterval) / 2 + } + if params.Timeout != 0 { + connParams.conn_sup_timeout = C.uint16_t(params.Timeout) / 16 + } + + // Send them to peer device. + errCode := C.sd_ble_gap_conn_param_update(d.connectionHandle, &connParams) + return makeError(errCode) +} diff --git a/gap_windows.go b/gap_windows.go index 5ae0f97..c800c25 100644 --- a/gap_windows.go +++ b/gap_windows.go @@ -247,3 +247,15 @@ func (d Device) Disconnect() error { return nil } + +// RequestConnectionParams requests a different connection latency and timeout +// of the given device connection. Fields that are unset will be left alone. +// Whether or not the device will actually honor this, depends on the device and +// on the specific parameters. +// +// On Windows, this call doesn't do anything. +func (d Device) RequestConnectionParams(params ConnectionParams) error { + // TODO: implement this using + // BluetoothLEDevice.RequestPreferredConnectionParameters. + return nil +}