diff --git a/adapter.go b/adapter.go index 03d25da..b2dfa78 100644 --- a/adapter.go +++ b/adapter.go @@ -1,8 +1,11 @@ package bluetooth +import "context" + // BLEAdapter is the shared interface that all platform-specific Adapter types must implement. type BLEAdapter interface { Connect(address Address, params ConnectionParams) (Device, error) + ConnectWithContext(ctx context.Context, address Address, params ConnectionParams) (Device, error) Enable() error Scan(callback func(*Adapter, ScanResult)) (err error) SetConnectHandler(c func(device Device, connected bool)) diff --git a/gap.go b/gap.go index 8190fb6..6cb4c9b 100644 --- a/gap.go +++ b/gap.go @@ -1,6 +1,7 @@ package bluetooth import ( + "context" "errors" "time" ) @@ -134,6 +135,7 @@ type Connection uint16 // GAPDevice is the shared interface that all platform-specific Device types must implement. type GAPDevice interface { DiscoverServices(uuids []UUID) ([]DeviceService, error) + DiscoverServicesWithContext(ctx context.Context, uuids []UUID) ([]DeviceService, error) RequestConnectionParams(params ConnectionParams) error Connected() (bool, error) Disconnect() error diff --git a/gap_darwin.go b/gap_darwin.go index 51ba7ed..90de6e3 100644 --- a/gap_darwin.go +++ b/gap_darwin.go @@ -1,6 +1,7 @@ package bluetooth import ( + "context" "errors" "fmt" "time" @@ -38,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") } @@ -62,6 +69,16 @@ 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(): + err = a.StopScan() + if err != nil { + return errors.Join(err, ctx.Err()) + } + + <-a.scanChan + close(a.scanChan) + a.scanChan = nil + return ctx.Err() case <-a.scanChan: close(a.scanChan) a.scanChan = nil @@ -106,6 +123,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 @@ -164,6 +186,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 diff --git a/gattc.go b/gattc.go index 3435eb4..2822d3c 100644 --- a/gattc.go +++ b/gattc.go @@ -2,6 +2,8 @@ package bluetooth +import "context" + // GATTCService is the common interface that all platform-specific // DeviceService types must implement. type GATTCService interface { @@ -13,6 +15,9 @@ type GATTCService interface { // function. Either a list of all requested services is returned, or if // some services could not be discovered an error is returned. DiscoverCharacteristics(uuids []UUID) ([]DeviceCharacteristic, error) + + // DiscoverCharacteristicsWithContext is the same as DiscoverCharacteristics but allows passing a context for cancellation and timeouts. + DiscoverCharacteristicsWithContext(ctx context.Context, uuids []UUID) ([]DeviceCharacteristic, error) } // GATTCCharacteristic is the common interface that all platform-specific @@ -24,13 +29,22 @@ type GATTCCharacteristic interface { // Read reads the current characteristic value. Read(data []byte) (int, error) + // ReadWithContext is the same as Read but allows passing a context for cancellation and timeouts. + ReadWithContext(ctx context.Context, data []byte) (int, error) + // Write replaces the characteristic value with a new value. Write(p []byte) (n int, err error) + // WriteWithContext is the same as Write but allows passing a context for cancellation and timeouts. + WriteWithContext(ctx context.Context, p []byte) (n int, err error) + // WriteWithoutResponse replaces the characteristic value with a new // value. The call will return before all data has been written. WriteWithoutResponse(p []byte) (n int, err error) + // WriteWithoutResponseWithContext is the same as WriteWithoutResponse but allows passing a context for cancellation and timeouts. + WriteWithoutResponseWithContext(ctx context.Context, p []byte) (n int, err error) + // EnableNotifications enables notifications for this characteristic, // calling the provided callback with the new value when the // characteristic value is changed by the remote peripheral. diff --git a/gattc_darwin.go b/gattc_darwin.go index 0362117..71ded21 100644 --- a/gattc_darwin.go +++ b/gattc_darwin.go @@ -1,6 +1,7 @@ package bluetooth import ( + "context" "errors" "slices" "time" @@ -23,6 +24,19 @@ var ( // 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) { cbuuids := make([]cbgo.UUID, len(uuids)) for i, u := range uuids { cbuuid, err := cbgo.ParseUUID(u.String()) @@ -70,8 +84,8 @@ func (d Device) DiscoverServices(uuids []UUID) ([]DeviceService, error) { } return svcs, nil - case <-time.NewTimer(10 * time.Second).C: - return nil, errors.New("timeout on DiscoverServices") + case <-ctx.Done(): + return nil, ctx.Err() } } @@ -122,6 +136,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 := make([]cbgo.UUID, len(uuids)) for i, u := range uuids { cbuuid, err := cbgo.ParseUUID(u.String()) @@ -175,8 +204,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() } } @@ -218,13 +247,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: } @@ -250,6 +287,13 @@ func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (int, error) { return len(p), nil } +// WriteWithoutResponseWithContext replaces the characteristic value with a new value. +// On Darwin, the context parameter is accepted for interface compatibility but +// not used for cancellation since WriteWithoutResponse is non-blocking. +func (c DeviceCharacteristic) WriteWithoutResponseWithContext(ctx context.Context, p []byte) (int, error) { + return c.WriteWithoutResponse(p) +} + // EnableNotifications enables notifications in the Client Characteristic // Configuration Descriptor (CCCD). This means that most peripherals will send a // notification with a new value every time the value of the characteristic @@ -275,6 +319,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) @@ -285,9 +336,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())