Generic ATTribute Profile (GATT) is a protocol used by BLE devices for data exchange. Broadly, every LE device contains one or more GATT services, with each service having multiple characteristics, which contain the actual piece of data to be exchanged. The Win32 BLE api is defined in [`bluetoothleapis.h`](https://learn.microsoft.com/en-us/windows/win32/api/bluetoothleapis/), and operates on `HANDLE`s to PDOs created by the driver to remote devices and services, using the `GUID_BLUETOOTHLE_DEVICE_INTERFACE` and `GUID_BLUETOOTH_GATT_SERVICE_DEVICE_INTERFACE` class interfaces respectively.
This MR introduces initial support for accessing GATT services on LE devices by:
* Creating PDOs for remote devices that we discover GATT services on, and enabling `GUID_BLUETOOTHLE_DEVICE_INTERFACE` for them. * Because the driver also creates PDOs for bluetooth radios, a tagged union is used to distinguish between device extension values to dispatch IOCTL and PnP IRPs appropriately. * Enumerate through all `org.bluez.GattService1` objects on BlueZ, and store them on the PE driver inside the associated devices. * Implement IOCTL_WINEBTH_LE_DEVICE_GET_GATT_SERVICES for device PDOs. * Use the newly added IOCTL to implement [`BluetoothGATTGetServices`](https://learn.microsoft.com/en-us/windows/win32/api/bluetoothleapis/nf-bluet...).
-- v2: bluetoothapis/tests: Add tests for BluetoothGATTGetServices. bluetoothapis: Implement BluetoothGATTGetServices. winebth.sys: Implement IOCTL_WINEBTH_LE_DEVICE_GET_GATT_SERVICES. winebth.sys: Store a list of GATT services discovered on LE devices.
From: Vibhav Pant vibhavp@gmail.com
--- dlls/winebth.sys/winebth.c | 304 ++++++++++++++++++++++++++++++++----- include/Makefile.in | 1 + include/ddk/bthguid.h | 25 +++ 3 files changed, 289 insertions(+), 41 deletions(-) create mode 100644 include/ddk/bthguid.h
diff --git a/dlls/winebth.sys/winebth.c b/dlls/winebth.sys/winebth.c index 47006d04aad..57fb934b505 100644 --- a/dlls/winebth.sys/winebth.c +++ b/dlls/winebth.sys/winebth.c @@ -36,6 +36,7 @@ #include <winioctl.h> #include <bthioctl.h> #include <ddk/wdm.h> +#include <ddk/bthguid.h>
#include <wine/winebth.h> #include <wine/debug.h> @@ -90,10 +91,28 @@ struct bluetooth_remote_device { struct list entry;
+ DEVICE_OBJECT *device_obj; + struct bluetooth_radio *radio; /* The radio associated with this remote device. */ winebluetooth_device_t device; CRITICAL_SECTION props_cs; winebluetooth_device_props_mask_t props_mask; /* Guarded by props_cs */ struct winebluetooth_device_properties props; /* Guarded by props_cs */ + unsigned int removed : 1; +}; + +enum bluetooth_pdo_ext_type +{ + BLUETOOTH_PDO_EXT_RADIO, + BLUETOOTH_PDO_EXT_REMOTE_DEVICE, +}; + +struct bluetooth_pdo_ext +{ + enum bluetooth_pdo_ext_type type; + union { + struct bluetooth_radio radio; + struct bluetooth_remote_device remote_device; + }; };
static NTSTATUS WINAPI dispatch_auth( DEVICE_OBJECT *device, IRP *irp ) @@ -118,19 +137,26 @@ static NTSTATUS WINAPI dispatch_auth( DEVICE_OBJECT *device, IRP *irp ) return status; }
-static NTSTATUS WINAPI dispatch_bluetooth( DEVICE_OBJECT *device, IRP *irp ) +static NTSTATUS bluetooth_remote_device_dispatch( DEVICE_OBJECT *device, struct bluetooth_remote_device *ext, IRP *irp ) +{ + IO_STACK_LOCATION *stack = IoGetCurrentIrpStackLocation( irp ); + ULONG code = stack->Parameters.DeviceIoControl.IoControlCode; + NTSTATUS status = irp->IoStatus.Status; + + FIXME( "device=%p, ext=%p, irp=%p, code=%#lx: stub!\n", device, ext, irp, code ); + IoCompleteRequest( irp, IO_NO_INCREMENT ); + return status; +} + +static NTSTATUS bluetooth_radio_dispatch( DEVICE_OBJECT *device, struct bluetooth_radio *ext, IRP *irp ) { - struct bluetooth_radio *ext = (struct bluetooth_radio *)device->DeviceExtension; IO_STACK_LOCATION *stack = IoGetCurrentIrpStackLocation( irp ); ULONG code = stack->Parameters.DeviceIoControl.IoControlCode; ULONG insize = stack->Parameters.DeviceIoControl.InputBufferLength; ULONG outsize = stack->Parameters.DeviceIoControl.OutputBufferLength; NTSTATUS status = irp->IoStatus.Status;
- TRACE( "device %p irp %p code %#lx\n", device, irp, code ); - - if (device == device_auth) - return dispatch_auth( device, irp ); + TRACE( "device=%p, ext=%p, irp=%p code=%#lx\n", device, ext, irp, code );
switch (code) { @@ -421,6 +447,24 @@ static NTSTATUS WINAPI dispatch_bluetooth( DEVICE_OBJECT *device, IRP *irp ) return status; }
+static NTSTATUS WINAPI dispatch_bluetooth( DEVICE_OBJECT *device, IRP *irp ) +{ + struct bluetooth_pdo_ext *ext = device->DeviceExtension; + TRACE( "(%p, %p)\n", device, irp ); + + if (device == device_auth) + return dispatch_auth( device, irp ); + + switch (ext->type) + { + case BLUETOOTH_PDO_EXT_RADIO: + return bluetooth_radio_dispatch( device, &ext->radio, irp ); + case BLUETOOTH_PDO_EXT_REMOTE_DEVICE: + return bluetooth_remote_device_dispatch( device, &ext->remote_device, irp ); + DEFAULT_UNREACHABLE; + } +} + void WINAPIV append_id( struct string_buffer *buffer, const WCHAR *format, ... ) { va_list args; @@ -492,7 +536,7 @@ static NTSTATUS radio_get_hw_name_w( winebluetooth_radio_t radio, WCHAR **name ) } static void add_bluetooth_radio( struct winebluetooth_watcher_event_radio_added event ) { - struct bluetooth_radio *device; + struct bluetooth_pdo_ext *ext; DEVICE_OBJECT *device_obj; UNICODE_STRING string; NTSTATUS status; @@ -511,7 +555,7 @@ static void add_bluetooth_radio( struct winebluetooth_watcher_event_radio_added }
RtlInitUnicodeString( &string, name ); - status = IoCreateDevice( driver_obj, sizeof( *device ), &string, FILE_DEVICE_BLUETOOTH, 0, + status = IoCreateDevice( driver_obj, sizeof( *ext ), &string, FILE_DEVICE_BLUETOOTH, 0, FALSE, &device_obj ); if (status) { @@ -519,21 +563,22 @@ static void add_bluetooth_radio( struct winebluetooth_watcher_event_radio_added return; }
- device = device_obj->DeviceExtension; - device->device_obj = device_obj; - device->radio = event.radio; - device->removed = FALSE; - device->hw_name = hw_name; - device->props = event.props; - device->props_mask = event.props_mask; - list_init( &device->remote_devices ); + ext = device_obj->DeviceExtension; + ext->type = BLUETOOTH_PDO_EXT_RADIO; + ext->radio.device_obj = device_obj; + ext->radio.radio = event.radio; + ext->radio.removed = FALSE; + ext->radio.hw_name = hw_name; + ext->radio.props = event.props; + ext->radio.props_mask = event.props_mask; + list_init( &ext->radio.remote_devices );
- InitializeCriticalSection( &device->props_cs ); - InitializeCriticalSection( &device->remote_devices_cs ); - InitializeListHead( &device->irp_list ); + InitializeCriticalSection( &ext->radio.props_cs ); + InitializeCriticalSection( &ext->radio.remote_devices_cs ); + InitializeListHead( &ext->radio.irp_list );
EnterCriticalSection( &device_list_cs ); - list_add_tail( &device_list, &device->entry ); + list_add_tail( &device_list, &ext->radio.entry ); LeaveCriticalSection( &device_list_cs );
IoInvalidateDeviceRelations( bus_pdo, BusRelations ); @@ -645,34 +690,54 @@ static void bluetooth_radio_add_remote_device( struct winebluetooth_watcher_even { if (winebluetooth_radio_equal( event.radio, radio->radio )) { - struct bluetooth_remote_device *remote_device; - - remote_device = calloc( 1, sizeof( *remote_device ) ); - if (!remote_device) + struct bluetooth_pdo_ext *ext; + static unsigned int device_index; + DEVICE_OBJECT *device_obj; + UNICODE_STRING string; + NTSTATUS status; + WCHAR name[256]; + + swprintf( name, ARRAY_SIZE( name ), L"\Device\WINEBTH-DEVICE-%d", device_index++ ); + TRACE( "Adding new bluetooth remote device: %p: %s\n", (void *)event.device.handle, debugstr_w( name ) ); + + RtlInitUnicodeString( &string, name ); + status = IoCreateDevice( driver_obj, sizeof( *ext ), &string, FILE_DEVICE_BLUETOOTH, 0, FALSE, + &device_obj ); + if (status) { + ERR( "Failed to create remote device, status %#lx\n", status ); winebluetooth_device_free( event.device ); break; }
- InitializeCriticalSection( &remote_device->props_cs ); - remote_device->device = event.device; - remote_device->props_mask = event.known_props_mask; - remote_device->props = event.props; + ext = device_obj->DeviceExtension; + ext->type = BLUETOOTH_PDO_EXT_REMOTE_DEVICE; + ext->remote_device.radio = radio; + + ext->remote_device.device_obj = device_obj; + InitializeCriticalSection( &ext->remote_device.props_cs ); + ext->remote_device.device = event.device; + ext->remote_device.props_mask = event.known_props_mask; + ext->remote_device.props = event.props; + ext->remote_device.removed = 0; +
if (!event.init_entry) { BTH_DEVICE_INFO device_info = {0}; - winebluetooth_device_properties_to_info( remote_device->props_mask, &remote_device->props, &device_info ); + winebluetooth_device_properties_to_info( ext->remote_device.props_mask, &ext->remote_device.props, &device_info ); bluetooth_radio_report_radio_in_range_event( radio->device_obj, 0, &device_info ); }
EnterCriticalSection( &radio->remote_devices_cs ); - list_add_tail( &radio->remote_devices, &remote_device->entry ); + list_add_tail( &radio->remote_devices, &ext->remote_device.entry ); LeaveCriticalSection( &radio->remote_devices_cs ); + IoInvalidateDeviceRelations( radio->device_obj, BusRelations ); break; } } LeaveCriticalSection( &device_list_cs ); + winebluetooth_radio_free( event.radio ); }
@@ -690,8 +755,20 @@ static void bluetooth_radio_remove_remote_device( struct winebluetooth_watcher_e { if (winebluetooth_device_equal( event.device, device->device )) { + BOOL has_addr; + + TRACE( "Removing bluetooth remote device %p\n", (void *)device->device.handle ); + list_remove( &device->entry ); - if (device->props_mask & WINEBLUETOOTH_DEVICE_PROPERTY_ADDRESS) + + EnterCriticalSection( &device->props_cs ); + has_addr = device->props_mask & WINEBLUETOOTH_DEVICE_PROPERTY_ADDRESS; + LeaveCriticalSection( &device->props_cs ); + + device->removed = 1; + list_remove( &device->entry ); + + if (has_addr) { TARGET_DEVICE_CUSTOM_NOTIFICATION *notification; BLUETOOTH_ADDRESS *addr; @@ -715,12 +792,9 @@ static void bluetooth_radio_remove_remote_device( struct winebluetooth_watcher_e ExFreePool( notification ); } } - winebluetooth_device_free( device->device ); - DeleteCriticalSection( &device->props_cs ); - free( device ); - LeaveCriticalSection( &radio->remote_devices_cs ); LeaveCriticalSection( &device_list_cs ); + IoInvalidateDeviceRelations( radio->device_obj, BusRelations ); winebluetooth_device_free( event.device ); return; } @@ -998,7 +1072,44 @@ static NTSTATUS WINAPI fdo_pnp( DEVICE_OBJECT *device_obj, IRP *irp ) return IoCallDriver( bus_pdo, irp ); }
-static NTSTATUS query_id(const struct bluetooth_radio *ext, IRP *irp, BUS_QUERY_ID_TYPE type ) +static NTSTATUS remote_device_query_id( struct bluetooth_remote_device *ext, IRP *irp, BUS_QUERY_ID_TYPE type ) +{ + struct string_buffer buf = {0}; + + TRACE( "(%p, %p, %s)\n", ext, irp, debugstr_BUS_QUERY_ID_TYPE( type ) ); + switch (type) + { + case BusQueryDeviceID: + append_id( &buf, L"WINEBTH\RADIO" ); + break; + case BusQueryInstanceID: + { + BLUETOOTH_ADDRESS addr; + + EnterCriticalSection( &ext->props_cs ); + addr = ext->props.address; + LeaveCriticalSection( &ext->props_cs ); + + append_id( &buf, L"%s\%02X%02X%02X%02X%02X%02X", ext->radio->hw_name, addr.rgBytes[0], addr.rgBytes[1], + addr.rgBytes[2], addr.rgBytes[3], addr.rgBytes[4], addr.rgBytes[5] ); + break; + } + case BusQueryHardwareIDs: + case BusQueryCompatibleIDs: + append_id( &buf, L"" ); + break; + default: + return irp->IoStatus.Status; + } + + if (!buf.string) + return STATUS_NO_MEMORY; + + irp->IoStatus.Information = (ULONG_PTR)buf.string; + return STATUS_SUCCESS; +} + +static NTSTATUS radio_query_id( const struct bluetooth_radio *ext, IRP *irp, BUS_QUERY_ID_TYPE type ) { struct string_buffer buf = {0};
@@ -1071,17 +1182,117 @@ static void remove_pending_irps( struct bluetooth_radio *radio ) } }
-static NTSTATUS WINAPI pdo_pnp( DEVICE_OBJECT *device_obj, IRP *irp ) +static NTSTATUS WINAPI remote_device_pdo_pnp( DEVICE_OBJECT *device_obj, struct bluetooth_remote_device *ext, IRP *irp ) +{ + IO_STACK_LOCATION *stack = IoGetCurrentIrpStackLocation( irp ); + NTSTATUS ret = irp->IoStatus.Status; + + TRACE( "device_obj=%p, ext=%p, irp=%p, minor function=%s\n", device_obj, ext, irp, + debugstr_minor_function_code( stack->MinorFunction ) ); + + switch (stack->MinorFunction) + { + case IRP_MN_QUERY_ID: + ret = remote_device_query_id( ext, irp, stack->Parameters.QueryId.IdType ); + break; + case IRP_MN_QUERY_CAPABILITIES: + { + DEVICE_CAPABILITIES *caps = stack->Parameters.DeviceCapabilities.Capabilities; + caps->Removable = TRUE; + caps->SurpriseRemovalOK = TRUE; + caps->RawDeviceOK = TRUE; + ret = STATUS_SUCCESS; + break; + } + case IRP_MN_START_DEVICE: + { + WCHAR addr_str[13]; + BLUETOOTH_ADDRESS addr; + + EnterCriticalSection( &ext->props_cs ); + addr = ext->props.address; + LeaveCriticalSection( &ext->props_cs ); + swprintf( addr_str, ARRAY_SIZE( addr_str ), L"%02x%02x%02x%02x%02x%02x", addr.rgBytes[0], addr.rgBytes[1], + addr.rgBytes[2], addr.rgBytes[3], addr.rgBytes[4], addr.rgBytes[5] ); + IoSetDevicePropertyData( device_obj, &DEVPKEY_Bluetooth_DeviceAddress, LOCALE_NEUTRAL, 0, DEVPROP_TYPE_STRING, + sizeof( addr_str ), addr_str ); + ret = STATUS_SUCCESS; + break; + } + case IRP_MN_REMOVE_DEVICE: + { + assert( ext->removed ); + DeleteCriticalSection( &ext->props_cs ); + winebluetooth_device_free( ext->device ); + IoDeleteDevice( ext->device_obj ); + ret = STATUS_SUCCESS; + break; + } + case IRP_MN_SURPRISE_REMOVAL: + { + EnterCriticalSection( &ext->radio->remote_devices_cs ); + if (!ext->removed) + { + ext->removed = 1; + list_remove( &ext->entry ); + } + LeaveCriticalSection( &ext->radio->remote_devices_cs ); + ret = STATUS_SUCCESS; + break; + } + default: + FIXME( "Unhandled minor function %#x.\n", stack->MinorFunction ); + } + + irp->IoStatus.Status = ret; + IoCompleteRequest( irp, IO_NO_INCREMENT ); + return ret; +} + +static NTSTATUS WINAPI radio_pdo_pnp( DEVICE_OBJECT *device_obj, struct bluetooth_radio *device, IRP *irp ) { IO_STACK_LOCATION *stack = IoGetCurrentIrpStackLocation(irp); - struct bluetooth_radio *device = device_obj->DeviceExtension; NTSTATUS ret = irp->IoStatus.Status;
- TRACE( "device_obj %p, irp %p, minor function %s\n", device_obj, irp, debugstr_minor_function_code( stack->MinorFunction ) ); + TRACE( "device_obj %p, device %p, irp %p, minor function %s\n", device_obj, device, irp, + debugstr_minor_function_code( stack->MinorFunction ) ); switch (stack->MinorFunction) { + case IRP_MN_QUERY_DEVICE_RELATIONS: + { + struct bluetooth_remote_device *remote_device; + DEVICE_RELATIONS *devices; + SIZE_T i = 0; + + if (stack->Parameters.QueryDeviceRelations.Type != BusRelations) + { + FIXME( "Unhandled Device Relation %x\n", stack->Parameters.QueryDeviceRelations.Type ); + break; + } + + EnterCriticalSection( &device->remote_devices_cs ); + devices = ExAllocatePool( PagedPool, + offsetof( DEVICE_RELATIONS, Objects[list_count( &device->remote_devices )] ) ); + if (!devices) + { + LeaveCriticalSection( &device->remote_devices_cs ); + irp->IoStatus.Status = STATUS_NO_MEMORY; + break; + } + LIST_FOR_EACH_ENTRY( remote_device, &device->remote_devices, struct bluetooth_remote_device, entry ) + { + devices->Objects[i++] = remote_device->device_obj; + call_fastcall_func1( ObfReferenceObject, remote_device->device_obj ); + } + LeaveCriticalSection( &device->remote_devices_cs ); + + devices->Count = i; + irp->IoStatus.Information = (ULONG_PTR)devices; + ret = STATUS_SUCCESS; + break; + } case IRP_MN_QUERY_ID: - ret = query_id( device, irp, stack->Parameters.QueryId.IdType ); + ret = radio_query_id( device, irp, stack->Parameters.QueryId.IdType ); break; case IRP_MN_QUERY_CAPABILITIES: { @@ -1178,11 +1389,22 @@ static NTSTATUS auth_pnp( DEVICE_OBJECT *device, IRP *irp )
static NTSTATUS WINAPI bluetooth_pnp( DEVICE_OBJECT *device, IRP *irp ) { + struct bluetooth_pdo_ext *ext; + if (device == bus_fdo) return fdo_pnp( device, irp ); else if (device == device_auth) return auth_pnp( device, irp ); - return pdo_pnp( device, irp ); + + ext = device->DeviceExtension; + switch (ext->type) + { + case BLUETOOTH_PDO_EXT_RADIO: + return radio_pdo_pnp( device, &ext->radio, irp ); + case BLUETOOTH_PDO_EXT_REMOTE_DEVICE: + return remote_device_pdo_pnp( device, &ext->remote_device, irp ); + DEFAULT_UNREACHABLE; + } }
static NTSTATUS WINAPI driver_add_device( DRIVER_OBJECT *driver, DEVICE_OBJECT *pdo ) diff --git a/include/Makefile.in b/include/Makefile.in index ab4957969d3..4446d4821d6 100644 --- a/include/Makefile.in +++ b/include/Makefile.in @@ -196,6 +196,7 @@ SOURCES = \ dde.h \ dde.rh \ ddeml.h \ + ddk/bthguid.h \ ddk/compstui.h \ ddk/csq.h \ ddk/d3dkmthk.h \ diff --git a/include/ddk/bthguid.h b/include/ddk/bthguid.h new file mode 100644 index 00000000000..d2dfbe43e68 --- /dev/null +++ b/include/ddk/bthguid.h @@ -0,0 +1,25 @@ +/* + * Copyright 2025 Vibhav Pant + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#ifndef __DDK_BTHGUID_H__ +#define __DDK_BTHGUID_H__ + +DEFINE_DEVPROPKEY( DEVPKEY_Bluetooth_DeviceAddress, 0x2bd67d8b, 0x8beb, 0x48d5, 0x87, 0xe0, 0x6c, 0xda, 0x34, 0x28, + 0x04, 0x0a, 1 ); /* DEVPROP_TYPE_STRING */ + +#endif
From: Vibhav Pant vibhavp@gmail.com
Enable GUID_BLUETOOTHLE_DEVICE_INTERFACE for remote devices that have GATT services, and store them on the PE driver. --- dlls/winebth.sys/dbus.c | 112 ++++++++++++++++++++++++- dlls/winebth.sys/unixlib.c | 9 +++ dlls/winebth.sys/unixlib.h | 7 ++ dlls/winebth.sys/winebluetooth.c | 10 +++ dlls/winebth.sys/winebth.c | 135 ++++++++++++++++++++++++++++++- dlls/winebth.sys/winebth_priv.h | 20 +++++ include/Makefile.in | 1 + include/bthledef.h | 22 +++++ 8 files changed, 310 insertions(+), 6 deletions(-) create mode 100644 include/bthledef.h
diff --git a/dlls/winebth.sys/dbus.c b/dlls/winebth.sys/dbus.c index 3c0c21460fd..d20a70852f0 100644 --- a/dlls/winebth.sys/dbus.c +++ b/dlls/winebth.sys/dbus.c @@ -119,6 +119,7 @@ const int bluez_timeout = -1; #define BLUEZ_INTERFACE_DEVICE "org.bluez.Device1" #define BLUEZ_INTERFACE_AGENT_MANAGER "org.bluez.AgentManager1" #define BLUEZ_INTERFACE_AGENT "org.bluez.Agent1" +#define BLUEZ_INTERFACE_GATT_SERVICE "org.bluez.GattService1"
#define DO_FUNC( f ) typeof( f ) (*p_##f) DBUS_FUNCS; @@ -306,6 +307,19 @@ static void parse_mac_address( const char *addr_str, BYTE dest[6] ) dest[i] = addr[i]; }
+static BOOL parse_uuid( GUID *guid, const char *str ) +{ + int ret; + if (strlen( str ) != 36) + return FALSE; + ret = sscanf( str, "%08x-%hx-%hx-%2hhx%2hhx-%2hhx%2hhx%2hhx%2hhx%2hhx%2hhx", &guid->Data1, &guid->Data2, &guid->Data3, + &guid->Data4[0], &guid->Data4[1], &guid->Data4[2], &guid->Data4[3], &guid->Data4[4], &guid->Data4[5], + &guid->Data4[6], &guid->Data4[7] ); + if (ret != 11) + return FALSE; + return TRUE; +} + static void bluez_dbus_wait_for_reply_callback( DBusPendingCall *pending_call, void *wait ) { sem_post( wait ); @@ -840,6 +854,8 @@ struct bluez_watcher_ctx struct list initial_radio_list; /* struct bluez_init_entry */ struct list initial_device_list; + /* struct bluez_init_entry */ + struct list initial_gatt_service_list;
/* struct bluez_watcher_event */ struct list event_list; @@ -850,6 +866,7 @@ struct bluez_init_entry union { struct winebluetooth_watcher_event_radio_added radio; struct winebluetooth_watcher_event_device_added device; + struct winebluetooth_watcher_event_gatt_service_added service; } object; struct list entry; }; @@ -1897,6 +1914,10 @@ static void bluez_watcher_free( struct bluez_watcher_ctx *watcher ) break; case BLUETOOTH_WATCHER_EVENT_TYPE_PAIRING_FINISHED: break; + case BLUETOOTH_WATCHER_EVENT_TYPE_DEVICE_GATT_SERVICE_ADDED: + unix_name_free( (struct unix_name *)event1->event.gatt_service_added.device.handle ); + unix_name_free( (struct unix_name *)event1->event.gatt_service_added.service.handle ); + break; } free( event1 ); } @@ -1924,6 +1945,7 @@ NTSTATUS bluez_watcher_init( void *connection, void **ctx ) watcher_ctx->init_device_list_call = call; list_init( &watcher_ctx->initial_radio_list ); list_init( &watcher_ctx->initial_device_list ); + list_init( &watcher_ctx->initial_gatt_service_list ); list_init( &watcher_ctx->event_list );
/* The bluez_dbus_loop thread will free up the watcher when the disconnect message is processed (i.e, @@ -1979,7 +2001,7 @@ void bluez_watcher_close( void *connection, void *ctx ) }
static NTSTATUS bluez_build_initial_device_lists( DBusMessage *reply, struct list *adapter_list, - struct list *device_list ) + struct list *device_list, struct list *gatt_service_list ) { DBusMessageIter dict, paths_iter, iface_iter, prop_iter; const char *path; @@ -2087,10 +2109,82 @@ static NTSTATUS bluez_build_initial_device_lists( DBusMessage *reply, struct lis TRACE( "Found BlueZ org.bluez.Device1 object %s: %p\n", debugstr_a( path ), device_name ); break; } + else if (!strcmp( iface, BLUEZ_INTERFACE_GATT_SERVICE )) + { + struct unix_name *service_name, *device_name = NULL; + struct bluez_init_entry *init_device; + DBusMessageIter variant; + const char *prop_name; + + init_device = calloc( 1, sizeof( *init_device ) ); + if (!init_device) + { + status = STATUS_NO_MEMORY; + goto done; + } + + service_name = unix_name_get_or_create( path ); + if (!service_name) + { + ERR( "Failed to allocate memory for service path %s\n", debugstr_a( path ) ); + break; + } + init_device->object.service.service.handle = (UINT_PTR)service_name; + + while ((prop_name = bluez_next_dict_entry( &prop_iter, &variant ))) + { + if (!strcmp( prop_name, "Device" ) + && p_dbus_message_iter_get_arg_type( &variant ) == DBUS_TYPE_OBJECT_PATH ) + { + const char *device_path; + + p_dbus_message_iter_get_basic( &variant, &device_path ); + device_name = unix_name_get_or_create( device_path ); + if (!device_name) + { + unix_name_free( service_name ); + free( init_device ); + ERR( "Failed to allocate memory for device path %s\n", debugstr_a( device_path )); + status = STATUS_NO_MEMORY; + goto done; + } + init_device->object.service.device.handle = (UINT_PTR)device_name; + } + else if (!strcmp( prop_name, "Handle" ) + && p_dbus_message_iter_get_arg_type( &variant ) == DBUS_TYPE_UINT16) + p_dbus_message_iter_get_basic( &variant, &init_device->object.service.attr_handle ); + else if (!strcmp( prop_name, "Primary" ) + && p_dbus_message_iter_get_arg_type( &variant ) == DBUS_TYPE_BOOLEAN) + { + dbus_bool_t primary; + p_dbus_message_iter_get_basic( &variant, &primary ); + init_device->object.service.is_primary = !!primary; + } + else if (!strcmp( prop_name, "UUID" ) + && p_dbus_message_iter_get_arg_type( &variant ) == DBUS_TYPE_STRING) + { + const char *uuid_str; + p_dbus_message_iter_get_basic( &variant, &uuid_str ); + if (!parse_uuid( &init_device->object.service.uuid, uuid_str )) + ERR("Failed to parse UUID %s for GATT service %s\n", debugstr_a( uuid_str ), path ); + } + } + if (!device_name) + { + unix_name_free( service_name ); + free( init_device ); + ERR( "Could not find the associated device for the GATT service %s\n", debugstr_a( path ) ); + break; + } + list_add_tail( gatt_service_list, &init_device->entry ); + TRACE( "Found BlueZ org.bluez.GattService1 object %s %p\n", debugstr_a( path ), service_name ); + break; + } } }
- TRACE( "Initial device list: radios: %d, devices: %d\n", list_count( adapter_list ), list_count( device_list ) ); + TRACE( "Initial device list: radios: %d, devices: %d, GATT services: %d\n", list_count( adapter_list ), + list_count( device_list ), list_count( gatt_service_list ) ); done: return status; } @@ -2119,6 +2213,17 @@ static BOOL bluez_watcher_event_queue_ready( struct bluez_watcher_ctx *ctx, stru free( device ); return TRUE; } + if (!list_empty( &ctx->initial_gatt_service_list )) + { + struct bluez_init_entry *service; + + service = LIST_ENTRY( list_head( &ctx->initial_gatt_service_list ), struct bluez_init_entry, entry ); + event->event_type = BLUETOOTH_WATCHER_EVENT_TYPE_DEVICE_GATT_SERVICE_ADDED; + event->event_data.gatt_service_added = service->object.service; + list_remove( &service->entry ); + free( service ); + return TRUE; + } if (!list_empty( &ctx->event_list )) { struct bluez_watcher_event *watcher_event = @@ -2215,7 +2320,8 @@ NTSTATUS bluez_dbus_loop( void *c, void *watcher, void *auth_agent, return STATUS_NO_MEMORY; } status = bluez_build_initial_device_lists( reply, &watcher_ctx->initial_radio_list, - &watcher_ctx->initial_device_list ); + &watcher_ctx->initial_device_list, + &watcher_ctx->initial_gatt_service_list ); p_dbus_message_unref( reply ); if (status != STATUS_SUCCESS) { diff --git a/dlls/winebth.sys/unixlib.c b/dlls/winebth.sys/unixlib.c index 543c6d2ff1a..5e020464ca4 100644 --- a/dlls/winebth.sys/unixlib.c +++ b/dlls/winebth.sys/unixlib.c @@ -248,6 +248,13 @@ static NTSTATUS bluetooth_device_start_pairing( void *args ) return bluez_device_start_pairing( dbus_connection, bluetooth_watcher, params->device, params->irp ); }
+static NTSTATUS bluetooth_gatt_service_free( void *args ) +{ + struct bluetooth_gatt_service_free_params *params = args; + unix_name_free( params->service ); + return STATUS_SUCCESS; +} + static NTSTATUS bluetooth_get_event( void *args ) { struct bluetooth_get_event_params *params = args; @@ -275,6 +282,8 @@ const unixlib_entry_t __wine_unix_call_funcs[] = { bluetooth_auth_agent_enable_incoming, bluetooth_auth_send_response,
+ bluetooth_gatt_service_free, + bluetooth_get_event, };
diff --git a/dlls/winebth.sys/unixlib.h b/dlls/winebth.sys/unixlib.h index 273023f5b16..42783ccf400 100644 --- a/dlls/winebth.sys/unixlib.h +++ b/dlls/winebth.sys/unixlib.h @@ -54,6 +54,11 @@ struct bluetooth_device_free_params unix_name_t device; };
+struct bluetooth_gatt_service_free_params +{ + unix_name_t service; +}; + struct bluetooth_device_disconnect_params { unix_name_t device; @@ -130,6 +135,8 @@ enum bluetoothapis_funcs unix_bluetooth_auth_agent_enable_incoming, unix_bluetooth_auth_send_response,
+ unix_bluetooth_gatt_service_free, + unix_bluetooth_get_event,
unix_funcs_count diff --git a/dlls/winebth.sys/winebluetooth.c b/dlls/winebth.sys/winebluetooth.c index 448fd3f4dda..add7020a0b3 100644 --- a/dlls/winebth.sys/winebluetooth.c +++ b/dlls/winebth.sys/winebluetooth.c @@ -188,6 +188,16 @@ NTSTATUS winebluetooth_device_start_pairing( winebluetooth_device_t device, IRP return UNIX_BLUETOOTH_CALL( bluetooth_device_start_pairing, &args ); }
+void winebluetooth_gatt_service_free( winebluetooth_gatt_service_t service ) +{ + struct bluetooth_gatt_service_free_params args = {0}; + + TRACE( "(%p)\n", (void *)service.handle ); + + args.service = service.handle; + UNIX_BLUETOOTH_CALL( bluetooth_gatt_service_free, &args ); +} + NTSTATUS winebluetooth_get_event( struct winebluetooth_event *result ) { struct bluetooth_get_event_params params = {0}; diff --git a/dlls/winebth.sys/winebth.c b/dlls/winebth.sys/winebth.c index 57fb934b505..73e2e9fdd1d 100644 --- a/dlls/winebth.sys/winebth.c +++ b/dlls/winebth.sys/winebth.c @@ -33,6 +33,7 @@ #include <bthsdpdef.h> #include <bluetoothapis.h> #include <bthdef.h> +#include <bthledef.h> #include <winioctl.h> #include <bthioctl.h> #include <ddk/wdm.h> @@ -97,7 +98,23 @@ struct bluetooth_remote_device CRITICAL_SECTION props_cs; winebluetooth_device_props_mask_t props_mask; /* Guarded by props_cs */ struct winebluetooth_device_properties props; /* Guarded by props_cs */ + unsigned int started : 1; /* Whether the device has been started. Guarded by props_cs */ unsigned int removed : 1; + + CRITICAL_SECTION le_cs; + unsigned int le : 1; /* Guarded by le_cs */ + UNICODE_STRING bthle_symlink_name; /* Guarded by le_cs */ + struct list gatt_services; /* Guarded by le_cs */ +}; + +struct bluetooth_gatt_service +{ + struct list entry; + + winebluetooth_gatt_service_t service; + GUID uuid; + unsigned int primary : 1; + UINT16 handle; };
enum bluetooth_pdo_ext_type @@ -720,7 +737,11 @@ static void bluetooth_radio_add_remote_device( struct winebluetooth_watcher_even ext->remote_device.props_mask = event.known_props_mask; ext->remote_device.props = event.props; ext->remote_device.removed = 0; + ext->remote_device.started = 0;
+ InitializeCriticalSection( &ext->remote_device.le_cs ); + ext->remote_device.le = 0; + list_init( &ext->remote_device.gatt_services );
if (!event.init_entry) { @@ -929,6 +950,84 @@ static void complete_irp( IRP *irp, NTSTATUS result ) IoCompleteRequest( irp, IO_NO_INCREMENT ); }
+static void bluetooth_device_create_le_symlink( struct bluetooth_remote_device *device ) +{ + EnterCriticalSection( &device->props_cs ); + /* The device hasn't been started by the PnP manager yet. Set le, and let remote_device_pdo_pnp enable the + * interface. */ + if (!device->started) + { + EnterCriticalSection( &device->le_cs ); + device->le = 1; + LeaveCriticalSection( &device->le_cs ); + LeaveCriticalSection( &device->props_cs ); + return; + } + LeaveCriticalSection( &device->props_cs ); + + EnterCriticalSection( &device->le_cs ); + if (device->le) + { + LeaveCriticalSection( &device->le_cs ); + return; + } + device->le = 1; + if (!IoRegisterDeviceInterface( device->device_obj, &GUID_BLUETOOTHLE_DEVICE_INTERFACE, NULL, + &device->bthle_symlink_name )) + IoSetDeviceInterfaceState( &device->bthle_symlink_name, TRUE ); + LeaveCriticalSection( &device->le_cs ); +} + +static void bluetooth_device_add_gatt_service( struct winebluetooth_watcher_event_gatt_service_added event ) +{ + struct bluetooth_radio *radio; + + EnterCriticalSection( &device_list_cs ); + LIST_FOR_EACH_ENTRY( radio, &device_list, struct bluetooth_radio, entry ) + { + struct bluetooth_remote_device *device; + + EnterCriticalSection( &radio->remote_devices_cs ); + LIST_FOR_EACH_ENTRY( device, &radio->remote_devices, struct bluetooth_remote_device, entry ) + { + if (winebluetooth_device_equal( event.device, device->device )) + { + struct bluetooth_gatt_service *service; + + TRACE( "Adding GATT service %s for remote device %p\n", debugstr_guid( &event.uuid ), + (void *)event.device.handle ); + + service = calloc( 1, sizeof( *service ) ); + if (!service) + { + LeaveCriticalSection( &radio->remote_devices_cs ); + LeaveCriticalSection( &device_list_cs ); + return; + } + + service->service = event.service; + service->uuid = event.uuid; + service->primary = !!event.is_primary; + service->handle = event.attr_handle; + bluetooth_device_create_le_symlink( device ); + + EnterCriticalSection( &device->le_cs ); + list_add_tail( &device->gatt_services, &service->entry ); + LeaveCriticalSection( &device->le_cs ); + LeaveCriticalSection( &radio->remote_devices_cs ); + LeaveCriticalSection( &device_list_cs ); + winebluetooth_device_free( event.device ); + return; + } + } + LeaveCriticalSection( &radio->remote_devices_cs ); + } + LeaveCriticalSection( &device_list_cs ); + + winebluetooth_device_free( event.device ); + winebluetooth_gatt_service_free( event.service ); +} + static DWORD CALLBACK bluetooth_event_loop_thread_proc( void *arg ) { NTSTATUS status; @@ -967,6 +1066,8 @@ static DWORD CALLBACK bluetooth_event_loop_thread_proc( void *arg ) case BLUETOOTH_WATCHER_EVENT_TYPE_PAIRING_FINISHED: complete_irp( event->event_data.pairing_finished.irp, event->event_data.pairing_finished.result ); + case BLUETOOTH_WATCHER_EVENT_TYPE_DEVICE_GATT_SERVICE_ADDED: + bluetooth_device_add_gatt_service( event->event_data.gatt_service_added ); break; default: FIXME( "Unknown bluetooth watcher event code: %#x\n", event->event_type ); @@ -1182,6 +1283,27 @@ static void remove_pending_irps( struct bluetooth_radio *radio ) } }
+static void remote_device_destroy( struct bluetooth_remote_device *ext ) +{ + struct bluetooth_gatt_service *svc, *next; + + if (ext->le && ext->bthle_symlink_name.Buffer) + { + IoSetDeviceInterfaceState( &ext->bthle_symlink_name, FALSE ); + RtlFreeUnicodeString( &ext->bthle_symlink_name ); + } + DeleteCriticalSection( &ext->props_cs ); + DeleteCriticalSection( &ext->le_cs ); + winebluetooth_device_free( ext->device ); + LIST_FOR_EACH_ENTRY_SAFE( svc, next, &ext->gatt_services, struct bluetooth_gatt_service, entry ) + { + winebluetooth_gatt_service_free( svc->service ); + list_remove( &svc->entry ); + free( svc ); + } + IoDeleteDevice( ext->device_obj ); +} + static NTSTATUS WINAPI remote_device_pdo_pnp( DEVICE_OBJECT *device_obj, struct bluetooth_remote_device *ext, IRP *irp ) { IO_STACK_LOCATION *stack = IoGetCurrentIrpStackLocation( irp ); @@ -1210,6 +1332,15 @@ static NTSTATUS WINAPI remote_device_pdo_pnp( DEVICE_OBJECT *device_obj, struct BLUETOOTH_ADDRESS addr;
EnterCriticalSection( &ext->props_cs ); + EnterCriticalSection( &ext->le_cs ); + if (ext->le) + { + if (!IoRegisterDeviceInterface( device_obj, &GUID_BLUETOOTHLE_DEVICE_INTERFACE, NULL, + &ext->bthle_symlink_name )) + IoSetDeviceInterfaceState( &ext->bthle_symlink_name, TRUE ); + } + LeaveCriticalSection( &ext->le_cs ); + ext->started = 1; addr = ext->props.address; LeaveCriticalSection( &ext->props_cs ); swprintf( addr_str, ARRAY_SIZE( addr_str ), L"%02x%02x%02x%02x%02x%02x", addr.rgBytes[0], addr.rgBytes[1], @@ -1222,9 +1353,7 @@ static NTSTATUS WINAPI remote_device_pdo_pnp( DEVICE_OBJECT *device_obj, struct case IRP_MN_REMOVE_DEVICE: { assert( ext->removed ); - DeleteCriticalSection( &ext->props_cs ); - winebluetooth_device_free( ext->device ); - IoDeleteDevice( ext->device_obj ); + remote_device_destroy( ext ); ret = STATUS_SUCCESS; break; } diff --git a/dlls/winebth.sys/winebth_priv.h b/dlls/winebth.sys/winebth_priv.h index 5addf6303ec..afacccdfb2c 100644 --- a/dlls/winebth.sys/winebth_priv.h +++ b/dlls/winebth.sys/winebth_priv.h @@ -187,6 +187,11 @@ struct winebluetooth_device_properties UINT32 class; };
+typedef struct +{ + UINT_PTR handle; +} winebluetooth_gatt_service_t; + NTSTATUS winebluetooth_radio_get_unique_name( winebluetooth_radio_t radio, char *name, SIZE_T *size ); void winebluetooth_radio_free( winebluetooth_radio_t radio ); @@ -215,6 +220,9 @@ NTSTATUS winebluetooth_device_disconnect( winebluetooth_device_t device ); NTSTATUS winebluetooth_auth_send_response( winebluetooth_device_t device, BLUETOOTH_AUTHENTICATION_METHOD method, UINT32 numeric_or_passkey, BOOL negative, BOOL *authenticated ); NTSTATUS winebluetooth_device_start_pairing( winebluetooth_device_t device, IRP *irp ); + +void winebluetooth_gatt_service_free( winebluetooth_gatt_service_t service ); + enum winebluetooth_watcher_event_type { BLUETOOTH_WATCHER_EVENT_TYPE_RADIO_ADDED, @@ -224,6 +232,7 @@ enum winebluetooth_watcher_event_type BLUETOOTH_WATCHER_EVENT_TYPE_DEVICE_REMOVED, BLUETOOTH_WATCHER_EVENT_TYPE_DEVICE_PROPERTIES_CHANGED, BLUETOOTH_WATCHER_EVENT_TYPE_PAIRING_FINISHED, + BLUETOOTH_WATCHER_EVENT_TYPE_DEVICE_GATT_SERVICE_ADDED, };
struct winebluetooth_watcher_event_radio_added @@ -271,6 +280,16 @@ struct winebluetooth_watcher_event_pairing_finished NTSTATUS result; };
+struct winebluetooth_watcher_event_gatt_service_added +{ + winebluetooth_device_t device; + winebluetooth_gatt_service_t service; + + UINT16 attr_handle; + BOOL is_primary; + GUID uuid; +}; + union winebluetooth_watcher_event_data { struct winebluetooth_watcher_event_radio_added radio_added; @@ -280,6 +299,7 @@ union winebluetooth_watcher_event_data struct winebluetooth_watcher_event_device_removed device_removed; struct winebluetooth_watcher_event_device_props_changed device_props_changed; struct winebluetooth_watcher_event_pairing_finished pairing_finished; + struct winebluetooth_watcher_event_gatt_service_added gatt_service_added; };
struct winebluetooth_watcher_event diff --git a/include/Makefile.in b/include/Makefile.in index 4446d4821d6..d9190ec2599 100644 --- a/include/Makefile.in +++ b/include/Makefile.in @@ -59,6 +59,7 @@ SOURCES = \ bitsmsg.h \ bluetoothapis.h \ bthdef.h \ + bthledef.h \ bthioctl.h \ bthsdpdef.h \ cderr.h \ diff --git a/include/bthledef.h b/include/bthledef.h new file mode 100644 index 00000000000..d6464e4c9b4 --- /dev/null +++ b/include/bthledef.h @@ -0,0 +1,22 @@ +/* + * Copyright 2025 Vibhav Pant + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ +#ifndef __BTHLEDEFS_H +#define __BTHLEDEFS_H +DEFINE_GUID( GUID_BLUETOOTHLE_DEVICE_INTERFACE, 0x781aee18, 0x7733, 0x4ce4, 0xad, 0xd0, 0x91, 0xf4, 0x1c, 0x67, 0xb5, + 0x92 ); +#endif
From: Vibhav Pant vibhavp@gmail.com
--- dlls/winebth.sys/winebth.c | 52 +++++++++++++++++++++++++++++++++++++- include/wine/winebth.h | 15 +++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-)
diff --git a/dlls/winebth.sys/winebth.c b/dlls/winebth.sys/winebth.c index 73e2e9fdd1d..7ba7761031a 100644 --- a/dlls/winebth.sys/winebth.c +++ b/dlls/winebth.sys/winebth.c @@ -157,10 +157,60 @@ static NTSTATUS WINAPI dispatch_auth( DEVICE_OBJECT *device, IRP *irp ) static NTSTATUS bluetooth_remote_device_dispatch( DEVICE_OBJECT *device, struct bluetooth_remote_device *ext, IRP *irp ) { IO_STACK_LOCATION *stack = IoGetCurrentIrpStackLocation( irp ); + ULONG outsize = stack->Parameters.DeviceIoControl.OutputBufferLength; ULONG code = stack->Parameters.DeviceIoControl.IoControlCode; NTSTATUS status = irp->IoStatus.Status;
- FIXME( "device=%p, ext=%p, irp=%p, code=%#lx: stub!\n", device, ext, irp, code ); + TRACE( "device=%p, ext=%p, irp=%p, code=%#lx\n", device, ext, irp, code ); + + switch (code) + { + case IOCTL_WINEBTH_LE_DEVICE_GET_GATT_SERVICES: + { + struct winebth_le_device_get_gatt_services_params *services = irp->AssociatedIrp.SystemBuffer; + struct bluetooth_gatt_service *svc; + SIZE_T rem; + + if (!services || outsize < sizeof( *services )) + { + status = STATUS_INVALID_USER_BUFFER; + break; + } + + rem = (outsize - sizeof( *services ))/sizeof( struct winebth_gatt_service ) + 1; + status = STATUS_SUCCESS; + irp->IoStatus.Information = 0; + services->count = 0; + + EnterCriticalSection( &ext->le_cs ); + LIST_FOR_EACH_ENTRY( svc, &ext->gatt_services, struct bluetooth_gatt_service, entry ) + { + services->count++; + if (rem > 0) + { + struct winebth_gatt_service *info; + + info = &services->services[services->count - 1]; + info->uuid = svc->uuid; + info->handle = svc->handle; + + irp->IoStatus.Information += sizeof( *info ); + rem--; + } + } + LeaveCriticalSection( &ext->le_cs ); + + irp->IoStatus.Information += sizeof( *services ); + if (services->count) + irp->IoStatus.Information -= sizeof( struct winebth_gatt_service ); + if (rem) + status = STATUS_INVALID_BUFFER_SIZE; + break; + } + default: + FIXME( "Unimplemented IOCTL code: %#lx\n", code ); + } + IoCompleteRequest( irp, IO_NO_INCREMENT ); return status; } diff --git a/include/wine/winebth.h b/include/wine/winebth.h index c785d08d88e..c21267a86ac 100644 --- a/include/wine/winebth.h +++ b/include/wine/winebth.h @@ -36,6 +36,9 @@ #define IOCTL_WINEBTH_RADIO_START_AUTH CTL_CODE(FILE_DEVICE_BLUETOOTH, 0xaa, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_WINEBTH_RADIO_REMOVE_DEVICE CTL_CODE(FILE_DEVICE_BLUETOOTH, 0xab, METHOD_BUFFERED, FILE_ANY_ACCESS)
+/* Get all primary GATT services for the LE device. */ +#define IOCTL_WINEBTH_LE_DEVICE_GET_GATT_SERVICES CTL_CODE(FILE_DEVICE_BLUETOOTH, 0xc0, METHOD_BUFFERED, FILE_ANY_ACCESS) + DEFINE_GUID( GUID_WINEBTH_AUTHENTICATION_REQUEST, 0xca67235f, 0xf621, 0x4c27, 0x85, 0x65, 0xa4, 0xd5, 0x5e, 0xa1, 0x26, 0xe8 );
@@ -75,6 +78,18 @@ struct winebth_radio_start_auth_params BTH_ADDR address; };
+struct winebth_gatt_service +{ + GUID uuid; + UINT16 handle; +}; + +struct winebth_le_device_get_gatt_services_params +{ + ULONG count; + struct winebth_gatt_service services[1]; +}; + #pragma pack(pop)
#endif /* __WINEBTH_H__ */
From: Vibhav Pant vibhavp@gmail.com
--- dlls/bluetoothapis/Makefile.in | 1 + dlls/bluetoothapis/bluetoothapis.spec | 2 +- dlls/bluetoothapis/gatt.c | 121 ++++++++++++++++++++++++++ include/Makefile.in | 1 + include/bluetoothleapis.h | 32 +++++++ include/bthledef.h | 15 ++++ 6 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 dlls/bluetoothapis/gatt.c create mode 100644 include/bluetoothleapis.h
diff --git a/dlls/bluetoothapis/Makefile.in b/dlls/bluetoothapis/Makefile.in index d4568065953..aaa85e26f84 100644 --- a/dlls/bluetoothapis/Makefile.in +++ b/dlls/bluetoothapis/Makefile.in @@ -6,5 +6,6 @@ EXTRADLLFLAGS = -Wb,--prefer-native
SOURCES = \ bluetoothapis.rc \ + gatt.c \ main.c \ sdp.c diff --git a/dlls/bluetoothapis/bluetoothapis.spec b/dlls/bluetoothapis/bluetoothapis.spec index e26932496dc..2cc00f46c9d 100644 --- a/dlls/bluetoothapis/bluetoothapis.spec +++ b/dlls/bluetoothapis/bluetoothapis.spec @@ -39,7 +39,7 @@ @ stub BluetoothGATTGetDescriptorValue @ stub BluetoothGATTGetDescriptors @ stub BluetoothGATTGetIncludedServices -@ stub BluetoothGATTGetServices +@ stdcall BluetoothGATTGetServices(ptr long ptr ptr long) @ stub BluetoothGATTRegisterEvent @ stub BluetoothGATTSetCharacteristicValue @ stub BluetoothGATTSetDescriptorValue diff --git a/dlls/bluetoothapis/gatt.c b/dlls/bluetoothapis/gatt.c new file mode 100644 index 00000000000..123ba873653 --- /dev/null +++ b/dlls/bluetoothapis/gatt.c @@ -0,0 +1,121 @@ +/* + * BLE General Attribute Profile (GATT) APIs + * + * Copyright 2025 Vibhav Pant + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + + +#include <stdarg.h> +#include <stdint.h> + +#include <windef.h> +#include <winbase.h> + +#include <bthsdpdef.h> +#include <bluetoothapis.h> +#include <bthdef.h> +#include <winioctl.h> +#include <wine/winebth.h> + +#include <initguid.h> +#include <bthledef.h> +#include <bluetoothleapis.h> + +#include "wine/debug.h" +#include "winerror.h" + +WINE_DEFAULT_DEBUG_CHANNEL( bluetoothapis ); + +static void uuid_to_le( const GUID *uuid, BTH_LE_UUID *le_uuid ) +{ + if (uuid->Data1 <= UINT16_MAX && uuid->Data2 == GUID_BTH_LE_ATT_BLUETOOTH_BASE_GUID.Data2 + && uuid->Data3 == GUID_BTH_LE_ATT_BLUETOOTH_BASE_GUID.Data3 + && !memcmp( uuid->Data4, GUID_BTH_LE_ATT_BLUETOOTH_BASE_GUID.Data4, sizeof( uuid->Data4 ) )) + { + le_uuid->IsShortUuid = TRUE; + le_uuid->Value.ShortUuid = uuid->Data1; + } + else + { + le_uuid->IsShortUuid = FALSE; + le_uuid->Value.LongUuid = *uuid; + } +} + +HRESULT WINAPI BluetoothGATTGetServices( HANDLE le_device, USHORT count, BTH_LE_GATT_SERVICE *buf, + USHORT *actual, ULONG flags ) +{ + struct winebth_le_device_get_gatt_services_params *services; + SIZE_T services_count = 1, i; + + FIXME( "(%p, %u, %p, %p, %#lx): semi-stub!\n", le_device, count, buf, actual, flags ); + + if (!actual) + return E_POINTER; + + if ((!buf && count) || (buf && !count) || !actual) + return HRESULT_FROM_WIN32( ERROR_INVALID_PARAMETER ); + + for (;;) + { + DWORD size, bytes; + + size = sizeof( *services ) + (services_count - 1) * sizeof( services->services[0] ); + services = calloc( 1, size ); + if (!services) + return HRESULT_FROM_WIN32( ERROR_NO_SYSTEM_RESOURCES ); + if (!DeviceIoControl( le_device, IOCTL_WINEBTH_LE_DEVICE_GET_GATT_SERVICES, NULL, 0, services, size, &bytes, NULL ) + && GetLastError() != ERROR_INVALID_USER_BUFFER) + { + free( services ); + return HRESULT_FROM_WIN32( GetLastError() ); + } + if (!services->count) + { + *actual = 0; + free( services ); + return S_OK; + } + if (services_count != services->count) + { + services_count = services->count; + free( services ); + continue; + } + break; + } + + *actual = services_count; + if (!buf) + { + free( services ); + return HRESULT_FROM_WIN32( ERROR_MORE_DATA ); + } + + for (i = 0; i < services_count && i < count; i++) + { + memset( &buf[i], 0, sizeof( *buf ) ); + uuid_to_le( &services->services[i].uuid, &buf[i].ServiceUuid ); + buf[i].AttributeHandle = services->services[i].handle; + } + + free( services ); + if (count < services_count) + return HRESULT_FROM_WIN32( ERROR_INVALID_USER_BUFFER ); + + return S_OK; +} diff --git a/include/Makefile.in b/include/Makefile.in index d9190ec2599..5b542b34f24 100644 --- a/include/Makefile.in +++ b/include/Makefile.in @@ -58,6 +58,7 @@ SOURCES = \ bits5_0.idl \ bitsmsg.h \ bluetoothapis.h \ + bluetoothleapis.h \ bthdef.h \ bthledef.h \ bthioctl.h \ diff --git a/include/bluetoothleapis.h b/include/bluetoothleapis.h new file mode 100644 index 00000000000..64cdd366f7d --- /dev/null +++ b/include/bluetoothleapis.h @@ -0,0 +1,32 @@ +/* + * Copyright 2025 Vibhav Pant + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ +#ifndef __BLUETOOTHLEAPIS_H +#define __BLUETOOTHLEAPIS_H + +#include <bthledef.h> + +#ifdef __cplusplus +extern "C" { +#endif + +HRESULT WINAPI BluetoothGATTGetServices( HANDLE, USHORT, BTH_LE_GATT_SERVICE *, USHORT *, ULONG ); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/include/bthledef.h b/include/bthledef.h index d6464e4c9b4..a043c99621f 100644 --- a/include/bthledef.h +++ b/include/bthledef.h @@ -17,6 +17,21 @@ */ #ifndef __BTHLEDEFS_H #define __BTHLEDEFS_H + +typedef struct _BTH_LE_UUID { + BOOLEAN IsShortUuid; + union { + USHORT ShortUuid; + GUID LongUuid; + } Value; +} BTH_LE_UUID, *PBTH_LE_UUID; + +typedef struct _BTH_LE_GATT_SERVICE { + BTH_LE_UUID ServiceUuid; + USHORT AttributeHandle; +} BTH_LE_GATT_SERVICE, *PBTH_LE_GATT_SERVICE; + DEFINE_GUID( GUID_BLUETOOTHLE_DEVICE_INTERFACE, 0x781aee18, 0x7733, 0x4ce4, 0xad, 0xd0, 0x91, 0xf4, 0x1c, 0x67, 0xb5, 0x92 ); +DEFINE_GUID( GUID_BTH_LE_ATT_BLUETOOTH_BASE_GUID, 0, 0, 0x1000, 0x80, 0x00, 0x00, 0x80, 0x5f, 0x9b, 0x34, 0xfb ); #endif
From: Vibhav Pant vibhavp@gmail.com
--- dlls/bluetoothapis/tests/Makefile.in | 3 +- dlls/bluetoothapis/tests/gatt.c | 172 +++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 dlls/bluetoothapis/tests/gatt.c
diff --git a/dlls/bluetoothapis/tests/Makefile.in b/dlls/bluetoothapis/tests/Makefile.in index dad13359591..f0509a19357 100644 --- a/dlls/bluetoothapis/tests/Makefile.in +++ b/dlls/bluetoothapis/tests/Makefile.in @@ -1,8 +1,9 @@ TESTDLL = bluetoothapis.dll -IMPORTS = bluetoothapis user32 +IMPORTS = bluetoothapis setupapi user32
SOURCES = \ device.c \ + gatt.c \ radio.c \ resource.rc \ sdp.c diff --git a/dlls/bluetoothapis/tests/gatt.c b/dlls/bluetoothapis/tests/gatt.c new file mode 100644 index 00000000000..8c57c79b172 --- /dev/null +++ b/dlls/bluetoothapis/tests/gatt.c @@ -0,0 +1,172 @@ +/* + * Tests for Bluetooth GATT methods + * + * Copyright 2025 Vibhav Pant + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include <stdarg.h> +#include <stdint.h> + +#include <windef.h> +#include <winbase.h> +#include <winuser.h> +#include <winreg.h> +#include <setupapi.h> + +#include <setupapi.h> + +#include <initguid.h> +#include <bthledef.h> +#include <bluetoothleapis.h> +#include <devpkey.h> +#include <ddk/bthguid.h> + +#include <wine/test.h> + +static void test_for_all_le_devices( int line, void (*func)( HANDLE, void * ), void *data ) +{ + char buffer[sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W) + MAX_PATH * sizeof( WCHAR )]; + SP_DEVICE_INTERFACE_DETAIL_DATA_W *iface_detail = (SP_DEVICE_INTERFACE_DETAIL_DATA_W *)buffer; + SP_DEVICE_INTERFACE_DATA iface_data; + HDEVINFO devinfo; + DWORD idx = 0, ret, n = 0; + BOOL found = FALSE; + + devinfo = SetupDiGetClassDevsW( &GUID_BLUETOOTHLE_DEVICE_INTERFACE, NULL, NULL, + DIGCF_PRESENT | DIGCF_DEVICEINTERFACE ); + ret = GetLastError(); + ok( devinfo != INVALID_HANDLE_VALUE, "SetupDiGetClassDevsW failed: %lu\n", ret ); + if (devinfo == INVALID_HANDLE_VALUE) + { + skip( "No LE devices found.\n" ); + return; + } + + iface_detail->cbSize = sizeof( *iface_detail ); + iface_data.cbSize = sizeof( iface_data ); + while (SetupDiEnumDeviceInterfaces( devinfo, NULL, &GUID_BLUETOOTHLE_DEVICE_INTERFACE, idx++, + &iface_data )) + { + SP_DEVINFO_DATA devinfo_data; + HANDLE device; + DEVPROPTYPE type; + WCHAR addr_str[13]; + BOOL success; + + devinfo_data.cbSize = sizeof( devinfo_data ); + if (!SetupDiGetDeviceInterfaceDetailW( devinfo, &iface_data, iface_detail, sizeof( buffer ), NULL, + &devinfo_data )) + continue; + success = SetupDiGetDevicePropertyW( devinfo, &devinfo_data, &DEVPKEY_Bluetooth_DeviceAddress, &type, (BYTE *)addr_str, + sizeof( addr_str ), NULL, 0 ); + ok( success, "SetupDiGetDevicePropertyW failed: %lu\n", GetLastError() ); + ok( type == DEVPROP_TYPE_STRING, "%lu != %d\n", type, DEVPROP_TYPE_STRING ); + device = CreateFileW( iface_detail->DevicePath, GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL ); + if (device != INVALID_HANDLE_VALUE) + { + winetest_push_context( "device %lu", n++ ); + trace( "Address: %s\n", debugstr_w( addr_str ) ); + func( device, data ); + winetest_pop_context(); + CloseHandle( device ); + found = TRUE; + } + } + + SetupDiDestroyDeviceInfoList( devinfo ); + if (!found) + { + skip_( __FILE__, line )( "No LE devices found.\n" ); + return; + } +} + +static const char *debugstr_BTH_LE_UUID( const BTH_LE_UUID *uuid ) +{ + if (!uuid) + return wine_dbg_sprintf( "(null)" ); + if (uuid->IsShortUuid) + return wine_dbg_sprintf("{ IsShortUuid=1 {%#x} }", uuid->Value.ShortUuid ); + return wine_dbg_sprintf( "{ IsShortUuid=0 %s }", debugstr_guid( &uuid->Value.LongUuid ) ); +} + +static const char *debugstr_BTH_LE_GATT_SERVICE( const BTH_LE_GATT_SERVICE *svc ) +{ + if (!svc) + return wine_dbg_sprintf( "(null)" ); + return wine_dbg_sprintf( "{ %s %#x }", debugstr_BTH_LE_UUID( &svc->ServiceUuid ), svc->AttributeHandle ); +} + +static void test_device_BluetoothGATTGetServices( HANDLE device, void *param ) +{ + HRESULT ret; + USHORT actual = 0, i; + BTH_LE_GATT_SERVICE *buf; + + ret = BluetoothGATTGetServices( device, 0, NULL, NULL, 0 ); + ok( ret == E_POINTER, "%#lx != %#lx\n", ret, E_POINTER ); + + buf = calloc( 1, sizeof( *buf ) ); + ret = BluetoothGATTGetServices( device, 1, buf, NULL, 0 ); + ok( ret == E_POINTER, "%#lx != %#lx\n", ret, E_POINTER ); + ret = BluetoothGATTGetServices( NULL, 1, buf, &actual, 0 ); + ok( HRESULT_CODE( ret ) == ERROR_INVALID_HANDLE, "%lu != %d\n", HRESULT_CODE( ret ), ERROR_INVALID_HANDLE ); + ret = BluetoothGATTGetServices( INVALID_HANDLE_VALUE, 1, buf, &actual, 0 ); + ok( HRESULT_CODE( ret ) == ERROR_INVALID_HANDLE, "%lu != %d\n", HRESULT_CODE( ret ), ERROR_INVALID_HANDLE ); + ret = BluetoothGATTGetServices( device, 0, buf, &actual, 0 ); + ok( HRESULT_CODE( ret ) == ERROR_INVALID_PARAMETER, "%lu != %d\n", HRESULT_CODE( ret ), ERROR_INVALID_PARAMETER ); + + ret = BluetoothGATTGetServices( device, 0, NULL, &actual, 0 ); + ok( HRESULT_CODE( ret ) == ERROR_NOT_FOUND || HRESULT_CODE( ret ) == ERROR_MORE_DATA, + "BluetoothGATTGetServices failed: %#lx\n", ret ); + if (!actual) + { + free( buf ); + return; + } + + buf = calloc( actual, sizeof( *buf ) ); + ret = BluetoothGATTGetServices( device, actual, buf, &actual, 0 ); + ok( ret == S_OK, "BluetoothGATTGetServices failed: %#lx\n", ret ); + if (FAILED( ret )) + { + skip( "BluetoothGATTGetServices failed.\n" ); + free( buf ); + return; + } + + for (i = 0; i < actual; i++) + trace( "service %u: %s\n", i, debugstr_BTH_LE_GATT_SERVICE( &buf[i] ) ); + + free( buf ); +} + +static void test_BluetoothGATTGetServices( void ) +{ + HRESULT ret; + + ret = BluetoothGATTGetServices( NULL, 0, NULL, NULL, 0 ); + ok( ret == E_POINTER, "%lu != %lu\n", ret, E_POINTER ); + + test_for_all_le_devices( __LINE__, test_device_BluetoothGATTGetServices, NULL ); +} + +START_TEST( gatt ) +{ + test_BluetoothGATTGetServices(); +}