diff --git a/.circleci/config.yml b/.circleci/config.yml index 909c810..936f004 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 diff --git a/Makefile b/Makefile index 02c4287..9461f5e 100644 --- a/Makefile +++ b/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 diff --git a/adapter_windows.go b/adapter_windows.go new file mode 100644 index 0000000..1ef0cbe --- /dev/null +++ b/adapter_windows.go @@ -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 +} diff --git a/gap_windows.go b/gap_windows.go new file mode 100644 index 0000000..dd64cf6 --- /dev/null +++ b/gap_windows.go @@ -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() +} diff --git a/go.mod b/go.mod index 2c83267..92ff605 100644 --- a/go.mod +++ b/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 +) diff --git a/go.sum b/go.sum index a567142..fc564b9 100644 --- a/go.sum +++ b/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= diff --git a/winbt/advertisement.go b/winbt/advertisement.go new file mode 100644 index 0000000..933a066 --- /dev/null +++ b/winbt/advertisement.go @@ -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* handler, [out] [retval] EventRegistrationToken* token); + RemoveReceivedEvent uintptr // ([in] EventRegistrationToken token); + AddStoppedEvent uintptr // ([in] Windows.Foundation.TypedEventHandler* 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** value); + SetFlags uintptr // ([in] Windows.Foundation.IReference* value); + GetLocalName uintptr // ([out] [retval] HSTRING* value); + SetLocalName uintptr // ([in] HSTRING value); + GetServiceUuids uintptr // ([out] [retval] Windows.Foundation.Collections.IVector** value); + GetManufacturerData uintptr // ([out] [retval] Windows.Foundation.Collections.IVector** value); + GetDataSections uintptr // ([out] [retval] Windows.Foundation.Collections.IVector** value); + GetManufacturerDataByCompanyId uintptr // ([in] UINT16 companyId, [out] [retval] Windows.Foundation.Collections.IVectorView** dataList); + GetSectionsByType uintptr // ([in] BYTE type, [out] [retval] Windows.Foundation.Collections.IVectorView** 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 +} diff --git a/winbt/event.c b/winbt/event.c new file mode 100644 index 0000000..61c51a6 --- /dev/null +++ b/winbt/event.c @@ -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 + +// 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; +} diff --git a/winbt/event.go b/winbt/event.go new file mode 100644 index 0000000..ec87046 --- /dev/null +++ b/winbt/event.go @@ -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 +} diff --git a/winbt/vector.go b/winbt/vector.go new file mode 100644 index 0000000..3f18f34 --- /dev/null +++ b/winbt/vector.go @@ -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 **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) +} diff --git a/winbt/winbt.go b/winbt/winbt.go new file mode 100644 index 0000000..9e8161e --- /dev/null +++ b/winbt/winbt.go @@ -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()) + } +}