parent
6b08161955
commit
314ca89209
8 changed files with 427 additions and 5 deletions
2
Makefile
2
Makefile
|
@ -58,6 +58,8 @@ smoketest-windows:
|
|||
GOOS=windows go build -o /tmp/go-build-discard ./examples/scanner
|
||||
GOOS=windows go build -o /tmp/go-build-discard ./examples/discover
|
||||
GOOS=windows go build -o /tmp/go-build-discard ./examples/heartrate-monitor
|
||||
GOOS=windows go build -o /tmp/go-build-discard ./examples/advertisement
|
||||
GOOS=windows go build -o /tmp/go-build-discard ./examples/heartrate
|
||||
|
||||
smoketest-macos:
|
||||
# Test on macos.
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package bluetooth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-ole/go-ole"
|
||||
|
@ -13,6 +14,8 @@ type Adapter struct {
|
|||
watcher *advertisement.BluetoothLEAdvertisementWatcher
|
||||
|
||||
connectHandler func(device Device, connected bool)
|
||||
|
||||
defaultAdvertisement *Advertisement
|
||||
}
|
||||
|
||||
// DefaultAdapter is the default adapter on the system.
|
||||
|
@ -56,3 +59,8 @@ func awaitAsyncOperation(asyncOperation *foundation.IAsyncOperation, genericPara
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Adapter) Address() (MACAddress, error) {
|
||||
// TODO: get mac address
|
||||
return MACAddress{}, errors.New("not implemented")
|
||||
}
|
||||
|
|
102
gap_windows.go
102
gap_windows.go
|
@ -18,6 +18,108 @@ type Address struct {
|
|||
MACAddress
|
||||
}
|
||||
|
||||
type Advertisement struct {
|
||||
advertisement *advertisement.BluetoothLEAdvertisement
|
||||
publisher *advertisement.BluetoothLEAdvertisementPublisher
|
||||
}
|
||||
|
||||
// DefaultAdvertisement returns the default advertisement instance but does not
|
||||
// configure it.
|
||||
func (a *Adapter) DefaultAdvertisement() *Advertisement {
|
||||
if a.defaultAdvertisement == nil {
|
||||
a.defaultAdvertisement = &Advertisement{}
|
||||
}
|
||||
|
||||
return a.defaultAdvertisement
|
||||
}
|
||||
|
||||
// Configure this advertisement.
|
||||
// on Windows we're only able to set "Manufacturer Data" for advertisements.
|
||||
// https://learn.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.advertisement.bluetoothleadvertisementpublisher?view=winrt-22621#remarks
|
||||
// following this c# source for this implementation: https://github.com/microsoft/Windows-universal-samples/blob/main/Samples/BluetoothAdvertisement/cs/Scenario2_Publisher.xaml.cs
|
||||
// adding service data / localname leads to errors when starting the advertisement.
|
||||
func (a *Advertisement) Configure(options AdvertisementOptions) error {
|
||||
// we can only advertise manufacturer / company data on windows, so no need to continue if we have none
|
||||
if len(options.ManufacturerData) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if a.publisher != nil {
|
||||
a.publisher.Release()
|
||||
}
|
||||
|
||||
if a.advertisement != nil {
|
||||
a.advertisement.Release()
|
||||
}
|
||||
|
||||
pub, err := advertisement.NewBluetoothLEAdvertisementPublisher()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.publisher = pub
|
||||
|
||||
ad, err := a.publisher.GetAdvertisement()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.advertisement = ad
|
||||
|
||||
vec, err := ad.GetManufacturerData()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, optManData := range options.ManufacturerData {
|
||||
writer, err := streams.NewDataWriter()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer writer.Release()
|
||||
|
||||
err = writer.WriteBytes(uint32(len(optManData.Data)), optManData.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf, err := writer.DetachBuffer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manData, err := advertisement.BluetoothLEManufacturerDataCreate(optManData.CompanyID, buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = vec.Append(unsafe.Pointer(&manData.IUnknown.RawVTable)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start advertisement. May only be called after it has been configured.
|
||||
func (a *Advertisement) Start() error {
|
||||
// publisher will be present if we actually have manufacturer data to advertise.
|
||||
if a.publisher != nil {
|
||||
return a.publisher.Start()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop advertisement. May only be called after it has been started.
|
||||
func (a *Advertisement) Stop() error {
|
||||
if a.publisher != nil {
|
||||
return a.publisher.Stop()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
|
4
gatts.go
4
gatts.go
|
@ -7,6 +7,8 @@ type Service struct {
|
|||
Characteristics []CharacteristicConfig
|
||||
}
|
||||
|
||||
type WriteEvent = func(client Connection, offset int, value []byte)
|
||||
|
||||
// CharacteristicConfig contains some parameters for the configuration of a
|
||||
// single characteristic.
|
||||
//
|
||||
|
@ -17,7 +19,7 @@ type CharacteristicConfig struct {
|
|||
UUID
|
||||
Value []byte
|
||||
Flags CharacteristicPermissions
|
||||
WriteEvent func(client Connection, offset int, value []byte)
|
||||
WriteEvent WriteEvent
|
||||
}
|
||||
|
||||
// CharacteristicPermissions lists a number of basic permissions/capabilities
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
//go:build !linux
|
||||
//go:build !linux && !windows
|
||||
|
||||
package bluetooth
|
||||
|
||||
|
|
308
gatts_windows.go
Normal file
308
gatts_windows.go
Normal file
|
@ -0,0 +1,308 @@
|
|||
package bluetooth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/go-ole/go-ole"
|
||||
"github.com/saltosystems/winrt-go"
|
||||
"github.com/saltosystems/winrt-go/windows/devices/bluetooth/genericattributeprofile"
|
||||
"github.com/saltosystems/winrt-go/windows/foundation"
|
||||
"github.com/saltosystems/winrt-go/windows/foundation/collections"
|
||||
"github.com/saltosystems/winrt-go/windows/storage/streams"
|
||||
)
|
||||
|
||||
// Characteristic is a single characteristic in a service. It has an UUID and a
|
||||
// value.
|
||||
type Characteristic struct {
|
||||
wintCharacteristic *genericattributeprofile.GattLocalCharacteristic
|
||||
writeEvent WriteEvent
|
||||
flags CharacteristicPermissions
|
||||
|
||||
valueMtx *sync.Mutex
|
||||
value []byte
|
||||
}
|
||||
|
||||
// AddService creates a new service with the characteristics listed in the
|
||||
// Service struct.
|
||||
func (a *Adapter) AddService(s *Service) error {
|
||||
gattServiceOp, err := genericattributeprofile.GattServiceProviderCreateAsync(syscallUUIDFromUUID(s.UUID))
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = awaitAsyncOperation(gattServiceOp, genericattributeprofile.SignatureGattServiceProviderResult); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := gattServiceOp.GetResults()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceProviderResult := (*genericattributeprofile.GattServiceProviderResult)(res)
|
||||
serviceProvider, err := serviceProviderResult.GetServiceProvider()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
localService, err := serviceProvider.GetService()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: "ParameterizedInstanceGUID" + "foundation.NewTypedEventHandler"
|
||||
// seems to always return the same instance, need to figure out how to get different instances each time...
|
||||
// was following c# source for this flow: https://github.com/microsoft/Windows-universal-samples/blob/main/Samples/BluetoothLE/cs/Scenario3_ServerForeground.xaml.cs
|
||||
// which relies on instanced event handlers. for now we'll manually setup our handlers with a map of golang characteristics
|
||||
//
|
||||
// TypedEventHandler<GattLocalCharacteristic,GattWriteRequestedEventArgs>
|
||||
guid := winrt.ParameterizedInstanceGUID(
|
||||
foundation.GUIDTypedEventHandler,
|
||||
genericattributeprofile.SignatureGattLocalCharacteristic,
|
||||
genericattributeprofile.SignatureGattWriteRequestedEventArgs)
|
||||
|
||||
goChars := map[syscall.GUID]*Characteristic{}
|
||||
|
||||
writeRequestedHandler := foundation.NewTypedEventHandler(ole.NewGUID(guid), func(instance *foundation.TypedEventHandler, sender, args unsafe.Pointer) {
|
||||
writeReqArgs := (*genericattributeprofile.GattWriteRequestedEventArgs)(args)
|
||||
reqAsyncOp, err := writeReqArgs.GetRequestAsync()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = awaitAsyncOperation(reqAsyncOp, genericattributeprofile.SignatureGattWriteRequest); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := reqAsyncOp.GetResults()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
gattWriteRequest := (*genericattributeprofile.GattWriteRequest)(res)
|
||||
|
||||
buf, err := gattWriteRequest.GetValue()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
offset, err := gattWriteRequest.GetOffset()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
characteristic := (*genericattributeprofile.GattLocalCharacteristic)(sender)
|
||||
uuid, err := characteristic.GetUuid()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
goChar, ok := goChars[uuid]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if goChar.writeEvent != nil {
|
||||
// TODO: connection?
|
||||
goChar.writeEvent(0, int(offset), bufferToSlice(buf))
|
||||
}
|
||||
})
|
||||
|
||||
guid = winrt.ParameterizedInstanceGUID(
|
||||
foundation.GUIDTypedEventHandler,
|
||||
genericattributeprofile.SignatureGattLocalCharacteristic,
|
||||
genericattributeprofile.SignatureGattReadRequestedEventArgs)
|
||||
|
||||
readRequestedHandler := foundation.NewTypedEventHandler(ole.NewGUID(guid), func(instance *foundation.TypedEventHandler, sender, args unsafe.Pointer) {
|
||||
readReqArgs := (*genericattributeprofile.GattReadRequestedEventArgs)(args)
|
||||
reqAsyncOp, err := readReqArgs.GetRequestAsync()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = awaitAsyncOperation(reqAsyncOp, genericattributeprofile.SignatureGattReadRequest); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := reqAsyncOp.GetResults()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
gattReadRequest := (*genericattributeprofile.GattReadRequest)(res)
|
||||
|
||||
characteristic := (*genericattributeprofile.GattLocalCharacteristic)(sender)
|
||||
uuid, err := characteristic.GetUuid()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
goChar, ok := goChars[uuid]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
writer, err := streams.NewDataWriter()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer writer.Release()
|
||||
|
||||
goChar.valueMtx.Lock()
|
||||
defer goChar.valueMtx.Unlock()
|
||||
if len(goChar.value) > 0 {
|
||||
if err = writer.WriteBytes(uint32(len(goChar.value)), goChar.value); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
buf, err := writer.DetachBuffer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
gattReadRequest.RespondWithValue(buf)
|
||||
buf.Release()
|
||||
})
|
||||
|
||||
for _, char := range s.Characteristics {
|
||||
params, err := genericattributeprofile.NewGattLocalCharacteristicParameters()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = params.SetCharacteristicProperties(genericattributeprofile.GattCharacteristicProperties(char.Flags)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uuid := syscallUUIDFromUUID(char.UUID)
|
||||
createCharOp, err := localService.CreateCharacteristicAsync(uuid, params)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = awaitAsyncOperation(createCharOp, genericattributeprofile.SignatureGattLocalCharacteristicResult); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res, err := createCharOp.GetResults()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
characteristicResults := (*genericattributeprofile.GattLocalCharacteristicResult)(res)
|
||||
characteristic, err := characteristicResults.GetCharacteristic()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = characteristic.AddWriteRequested(writeRequestedHandler)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = characteristic.AddReadRequested(readRequestedHandler)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Keep the object around for Characteristic.Write.
|
||||
if char.Handle != nil {
|
||||
char.Handle.wintCharacteristic = characteristic
|
||||
char.Handle.value = char.Value
|
||||
char.Handle.valueMtx = &sync.Mutex{}
|
||||
char.Handle.flags = char.Flags
|
||||
goChars[uuid] = char.Handle
|
||||
}
|
||||
}
|
||||
|
||||
params, err := genericattributeprofile.NewGattServiceProviderAdvertisingParameters()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = params.SetIsConnectable(true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = params.SetIsDiscoverable(true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return serviceProvider.StartAdvertisingWithParameters(params)
|
||||
}
|
||||
|
||||
// Write replaces the characteristic value with a new value.
|
||||
func (c *Characteristic) Write(p []byte) (n int, err error) {
|
||||
length := len(p)
|
||||
|
||||
if length == 0 {
|
||||
return 0, nil // nothing to do
|
||||
}
|
||||
|
||||
if c.writeEvent != nil {
|
||||
c.writeEvent(0, 0, p)
|
||||
}
|
||||
|
||||
// writes are only actually processed on read events from clients, we just set a variable here.
|
||||
c.valueMtx.Lock()
|
||||
defer c.valueMtx.Unlock()
|
||||
c.value = p
|
||||
|
||||
// only notify if it's enabled, otherwise the below leads to an error
|
||||
if c.flags&CharacteristicNotifyPermission != 0 {
|
||||
writer, err := streams.NewDataWriter()
|
||||
if err != nil {
|
||||
return length, err
|
||||
}
|
||||
|
||||
defer writer.Release()
|
||||
err = writer.WriteBytes(uint32(len(p)), p)
|
||||
if err != nil {
|
||||
return length, err
|
||||
}
|
||||
|
||||
buf, err := writer.DetachBuffer()
|
||||
if err != nil {
|
||||
return length, err
|
||||
}
|
||||
defer buf.Release()
|
||||
|
||||
op, err := c.wintCharacteristic.NotifyValueAsync(buf)
|
||||
if err != nil {
|
||||
return length, err
|
||||
}
|
||||
|
||||
// IVectorView<GattClientNotificationResult>
|
||||
signature := fmt.Sprintf("pinterface({%s};%s)", collections.GUIDIVectorView, genericattributeprofile.SignatureGattClientNotificationResult)
|
||||
if err = awaitAsyncOperation(op, signature); err != nil {
|
||||
return length, err
|
||||
}
|
||||
defer op.Release()
|
||||
|
||||
res, err := op.GetResults()
|
||||
if err != nil {
|
||||
return length, err
|
||||
}
|
||||
|
||||
// TODO: process notification results, just getting this to release
|
||||
vec := (*collections.IVectorView)(res)
|
||||
vec.Release()
|
||||
}
|
||||
|
||||
return length, nil
|
||||
}
|
||||
|
||||
func syscallUUIDFromUUID(uuid UUID) syscall.GUID {
|
||||
guid := ole.NewGUID(uuid.String())
|
||||
return syscall.GUID{
|
||||
Data1: guid.Data1,
|
||||
Data2: guid.Data2,
|
||||
Data3: guid.Data3,
|
||||
Data4: guid.Data4,
|
||||
}
|
||||
}
|
2
go.mod
2
go.mod
|
@ -5,7 +5,7 @@ go 1.18
|
|||
require (
|
||||
github.com/go-ole/go-ole v1.2.6
|
||||
github.com/godbus/dbus/v5 v5.1.0
|
||||
github.com/saltosystems/winrt-go v0.0.0-20240320113951-a2e4fc03f5f4
|
||||
github.com/saltosystems/winrt-go v0.0.0-20240509090452-45c2d7a6235a
|
||||
github.com/tinygo-org/cbgo v0.0.4
|
||||
golang.org/x/crypto v0.12.0
|
||||
tinygo.org/x/drivers v0.26.1-0.20230922160320-ed51435c2ef6
|
||||
|
|
4
go.sum
4
go.sum
|
@ -10,8 +10,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3
|
|||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/saltosystems/winrt-go v0.0.0-20240320113951-a2e4fc03f5f4 h1:zurEWtOr/OYiTb5bcD7eeHLOfj6vCR30uldlwse1cSM=
|
||||
github.com/saltosystems/winrt-go v0.0.0-20240320113951-a2e4fc03f5f4/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
|
||||
github.com/saltosystems/winrt-go v0.0.0-20240509090452-45c2d7a6235a h1:i7iQTpf9AlppS14KWzsTvSS8jx4BLPCAwM2lywguZCY=
|
||||
github.com/saltosystems/winrt-go v0.0.0-20240509090452-45c2d7a6235a/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
|
||||
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
|
|
Loading…
Reference in a new issue