Search code examples
c#winapibluetooth-lowenergy

Register for GATT Characteristic value change notifications from c# using win32 API


I am trying to register for GATT Characteristic value change notifications using the win32 API from c#. I am able to get a handle to the device and get the relevant GATT service and characteristic. The problem comes when I try to register for value change notifications using BluetoothGATTRegisterEvent: https://learn.microsoft.com/en-us/windows/win32/api/bluetoothleapis/nf-bluetoothleapis-bluetoothgattregisterevent

I always get a 0x57 (E_INVALIDARG) when calling the method. Unfortunately I don't get any hint to what arg is invalid... I have tried many different variations without luck. Below is the structures used for calling BluetoothGATTRegisterEvent. I have omitted service and characteristic discovery, as that seems to be working fine.

Any suggestions to what could be wrong with my definition and calling BluetoothGATTRegisterEvent would be highly appreciated.

public enum BTH_LE_GATT_EVENT_TYPE
{
    CharacteristicValueChangedEvent = 0
}

[StructLayout(LayoutKind.Sequential)]
public struct BLUETOOTH_GATT_NOTIFICATION_REGISTRATION
{
    public ushort NumCharacteristics;
    public BTH_LE_GATT_CHARACTERISTIC[] Characteristics;
}

[StructLayout(LayoutKind.Sequential)]
public struct BLUETOOTH_GATT_EVENT_HANDLE
{
    public IntPtr handle;
}

// Define the callback delegate
internal delegate void PFNBLUETOOTH_GATT_EVENT_CALLBACK(
    BTH_LE_GATT_EVENT_TYPE EventType,
    IntPtr EventOutParameter,
    IntPtr Context
);

[DllImport("BluetoothApis.dll", SetLastError = true)]
public static extern uint BluetoothGATTRegisterEvent(
    ushort Service,
    BTH_LE_GATT_EVENT_TYPE eventType,
    ref BLUETOOTH_GATT_NOTIFICATION_REGISTRATION eventParameterIn,
    PFNBLUETOOTH_GATT_EVENT_CALLBACK callback,
    IntPtr context,
    ref BLUETOOTH_GATT_EVENT_HANDLE eventHandle,
    uint Flags
);

public void RegisterEvents(
    SafeFileHandle deviceHandle,
    BTH_LE_GATT_SERVICE service,
    BTH_LE_GATT_CHARACTERISTIC characteristic,
    PFNBLUETOOTH_GATT_EVENT_CALLBACK eventHandler
)
{
    var eventHandle = new BLUETOOTH_GATT_EVENT_HANDLE();
    var eventParameters = new BLUETOOTH_GATT_NOTIFICATION_REGISTRATION
    {
        NumCharacteristics = 1,
        Characteristics = new[] { characteristic }
    };

    BluetoothGATTRegisterEvent(
        characteristic.ServiceHandle,
        BTH_LE_GATT_EVENT_TYPE.CharacteristicValueChangedEvent,
        ref eventParameters,
        eventHandler,
        IntPtr.Zero,
        ref eventHandle,
        BLUETOOTH_GATT_FLAG_NONE
    );
}

I have tried using both the device handle, and the service handle obtained from the characteristic as the first argument. Changing that didn't make a difference


