Add initial Windows support
Only scanning has been implemented so far. The most work was really just understanding WinRT well enough to get to this point.
This commit is contained in:
parent
7a11ef8562
commit
22553053ff
11 changed files with 655 additions and 7 deletions
|
@ -9,5 +9,16 @@ jobs:
|
|||
- checkout
|
||||
- run: tinygo version
|
||||
- run:
|
||||
name: "Run smoke tests"
|
||||
command: make smoketest
|
||||
name: "Run TinyGo smoke tests"
|
||||
command: make smoketest-tinygo
|
||||
- run:
|
||||
name: "Run Linux smoke tests"
|
||||
command: make smoketest-linux
|
||||
- run:
|
||||
name: "Install Windows cross compiler"
|
||||
command: |
|
||||
# Install the tools themselves.
|
||||
apt-get install -y gcc-mingw-w64-x86-64
|
||||
- run:
|
||||
name: "Run Windows smoke tests"
|
||||
command: make smoketest-windows
|
||||
|
|
17
Makefile
17
Makefile
|
@ -1,7 +1,9 @@
|
|||
|
||||
TINYGO=tinygo
|
||||
|
||||
smoketest:
|
||||
smoketest: smoketest-tinygo smoketest-linux smoketest-windows
|
||||
|
||||
smoketest-tinygo:
|
||||
# Test all examples (and some boards)
|
||||
$(TINYGO) build -o test.hex -size=short -target=pca10040-s132v6 ./examples/advertisement
|
||||
@md5sum test.hex
|
||||
|
@ -12,6 +14,13 @@ smoketest:
|
|||
# Test some more boards that are not tested above.
|
||||
$(TINYGO) build -o test.hex -size=short -target=pca10056-s140v7 ./examples/advertisement
|
||||
@md5sum test.hex
|
||||
# Test on the host
|
||||
go build -o /tmp/go-build-discard ./examples/advertisement
|
||||
go build -o /tmp/go-build-discard ./examples/heartrate
|
||||
|
||||
smoketest-linux:
|
||||
# Test on Linux.
|
||||
GOOS=linux go build -o /tmp/go-build-discard ./examples/advertisement
|
||||
GOOS=linux go build -o /tmp/go-build-discard ./examples/heartrate
|
||||
GOOS=linux go build -o /tmp/go-build-discard ./examples/scanner
|
||||
|
||||
smoketest-windows:
|
||||
# Test on Windows.
|
||||
GOOS=windows CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc go build -o /tmp/go-build-discard ./examples/scanner
|
||||
|
|
24
adapter_windows.go
Normal file
24
adapter_windows.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package bluetooth
|
||||
|
||||
import (
|
||||
"github.com/aykevl/go-bluetooth/winbt"
|
||||
"github.com/go-ole/go-ole"
|
||||
)
|
||||
|
||||
type Adapter struct {
|
||||
handler func(Event)
|
||||
watcher *winbt.IBluetoothLEAdvertisementWatcher
|
||||
}
|
||||
|
||||
var defaultAdapter Adapter
|
||||
|
||||
// DefaultAdapter returns the default adapter on the current system.
|
||||
func DefaultAdapter() (*Adapter, error) {
|
||||
return &defaultAdapter, nil
|
||||
}
|
||||
|
||||
// Enable configures the BLE stack. It must be called before any
|
||||
// Bluetooth-related calls (unless otherwise indicated).
|
||||
func (a *Adapter) Enable() error {
|
||||
return ole.RoInitialize(1) // initialize with multithreading enabled
|
||||
}
|
77
gap_windows.go
Normal file
77
gap_windows.go
Normal file
|
@ -0,0 +1,77 @@
|
|||
package bluetooth
|
||||
|
||||
import (
|
||||
"github.com/aykevl/go-bluetooth/winbt"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
if a.watcher != nil {
|
||||
// Cannot scan more than once: which one should ScanStop()
|
||||
// stop?
|
||||
return errScanning
|
||||
}
|
||||
|
||||
a.watcher, err = winbt.NewBluetoothLEAdvertisementWatcher()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer a.watcher.Release()
|
||||
|
||||
// Listen for incoming BLE advertisement packets.
|
||||
err = a.watcher.AddReceivedEvent(func(watcher *winbt.IBluetoothLEAdvertisementWatcher, args *winbt.IBluetoothLEAdvertisementReceivedEventArgs) {
|
||||
var result ScanResult
|
||||
result.RSSI = args.RawSignalStrengthInDBm()
|
||||
addr := args.BluetoothAddress()
|
||||
for i := range result.Address {
|
||||
result.Address[i] = byte(addr)
|
||||
addr >>= 8
|
||||
}
|
||||
advertisement := args.Advertisement()
|
||||
result.AdvertisementPayload = &advertisementFields{
|
||||
AdvertisementFields{
|
||||
LocalName: advertisement.LocalName(),
|
||||
},
|
||||
}
|
||||
callback(a, result)
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Wait for when advertisement has stopped by a call to StopScan().
|
||||
// Advertisement doesn't seem to stop right away, there is an
|
||||
// intermediate Stopping state.
|
||||
stoppingChan := make(chan struct{})
|
||||
err = a.watcher.AddStoppedEvent(func(watcher *winbt.IBluetoothLEAdvertisementWatcher, args *winbt.IBluetoothLEAdvertisementWatcherStoppedEventArgs) {
|
||||
// Note: the args parameter has an Error property that should
|
||||
// probably be checked, but I'm not sure when stopping the
|
||||
// advertisement watcher could ever result in an error (except
|
||||
// for bugs).
|
||||
close(stoppingChan)
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = a.watcher.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait until advertisement has stopped, and finish.
|
||||
<-stoppingChan
|
||||
a.watcher = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopScan stops any in-progress scan. It can be called from within a Scan
|
||||
// callback to stop the current scan. If no scan is in progress, an error will
|
||||
// be returned.
|
||||
func (a *Adapter) StopScan() error {
|
||||
if a.watcher == nil {
|
||||
return errNotScanning
|
||||
}
|
||||
return a.watcher.Stop()
|
||||
}
|
5
go.mod
5
go.mod
|
@ -2,4 +2,7 @@ module github.com/aykevl/go-bluetooth
|
|||
|
||||
go 1.14
|
||||
|
||||
require github.com/muka/go-bluetooth v0.0.0-20200518110738-ed2c87e2f9fa
|
||||
require (
|
||||
github.com/go-ole/go-ole v1.2.4
|
||||
github.com/muka/go-bluetooth v0.0.0-20200518110738-ed2c87e2f9fa
|
||||
)
|
||||
|
|
2
go.sum
2
go.sum
|
@ -2,6 +2,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
||||
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
|
|
250
winbt/advertisement.go
Normal file
250
winbt/advertisement.go
Normal file
|
@ -0,0 +1,250 @@
|
|||
package winbt
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/go-ole/go-ole"
|
||||
)
|
||||
|
||||
type WatcherStatus uint32
|
||||
|
||||
const (
|
||||
WatcherStatusCreated WatcherStatus = 0
|
||||
WatcherStatusStarted WatcherStatus = 1
|
||||
WatcherStatusStopping WatcherStatus = 2
|
||||
WatcherStatusStopped WatcherStatus = 3
|
||||
WatcherStatusAborted WatcherStatus = 4
|
||||
)
|
||||
|
||||
type IBluetoothLEAdvertisementWatcher struct {
|
||||
ole.IInspectable
|
||||
}
|
||||
|
||||
type IBluetoothLEAdvertisementWatcherVtbl struct {
|
||||
ole.IInspectableVtbl
|
||||
GetMinSamplingInterval uintptr // ([out] [retval] Windows.Foundation.TimeSpan* value);
|
||||
GetMaxSamplingInterval uintptr // ([out] [retval] Windows.Foundation.TimeSpan* value);
|
||||
GetMinOutOfRangeTimeout uintptr // ([out] [retval] Windows.Foundation.TimeSpan* value);
|
||||
GetMaxOutOfRangeTimeout uintptr // ([out] [retval] Windows.Foundation.TimeSpan* value);
|
||||
GetStatus uintptr // ([out] [retval] Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementWatcherStatus* value);
|
||||
GetScanningMode uintptr // ([out] [retval] Windows.Devices.Bluetooth.Advertisement.BluetoothLEScanningMode* value);
|
||||
SetScanningMode uintptr // ([in] Windows.Devices.Bluetooth.Advertisement.BluetoothLEScanningMode value);
|
||||
GetSignalStrengthFilter uintptr // ([out] [retval] Windows.Devices.Bluetooth.BluetoothSignalStrengthFilter** value);
|
||||
SetSignalStrengthFilter uintptr // ([in] Windows.Devices.Bluetooth.BluetoothSignalStrengthFilter* value);
|
||||
GetAdvertisementFilter uintptr // ([out] [retval] Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFilter** value);
|
||||
SetAdvertisementFilter uintptr // ([in] Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFilter* value);
|
||||
Start uintptr // ();
|
||||
Stop uintptr // ();
|
||||
AddReceivedEvent uintptr // ([in] Windows.Foundation.TypedEventHandler<Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementWatcher*, Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementReceivedEventArgs*>* handler, [out] [retval] EventRegistrationToken* token);
|
||||
RemoveReceivedEvent uintptr // ([in] EventRegistrationToken token);
|
||||
AddStoppedEvent uintptr // ([in] Windows.Foundation.TypedEventHandler<Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementWatcher*, Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementWatcherStoppedEventArgs*>* handler, [out] [retval] EventRegistrationToken* token);
|
||||
RemoveStoppedEvent uintptr // ([in] EventRegistrationToken token);
|
||||
}
|
||||
|
||||
func (v *IBluetoothLEAdvertisementWatcher) VTable() *IBluetoothLEAdvertisementWatcherVtbl {
|
||||
return (*IBluetoothLEAdvertisementWatcherVtbl)(unsafe.Pointer(v.RawVTable))
|
||||
}
|
||||
|
||||
func NewBluetoothLEAdvertisementWatcher() (*IBluetoothLEAdvertisementWatcher, error) {
|
||||
inspectable, err := ole.RoActivateInstance("Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementWatcher")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
watcherItf := inspectable.MustQueryInterface(ole.NewGUID("A6AC336F-F3D3-4297-8D6C-C81EA6623F40"))
|
||||
return (*IBluetoothLEAdvertisementWatcher)(unsafe.Pointer(watcherItf)), nil
|
||||
}
|
||||
|
||||
func (v *IBluetoothLEAdvertisementWatcher) AddReceivedEvent(handler func(*IBluetoothLEAdvertisementWatcher, *IBluetoothLEAdvertisementReceivedEventArgs)) (err error) {
|
||||
event := NewEvent(ole.NewGUID("{90EB4ECA-D465-5EA0-A61C-033C8C5ECEF2}"), func(event *Event, argsInspectable *ole.IInspectable) {
|
||||
args := (*IBluetoothLEAdvertisementReceivedEventArgs)(unsafe.Pointer(argsInspectable.MustQueryInterface(IID_IBluetoothLEAdvertisementReceivedEventArgs)))
|
||||
defer args.Release()
|
||||
handler(v, args)
|
||||
})
|
||||
hr, _, _ := syscall.Syscall(
|
||||
v.VTable().AddReceivedEvent,
|
||||
3,
|
||||
uintptr(unsafe.Pointer(v)),
|
||||
uintptr(unsafe.Pointer(event)),
|
||||
uintptr(unsafe.Pointer(&event.token)),
|
||||
)
|
||||
return makeError(hr)
|
||||
}
|
||||
|
||||
func (v *IBluetoothLEAdvertisementWatcher) AddStoppedEvent(handler func(*IBluetoothLEAdvertisementWatcher, *IBluetoothLEAdvertisementWatcherStoppedEventArgs)) (err error) {
|
||||
event := NewEvent(ole.NewGUID("{9936A4DB-DC99-55C3-9E9B-BF4854BD9EAB}"), func(event *Event, argsInspectable *ole.IInspectable) {
|
||||
args := (*IBluetoothLEAdvertisementWatcherStoppedEventArgs)(unsafe.Pointer(argsInspectable.MustQueryInterface(IID_IBluetoothLEAdvertisementWatcherStoppedEventArgs)))
|
||||
defer args.Release()
|
||||
handler(v, args)
|
||||
})
|
||||
hr, _, _ := syscall.Syscall(
|
||||
v.VTable().AddStoppedEvent,
|
||||
3,
|
||||
uintptr(unsafe.Pointer(v)),
|
||||
uintptr(unsafe.Pointer(event)),
|
||||
uintptr(unsafe.Pointer(&event.token)),
|
||||
)
|
||||
return makeError(hr)
|
||||
}
|
||||
|
||||
func (v *IBluetoothLEAdvertisementWatcher) Start() error {
|
||||
hr, _, _ := syscall.Syscall(
|
||||
v.VTable().Start,
|
||||
1,
|
||||
uintptr(unsafe.Pointer(v)),
|
||||
0,
|
||||
0)
|
||||
return makeError(hr)
|
||||
}
|
||||
|
||||
func (v *IBluetoothLEAdvertisementWatcher) Stop() error {
|
||||
hr, _, _ := syscall.Syscall(
|
||||
v.VTable().Stop,
|
||||
1,
|
||||
uintptr(unsafe.Pointer(v)),
|
||||
0,
|
||||
0)
|
||||
return makeError(hr)
|
||||
}
|
||||
|
||||
func (v *IBluetoothLEAdvertisementWatcher) Status() WatcherStatus {
|
||||
var status WatcherStatus
|
||||
hr, _, _ := syscall.Syscall(
|
||||
v.VTable().GetStatus,
|
||||
2,
|
||||
uintptr(unsafe.Pointer(v)),
|
||||
uintptr(unsafe.Pointer(&status)),
|
||||
0)
|
||||
mustSucceed(hr)
|
||||
return status
|
||||
}
|
||||
|
||||
type IBluetoothLEAdvertisementReceivedEventArgs struct {
|
||||
ole.IInspectable
|
||||
}
|
||||
|
||||
type IBluetoothLEAdvertisementReceivedEventArgsVtbl struct {
|
||||
ole.IInspectableVtbl
|
||||
RawSignalStrengthInDBm uintptr // ([out] [retval] INT16* value);
|
||||
BluetoothAddress uintptr // ([out] [retval] UINT64* value);
|
||||
AdvertisementType uintptr // ([out] [retval] Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementType* value);
|
||||
Timestamp uintptr // ([out] [retval] Windows.Foundation.DateTime* value);
|
||||
Advertisement uintptr // ([out] [retval] Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisement** value);
|
||||
}
|
||||
|
||||
func (v *IBluetoothLEAdvertisementReceivedEventArgs) VTable() *IBluetoothLEAdvertisementReceivedEventArgsVtbl {
|
||||
return (*IBluetoothLEAdvertisementReceivedEventArgsVtbl)(unsafe.Pointer(v.RawVTable))
|
||||
}
|
||||
|
||||
func (v *IBluetoothLEAdvertisementReceivedEventArgs) RawSignalStrengthInDBm() (rssi int16) {
|
||||
hr, _, _ := syscall.Syscall(
|
||||
v.VTable().RawSignalStrengthInDBm,
|
||||
2,
|
||||
uintptr(unsafe.Pointer(v)),
|
||||
uintptr(unsafe.Pointer(&rssi)),
|
||||
0)
|
||||
mustSucceed(hr)
|
||||
return
|
||||
}
|
||||
|
||||
func (v *IBluetoothLEAdvertisementReceivedEventArgs) BluetoothAddress() (address uint64) {
|
||||
hr, _, _ := syscall.Syscall(
|
||||
v.VTable().BluetoothAddress,
|
||||
2,
|
||||
uintptr(unsafe.Pointer(v)),
|
||||
uintptr(unsafe.Pointer(&address)),
|
||||
0)
|
||||
mustSucceed(hr)
|
||||
return
|
||||
}
|
||||
|
||||
func (v *IBluetoothLEAdvertisementReceivedEventArgs) Advertisement() (advertisement *IBluetoothLEAdvertisement) {
|
||||
hr, _, _ := syscall.Syscall(
|
||||
v.VTable().Advertisement,
|
||||
2,
|
||||
uintptr(unsafe.Pointer(v)),
|
||||
uintptr(unsafe.Pointer(&advertisement)),
|
||||
0)
|
||||
mustSucceed(hr)
|
||||
return
|
||||
}
|
||||
|
||||
type IBluetoothLEAdvertisementWatcherStoppedEventArgs struct {
|
||||
ole.IInspectable
|
||||
}
|
||||
|
||||
type IBluetoothLEAdvertisement struct {
|
||||
ole.IInspectable
|
||||
}
|
||||
|
||||
type IBluetoothLEAdvertisementVtbl struct {
|
||||
ole.IInspectableVtbl
|
||||
GetFlags uintptr // ([out] [retval] Windows.Foundation.IReference<Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFlags>** value);
|
||||
SetFlags uintptr // ([in] Windows.Foundation.IReference<Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementFlags>* value);
|
||||
GetLocalName uintptr // ([out] [retval] HSTRING* value);
|
||||
SetLocalName uintptr // ([in] HSTRING value);
|
||||
GetServiceUuids uintptr // ([out] [retval] Windows.Foundation.Collections.IVector<GUID>** value);
|
||||
GetManufacturerData uintptr // ([out] [retval] Windows.Foundation.Collections.IVector<Windows.Devices.Bluetooth.Advertisement.BluetoothLEManufacturerData*>** value);
|
||||
GetDataSections uintptr // ([out] [retval] Windows.Foundation.Collections.IVector<Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementDataSection*>** value);
|
||||
GetManufacturerDataByCompanyId uintptr // ([in] UINT16 companyId, [out] [retval] Windows.Foundation.Collections.IVectorView<Windows.Devices.Bluetooth.Advertisement.BluetoothLEManufacturerData*>** dataList);
|
||||
GetSectionsByType uintptr // ([in] BYTE type, [out] [retval] Windows.Foundation.Collections.IVectorView<Windows.Devices.Bluetooth.Advertisement.BluetoothLEAdvertisementDataSection*>** sectionList);
|
||||
}
|
||||
|
||||
func (v *IBluetoothLEAdvertisement) VTable() *IBluetoothLEAdvertisementVtbl {
|
||||
return (*IBluetoothLEAdvertisementVtbl)(unsafe.Pointer(v.RawVTable))
|
||||
}
|
||||
|
||||
func (v *IBluetoothLEAdvertisement) LocalName() string {
|
||||
var hstring ole.HString
|
||||
hr, _, _ := syscall.Syscall(
|
||||
v.VTable().GetLocalName,
|
||||
2,
|
||||
uintptr(unsafe.Pointer(v)),
|
||||
uintptr(unsafe.Pointer(&hstring)),
|
||||
0)
|
||||
if hr != 0 {
|
||||
// Should not happen.
|
||||
panic(ole.NewError(hr))
|
||||
}
|
||||
name := hstring.String()
|
||||
ole.DeleteHString(hstring)
|
||||
return name
|
||||
}
|
||||
|
||||
func (v *IBluetoothLEAdvertisement) DataSections() (vector *IVector) {
|
||||
hr, _, _ := syscall.Syscall(
|
||||
v.VTable().GetDataSections,
|
||||
2,
|
||||
uintptr(unsafe.Pointer(v)),
|
||||
uintptr(unsafe.Pointer(&vector)),
|
||||
0)
|
||||
mustSucceed(hr)
|
||||
return
|
||||
}
|
||||
|
||||
type IBluetoothLEAdvertisementDataSection struct {
|
||||
ole.IInspectable
|
||||
}
|
||||
|
||||
type IBluetoothLEAdvertisementDataSectionVtbl struct {
|
||||
ole.IInspectableVtbl
|
||||
GetDataType uintptr // ([out] [retval] BYTE* value)
|
||||
SetDataType uintptr // ([in] BYTE value)
|
||||
GetData uintptr // ([out] [retval] Windows.Storage.Streams.IBuffer** value)
|
||||
SetData uintptr // ([in] Windows.Storage.Streams.IBuffer* value)
|
||||
}
|
||||
|
||||
func (v *IBluetoothLEAdvertisementDataSection) VTable() *IBluetoothLEAdvertisementDataSectionVtbl {
|
||||
return (*IBluetoothLEAdvertisementDataSectionVtbl)(unsafe.Pointer(v.RawVTable))
|
||||
}
|
||||
|
||||
func (v *IBluetoothLEAdvertisementDataSection) DataType() (value byte) {
|
||||
hr, _, _ := syscall.Syscall(
|
||||
v.VTable().GetDataType,
|
||||
2,
|
||||
uintptr(unsafe.Pointer(v)),
|
||||
uintptr(unsafe.Pointer(&value)),
|
||||
0)
|
||||
mustSucceed(hr)
|
||||
return
|
||||
}
|
56
winbt/event.c
Normal file
56
winbt/event.c
Normal file
|
@ -0,0 +1,56 @@
|
|||
|
||||
// This file implements the C side of WinRT events.
|
||||
// Unfortunately, this cannot be done entirely in pure go, for two reasons:
|
||||
// * An Event object must be shared with the C world (WinRT) after
|
||||
// syscall.Syscall returns. This is not allowed in Go, to keep the option
|
||||
// open to switch to a moving GC in the future. For this, it is needed to
|
||||
// allocate the Event object on the C heap (using malloc).
|
||||
// * Building a virtual function table is very difficult (if not impossible)
|
||||
// in pure Go. It might be possible using reflect, but due the the previous
|
||||
// issue I haven't investigated that.
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// Note: these functions have a different signature but because they are only
|
||||
// used as function pointers (and never called) and because they use C name
|
||||
// mangling, the signature doesn't really matter.
|
||||
void winbt_Event_Invoke(void);
|
||||
void winbt_Event_QueryInterface(void);
|
||||
|
||||
// This is the contract the functions below should adhere to:
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nn-unknwn-iunknown
|
||||
|
||||
static uint64_t winbt_Event_AddRef(void) {
|
||||
// This is safe, see winbt_Event_Release.
|
||||
return 2;
|
||||
}
|
||||
|
||||
static uint64_t winbt_Event_Release(void) {
|
||||
// Pretend there is one reference left.
|
||||
// The docs say:
|
||||
// > This value is intended to be used only for test purposes.
|
||||
// Also see:
|
||||
// https://docs.microsoft.com/en-us/archive/msdn-magazine/2013/august/windows-with-c-the-windows-runtime-application-model
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The Vtable structure for WinRT event interfaces.
|
||||
typedef struct {
|
||||
void *QueryInterface;
|
||||
void *AddRef;
|
||||
void *Release;
|
||||
void *Invoke;
|
||||
} EventVtbl_t;
|
||||
|
||||
// The Vtable itself. It can be kept constant.
|
||||
static const EventVtbl_t winbt_EventVtbl = {
|
||||
(void*)winbt_Event_QueryInterface,
|
||||
(void*)winbt_Event_AddRef,
|
||||
(void*)winbt_Event_Release,
|
||||
(void*)winbt_Event_Invoke,
|
||||
};
|
||||
|
||||
// A small helper function to get the Vtable.
|
||||
const EventVtbl_t * winbt_getEventVtbl(void) {
|
||||
return &winbt_EventVtbl;
|
||||
}
|
82
winbt/event.go
Normal file
82
winbt/event.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package winbt
|
||||
|
||||
// This file implements a COM object that can be used for various event
|
||||
// callbacks. In C++, it would use an ITypedEventHandler instantiation.
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/go-ole/go-ole"
|
||||
)
|
||||
|
||||
// const void * winbt_getEventVtbl(void);
|
||||
import "C"
|
||||
|
||||
// Event implements event handler interfaces (ITypedEventHandler). This is
|
||||
// therefore a valid COM object, derived from IUnknown.
|
||||
type Event struct {
|
||||
ole.IUnknown
|
||||
IID *ole.GUID
|
||||
Callback func(*Event, *ole.IInspectable)
|
||||
token uintptr
|
||||
}
|
||||
|
||||
// NewEvent creates a new COM object for event callbacks. The IID must be the
|
||||
// IID for a particular event, which is unfortunately not included in the *.idl
|
||||
// files but has to be found in the relevant C++/WinRT header files.
|
||||
func NewEvent(iid *ole.GUID, callback func(*Event, *ole.IInspectable)) *Event {
|
||||
// Another way to find the IID is to print the requested IID in the
|
||||
// QueryInterface method below. The first queried IID is likely the one
|
||||
// that is the event interface IID.
|
||||
|
||||
// The event must be allocated on the C heap, because it is not allowed to
|
||||
// retain a pointer on the Go heap after a non-Go call returns (e.g.
|
||||
// syscall.Syscall).
|
||||
event := (*Event)(C.malloc(C.size_t(unsafe.Sizeof(Event{}))))
|
||||
event.RawVTable = (*interface{})(C.winbt_getEventVtbl())
|
||||
event.IID = iid
|
||||
event.Callback = callback
|
||||
return event
|
||||
}
|
||||
|
||||
// The two functions below implement IUnknown and an interface I couldn't
|
||||
// really get the definition of (possibly ITypedEventHandler, although
|
||||
// ITypedEventHandler is really a C++ template). The closest thing I could find
|
||||
// to documentation are the C++/WinRT headers and this mention by Kenny Kerr
|
||||
// (the original developer of C++/WinRT):
|
||||
// https://docs.microsoft.com/en-us/archive/msdn-magazine/2013/august/windows-with-c-the-windows-runtime-application-model
|
||||
// > [...] Fortunately, all of these interfaces are generated by the MIDL
|
||||
// > compiler, which takes care to specialize each one, and it’s on these
|
||||
// > specializations that it attaches the GUID representing the interface
|
||||
// > identifier. As complicated as the previous typedef might appear, it
|
||||
// > defines a COM interface that derives directly from IUnknown and provides
|
||||
// > a single method called Invoke.
|
||||
|
||||
//export winbt_Event_QueryInterface
|
||||
func winbt_Event_QueryInterface(eventPtr, iidPtr unsafe.Pointer, ppvObject *unsafe.Pointer) uintptr {
|
||||
// This function must adhere to the QueryInterface defined here:
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/unknwn/nn-unknwn-iunknown
|
||||
iid := (*ole.GUID)(iidPtr)
|
||||
event := (*Event)(eventPtr)
|
||||
if ole.IsEqualGUID(iid, event.IID) {
|
||||
// This is us.
|
||||
*ppvObject = eventPtr
|
||||
return ole.S_OK
|
||||
}
|
||||
if ole.IsEqualGUID(iid, ole.IID_IUnknown) {
|
||||
// This is our parent. There are some limitations as to what we can
|
||||
// return here, but returning the *Event pointer is fine.
|
||||
*ppvObject = eventPtr
|
||||
return ole.S_OK
|
||||
}
|
||||
return 0x80004002 // E_NOTINTERFACE
|
||||
}
|
||||
|
||||
//export winbt_Event_Invoke
|
||||
func winbt_Event_Invoke(eventPtr, senderPtr, argsPtr unsafe.Pointer) uintptr {
|
||||
// See the quote above.
|
||||
event := (*Event)(eventPtr)
|
||||
argsInspectable := (*ole.IInspectable)(argsPtr)
|
||||
event.Callback(event, argsInspectable)
|
||||
return ole.S_OK
|
||||
}
|
73
winbt/vector.go
Normal file
73
winbt/vector.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package winbt
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/go-ole/go-ole"
|
||||
)
|
||||
|
||||
type IVector struct {
|
||||
ole.IInspectable
|
||||
}
|
||||
|
||||
type IVectorVtbl struct {
|
||||
ole.IInspectableVtbl
|
||||
|
||||
// These methods have been obtained from windows.foundation.collections.h
|
||||
// in the WinRT API.
|
||||
|
||||
// read methods
|
||||
GetAt uintptr // (_In_opt_ unsigned index, _Out_ T_abi *item)
|
||||
GetSize uintptr // (_Out_ unsigned *size)
|
||||
GetView uintptr // (_Outptr_result_maybenull_ IVectorView<T_logical> **view)
|
||||
IndexOf uintptr // (_In_opt_ T_abi value, _Out_ unsigned *index, _Out_ boolean *found)
|
||||
|
||||
// write methods
|
||||
SetAt uintptr // (_In_ unsigned index, _In_opt_ T_abi item)
|
||||
InsertAt uintptr // (_In_ unsigned index, _In_opt_ T_abi item)
|
||||
RemoveAt uintptr // (_In_ unsigned index)
|
||||
Append uintptr // (_In_opt_ T_abi item)
|
||||
RemoveAtEnd uintptr // ()
|
||||
Clear uintptr // ()
|
||||
|
||||
// bulk transfer methods
|
||||
GetMany uintptr // (_In_ unsigned startIndex, _In_ unsigned capacity, _Out_writes_to_(capacity,*actual) T_abi *value, _Out_ unsigned *actual)
|
||||
ReplaceAll uintptr // (_In_ unsigned count, _In_reads_(count) T_abi *value)
|
||||
}
|
||||
|
||||
func (v *IVector) VTable() *IVectorVtbl {
|
||||
return (*IVectorVtbl)(unsafe.Pointer(v.RawVTable))
|
||||
}
|
||||
|
||||
func (v *IVector) At(index int) (element unsafe.Pointer) {
|
||||
// The caller will need to cast the element to the correct type (for
|
||||
// example, *IBluetoothLEAdvertisementDataSection).
|
||||
hr, _, _ := syscall.Syscall(
|
||||
v.VTable().GetAt,
|
||||
3,
|
||||
uintptr(unsafe.Pointer(v)),
|
||||
uintptr(index),
|
||||
uintptr(unsafe.Pointer(&element)),
|
||||
)
|
||||
mustSucceed(hr)
|
||||
return
|
||||
}
|
||||
|
||||
func (v *IVector) Size() int {
|
||||
// Note that because the size is defined as `unsigned`, and `unsigned`
|
||||
// means 32-bit in Windows (even 64-bit windows), the size is always a
|
||||
// uint32.
|
||||
// Casting to int because that is the common data type for sizes in Go. It
|
||||
// should practically always fit on 32-bit Windows and definitely always
|
||||
// fit on 64-bit Windows (with a 64-bit Go int).
|
||||
var size uint32
|
||||
hr, _, _ := syscall.Syscall(
|
||||
v.VTable().GetSize,
|
||||
2,
|
||||
uintptr(unsafe.Pointer(v)),
|
||||
uintptr(unsafe.Pointer(&size)),
|
||||
0)
|
||||
mustSucceed(hr)
|
||||
return int(size)
|
||||
}
|
61
winbt/winbt.go
Normal file
61
winbt/winbt.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
// Package winbt provides a thin layer over the WinRT Bluetooth interfaces. It
|
||||
// is not designed to be used directly by applications: the bluetooth package
|
||||
// will wrap the API exposed here in a nice platform-independent way.
|
||||
//
|
||||
// You can find the original *.idl and *.h files in a directory like this,
|
||||
// after installing the Windows SDK:
|
||||
//
|
||||
// C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\winrt
|
||||
//
|
||||
// Some helpful articles to understand WinRT at a low level:
|
||||
// https://blog.xojo.com/2019/07/02/accessing-windows-runtime-winrt/
|
||||
// https://docs.microsoft.com/en-us/archive/msdn-magazine/2013/august/windows-with-c-the-windows-runtime-application-model
|
||||
// https://blog.magnusmontin.net/2017/12/30/minimal-uwp-wrl-xaml-app/
|
||||
// https://yizhang82.dev/what-is-winrt
|
||||
// https://www.slideshare.net/goldshtn/deep-dive-into-winrt
|
||||
package winbt
|
||||
|
||||
import (
|
||||
"github.com/go-ole/go-ole"
|
||||
)
|
||||
|
||||
var (
|
||||
IID_IBluetoothLEAdvertisementReceivedEventArgs = ole.NewGUID("27987DDF-E596-41BE-8D43-9E6731D4A913")
|
||||
IID_IBluetoothLEAdvertisementWatcherStoppedEventArgs = ole.NewGUID("DD40F84D-E7B9-43E3-9C04-0685D085FD8C")
|
||||
)
|
||||
|
||||
// printGUIDs prints the GUIDs this IInspectable implements. It is primarily
|
||||
// intended for debugging.
|
||||
func printGUIDs(inspectable *ole.IInspectable) {
|
||||
guids, err := inspectable.GetIids()
|
||||
if err != nil {
|
||||
println("could not get GUIDs for IInspectable:", err.Error())
|
||||
return
|
||||
}
|
||||
for _, guid := range guids {
|
||||
println("guid:", guid.String())
|
||||
}
|
||||
}
|
||||
|
||||
// makeError makes a *ole.OleError if hr is non-nil. If it is nil, it will
|
||||
// return nil.
|
||||
// This is an utility function to easily convert an HRESULT into a Go error
|
||||
// value.
|
||||
func makeError(hr uintptr) error {
|
||||
if hr != 0 {
|
||||
return ole.NewError(hr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mustSucceed can be called to check the return value of getters, which should
|
||||
// always succeed. If hr is non-zero, it will panic with an error message.
|
||||
func mustSucceed(hr uintptr) {
|
||||
if hr != 0 {
|
||||
// Status is a getter, so should never return an error unless
|
||||
// an invalid `v` is passed in (for example, `v` is nil) - in
|
||||
// which case, there is definitely a bug and we should fail
|
||||
// early.
|
||||
panic("winbt: unexpected error: " + ole.NewError(hr).String())
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue