From dadb65cf14cf9835823c1b7ba3d24a673545a642 Mon Sep 17 00:00:00 2001 From: Alexis Couvreur Date: Sat, 4 Oct 2025 12:28:35 -0400 Subject: [PATCH 1/2] feat(gap): add ConnectWithContext in gap_darwin.go Related https://github.com/tinygo-org/bluetooth/issues/339 --- gap_darwin.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/gap_darwin.go b/gap_darwin.go index 542535c4..dae4f712 100644 --- a/gap_darwin.go +++ b/gap_darwin.go @@ -1,6 +1,7 @@ package bluetooth import ( + "context" "errors" "fmt" "time" @@ -104,6 +105,11 @@ type deviceInternal struct { // Connect starts a connection attempt to the given peripheral device address. func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, error) { + return a.ConnectWithContext(context.Background(), address, params) +} + +// ConnectWithContext starts a connection attempt to the given peripheral device address. +func (a *Adapter) ConnectWithContext(ctx context.Context, address Address, params ConnectionParams) (Device, error) { uuid, err := cbgo.ParseUUID(address.UUID.String()) if err != nil { return Device{}, err @@ -162,6 +168,16 @@ func (a *Adapter) Connect(address Address, params ConnectionParams) (Device, err // record an error to use when the disconnect comes through later. connectionError = errors.New("timeout on Connect") + // we are not ready to return yet, we need to wait for the disconnect event to come through + // so continue on from this case and wait for something to show up on prphCh + continue + case <-ctx.Done(): + // we need to cancel the connection if the context is done + a.cm.CancelConnect(prphs[0]) + + // record an error to use when the disconnect comes through later. + connectionError = ctx.Err() + // we are not ready to return yet, we need to wait for the disconnect event to come through // so continue on from this case and wait for something to show up on prphCh continue From 434206ba68f6bf0f3793fef29107d2c5d3504078 Mon Sep 17 00:00:00 2001 From: Alexis Couvreur Date: Sat, 4 Oct 2025 12:48:41 -0400 Subject: [PATCH 2/2] feat(gap): add DiscoverServicesWithContext DiscoverCharacteristicsWithContext WriteWithContext and ReadWithContext Related https://github.com/tinygo-org/bluetooth/issues/339 --- gap_darwin.go | 11 +++++++++ gattc_darwin.go | 60 ++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/gap_darwin.go b/gap_darwin.go index dae4f712..fc1c1434 100644 --- a/gap_darwin.go +++ b/gap_darwin.go @@ -39,6 +39,12 @@ func (ad *Address) Set(val string) { // 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)) (err error) { + return a.ScanWithContext(context.Background(), callback) +} + +// ScanWithContext 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) ScanWithContext(ctx context.Context, callback func(*Adapter, ScanResult)) (err error) { if callback == nil { return errors.New("must provide callback to Scan function") } @@ -63,6 +69,11 @@ func (a *Adapter) Scan(callback func(*Adapter, ScanResult)) (err error) { // the callback calls StopScan() (no new callbacks may be called after // StopScan is called). select { + case <-ctx.Done(): + // StopScan can return an error, but we ignore it here since + // it only returns an error if no scan is in progress. + _ = a.StopScan() + return ctx.Err() case <-a.scanChan: close(a.scanChan) a.scanChan = nil diff --git a/gattc_darwin.go b/gattc_darwin.go index 2d737dad..ca9ff571 100644 --- a/gattc_darwin.go +++ b/gattc_darwin.go @@ -1,6 +1,7 @@ package bluetooth import ( + "context" "errors" "time" @@ -15,6 +16,19 @@ import ( // Passing a nil slice of UUIDs will return a complete list of // services. func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { + ctx, cancel := context.WithTimeoutCause(context.Background(), 10*time.Second, errors.New("timeout on DiscoverServices")) + defer cancel() + return d.DiscoverServicesWithContext(ctx, uuids) +} + +// DiscoverServicesWithContext 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 return a complete list of +// services. +func (d Device) DiscoverServicesWithContext(ctx context.Context, uuids []UUID) ([]DeviceService, error) { d.prph.DiscoverServices([]cbgo.UUID{}) // clear cache of services @@ -52,8 +66,8 @@ func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { d.services[svc.uuidWrapper] = svc } return svcs, nil - case <-time.NewTimer(10 * time.Second).C: - return nil, errors.New("timeout on DiscoverServices") + case <-ctx.Done(): + return nil, ctx.Err() } } @@ -90,6 +104,21 @@ func (s DeviceService) UUID() UUID { // Passing a nil slice of UUIDs will return a complete list of // characteristics. func (s DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) { + ctx, cancel := context.WithTimeoutCause(context.Background(), 10*time.Second, errors.New("timeout on DiscoverCharacteristics")) + defer cancel() + return s.DiscoverCharacteristicsWithContext(ctx, uuids) +} + +// DiscoverCharacteristicsWithContext 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 +// discovered an error is returned. If there is no error, the characteristics +// 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 return a complete list of +// characteristics. +func (s DeviceService) DiscoverCharacteristicsWithContext(ctx context.Context, uuids []UUID) ([]DeviceCharacteristic, error) { cbuuids := []cbgo.UUID{} s.device.prph.DiscoverCharacteristics(cbuuids, s.service) @@ -136,8 +165,8 @@ func (s DeviceService) DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteri } } return chars, nil - case <-time.NewTimer(10 * time.Second).C: - return nil, errors.New("timeout on DiscoverCharacteristics") + case <-ctx.Done(): + return nil, ctx.Err() } } @@ -179,13 +208,21 @@ func (c DeviceCharacteristic) UUID() UUID { // Write replaces the characteristic value with a new value. The // call will return after all data has been written. func (c DeviceCharacteristic) Write(p []byte) (n int, err error) { + ctx, cancel := context.WithTimeoutCause(context.Background(), 10*time.Second, errors.New("timeout on Write()")) + defer cancel() + return c.WriteWithContext(ctx, p) +} + +// WriteWithContext replaces the characteristic value with a new value. The +// call will return after all data has been written. +func (c DeviceCharacteristic) WriteWithContext(ctx context.Context, p []byte) (n int, err error) { c.writeChan = make(chan error) c.service.device.prph.WriteCharacteristic(p, c.characteristic, true) // wait for result select { - case <-time.NewTimer(10 * time.Second).C: - err = errors.New("timeout on Write()") + case <-ctx.Done(): + err = ctx.Err() case err = <-c.writeChan: } @@ -229,6 +266,13 @@ func (c DeviceCharacteristic) GetMTU() (uint16, error) { // Read reads the current characteristic value. func (c *deviceCharacteristic) Read(data []byte) (n int, err error) { + ctx, cancel := context.WithTimeoutCause(context.Background(), 10*time.Second, errors.New("timeout on Read()")) + defer cancel() + return c.ReadWithContext(ctx, data) +} + +// ReadWithContext reads the current characteristic value. +func (c *deviceCharacteristic) ReadWithContext(ctx context.Context, data []byte) (n int, err error) { c.readChan = make(chan error) c.service.device.prph.ReadCharacteristic(c.characteristic) @@ -239,9 +283,9 @@ func (c *deviceCharacteristic) Read(data []byte) (n int, err error) { if err != nil { return 0, err } - case <-time.NewTimer(10 * time.Second).C: + case <-ctx.Done(): c.readChan = nil - return 0, errors.New("timeout on Read()") + return 0, ctx.Err() } copy(data, c.characteristic.Value())