Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Winrt full support #266

Merged
merged 6 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions adapter_windows.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package bluetooth

import (
"errors"
"fmt"

"github.com/go-ole/go-ole"
Expand All @@ -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.
Expand Down Expand Up @@ -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")
}
121 changes: 121 additions & 0 deletions gap_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package bluetooth

import (
"fmt"
"syscall"
"unsafe"

"github.com/go-ole/go-ole"
Expand All @@ -10,6 +11,7 @@ import (
"github.com/saltosystems/winrt-go/windows/devices/bluetooth/advertisement"
"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"
)

Expand All @@ -18,6 +20,125 @@ 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we check if a.publisher is null and release the existing instance if not?

If someone calls Configure a few times they would end up leaking memory.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, added that for the publisher as well as the advertisement, which was unused by accident! Looks to be working in the heartrate example if I try to reconfigure it a couple times before starting.

Could you give some more insight on how the advertisement is switching? The advertisements were quite strange to get working, I copied most of their implementation for this C# source, which I was never really able to see from my phone using the ble tester app on android. But seems to work fine when running the heartrate example.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using the nRF Connect app to check the advertised data. And this is what I get when running the advertisement example:

screen-20240426-090119.2.mp4

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah okay when using nrf connect I can see it. It's not alternating for me, sometimes it pops up then disappears for a second or two though, but I never see any windows advertising data.

this behavior is the same when I run the C# source as well, seems to be a windows quirk?
when running the heartrate example, and adding some manufacturer data to the advertisement, it shows up steady

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you give some more insight on how the advertisement is switching?

This is pretty common behavior. If there's more than one advertisement happening at the same time, many systems will alternate between them. Because they use the same MAC address for both advertisements, it looks like it's constantly changing.
I wouldn't worry about it if it doesn't cause any problems.


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
}

err = vecAppend(vec, unsafe.Pointer(&manData.IUnknown.RawVTable))
if err != nil {
return err
}
}

return nil
}

// cloned from IVector.Append in winrt-go, where the syscall wraps the value again in an
// unsafe.Pointer unnecessarily, resulting in an exception.
// TODO: create pr in winrt repo to fix: https://github.com/saltosystems/winrt-go
func vecAppend(v *collections.IVector, value unsafe.Pointer) error {
hr, _, _ := syscall.SyscallN(
v.VTable().Append,
uintptr(unsafe.Pointer(v)),
uintptr(value),
)

if hr != 0 {
return ole.NewError(hr)
}
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) {
Expand Down
4 changes: 3 additions & 1 deletion gatts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion gatts_other.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !linux
//go:build !linux && !windows

package bluetooth

Expand Down
Loading
Loading