bluetooth/gattc_windows.go
Ayke van Laethem 5746ccfb60 all: don't use a pointer receiver for many method calls
This is unnecessary because the values are passed by value in other
cases, and can in some cases lead to more (heap or stack) allocation
than is necessary.
2024-01-04 20:43:16 +01:00

435 lines
12 KiB
Go

package bluetooth
import (
"errors"
"fmt"
"syscall"
"unsafe"
"github.com/go-ole/go-ole"
"github.com/saltosystems/winrt-go"
"github.com/saltosystems/winrt-go/windows/devices/bluetooth"
"github.com/saltosystems/winrt-go/windows/devices/bluetooth/genericattributeprofile"
"github.com/saltosystems/winrt-go/windows/foundation"
"github.com/saltosystems/winrt-go/windows/storage/streams"
)
var (
errNoWrite = errors.New("bluetooth: write not supported")
errNoWriteWithoutResponse = errors.New("bluetooth: write without response not supported")
errWriteFailed = errors.New("bluetooth: write failed")
errNoRead = errors.New("bluetooth: read not supported")
errNoNotify = errors.New("bluetooth: notify/indicate not supported")
errEnableNotificationsFailed = errors.New("bluetooth: enable notifications failed")
)
// DiscoverServices 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) DiscoverServices(filterUUIDs []UUID) ([]DeviceService, error) {
// IAsyncOperation<GattDeviceServicesResult>
getServicesOperation, err := d.device.GetGattServicesWithCacheModeAsync(bluetooth.BluetoothCacheModeUncached)
if err != nil {
return nil, err
}
if err := awaitAsyncOperation(getServicesOperation, genericattributeprofile.SignatureGattDeviceServicesResult); err != nil {
return nil, err
}
res, err := getServicesOperation.GetResults()
if err != nil {
return nil, err
}
servicesResult := (*genericattributeprofile.GattDeviceServicesResult)(res)
status, err := servicesResult.GetStatus()
if err != nil {
return nil, err
} else if status != genericattributeprofile.GattCommunicationStatusSuccess {
return nil, fmt.Errorf("could not retrieve device services, operation failed with code %d", status)
}
// IVectorView<GattDeviceService>
servicesVector, err := servicesResult.GetServices()
if err != nil {
return nil, err
}
// Convert services vector to array
servicesSize, err := servicesVector.GetSize()
if err != nil {
return nil, err
}
var services []DeviceService
for i := uint32(0); i < servicesSize; i++ {
s, err := servicesVector.GetAt(i)
if err != nil {
return nil, err
}
srv := (*genericattributeprofile.GattDeviceService)(s)
guid, err := srv.GetUuid()
if err != nil {
return nil, err
}
serviceUuid := winRTUuidToUuid(guid)
// only include services that are included in the input filter
if len(filterUUIDs) > 0 {
found := false
for _, uuid := range filterUUIDs {
if serviceUuid.String() == uuid.String() {
// One of the services we're looking for.
found = true
break
}
}
if !found {
continue
}
}
services = append(services, DeviceService{
uuidWrapper: serviceUuid,
service: srv,
device: d,
})
}
return services, nil
}
func winRTUuidToUuid(uuid syscall.GUID) UUID {
return NewUUID([16]byte{
byte(uuid.Data1 >> 24),
byte(uuid.Data1 >> 16),
byte(uuid.Data1 >> 8),
byte(uuid.Data1),
byte(uuid.Data2 >> 8),
byte(uuid.Data2),
byte(uuid.Data3 >> 8),
byte(uuid.Data3),
uuid.Data4[0], uuid.Data4[1],
uuid.Data4[2], uuid.Data4[3],
uuid.Data4[4], uuid.Data4[5],
uuid.Data4[6], uuid.Data4[7],
})
}
// uuidWrapper is a type alias for UUID so we ensure no conflicts with
// struct method of the same name.
type uuidWrapper = UUID
// DeviceService is a BLE service on a connected peripheral device.
type DeviceService struct {
uuidWrapper
service *genericattributeprofile.GattDeviceService
device *Device
}
// UUID returns the UUID for this DeviceService.
func (s DeviceService) UUID() UUID {
return s.uuidWrapper
}
// DiscoverCharacteristics discovers characteristics in this service. Pass a
// list of characteristic UUIDs you are interested in to this function. Either a
// list of all requested characteristics is returned, or if some characteristics 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) DiscoverCharacteristics(filterUUIDs []UUID) ([]DeviceCharacteristic, error) {
getCharacteristicsOp, err := s.service.GetCharacteristicsWithCacheModeAsync(bluetooth.BluetoothCacheModeUncached)
if err != nil {
return nil, err
}
// IAsyncOperation<GattCharacteristicsResult>
if err := awaitAsyncOperation(getCharacteristicsOp, genericattributeprofile.SignatureGattCharacteristicsResult); err != nil {
return nil, err
}
res, err := getCharacteristicsOp.GetResults()
if err != nil {
return nil, err
}
gattCharResult := (*genericattributeprofile.GattCharacteristicsResult)(res)
// IVectorView<GattCharacteristic>
charVector, err := gattCharResult.GetCharacteristics()
if err != nil {
return nil, err
}
// Convert characteristics vector to array
characteristicsSize, err := charVector.GetSize()
if err != nil {
return nil, err
}
var characteristics []DeviceCharacteristic
for i := uint32(0); i < characteristicsSize; i++ {
c, err := charVector.GetAt(i)
if err != nil {
return nil, err
}
characteristic := (*genericattributeprofile.GattCharacteristic)(c)
guid, err := characteristic.GetUuid()
if err != nil {
return nil, err
}
characteristicUUID := winRTUuidToUuid(guid)
properties, err := characteristic.GetCharacteristicProperties()
if err != nil {
return nil, err
}
// only include characteristics that are included in the input filter
if len(filterUUIDs) > 0 {
found := false
for _, uuid := range filterUUIDs {
if characteristicUUID.String() == uuid.String() {
// One of the characteristics we're looking for.
found = true
break
}
}
if !found {
continue
}
}
characteristics = append(characteristics, DeviceCharacteristic{
uuidWrapper: characteristicUUID,
service: s,
characteristic: characteristic,
properties: properties,
})
}
return characteristics, nil
}
// DeviceCharacteristic is a BLE characteristic on a connected peripheral
// device.
type DeviceCharacteristic struct {
uuidWrapper
characteristic *genericattributeprofile.GattCharacteristic
properties genericattributeprofile.GattCharacteristicProperties
service DeviceService
}
// UUID returns the UUID for this DeviceCharacteristic.
func (c DeviceCharacteristic) UUID() UUID {
return c.uuidWrapper
}
func (c DeviceCharacteristic) Properties() uint32 {
return uint32(c.properties)
}
// GetMTU returns the MTU for the characteristic.
func (c DeviceCharacteristic) GetMTU() (uint16, error) {
return c.service.device.session.GetMaxPduSize()
}
// 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) {
if c.properties&genericattributeprofile.GattCharacteristicPropertiesWrite == 0 {
return 0, errNoWrite
}
return c.write(p, genericattributeprofile.GattWriteOptionWriteWithResponse)
}
// WriteWithoutResponse replaces the characteristic value with a new value. The
// call will return before all data has been written. A limited number of such
// writes can be in flight at any given time. This call is also known as a
// "write command" (as opposed to a write request).
func (c DeviceCharacteristic) WriteWithoutResponse(p []byte) (n int, err error) {
if c.properties&genericattributeprofile.GattCharacteristicPropertiesWriteWithoutResponse == 0 {
return 0, errNoWriteWithoutResponse
}
return c.write(p, genericattributeprofile.GattWriteOptionWriteWithoutResponse)
}
func (c DeviceCharacteristic) write(p []byte, mode genericattributeprofile.GattWriteOption) (n int, err error) {
// Convert data to buffer
writer, err := streams.NewDataWriter()
if err != nil {
return 0, err
}
defer writer.Release()
// Add bytes to writer
if err := writer.WriteBytes(uint32(len(p)), p); err != nil {
return 0, err
}
value, err := writer.DetachBuffer()
if err != nil {
return 0, err
}
// IAsyncOperation<GattCommunicationStatus>
asyncOp, err := c.characteristic.WriteValueWithOptionAsync(value, mode)
if err := awaitAsyncOperation(asyncOp, genericattributeprofile.SignatureGattCommunicationStatus); err != nil {
return 0, err
}
res, err := asyncOp.GetResults()
if err != nil {
return 0, err
}
status := genericattributeprofile.GattCommunicationStatus(uintptr(res))
// Is the status success?
if status != genericattributeprofile.GattCommunicationStatusSuccess {
return 0, errWriteFailed
}
// Success
return len(p), nil
}
// Read reads the current characteristic value.
func (c DeviceCharacteristic) Read(data []byte) (int, error) {
if c.properties&genericattributeprofile.GattCharacteristicPropertiesRead == 0 {
return 0, errNoRead
}
readOp, err := c.characteristic.ReadValueWithCacheModeAsync(bluetooth.BluetoothCacheModeUncached)
if err != nil {
return 0, err
}
// IAsyncOperation<GattReadResult>
if err := awaitAsyncOperation(readOp, genericattributeprofile.SignatureGattReadResult); err != nil {
return 0, err
}
res, err := readOp.GetResults()
if err != nil {
return 0, err
}
result := (*genericattributeprofile.GattReadResult)(res)
buffer, err := result.GetValue()
if err != nil {
return 0, err
}
datareader, err := streams.FromBuffer(buffer)
if err != nil {
return 0, err
}
bufferlen, err := buffer.GetLength()
if err != nil {
return 0, err
}
readBuffer, err := datareader.ReadBytes(bufferlen)
if err != nil {
return 0, err
}
copy(data, readBuffer)
return len(readBuffer), nil
}
// 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
// changes.
func (c DeviceCharacteristic) EnableNotifications(callback func(buf []byte)) error {
if (c.properties&genericattributeprofile.GattCharacteristicPropertiesNotify == 0) &&
(c.properties&genericattributeprofile.GattCharacteristicPropertiesIndicate == 0) {
return errNoNotify
}
// listen value changed event
// TypedEventHandler<GattCharacteristic,GattValueChangedEventArgs>
guid := winrt.ParameterizedInstanceGUID(foundation.GUIDTypedEventHandler, genericattributeprofile.SignatureGattCharacteristic, genericattributeprofile.SignatureGattValueChangedEventArgs)
valueChangedEventHandler := foundation.NewTypedEventHandler(ole.NewGUID(guid), func(instance *foundation.TypedEventHandler, sender, args unsafe.Pointer) {
valueChangedEvent := (*genericattributeprofile.GattValueChangedEventArgs)(args)
buf, err := valueChangedEvent.GetCharacteristicValue()
if err != nil {
return
}
reader, err := streams.FromBuffer(buf)
if err != nil {
return
}
defer reader.Release()
buflen, err := buf.GetLength()
if err != nil {
return
}
data, err := reader.ReadBytes(buflen)
if err != nil {
return
}
callback(data)
})
_, err := c.characteristic.AddValueChanged(valueChangedEventHandler)
if err != nil {
return err
}
var writeOp *foundation.IAsyncOperation
if c.properties&genericattributeprofile.GattCharacteristicPropertiesNotify != 0 {
writeOp, err = c.characteristic.WriteClientCharacteristicConfigurationDescriptorAsync(genericattributeprofile.GattClientCharacteristicConfigurationDescriptorValueNotify)
} else {
writeOp, err = c.characteristic.WriteClientCharacteristicConfigurationDescriptorAsync(genericattributeprofile.GattClientCharacteristicConfigurationDescriptorValueIndicate)
}
if err != nil {
return err
}
// IAsyncOperation<GattCommunicationStatus>
if err := awaitAsyncOperation(writeOp, genericattributeprofile.SignatureGattCommunicationStatus); err != nil {
return err
}
res, err := writeOp.GetResults()
if err != nil {
return err
}
result := genericattributeprofile.GattCommunicationStatus(uintptr(res))
if result != genericattributeprofile.GattCommunicationStatusSuccess {
return errEnableNotificationsFailed
}
return nil
}