2020-05-30 21:55:20 +03:00
package bluetooth
import (
2022-08-03 14:15:10 +03:00
"fmt"
2022-07-04 19:48:45 +03:00
"unsafe"
"github.com/go-ole/go-ole"
"github.com/saltosystems/winrt-go"
2022-08-03 14:15:10 +03:00
"github.com/saltosystems/winrt-go/windows/devices/bluetooth"
2022-07-04 19:48:45 +03:00
"github.com/saltosystems/winrt-go/windows/devices/bluetooth/advertisement"
2022-08-03 14:15:10 +03:00
"github.com/saltosystems/winrt-go/windows/devices/bluetooth/genericattributeprofile"
2022-07-04 19:48:45 +03:00
"github.com/saltosystems/winrt-go/windows/foundation"
"github.com/saltosystems/winrt-go/windows/storage/streams"
2020-05-30 21:55:20 +03:00
)
2020-08-29 15:43:11 +03:00
// Address contains a Bluetooth MAC address.
2020-08-28 13:40:03 +03:00
type Address struct {
2020-08-29 15:43:11 +03:00
MACAddress
2020-08-28 13:40:03 +03:00
}
2024-05-09 19:34:24 +03:00
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
}
2020-05-30 21:55:20 +03:00
// 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
}
2022-07-04 19:48:45 +03:00
a . watcher , err = advertisement . NewBluetoothLEAdvertisementWatcher ( )
2020-05-30 21:55:20 +03:00
if err != nil {
return
}
2022-07-04 19:48:45 +03:00
defer func ( ) {
_ = a . watcher . Release ( )
a . watcher = nil
} ( )
2020-05-30 21:55:20 +03:00
2023-05-10 10:25:09 +03:00
// Set scanning mode to active so we receive scan responses
// from devices in advertising mode
err = a . watcher . SetScanningMode ( advertisement . BluetoothLEScanningModeActive )
if err != nil {
return
}
2020-05-30 21:55:20 +03:00
// Listen for incoming BLE advertisement packets.
2022-07-04 19:48:45 +03:00
// We need a TypedEventHandler<TSender, TResult> to listen to events, but since this is a parameterized delegate
// its GUID depends on the classes used as sender and result, so we need to compute it:
// TypedEventHandler<BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementReceivedEventArgs>
eventReceivedGuid := winrt . ParameterizedInstanceGUID (
foundation . GUIDTypedEventHandler ,
advertisement . SignatureBluetoothLEAdvertisementWatcher ,
advertisement . SignatureBluetoothLEAdvertisementReceivedEventArgs ,
)
handler := foundation . NewTypedEventHandler ( ole . NewGUID ( eventReceivedGuid ) , func ( instance * foundation . TypedEventHandler , sender , arg unsafe . Pointer ) {
args := ( * advertisement . BluetoothLEAdvertisementReceivedEventArgs ) ( arg )
result := getScanResultFromArgs ( args )
2020-05-30 21:55:20 +03:00
callback ( a , result )
} )
2022-07-04 19:48:45 +03:00
defer handler . Release ( )
token , err := a . watcher . AddReceived ( handler )
2020-05-30 21:55:20 +03:00
if err != nil {
return
}
2022-07-04 19:48:45 +03:00
defer a . watcher . RemoveReceived ( token )
2020-05-30 21:55:20 +03:00
// 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.
2024-03-12 17:01:13 +03:00
stoppingChan := make ( chan error )
2022-07-04 19:48:45 +03:00
// TypedEventHandler<BluetoothLEAdvertisementWatcher, BluetoothLEAdvertisementWatcherStoppedEventArgs>
eventStoppedGuid := winrt . ParameterizedInstanceGUID (
foundation . GUIDTypedEventHandler ,
advertisement . SignatureBluetoothLEAdvertisementWatcher ,
advertisement . SignatureBluetoothLEAdvertisementWatcherStoppedEventArgs ,
)
2024-03-12 17:01:13 +03:00
stoppedHandler := foundation . NewTypedEventHandler ( ole . NewGUID ( eventStoppedGuid ) , func ( _ * foundation . TypedEventHandler , _ , arg unsafe . Pointer ) {
args := ( * advertisement . BluetoothLEAdvertisementWatcherStoppedEventArgs ) ( arg )
errCode , err := args . GetError ( )
if err != nil {
// Got an error while getting the error value, that shouldn't
// happen.
stoppingChan <- fmt . Errorf ( "failed to get stopping error value: %w" , err )
} else if errCode != bluetooth . BluetoothErrorSuccess {
// Could not stop the scan? I'm not sure when this would actually
// happen.
stoppingChan <- fmt . Errorf ( "failed to stop scanning (error code %d)" , errCode )
}
2020-05-30 21:55:20 +03:00
close ( stoppingChan )
} )
2022-07-04 19:48:45 +03:00
defer stoppedHandler . Release ( )
token , err = a . watcher . AddStopped ( stoppedHandler )
2020-05-30 21:55:20 +03:00
if err != nil {
return
}
2022-07-04 19:48:45 +03:00
defer a . watcher . RemoveStopped ( token )
2020-05-30 21:55:20 +03:00
err = a . watcher . Start ( )
if err != nil {
return err
}
// Wait until advertisement has stopped, and finish.
2024-03-12 17:01:13 +03:00
return <- stoppingChan
2020-05-30 21:55:20 +03:00
}
2022-07-04 19:48:45 +03:00
func getScanResultFromArgs ( args * advertisement . BluetoothLEAdvertisementReceivedEventArgs ) ScanResult {
// parse bluetooth address
addr , _ := args . GetBluetoothAddress ( )
adr := Address { }
for i := range adr . MAC {
adr . MAC [ i ] = byte ( addr )
addr >>= 8
}
sigStrength , _ := args . GetRawSignalStrengthInDBm ( )
result := ScanResult {
RSSI : sigStrength ,
Address : adr ,
}
2024-02-20 23:16:29 +03:00
var manufacturerData [ ] ManufacturerDataElement
2022-07-04 19:48:45 +03:00
if winAdv , err := args . GetAdvertisement ( ) ; err == nil && winAdv != nil {
vector , _ := winAdv . GetManufacturerData ( )
size , _ := vector . GetSize ( )
for i := uint32 ( 0 ) ; i < size ; i ++ {
element , _ := vector . GetAt ( i )
manData := ( * advertisement . BluetoothLEManufacturerData ) ( element )
companyID , _ := manData . GetCompanyId ( )
buffer , _ := manData . GetData ( )
2024-02-20 23:16:29 +03:00
manufacturerData = append ( manufacturerData , ManufacturerDataElement {
CompanyID : companyID ,
Data : bufferToSlice ( buffer ) ,
} )
2022-07-04 19:48:45 +03:00
}
}
// Note: the IsRandom bit is never set.
advertisement , _ := args . GetAdvertisement ( )
localName , _ := advertisement . GetLocalName ( )
result . AdvertisementPayload = & advertisementFields {
AdvertisementFields {
LocalName : localName ,
ManufacturerData : manufacturerData ,
} ,
}
return result
}
func bufferToSlice ( buffer * streams . IBuffer ) [ ] byte {
2024-03-21 18:53:33 +03:00
dataReader , _ := streams . DataReaderFromBuffer ( buffer )
2022-07-04 19:48:45 +03:00
defer dataReader . Release ( )
bufferSize , _ := buffer . GetLength ( )
2022-10-19 09:03:59 +03:00
if bufferSize == 0 {
return nil
}
2022-07-04 19:48:45 +03:00
data , _ := dataReader . ReadBytes ( bufferSize )
return data
}
2020-05-30 21:55:20 +03:00
// 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 ( )
}
2022-08-03 14:15:10 +03:00
// Device is a connection to a remote peripheral.
type Device struct {
2024-04-03 05:40:02 +03:00
Address Address // the MAC address of the device
2022-08-03 14:15:10 +03:00
device * bluetooth . BluetoothLEDevice
session * genericattributeprofile . GattSession
}
// Connect starts a connection attempt to the given peripheral device address.
//
// On Linux and Windows, the IsRandom part of the address is ignored.
2023-12-25 16:28:56 +03:00
func ( a * Adapter ) Connect ( address Address , params ConnectionParams ) ( Device , error ) {
2022-08-03 14:15:10 +03:00
var winAddr uint64
for i := range address . MAC {
winAddr += uint64 ( address . MAC [ i ] ) << ( 8 * i )
}
// IAsyncOperation<BluetoothLEDevice>
2024-03-21 18:53:33 +03:00
bleDeviceOp , err := bluetooth . BluetoothLEDeviceFromBluetoothAddressAsync ( winAddr )
2022-08-03 14:15:10 +03:00
if err != nil {
2023-12-25 16:28:56 +03:00
return Device { } , err
2022-08-03 14:15:10 +03:00
}
// We need to pass the signature of the parameter returned by the async operation:
// IAsyncOperation<BluetoothLEDevice>
if err := awaitAsyncOperation ( bleDeviceOp , bluetooth . SignatureBluetoothLEDevice ) ; err != nil {
2023-12-25 16:28:56 +03:00
return Device { } , fmt . Errorf ( "error connecting to device: %w" , err )
2022-08-03 14:15:10 +03:00
}
res , err := bleDeviceOp . GetResults ( )
if err != nil {
2023-12-25 16:28:56 +03:00
return Device { } , err
2022-08-03 14:15:10 +03:00
}
// The returned BluetoothLEDevice is set to null if FromBluetoothAddressAsync can't find the device identified by bluetoothAddress
if uintptr ( res ) == 0x0 {
2023-12-25 16:28:56 +03:00
return Device { } , fmt . Errorf ( "device with the given address was not found" )
2022-08-03 14:15:10 +03:00
}
bleDevice := ( * bluetooth . BluetoothLEDevice ) ( res )
// Creating a BluetoothLEDevice object by calling this method alone doesn't (necessarily) initiate a connection.
// To initiate a connection, we need to set GattSession.MaintainConnection to true.
dID , err := bleDevice . GetBluetoothDeviceId ( )
if err != nil {
2023-12-25 16:28:56 +03:00
return Device { } , err
2022-08-03 14:15:10 +03:00
}
// Windows does not support explicitly connecting to a device.
// Instead it has the concept of a GATT session that is owned
// by the calling program.
2024-03-21 18:53:33 +03:00
gattSessionOp , err := genericattributeprofile . GattSessionFromDeviceIdAsync ( dID ) // IAsyncOperation<GattSession>
2022-08-03 14:15:10 +03:00
if err != nil {
2023-12-25 16:28:56 +03:00
return Device { } , err
2022-08-03 14:15:10 +03:00
}
if err := awaitAsyncOperation ( gattSessionOp , genericattributeprofile . SignatureGattSession ) ; err != nil {
2023-12-25 16:28:56 +03:00
return Device { } , fmt . Errorf ( "error getting gatt session: %w" , err )
2022-08-03 14:15:10 +03:00
}
gattRes , err := gattSessionOp . GetResults ( )
if err != nil {
2023-12-25 16:28:56 +03:00
return Device { } , err
2022-08-03 14:15:10 +03:00
}
newSession := ( * genericattributeprofile . GattSession ) ( gattRes )
// This keeps the device connected until we set maintain_connection = False.
if err := newSession . SetMaintainConnection ( true ) ; err != nil {
2023-12-25 16:28:56 +03:00
return Device { } , err
2022-08-03 14:15:10 +03:00
}
2024-04-03 05:40:02 +03:00
return Device { address , bleDevice , newSession } , nil
2022-08-03 14:15:10 +03:00
}
// Disconnect from the BLE device. This method is non-blocking and does not
// wait until the connection is fully gone.
2023-12-25 16:28:56 +03:00
func ( d Device ) Disconnect ( ) error {
2022-08-03 14:15:10 +03:00
defer d . device . Release ( )
defer d . session . Release ( )
if err := d . session . Close ( ) ; err != nil {
return err
}
if err := d . device . Close ( ) ; err != nil {
return err
}
return nil
}
2023-12-25 17:29:49 +03:00
// RequestConnectionParams requests a different connection latency and timeout
// of the given device connection. Fields that are unset will be left alone.
// Whether or not the device will actually honor this, depends on the device and
// on the specific parameters.
//
// On Windows, this call doesn't do anything.
func ( d Device ) RequestConnectionParams ( params ConnectionParams ) error {
// TODO: implement this using
// BluetoothLEDevice.RequestPreferredConnectionParameters.
return nil
}