2022-09-08 17:09:30 +03:00
|
|
|
package bluetooth
|
|
|
|
|
|
|
|
import (
|
2022-09-15 17:03:03 +03:00
|
|
|
"errors"
|
2022-09-08 17:09:30 +03:00
|
|
|
"fmt"
|
|
|
|
"syscall"
|
2022-09-15 17:03:03 +03:00
|
|
|
"unsafe"
|
2022-09-08 17:09:30 +03:00
|
|
|
|
2022-09-15 17:03:03 +03:00
|
|
|
"github.com/go-ole/go-ole"
|
|
|
|
"github.com/saltosystems/winrt-go"
|
2022-09-08 17:09:30 +03:00
|
|
|
"github.com/saltosystems/winrt-go/windows/devices/bluetooth"
|
|
|
|
"github.com/saltosystems/winrt-go/windows/devices/bluetooth/genericattributeprofile"
|
2022-09-15 17:03:03 +03:00
|
|
|
"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")
|
2023-02-21 00:32:56 +03:00
|
|
|
errNoNotify = errors.New("bluetooth: notify/indicate not supported")
|
2022-09-15 17:03:03 +03:00
|
|
|
errEnableNotificationsFailed = errors.New("bluetooth: enable notifications failed")
|
2022-09-08 17:09:30 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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.
|
2023-12-25 16:28:56 +03:00
|
|
|
func (d Device) DiscoverServices(filterUUIDs []UUID) ([]DeviceService, error) {
|
2022-09-08 17:09:30 +03:00
|
|
|
// 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)
|
|
|
|
|
2022-09-08 17:11:56 +03:00
|
|
|
// 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
|
2022-09-08 17:09:30 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-08 17:11:56 +03:00
|
|
|
services = append(services, DeviceService{
|
|
|
|
uuidWrapper: serviceUuid,
|
|
|
|
service: srv,
|
2022-05-12 12:49:03 +03:00
|
|
|
device: d,
|
2022-09-08 17:11:56 +03:00
|
|
|
})
|
2022-09-08 17:09:30 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2023-12-25 16:28:56 +03:00
|
|
|
device Device
|
2022-09-08 17:09:30 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// UUID returns the UUID for this DeviceService.
|
2024-01-03 18:10:21 +03:00
|
|
|
func (s DeviceService) UUID() UUID {
|
2022-09-08 17:09:30 +03:00
|
|
|
return s.uuidWrapper
|
|
|
|
}
|
2022-09-08 17:11:56 +03:00
|
|
|
|
|
|
|
// 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.
|
2024-01-03 18:10:21 +03:00
|
|
|
func (s DeviceService) DiscoverCharacteristics(filterUUIDs []UUID) ([]DeviceCharacteristic, error) {
|
2022-09-08 17:11:56 +03:00
|
|
|
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++ {
|
2022-05-12 12:49:03 +03:00
|
|
|
c, err := charVector.GetAt(i)
|
2022-09-08 17:11:56 +03:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-05-12 12:49:03 +03:00
|
|
|
characteristic := (*genericattributeprofile.GattCharacteristic)(c)
|
2022-09-08 17:11:56 +03:00
|
|
|
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,
|
2022-05-12 12:49:03 +03:00
|
|
|
service: s,
|
2022-09-08 17:11:56 +03:00
|
|
|
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
|
2022-05-12 12:49:03 +03:00
|
|
|
|
2024-01-03 18:10:21 +03:00
|
|
|
service DeviceService
|
2022-09-08 17:11:56 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// UUID returns the UUID for this DeviceCharacteristic.
|
2024-01-03 18:10:21 +03:00
|
|
|
func (c DeviceCharacteristic) UUID() UUID {
|
2022-09-08 17:11:56 +03:00
|
|
|
return c.uuidWrapper
|
|
|
|
}
|
|
|
|
|
2024-01-03 18:10:21 +03:00
|
|
|
func (c DeviceCharacteristic) Properties() uint32 {
|
2022-09-08 17:11:56 +03:00
|
|
|
return uint32(c.properties)
|
|
|
|
}
|
2022-09-15 17:03:03 +03:00
|
|
|
|
2022-05-12 12:49:03 +03:00
|
|
|
// GetMTU returns the MTU for the characteristic.
|
2024-01-03 18:10:21 +03:00
|
|
|
func (c DeviceCharacteristic) GetMTU() (uint16, error) {
|
2022-05-12 12:49:03 +03:00
|
|
|
return c.service.device.session.GetMaxPduSize()
|
|
|
|
}
|
|
|
|
|
2022-09-15 17:03:03 +03:00
|
|
|
// 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.
|
2024-01-03 18:10:21 +03:00
|
|
|
func (c DeviceCharacteristic) Read(data []byte) (int, error) {
|
2022-09-15 17:03:03 +03:00
|
|
|
if c.properties&genericattributeprofile.GattCharacteristicPropertiesRead == 0 {
|
|
|
|
return 0, errNoRead
|
|
|
|
}
|
|
|
|
|
2022-09-26 10:01:05 +03:00
|
|
|
readOp, err := c.characteristic.ReadValueWithCacheModeAsync(bluetooth.BluetoothCacheModeUncached)
|
2022-09-15 17:03:03 +03:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-03-21 18:53:33 +03:00
|
|
|
datareader, err := streams.DataReaderFromBuffer(buffer)
|
2022-09-15 17:03:03 +03:00
|
|
|
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 {
|
2023-02-21 00:32:56 +03:00
|
|
|
if (c.properties&genericattributeprofile.GattCharacteristicPropertiesNotify == 0) &&
|
2023-12-25 16:35:49 +03:00
|
|
|
(c.properties&genericattributeprofile.GattCharacteristicPropertiesIndicate == 0) {
|
2022-09-15 17:03:03 +03:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-03-21 18:53:33 +03:00
|
|
|
reader, err := streams.DataReaderFromBuffer(buf)
|
2022-09-15 17:03:03 +03:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-02-21 00:32:56 +03:00
|
|
|
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)
|
|
|
|
}
|
2022-09-15 17:03:03 +03:00
|
|
|
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
|
|
|
|
}
|