Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 60 additions & 1 deletion gap.go
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,38 @@ func (p ConnectionPriority) String() string {
}
}

// Params returns the connection parameters for the priority. The values obey
// the guidelines from Apple (section 35.6 Connection Parameters):
// https://developer.apple.com/accessories/Accessory-Design-Guidelines.pdf
func (p ConnectionPriority) Params() ConnectionParams {
switch p {
case ConnectionPriorityThroughput:
return ConnectionParams{
MinInterval: NewDuration(15 * time.Millisecond),
MaxInterval: NewDuration(30 * time.Millisecond),
Timeout: NewDuration(2 * time.Second),
Priority: p,
}
case ConnectionPriorityBalanced:
return ConnectionParams{
MinInterval: NewDuration(30 * time.Millisecond),
MaxInterval: NewDuration(60 * time.Millisecond),
Timeout: NewDuration(4 * time.Second),
Priority: p,
}
case ConnectionPriorityPowerSaving:
return ConnectionParams{
MinInterval: NewDuration(120 * time.Millisecond),
MaxInterval: NewDuration(165 * time.Millisecond),
PeripheralLatency: 4,
Timeout: NewDuration(6 * time.Second),
Priority: p,
}
default:
return ConnectionParams{}
}
}

// ConnectionParams are used when connecting to a peripherals or when changing
// the parameters of an active connection.
type ConnectionParams struct {
Expand All @@ -641,16 +673,43 @@ type ConnectionParams struct {
MinInterval Duration
MaxInterval Duration

// The number of connection events that the peripheral can skip. A higher
// value saves power on the peripheral. The default value of 0 skips none.
PeripheralLatency uint16

// 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

// Priority is an alternative to the fields above. Platforms that do not
// accept explicit parameters use it. Set both to make a portable request.
// accept explicit parameters use it. Resolved shows the two together.
Priority ConnectionPriority
}

// Resolved fills each unset field from Priority. Explicit values stay.
// An unset Priority changes nothing.
func (p ConnectionParams) Resolved() ConnectionParams {
if p.Priority == ConnectionPriorityUnspecified {
return p
}

preset := p.Priority.Params()
if p.MinInterval == 0 {
p.MinInterval = preset.MinInterval
}
if p.MaxInterval == 0 {
p.MaxInterval = preset.MaxInterval
}
if p.PeripheralLatency == 0 {
p.PeripheralLatency = preset.PeripheralLatency
}
if p.Timeout == 0 {
p.Timeout = preset.Timeout
}
return p
}

type PHY int

const (
Expand Down
86 changes: 86 additions & 0 deletions gap_connparams_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package bluetooth

import (
"testing"
"time"
)

// connectionPriorities are the priorities that give parameters.
var connectionPriorities = []ConnectionPriority{
ConnectionPriorityThroughput,
ConnectionPriorityBalanced,
ConnectionPriorityPowerSaving,
}

// The presets must obey the guidelines from Apple (section 35.6 Connection
// Parameters): https://developer.apple.com/accessories/Accessory-Design-Guidelines.pdf
func TestConnectionPriorityParams(t *testing.T) {
const (
unit = 625 * time.Microsecond
intervalStep = 15 * time.Millisecond
minTimeout = 2 * time.Second
maxTimeout = 6 * time.Second
)

if got := ConnectionPriorityUnspecified.Params(); got != (ConnectionParams{}) {
t.Errorf("ConnectionPriorityUnspecified.Params() = %+v, want the zero value", got)
}

for _, priority := range connectionPriorities {
params := priority.Params()
if params.Priority != priority {
t.Errorf("%s: Params().Priority = %s, want %s", priority, params.Priority, priority)
}

minInterval := time.Duration(params.MinInterval) * unit
maxInterval := time.Duration(params.MaxInterval) * unit
timeout := time.Duration(params.Timeout) * unit

if minInterval%intervalStep != 0 || maxInterval%intervalStep != 0 {
t.Errorf("%s: intervals %v/%v are not multiples of %v", priority, minInterval, maxInterval, intervalStep)
}
if maxInterval-minInterval < intervalStep {
t.Errorf("%s: MaxInterval %v is less than %v above MinInterval %v", priority, maxInterval, intervalStep, minInterval)
}
if timeout < minTimeout || timeout > maxTimeout {
t.Errorf("%s: supervision timeout %v outside [%v, %v]", priority, timeout, minTimeout, maxTimeout)
}

// The SoftDevice rejects a supervision timeout that is not longer than
// the time that the peripheral can stay silent.
silence := time.Duration(1+params.PeripheralLatency) * maxInterval * 2
if timeout <= silence {
t.Errorf("%s: supervision timeout %v does not exceed %v of allowed silence "+
"(peripheral latency %d, max interval %v)",
priority, timeout, silence, params.PeripheralLatency, maxInterval)
}
}
}

func TestConnectionParamsResolved(t *testing.T) {
t.Run("an unset priority changes nothing", func(t *testing.T) {
params := ConnectionParams{MinInterval: NewDuration(20 * time.Millisecond)}
if got := params.Resolved(); got != params {
t.Errorf("Resolved() = %+v, want %+v", got, params)
}
})

t.Run("a priority fills the unset fields", func(t *testing.T) {
got := ConnectionParams{Priority: ConnectionPriorityPowerSaving}.Resolved()
want := ConnectionPriorityPowerSaving.Params()
if got != want {
t.Errorf("Resolved() = %+v, want %+v", got, want)
}
})

t.Run("explicit values stay", func(t *testing.T) {
timeout := NewDuration(3 * time.Second)
got := ConnectionParams{Priority: ConnectionPriorityBalanced, Timeout: timeout}.Resolved()
if got.Timeout != timeout {
t.Errorf("Resolved().Timeout = %d, want the explicit %d", got.Timeout, timeout)
}
if got.MaxInterval != ConnectionPriorityBalanced.Params().MaxInterval {
t.Error("Resolved() did not fill the interval from the priority")
}
})
}
6 changes: 4 additions & 2 deletions gap_nrf528xx-central.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,13 +212,15 @@ func (d Device) Disconnect() error {
// 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.
// The SoftDevice uses the explicit fields. Priority fills the unset fields.
func (d Device) RequestConnectionParams(params ConnectionParams) error {
params = params.Resolved()

// 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,
slave_latency: C.uint16_t(params.PeripheralLatency),
conn_sup_timeout: C.BLE_GAP_CP_CONN_SUP_TIMEOUT_NONE,
}

Expand Down
Loading