Solution

  • Here is part of my Bluetooth Framework that shows how to subscribe to characteristic changes notifications with legacy GATT API.

    private enum BTH_LE_GATT_EVENT_TYPE : uint
    {
        CharacteristicValueChangedEvent
    };
    
    [StructLayout(LayoutKind.Sequential)]
    private struct BLUETOOTH_GATT_VALUE_CHANGED_EVENT_REGISTRATION
    {
        [MarshalAs(UnmanagedType.U2)]
        public UInt16 NumCharacteristics;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
        public BTH_LE_GATT_CHARACTERISTIC[] Characteristics;
    };
    
    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    private delegate void PFNBLUETOOTH_GATT_EVENT_CALLBACK(
        [param: MarshalAs(UnmanagedType.U4), In] BTH_LE_GATT_EVENT_TYPE EventType,
        [param: In, Out] ref BLUETOOTH_GATT_VALUE_CHANGED_EVENT EventOutParameter,
        [param: MarshalAs(UnmanagedType.SysInt), In] IntPtr Context);
    
    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    [return: MarshalAs(UnmanagedType.I4)]
    private delegate Int32 PBluetoothGATTRegisterEvent(
        [param: MarshalAs(UnmanagedType.SysInt), In] IntPtr hService,
        [param: MarshalAs(UnmanagedType.U4), In] BTH_LE_GATT_EVENT_TYPE EventType,
        [param: In, Out] ref BLUETOOTH_GATT_VALUE_CHANGED_EVENT_REGISTRATION EventParameterIn,
        [param: MarshalAs(UnmanagedType.FunctionPtr), In] PFNBLUETOOTH_GATT_EVENT_CALLBACK Callback,
        [param: MarshalAs(UnmanagedType.SysInt), In] IntPtr CallbackContext,
        [param: MarshalAs(UnmanagedType.SysInt), Out] out IntPtr pEventHandle,
        [param: MarshalAs(UnmanagedType.U4), In] UInt32 Flags);
    
    private Int32 GetDevicePath(Guid Service, out String Path)
    {
        Path = "";
        Int32 Result = wclBluetoothErrors.WCL_E_BLUETOOTH_DEVICE_NOT_FOUND;
        
        IntPtr DevInfo = Setup.SetupDiGetClassDevs(Service, null, IntPtr.Zero, Setup.DIGCF_PRESENT | Setup.DIGCF_DEVICEINTERFACE);
        if (DevInfo == Common.INVALID_HANDLE_VALUE)
        {
            Result = DecodeError((UInt32)Marshal.GetLastWin32Error());
            if (Result == wclBluetoothErrors.WCL_E_BLUETOOTH_ACCESS_DENIED)
                return wclBluetoothErrors.WCL_E_BLUETOOTH_LE_ACCESS_DENIED;
            return Result;
        }
        
        String AddrStr = Address.ToString("X12").ToLower();
        
        UInt32 Ndx = 0;
        while (true)
        {
            Setup.SP_DEVICE_INTERFACE_DATA Data = new Setup.SP_DEVICE_INTERFACE_DATA();
            Data.cbSize = (UInt32)Marshal.SizeOf(typeof(Setup.SP_DEVICE_INTERFACE_DATA));
            if (!Setup.SetupDiEnumDeviceInterfaces(DevInfo, IntPtr.Zero, Service, Ndx, ref Data))
                break;
            
            UInt32 DetailsSize;
            IntPtr Details;
            Setup.SetupDiGetDeviceInterfaceDetail(DevInfo, ref Data, IntPtr.Zero, 0, out DetailsSize, IntPtr.Zero);
            if (Marshal.GetLastWin32Error() == Common.ERROR_INSUFFICIENT_BUFFER)
            {
                Details = wclHelpers.AllocHGlobal((Int32)DetailsSize); // PSP_DEVICE_INTERFACE_DETAIL_DATA
                if (Details != IntPtr.Zero)
                {
                    Int32 StructSize = 6;
                    if (IntPtr.Size == 8)
                        StructSize = 8; // On 64 bit structure has 8 bytes size.
                    Marshal.WriteInt32(Details, StructSize);
                    Boolean Res = Setup.SetupDiGetDeviceInterfaceDetail(DevInfo, ref Data, Details, DetailsSize, out DetailsSize, IntPtr.Zero);
                    if (Res)
                    {
                        IntPtr p = wclHelpers.IncPtr(Details, 4); // String always has 4 bytes offset.
                        String DevPath = Marshal.PtrToStringAuto(p);
                        DevPath = DevPath.ToLower();
                        if (DevPath.IndexOf(AddrStr) >= 0)
                        {
                            Path = DevPath;
                            Result = wclErrors.WCL_E_SUCCESS;
                        }
                    }
                    Marshal.FreeHGlobal(Details);
                }
            }
            if (Path != "")
                break;
            
            Ndx++;
        }
        Setup.SetupDiDestroyDeviceInfoList(DevInfo);
        
        return Result;
    }
    
    private Int32 OpenServiceHandle(wclGattOperationFlag Flag, UInt16 SvcHdl, wclGattService Service, out IntPtr DevHdl)
    {
        DevHdl = Common.INVALID_HANDLE_VALUE;
        Guid Guid;
        if (Services[i].Uuid.IsShortUuid)
            Guid = new Guid("{0000" + Services.Uuid.ShortUuid.ToString("X4") + "-0000-1000-8000-00805F9B34FB}");
        else
            Guid = Services.Uuid.LongUuid;
    
        String Path;
        Result = GetDevicePath(Guid, out Path);
        if (Result != wclErrors.WCL_E_SUCCESS)
            return Result;
    
        // First try to open with read-write access.
        DevHdl = Io.CreateFile(Path, Io.GENERIC_READ | Io.GENERIC_WRITE, Io.FILE_SHARE_READ | Io.FILE_SHARE_WRITE, IntPtr.Zero, Io.OPEN_EXISTING, Io.FILE_ATTRIBUTE_NORMAL, IntPtr.Zero);
        // If failed, try to open with only read access.
        if (DevHdl == Common.INVALID_HANDLE_VALUE)
            DevHdl = Io.CreateFile(Path, Io.GENERIC_READ, Io.FILE_SHARE_READ, IntPtr.Zero, Io.OPEN_EXISTING, Io.FILE_ATTRIBUTE_NORMAL, IntPtr.Zero);
        // If failed again - return error.
        if (DevHdl == Common.INVALID_HANDLE_VALUE)
            return DecodeAttErrorCode(wclHelpers.HResultFromWin32(Marshal.GetLastWin32Error()));
        return wclErrors.WCL_E_SUCCESS;
    }
    
    protected override Int32 HalSubscribe(wclGattCharacteristic Characteristic, out IntPtr Hdl)
    {
        Hdl = IntPtr.Zero;
        IntPtr Dev;
        Int32 Result = OpenServiceHandle(wclGattOperationFlag.goNone, Characteristic.ServiceHandle, out Dev);
        if (Result != wclErrors.WCL_E_SUCCESS)
            return Result;
        
        BLUETOOTH_GATT_VALUE_CHANGED_EVENT_REGISTRATION Reg = new BLUETOOTH_GATT_VALUE_CHANGED_EVENT_REGISTRATION();
        Reg.NumCharacteristics = 1;
        Reg.Characteristics = new BTH_LE_GATT_CHARACTERISTIC[1];
        Reg.Characteristics[0].ServiceHandle = Characteristic.ServiceHandle;
        Reg.Characteristics[0].AttributeHandle = Characteristic.Handle;
        Reg.Characteristics[0].CharacteristicValueHandle = Characteristic.ValueHandle;
        // On Windows 10 we need only handles. On Windows 8 and 8.1 we need all characteristic properties.
        Reg.Characteristics[0].CharacteristicUuid = UuidToGattUuid(Characteristic.Uuid);
        Reg.Characteristics[0].IsBroadcastable = Characteristic.IsBroadcastable;
        Reg.Characteristics[0].IsReadable = Characteristic.IsReadable;
        Reg.Characteristics[0].IsWritable = Characteristic.IsWritable;
        Reg.Characteristics[0].IsWritableWithoutResponse = Characteristic.IsWritableWithoutResponse;
        Reg.Characteristics[0].IsSignedWritable = Characteristic.IsSignedWritable;
        Reg.Characteristics[0].IsNotifiable = Characteristic.IsNotifiable;
        Reg.Characteristics[0].IsIndicatable = Characteristic.IsIndicatable;
        Reg.Characteristics[0].HasExtendedProperties = Characteristic.HasExtendedProperties;
        
        if (FCb == null)
            FCb = new PFNBLUETOOTH_GATT_EVENT_CALLBACK(GattCharChangeCallback);
        if (FCharChangeHdl == IntPtr.Zero)
            FCharChangeHdl = (IntPtr)GCHandle.Alloc(Receiver);
        
        IntPtr TmpHdl;
        return DecodeAttErrorCode(BluetoothGATTRegisterEvent(Dev, BTH_LE_GATT_EVENT_TYPE.CharacteristicValueChangedEvent, ref Reg, FCb, FCharChangeHdl, out TmpHdl, BLUETOOTH_GATT_FLAG_NONE));
    
    }