diff --git a/gap.go b/gap.go index 3548d58f..ee1ab427 100644 --- a/gap.go +++ b/gap.go @@ -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 { @@ -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 ( diff --git a/gap_connparams_test.go b/gap_connparams_test.go new file mode 100644 index 00000000..dda95e0f --- /dev/null +++ b/gap_connparams_test.go @@ -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") + } + }) +} diff --git a/gap_nrf528xx-central.go b/gap_nrf528xx-central.go index ed9fafef..85f0dd0b 100644 --- a/gap_nrf528xx-central.go +++ b/gap_nrf528xx-central.go @@ -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, }