Signed-off-by: Zebediah Figura z.figura12@gmail.com --- v2: Actually disable wineusb.sys when libusb is missing.
configure.ac | 14 +++++++ dlls/wineusb.sys/Makefile.in | 8 ++++ dlls/wineusb.sys/wineusb.c | 61 +++++++++++++++++++++++++++++++ dlls/wineusb.sys/wineusb.sys.spec | 1 + loader/wine.inf.in | 2 + 5 files changed, 86 insertions(+) create mode 100644 dlls/wineusb.sys/Makefile.in create mode 100644 dlls/wineusb.sys/wineusb.c create mode 100644 dlls/wineusb.sys/wineusb.sys.spec
diff --git a/configure.ac b/configure.ac index 862745ef62..b8d70b6fe3 100644 --- a/configure.ac +++ b/configure.ac @@ -84,6 +84,7 @@ AC_ARG_WITH(sdl, AS_HELP_STRING([--without-sdl],[do not use SDL])) AC_ARG_WITH(tiff, AS_HELP_STRING([--without-tiff],[do not use TIFF])) AC_ARG_WITH(udev, AS_HELP_STRING([--without-udev],[do not use udev (plug and play support)])) AC_ARG_WITH(unwind, AS_HELP_STRING([--without-unwind],[do not use the libunwind library (exception handling)])) +AC_ARG_WITH(usb, AS_HELP_STRING([--without-usb],[do not use the libusb library])) AC_ARG_WITH(v4l2, AS_HELP_STRING([--without-v4l2],[do not use v4l2 (video capture)])) AC_ARG_WITH(vkd3d, AS_HELP_STRING([--without-vkd3d],[do not use vkd3d (Direct3D 12 support)])) AC_ARG_WITH(vulkan, AS_HELP_STRING([--without-vulkan],[do not use Vulkan])) @@ -1474,6 +1475,18 @@ fi WINE_NOTICE_WITH(sane,[test "x$ac_cv_lib_soname_sane" = "x"], [libsane ${notice_platform}development files not found, scanners won't be supported.])
+dnl **** Check for libusb **** +if test "x$with_usb" != "xno" +then + WINE_PACKAGE_FLAGS(USB,[libusb-1.0],[-lusb-1.0],,, + [AC_CHECK_HEADER([libusb.h], + [AC_CHECK_LIB(usb-1.0,libusb_init,[:],[USB_LIBS=""],[$USB_LIBS])], + [USB_LIBS=""])]) +fi +WINE_NOTICE_WITH(usb,[test "$ac_cv_lib_usb_1_0_libusb_init" != "yes"], + [libusb-1.0 ${notice_platform}development files not found, USB devices won't be supported.], + [enable_wineusb_sys]) + dnl **** Check for libv4l2 **** if test "x$with_v4l2" != "xno" then @@ -3781,6 +3794,7 @@ WINE_CONFIG_MAKEFILE(dlls/wineps.drv) WINE_CONFIG_MAKEFILE(dlls/wineps16.drv16,enable_win16) WINE_CONFIG_MAKEFILE(dlls/winepulse.drv) WINE_CONFIG_MAKEFILE(dlls/wineqtdecoder) +WINE_CONFIG_MAKEFILE(dlls/wineusb.sys) WINE_CONFIG_MAKEFILE(dlls/winevulkan) WINE_CONFIG_MAKEFILE(dlls/winex11.drv) WINE_CONFIG_MAKEFILE(dlls/wing.dll16,enable_win16) diff --git a/dlls/wineusb.sys/Makefile.in b/dlls/wineusb.sys/Makefile.in new file mode 100644 index 0000000000..bac79f668e --- /dev/null +++ b/dlls/wineusb.sys/Makefile.in @@ -0,0 +1,8 @@ +MODULE = wineusb.sys +IMPORTS = ntoskrnl +EXTRALIBS = $(USB_LIBS) +EXTRAINCL = $(USB_CFLAGS) +EXTRADLLFLAGS = -Wl,--subsystem,native + +C_SRCS = \ + wineusb.c diff --git a/dlls/wineusb.sys/wineusb.c b/dlls/wineusb.sys/wineusb.c new file mode 100644 index 0000000000..b26f1036d8 --- /dev/null +++ b/dlls/wineusb.sys/wineusb.c @@ -0,0 +1,61 @@ +/* + * USB root device enumerator using libusb + * + * Copyright 2020 Zebediah Figura + * + * 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 <assert.h> +#include <stdarg.h> +#include <stdlib.h> +#include <libusb.h> + +#include "ntstatus.h" +#define WIN32_NO_STATUS +#include "windef.h" +#include "winioctl.h" +#include "winternl.h" +#include "ddk/wdm.h" +#include "ddk/usb.h" +#include "ddk/usbioctl.h" +#include "wine/asm.h" +#include "wine/debug.h" +#include "wine/list.h" +#include "wine/unicode.h" + +WINE_DEFAULT_DEBUG_CHANNEL(wineusb); + +static void WINAPI driver_unload(DRIVER_OBJECT *driver) +{ + libusb_exit(NULL); +} + +NTSTATUS WINAPI DriverEntry(DRIVER_OBJECT *driver, UNICODE_STRING *path) +{ + int err; + + TRACE("driver %p, path %s.\n", driver, debugstr_w(path->Buffer)); + + if ((err = libusb_init(NULL))) + { + ERR("Failed to initialize libusb: %s\n", libusb_strerror(err)); + return STATUS_UNSUCCESSFUL; + } + + driver->DriverUnload = driver_unload; + + return STATUS_SUCCESS; +} diff --git a/dlls/wineusb.sys/wineusb.sys.spec b/dlls/wineusb.sys/wineusb.sys.spec new file mode 100644 index 0000000000..76421d7e35 --- /dev/null +++ b/dlls/wineusb.sys/wineusb.sys.spec @@ -0,0 +1 @@ +# nothing to export diff --git a/loader/wine.inf.in b/loader/wine.inf.in index d321c4c826..e1546713db 100644 --- a/loader/wine.inf.in +++ b/loader/wine.inf.in @@ -2614,6 +2614,7 @@ HKLM,%CurrentVersion%\Telephony\Country List\998,"SameAreaRule",,"G" 12,,tdi.sys,- 12,,winebus.sys,- 12,,winehid.sys,- +12,,wineusb.sys,- ; skip .NET fake dlls in Wine Mono package 11,,aspnet_regiis.exe,- 11,,ngen.exe,- @@ -2665,6 +2666,7 @@ HKLM,%CurrentVersion%\Telephony\Country List\998,"SameAreaRule",,"G" 12,,tdi.sys 12,,winebus.sys 12,,winehid.sys +12,,wineusb.sys ; skip .NET fake dlls in Wine Mono package 11,,aspnet_regiis.exe,- 11,,ngen.exe,-
Signed-off-by: Zebediah Figura z.figura12@gmail.com --- dlls/wineusb.sys/wineusb.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+)
diff --git a/dlls/wineusb.sys/wineusb.c b/dlls/wineusb.sys/wineusb.c index b26f1036d8..6af8a9aaa5 100644 --- a/dlls/wineusb.sys/wineusb.c +++ b/dlls/wineusb.sys/wineusb.c @@ -38,6 +38,27 @@
WINE_DEFAULT_DEBUG_CHANNEL(wineusb);
+static DEVICE_OBJECT *bus_fdo, *bus_pdo; + +static NTSTATUS WINAPI driver_add_device(DRIVER_OBJECT *driver, DEVICE_OBJECT *pdo) +{ + NTSTATUS ret; + + TRACE("driver %p, pdo %p.\n", driver, pdo); + + if ((ret = IoCreateDevice(driver, 0, NULL, FILE_DEVICE_BUS_EXTENDER, 0, FALSE, &bus_fdo))) + { + ERR("Failed to create FDO, status %#x.\n", ret); + return ret; + } + + IoAttachDeviceToDeviceStack(bus_fdo, pdo); + bus_pdo = pdo; + bus_fdo->Flags &= ~DO_DEVICE_INITIALIZING; + + return STATUS_SUCCESS; +} + static void WINAPI driver_unload(DRIVER_OBJECT *driver) { libusb_exit(NULL); @@ -55,6 +76,7 @@ NTSTATUS WINAPI DriverEntry(DRIVER_OBJECT *driver, UNICODE_STRING *path) return STATUS_UNSUCCESSFUL; }
+ driver->DriverExtension->AddDevice = driver_add_device; driver->DriverUnload = driver_unload;
return STATUS_SUCCESS;
Signed-off-by: Zebediah Figura z.figura12@gmail.com --- dlls/wineusb.sys/wineusb.c | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+)
diff --git a/dlls/wineusb.sys/wineusb.c b/dlls/wineusb.sys/wineusb.c index 6af8a9aaa5..14795f4ec4 100644 --- a/dlls/wineusb.sys/wineusb.c +++ b/dlls/wineusb.sys/wineusb.c @@ -40,6 +40,41 @@ WINE_DEFAULT_DEBUG_CHANNEL(wineusb);
static DEVICE_OBJECT *bus_fdo, *bus_pdo;
+static NTSTATUS fdo_pnp(IRP *irp) +{ + IO_STACK_LOCATION *stack = IoGetCurrentIrpStackLocation(irp); + NTSTATUS ret; + + TRACE("irp %p, minor function %#x.\n", irp, stack->MinorFunction); + + switch (stack->MinorFunction) + { + case IRP_MN_START_DEVICE: + case IRP_MN_SURPRISE_REMOVAL: + irp->IoStatus.Status = STATUS_SUCCESS; + break; + + case IRP_MN_REMOVE_DEVICE: + irp->IoStatus.Status = STATUS_SUCCESS; + IoSkipCurrentIrpStackLocation(irp); + ret = IoCallDriver(bus_pdo, irp); + IoDetachDevice(bus_pdo); + IoDeleteDevice(bus_fdo); + return ret; + + default: + FIXME("Unhandled minor function %#x.\n", stack->MinorFunction); + } + + IoSkipCurrentIrpStackLocation(irp); + return IoCallDriver(bus_pdo, irp); +} + +static NTSTATUS WINAPI driver_pnp(DEVICE_OBJECT *device, IRP *irp) +{ + return fdo_pnp(irp); +} + static NTSTATUS WINAPI driver_add_device(DRIVER_OBJECT *driver, DEVICE_OBJECT *pdo) { NTSTATUS ret; @@ -78,6 +113,7 @@ NTSTATUS WINAPI DriverEntry(DRIVER_OBJECT *driver, UNICODE_STRING *path)
driver->DriverExtension->AddDevice = driver_add_device; driver->DriverUnload = driver_unload; + driver->MajorFunction[IRP_MJ_PNP] = driver_pnp;
return STATUS_SUCCESS; }
Signed-off-by: Zebediah Figura z.figura12@gmail.com --- dlls/wineusb.sys/wineusb.c | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+)
diff --git a/dlls/wineusb.sys/wineusb.c b/dlls/wineusb.sys/wineusb.c index 14795f4ec4..060fb3d0d2 100644 --- a/dlls/wineusb.sys/wineusb.c +++ b/dlls/wineusb.sys/wineusb.c @@ -40,6 +40,25 @@ WINE_DEFAULT_DEBUG_CHANNEL(wineusb);
static DEVICE_OBJECT *bus_fdo, *bus_pdo;
+static BOOL thread_shutdown; +static HANDLE event_thread; + +static DWORD CALLBACK event_thread_proc(void *arg) +{ + int ret; + + TRACE("Starting event thread.\n"); + + while (!thread_shutdown) + { + if ((ret = libusb_handle_events(NULL))) + ERR("Error handling events: %s\n", libusb_strerror(ret)); + } + + TRACE("Shutting down event thread.\n"); + return 0; +} + static NTSTATUS fdo_pnp(IRP *irp) { IO_STACK_LOCATION *stack = IoGetCurrentIrpStackLocation(irp); @@ -55,6 +74,11 @@ static NTSTATUS fdo_pnp(IRP *irp) break;
case IRP_MN_REMOVE_DEVICE: + thread_shutdown = TRUE; + libusb_interrupt_event_handler(NULL); + WaitForSingleObject(event_thread, INFINITE); + CloseHandle(event_thread); + irp->IoStatus.Status = STATUS_SUCCESS; IoSkipCurrentIrpStackLocation(irp); ret = IoCallDriver(bus_pdo, irp); @@ -111,6 +135,8 @@ NTSTATUS WINAPI DriverEntry(DRIVER_OBJECT *driver, UNICODE_STRING *path) return STATUS_UNSUCCESSFUL; }
+ event_thread = CreateThread(NULL, 0, event_thread_proc, NULL, 0, NULL); + driver->DriverExtension->AddDevice = driver_add_device; driver->DriverUnload = driver_unload; driver->MajorFunction[IRP_MJ_PNP] = driver_pnp;
Signed-off-by: Zebediah Figura z.figura12@gmail.com --- dlls/wineusb.sys/wineusb.c | 160 ++++++++++++++++++++++++++++++++++++- include/ddk/usbioctl.h | 2 + 2 files changed, 161 insertions(+), 1 deletion(-)
diff --git a/dlls/wineusb.sys/wineusb.c b/dlls/wineusb.sys/wineusb.c index 060fb3d0d2..22804d8657 100644 --- a/dlls/wineusb.sys/wineusb.c +++ b/dlls/wineusb.sys/wineusb.c @@ -38,11 +38,115 @@
WINE_DEFAULT_DEBUG_CHANNEL(wineusb);
+#define DECLARE_CRITICAL_SECTION(cs) \ + static CRITICAL_SECTION cs; \ + static CRITICAL_SECTION_DEBUG cs##_debug = \ + { 0, 0, &cs, { &cs##_debug.ProcessLocksList, &cs##_debug.ProcessLocksList }, \ + 0, 0, { (DWORD_PTR)(__FILE__ ": " # cs) }}; \ + static CRITICAL_SECTION cs = { &cs##_debug, -1, 0, 0, 0, 0 }; + +DECLARE_CRITICAL_SECTION(wineusb_cs); + +static struct list device_list = LIST_INIT(device_list); + +struct usb_device +{ + struct list entry; + + DEVICE_OBJECT *device_obj; + + libusb_device *libusb_device; + libusb_device_handle *handle; +}; + +static DRIVER_OBJECT *driver_obj; static DEVICE_OBJECT *bus_fdo, *bus_pdo;
+static libusb_hotplug_callback_handle hotplug_cb_handle; + +static void add_usb_device(libusb_device *libusb_device) +{ + static const WCHAR formatW[] = {'\','D','e','v','i','c','e','\','U','S','B','P','D','O','-','%','u',0}; + struct libusb_device_descriptor device_desc; + static unsigned int name_index; + libusb_device_handle *handle; + struct usb_device *device; + DEVICE_OBJECT *device_obj; + UNICODE_STRING string; + NTSTATUS status; + WCHAR name[20]; + int ret; + + libusb_get_device_descriptor(libusb_device, &device_desc); + + TRACE("Adding new device %p, vendor %04x, product %04x.\n", libusb_device, + device_desc.idVendor, device_desc.idProduct); + + if ((ret = libusb_open(libusb_device, &handle))) + { + WARN("Failed to open device: %s\n", libusb_strerror(ret)); + return; + } + + sprintfW(name, formatW, name_index++); + RtlInitUnicodeString(&string, name); + if ((status = IoCreateDevice(driver_obj, sizeof(*device), &string, + FILE_DEVICE_USB, 0, FALSE, &device_obj))) + { + ERR("Failed to create device, status %#x.\n", status); + LeaveCriticalSection(&wineusb_cs); + libusb_close(handle); + return; + } + + device = device_obj->DeviceExtension; + device->device_obj = device_obj; + device->libusb_device = libusb_ref_device(libusb_device); + device->handle = handle; + + EnterCriticalSection(&wineusb_cs); + list_add_tail(&device_list, &device->entry); + LeaveCriticalSection(&wineusb_cs); + + IoInvalidateDeviceRelations(bus_pdo, BusRelations); +} + +static void remove_usb_device(libusb_device *libusb_device) +{ + struct usb_device *device; + + TRACE("Removing device %p.\n", libusb_device); + + EnterCriticalSection(&wineusb_cs); + LIST_FOR_EACH_ENTRY(device, &device_list, struct usb_device, entry) + { + if (device->libusb_device == libusb_device) + { + libusb_unref_device(device->libusb_device); + libusb_close(device->handle); + list_remove(&device->entry); + IoInvalidateDeviceRelations(bus_pdo, BusRelations); + IoDeleteDevice(device->device_obj); + break; + } + } + LeaveCriticalSection(&wineusb_cs); +} + static BOOL thread_shutdown; static HANDLE event_thread;
+static int LIBUSB_CALL hotplug_cb(libusb_context *context, libusb_device *device, + libusb_hotplug_event event, void *user_data) +{ + if (event == LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED) + add_usb_device(device); + else + remove_usb_device(device); + + return 0; +} + static DWORD CALLBACK event_thread_proc(void *arg) { int ret; @@ -69,22 +173,49 @@ static NTSTATUS fdo_pnp(IRP *irp) switch (stack->MinorFunction) { case IRP_MN_START_DEVICE: + if ((ret = libusb_hotplug_register_callback(NULL, + LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT, + LIBUSB_HOTPLUG_ENUMERATE, LIBUSB_HOTPLUG_MATCH_ANY, LIBUSB_HOTPLUG_MATCH_ANY, + LIBUSB_HOTPLUG_MATCH_ANY, hotplug_cb, NULL, &hotplug_cb_handle))) + { + ERR("Failed to register callback: %s\n", libusb_strerror(ret)); + irp->IoStatus.Status = STATUS_UNSUCCESSFUL; + break; + } + irp->IoStatus.Status = STATUS_SUCCESS; + break; + case IRP_MN_SURPRISE_REMOVAL: irp->IoStatus.Status = STATUS_SUCCESS; break;
case IRP_MN_REMOVE_DEVICE: + { + struct usb_device *device, *cursor; + + libusb_hotplug_deregister_callback(NULL, hotplug_cb_handle); thread_shutdown = TRUE; libusb_interrupt_event_handler(NULL); WaitForSingleObject(event_thread, INFINITE); CloseHandle(event_thread);
+ EnterCriticalSection(&wineusb_cs); + LIST_FOR_EACH_ENTRY_SAFE(device, cursor, &device_list, struct usb_device, entry) + { + libusb_unref_device(device->libusb_device); + libusb_close(device->handle); + list_remove(&device->entry); + IoDeleteDevice(device->device_obj); + } + LeaveCriticalSection(&wineusb_cs); + irp->IoStatus.Status = STATUS_SUCCESS; IoSkipCurrentIrpStackLocation(irp); ret = IoCallDriver(bus_pdo, irp); IoDetachDevice(bus_pdo); IoDeleteDevice(bus_fdo); return ret; + }
default: FIXME("Unhandled minor function %#x.\n", stack->MinorFunction); @@ -94,9 +225,34 @@ static NTSTATUS fdo_pnp(IRP *irp) return IoCallDriver(bus_pdo, irp); }
+static NTSTATUS pdo_pnp(DEVICE_OBJECT *device_obj, IRP *irp) +{ + IO_STACK_LOCATION *stack = IoGetCurrentIrpStackLocation(irp); + NTSTATUS ret = irp->IoStatus.Status; + + TRACE("device_obj %p, irp %p, minor function %#x.\n", device_obj, irp, stack->MinorFunction); + + switch (stack->MinorFunction) + { + case IRP_MN_START_DEVICE: + case IRP_MN_QUERY_CAPABILITIES: + 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 driver_pnp(DEVICE_OBJECT *device, IRP *irp) { - return fdo_pnp(irp); + if (device == bus_fdo) + return fdo_pnp(irp); + return pdo_pnp(device, irp); }
static NTSTATUS WINAPI driver_add_device(DRIVER_OBJECT *driver, DEVICE_OBJECT *pdo) @@ -129,6 +285,8 @@ NTSTATUS WINAPI DriverEntry(DRIVER_OBJECT *driver, UNICODE_STRING *path)
TRACE("driver %p, path %s.\n", driver, debugstr_w(path->Buffer));
+ driver_obj = driver; + if ((err = libusb_init(NULL))) { ERR("Failed to initialize libusb: %s\n", libusb_strerror(err)); diff --git a/include/ddk/usbioctl.h b/include/ddk/usbioctl.h index 003e2b6786..0438dce8bf 100644 --- a/include/ddk/usbioctl.h +++ b/include/ddk/usbioctl.h @@ -19,6 +19,8 @@ #ifndef __DDK_USBIOCTL_H__ #define __DDK_USBIOCTL_H__
+#include "ddk/usbiodef.h" + #define IOCTL_INTERNAL_USB_SUBMIT_URB \ CTL_CODE(FILE_DEVICE_USB, USB_SUBMIT_URB, METHOD_NEITHER, FILE_ANY_ACCESS)
Signed-off-by: Zebediah Figura z.figura12@gmail.com --- dlls/wineusb.sys/wineusb.c | 53 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+)
diff --git a/dlls/wineusb.sys/wineusb.c b/dlls/wineusb.sys/wineusb.c index 22804d8657..b60d496e9e 100644 --- a/dlls/wineusb.sys/wineusb.c +++ b/dlls/wineusb.sys/wineusb.c @@ -38,6 +38,23 @@
WINE_DEFAULT_DEBUG_CHANNEL(wineusb);
+#if defined(__i386__) && !defined(_WIN32) + +extern void * WINAPI wrap_fastcall_func1( void *func, const void *a ); +__ASM_STDCALL_FUNC( wrap_fastcall_func1, 8, + "popl %ecx\n\t" + "popl %eax\n\t" + "xchgl (%esp),%ecx\n\t" + "jmp *%eax" ); + +#define call_fastcall_func1(func,a) wrap_fastcall_func1(func,a) + +#else + +#define call_fastcall_func1(func,a) func(a) + +#endif + #define DECLARE_CRITICAL_SECTION(cs) \ static CRITICAL_SECTION cs; \ static CRITICAL_SECTION_DEBUG cs##_debug = \ @@ -172,6 +189,42 @@ static NTSTATUS fdo_pnp(IRP *irp)
switch (stack->MinorFunction) { + case IRP_MN_QUERY_DEVICE_RELATIONS: + { + struct usb_device *device; + DEVICE_RELATIONS *devices; + unsigned int i = 0; + + if (stack->Parameters.QueryDeviceRelations.Type != BusRelations) + { + FIXME("Unhandled device relations type %#x.\n", stack->Parameters.QueryDeviceRelations.Type); + break; + } + + EnterCriticalSection(&wineusb_cs); + + if (!(devices = ExAllocatePool(PagedPool, + offsetof(DEVICE_RELATIONS, Objects[list_count(&device_list)])))) + { + LeaveCriticalSection(&wineusb_cs); + irp->IoStatus.Status = STATUS_NO_MEMORY; + break; + } + + LIST_FOR_EACH_ENTRY(device, &device_list, struct usb_device, entry) + { + devices->Objects[i++] = device->device_obj; + call_fastcall_func1(ObfReferenceObject, device->device_obj); + } + + LeaveCriticalSection(&wineusb_cs); + + devices->Count = i; + irp->IoStatus.Information = (ULONG_PTR)devices; + irp->IoStatus.Status = STATUS_SUCCESS; + break; + } + case IRP_MN_START_DEVICE: if ((ret = libusb_hotplug_register_callback(NULL, LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT,