diff --git a/.github/workflows/create-test-plan.py b/.github/workflows/create-test-plan.py index c0e847c05f..15d3860b7f 100755 --- a/.github/workflows/create-test-plan.py +++ b/.github/workflows/create-test-plan.py @@ -621,7 +621,7 @@ def spec_to_job(spec: JobSpec, key: str, trackmem_symbol_names: bool, ctest_args "-ffile-prefix-map=${PWD}=/SDL", )) job.ldflags.extend(( - "--source-map-base", "/", + "--source-map-base", "/", "-s", "ASYNCIFY", )) pretest_cmd.extend(( "# Start local HTTP server", diff --git a/CMakeLists.txt b/CMakeLists.txt index 851e11add9..1c7eb0809e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1772,6 +1772,10 @@ elseif(EMSCRIPTEN) ) endif() + if(SDL_HIDAPI) + CheckHIDAPI() + endif() + if(SDL_JOYSTICK) set(SDL_JOYSTICK_EMSCRIPTEN 1) sdl_glob_sources( diff --git a/src/hidapi/SDL_hidapi.c b/src/hidapi/SDL_hidapi.c index 0dda4d4ba1..7e1ed944d1 100644 --- a/src/hidapi/SDL_hidapi.c +++ b/src/hidapi/SDL_hidapi.c @@ -595,6 +595,8 @@ typedef struct PLATFORM_hid_device_ PLATFORM_hid_device; #include "SDL_hidapi_android.h" #elif defined(SDL_PLATFORM_IOS) || defined(SDL_PLATFORM_TVOS) #include "SDL_hidapi_ios.h" +#elif defined(SDL_PLATFORM_EMSCRIPTEN) +#include "SDL_hidapi_emscripten.h" #endif #undef api_version diff --git a/src/hidapi/SDL_hidapi_emscripten.h b/src/hidapi/SDL_hidapi_emscripten.h new file mode 100644 index 0000000000..dbf6f1e1fa --- /dev/null +++ b/src/hidapi/SDL_hidapi_emscripten.h @@ -0,0 +1,25 @@ +/* + Simple DirectMedia Layer + Copyright (C) 1997-2026 Sam Lantinga + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#undef HIDAPI_H__ +#include "emscripten/hid.c" +#define HAVE_PLATFORM_BACKEND 1 +#define udev_ctx 1 diff --git a/src/hidapi/emscripten/hid.c b/src/hidapi/emscripten/hid.c new file mode 100644 index 0000000000..481d918515 --- /dev/null +++ b/src/hidapi/emscripten/hid.c @@ -0,0 +1,495 @@ +/******************************************************* + HIDAPI - Multi-Platform library for + communication with HID devices. + + Alan Ott + Signal 11 Software + + libusb/hidapi Team + + Copyright 2022, All Rights Reserved. + + At the discretion of the user of this library, + this software may be licensed under the terms of the + GNU General Public License v3, a BSD-Style license, or the + original HIDAPI license as outlined in the LICENSE.txt, + LICENSE-gpl3.txt, LICENSE-bsd.txt, and LICENSE-orig.txt + files located at the root of the source distribution. + These files may also be found in the public source + code repository located at: + https://github.com/libusb/hidapi . +********************************************************/ + +#include "../hidapi/hidapi.h" +#include +#include +#include + +EM_JS_DEPS(hidapi, "$dynCall"); + +struct hid_device_ { + int device_id; + unsigned char *last_report; + size_t last_report_length; + int last_report_read; + struct hid_device_info* device_info; +}; + +static struct hid_api_version api_version = { + .major = HID_API_VERSION_MAJOR, + .minor = HID_API_VERSION_MINOR, + .patch = HID_API_VERSION_PATCH +}; + +static hid_device *new_hid_device(void) +{ + hid_device *dev = (hid_device*) calloc(1, sizeof(hid_device)); + if (dev == NULL) { + return NULL; + } + + dev->device_id = -1; + dev->last_report = NULL; + dev->last_report_length = 0; + dev->last_report_read = 0; + + return dev; +} + +HID_API_EXPORT const struct hid_api_version* HID_API_CALL hid_version(void) +{ + return &api_version; +} + +HID_API_EXPORT const char* HID_API_CALL hid_version_str(void) +{ + return HID_API_VERSION_STR; +} + +// static void test(hid_device *dev, unsigned char *data, size_t length) +// static void test(unsigned char *data) +// { +// // printf("Test function, device_id=%d\n", dev->device_id); +// printf("Test function\n"); +// for (int i = 0; i < 3; i++) { +// printf("Test function, data[%d]=%d\n", i, data[i]); +// } +// free(data); +// } + +int HID_API_EXPORT hid_init(void) +{ + // MAIN_THREAD_EM_ASM({ + // const typedArray = Uint8Array.from([1,2,3]); + // const heapPointer = _malloc(typedArray.length * typedArray.BYTES_PER_ELEMENT); + // HEAPU8.set(typedArray, heapPointer); + // dynCall('vi', $0, [heapPointer]); + // }, &test); + + return MAIN_THREAD_EM_ASM_INT({ + return "hid" in navigator ? 0 : -1; + }); +} + +int HID_API_EXPORT hid_exit(void) +{ + return 0; +} + +static struct hid_device_info * create_device_info_for_device(int device_id) +{ + struct hid_device_info *root = NULL; + struct hid_device_info *cur_dev = NULL; + char path[16]; + /*wchar_t product_string[128];*/ + int ignore = 0; + int input_report_count; + + ignore = MAIN_THREAD_EM_ASM_INT({ + let device = window._hidDeviceList[$0]; + if (!device) { + return true; + } + return false; + }, device_id); + + if (ignore) + goto end; + + /* Create the record. */ + root = (struct hid_device_info*) calloc(1, sizeof(struct hid_device_info)); + if (!root) + goto end; + + cur_dev = root; + + snprintf(path, sizeof(path), "hid%d", device_id); + cur_dev->path = strdup(path); + + cur_dev->vendor_id = MAIN_THREAD_EM_ASM_INT({ + return window._hidDeviceList[$0].vendorId; + }, device_id); + cur_dev->product_id = MAIN_THREAD_EM_ASM_INT({ + return window._hidDeviceList[$0].productId; + }, device_id); + + cur_dev->serial_number = NULL; + cur_dev->release_number = 0; + cur_dev->manufacturer_string = NULL; + + /* I'm too tired to make stringToUTF32 work correctly here, cmake doesn't want to include it in the build */ + /*MAIN_THREAD_EM_ASM({ + stringToUTF32(window._hidDeviceList[$0].productName, $1, Number($2)); + }, device_id, product_string, sizeof(product_string)); + + cur_dev->product_string = wcsdup(product_string);*/ + + cur_dev->product_string = NULL; + + cur_dev->usage_page = MAIN_THREAD_EM_ASM_INT({ + return window._hidDeviceList[$0].collections[0].usagePage; + }, device_id); + cur_dev->usage = MAIN_THREAD_EM_ASM_INT({ + return window._hidDeviceList[$0].collections[0].usage; + }, device_id); + cur_dev->interface_number = 0; + + input_report_count = MAIN_THREAD_EM_ASM_INT({ + return window._hidDeviceList[$0].collections[0].inputReports.length; + }, device_id); + + /* WebHID doesn't provide the bus type, so we have to guess it ourselves */ + if (input_report_count >= 5) { + cur_dev->bus_type = HID_API_BUS_BLUETOOTH; + } else { + cur_dev->bus_type = HID_API_BUS_USB; + } + + cur_dev->next = NULL; + + printf("HIDAPI: New device found: %d %d\n", cur_dev->vendor_id, cur_dev->product_id); +end: + return root; +} + +// TODO: remove all EM_ASYNC_JS +EM_ASYNC_JS(int, hid_js_get_device_count, (), { + window._hidDeviceList = await navigator.hid.getDevices(); + return window._hidDeviceList.length; +}); + +struct hid_device_info HID_API_EXPORT *hid_enumerate(unsigned short vendor_id, unsigned short product_id) +{ + int device_count; + struct hid_device_info *root = NULL; /* return object */ + struct hid_device_info *cur_dev = NULL; + + hid_init(); + + device_count = hid_js_get_device_count(); + + printf("hid_enumerate, device_count=%d\n", device_count); + + int device_id; + for (device_id = 0; device_id < device_count; device_id++) { + struct hid_device_info * tmp; + /* TODO: handle vendor_id and product_id */ + tmp = create_device_info_for_device(device_id); + + if (tmp) { + if (cur_dev) { + cur_dev->next = tmp; + } + else { + root = tmp; + } + cur_dev = tmp; + + /* move the pointer to the tail of returned list */ + while (cur_dev->next != NULL) { + cur_dev = cur_dev->next; + } + } + } + + return root; +} + +void HID_API_EXPORT hid_free_enumeration(struct hid_device_info *devs) +{ + struct hid_device_info *d = devs; + while (d) { + struct hid_device_info *next = d->next; + free(d->path); + /*free(d->serial_number); + free(d->manufacturer_string); + free(d->product_string);*/ + free(d); + d = next; + } +} + +hid_device * hid_open(unsigned short vendor_id, unsigned short product_id, const wchar_t *serial_number) +{ + struct hid_device_info *devs, *cur_dev; + const char *path_to_open = NULL; + hid_device *handle = NULL; + + /* register_global_error: global error is reset by hid_enumerate/hid_init */ + devs = hid_enumerate(vendor_id, product_id); + if (devs == NULL) { + /* register_global_error: global error is already set by hid_enumerate */ + return NULL; + } + + cur_dev = devs; + while (cur_dev) { + if (cur_dev->vendor_id == vendor_id && + cur_dev->product_id == product_id) { + if (serial_number) { + if (wcscmp(serial_number, cur_dev->serial_number) == 0) { + path_to_open = cur_dev->path; + break; + } + } else { + path_to_open = cur_dev->path; + break; + } + } + cur_dev = cur_dev->next; + } + + if (path_to_open) { + /* Open the device */ + handle = hid_open_path(path_to_open); + } + + hid_free_enumeration(devs); + + return handle; +} + +typedef void(*SetByteCallback)(unsigned char *data, size_t length, int byte, size_t offset); +typedef void(*SetReportCallback)(hid_device *dev, unsigned char *data, size_t length); + +static void set_byte(unsigned char *data, size_t length, int byte, size_t offset) +{ + if (offset >= length) + return; + data[offset] = (unsigned char)byte; + // printf("set_byte: offset=%d byte=%d\n", (int)offset, byte); +} + +static void set_report(hid_device *dev, unsigned char *data, size_t length) +{ + if (dev->last_report) { + free(dev->last_report); + } + dev->last_report = data; + dev->last_report_length = length; + dev->last_report_read = 0; +} + +EM_ASYNC_JS(void, hid_js_open, (int device_id, hid_device *dev, SetByteCallback callback, SetReportCallback set_report_callback), { + let device = window._hidDeviceList[device_id]; + console.log("Opening device1 " + device_id); + if (device) { + console.log("Opening device2 " + device_id); + await device.open(); + device.addEventListener("inputreport", function (event) { + const { data, device, reportId } = event; + + let dataLength = data['byteLength']+1; + let pointer = _malloc(dataLength); + dynCall("viiii", callback, [pointer, dataLength, report_id, 0]); + for (let i = 0; i < data['byteLength']; i++) { + dynCall("viiii", callback, [pointer, dataLength, data['getUint8'](i), i+1]); + } + dynCall("viii", set_report_callback, [dev, pointer, dataLength]); + }); + } +}); + +hid_device * HID_API_EXPORT hid_open_path(const char *path) +{ + hid_device *dev = NULL; + int device_id = 0; + + hid_init(); + /* register_global_error: global error is reset by hid_init */ + + printf("hid_open_path: %s\n", path); + dev = new_hid_device(); + if (!dev) { + printf("hid_open_path: no memory\n"); + return NULL; + } + + if (sscanf(path, "hid%d", &device_id) != 1) { + free(dev); + printf("hid_open_path: invalid path\n"); + return NULL; + } + + dev->device_id = device_id; + hid_js_open(device_id, dev, set_byte, set_report); + + return dev; +} + +EM_ASYNC_JS(int, hid_js_write, (int device_id, int report_id, const unsigned char *data, size_t length), { + let device = window._hidDeviceList[device_id]; + if (device) { + let dataArray = new Uint8Array(length); + for (let i = 0; i < length; i++) { + // console.log("hid_write: offset=" + i + " byte=" + HEAPU8[data+i]); + dataArray[i] = HEAPU8[data+i]; + } + await device.sendReport(report_id, dataArray); + } +}); + +int HID_API_EXPORT hid_write(hid_device *dev, const unsigned char *data, size_t length) +{ + if (length < 1) + return 0; + hid_js_write(dev->device_id, data[0], data+1, length-1); + return length; +} + +EM_ASYNC_JS(int, hid_js_read_timeout, (int device_id, unsigned char *data, size_t length, SetByteCallback callback), { + let device = window._hidDeviceList[device_id]; + if (device) { + let [report_id, dataView] = await new Promise(function(resolve, reject) { + device.addEventListener("inputreport", function (event) { + const { data, device, reportId } = event; + resolve([reportId, data]); // done + }, {once: true}); + }); + dynCall("viiii", callback, [data, length, report_id, 0]); + for (let i = 0; i < dataView['byteLength']; i++) { + dynCall("viiii", callback, [data, length, dataView['getUint8'](i), i+1]); + } + return dataView['byteLength']+1; + } + return 0; +}); + +int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t length, int milliseconds) +{ + /* TODO: timeout */ + if (length < 1) + return -1; + if (milliseconds == 0) { + if (dev->last_report && !dev->last_report_read) { + size_t return_size = length; + if (dev->last_report_length < length) { + return_size = dev->last_report_length; + } + memcpy(data, dev->last_report, return_size); + dev->last_report_read = 1; + return return_size; + } else { + return 0; + } + } + return hid_js_read_timeout(dev->device_id, data, length, &set_byte); +} + +int HID_API_EXPORT hid_read(hid_device *dev, unsigned char *data, size_t length) +{ + /* TODO: blocking */ + return hid_read_timeout(dev, data, length, -1); +} + +int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock) +{ + /* TODO */ + return 0; +} + +int HID_API_EXPORT hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length) +{ + return 0; +} + +EM_ASYNC_JS(void, hid_js_get_feature_report, (int device_id, int report_id, unsigned char *data, size_t length, SetByteCallback callback), { + let device = window._hidDeviceList[device_id]; + if (device) { + let dataView = await device['receiveFeatureReport'](report_id); + for (let i = 0; i < dataView['byteLength']; i++) { + dynCall("viiii", callback, [data, length, dataView['getUint8'](i), i]); + } + } +}); + +int HID_API_EXPORT hid_get_feature_report(hid_device *dev, unsigned char *data, size_t length) +{ + if (length < 1) + return -1; + int report_id = (int)data[0]; + hid_js_get_feature_report(dev->device_id, report_id, data, length, &set_byte); + return 0; +} + +int HID_API_EXPORT HID_API_CALL hid_get_input_report(hid_device *dev, unsigned char *data, size_t length) +{ + return 0; +} + +EM_ASYNC_JS(int, hid_js_close, (int device_id), { + let device = window._hidDeviceList[device_id]; + if (device) { + await device.close(); + } +}); + +void HID_API_EXPORT hid_close(hid_device *dev) +{ + if (!dev) + return; + + if (dev->last_report) { + free(dev->last_report); + } + + hid_js_close(dev->device_id); + hid_free_enumeration(dev->device_info); + + free(dev); +} + +int HID_API_EXPORT_CALL hid_get_manufacturer_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + return 0; +} + +int HID_API_EXPORT_CALL hid_get_product_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + return 0; +} + +int HID_API_EXPORT_CALL hid_get_serial_number_string(hid_device *dev, wchar_t *string, size_t maxlen) +{ + return 0; +} + +HID_API_EXPORT struct hid_device_info *HID_API_CALL hid_get_device_info(hid_device *dev) +{ + return NULL; +} + +int HID_API_EXPORT_CALL hid_get_indexed_string(hid_device *dev, int string_index, wchar_t *string, size_t maxlen) +{ + return -1; +} + +int HID_API_EXPORT_CALL hid_get_report_descriptor(hid_device *dev, unsigned char *buf, size_t buf_size) +{ + return 0; +} + +HID_API_EXPORT const wchar_t * HID_API_CALL hid_error(hid_device *dev) +{ + return L""; +} \ No newline at end of file diff --git a/src/joystick/emscripten/SDL_sysjoystick.c b/src/joystick/emscripten/SDL_sysjoystick.c index adf471a199..c7318ea98e 100644 --- a/src/joystick/emscripten/SDL_sysjoystick.c +++ b/src/joystick/emscripten/SDL_sysjoystick.c @@ -28,6 +28,7 @@ #include "SDL_sysjoystick_c.h" #include "../SDL_joystick_c.h" #include "../usb_ids.h" +#include "../hidapi/SDL_hidapijoystick_c.h" static SDL_joylist_item *JoystickByIndex(int index); @@ -120,6 +121,17 @@ static int SDL_GetEmscriptenOSID() }); } +#ifdef SDL_JOYSTICK_HIDAPI +static void SDL_RequestWebHIDDevice(Uint16 vendor, Uint16 product) +{ + MAIN_THREAD_EM_ASM({ + if ("hid" in navigator) { + navigator["hid"]["requestDevice"]({ "filters": [ { "vendorId": $0, "productId": $1, } ]}); + } + }, vendor, product); +} +#endif + static EM_BOOL Emscripten_JoyStickConnected(int eventType, const EmscriptenGamepadEvent *gamepadEvent, void *userData) { SDL_joylist_item *item; @@ -148,6 +160,15 @@ static EM_BOOL Emscripten_JoyStickConnected(int eventType, const EmscriptenGamep product = SDL_GetEmscriptenJoystickProduct(gamepadEvent->index); is_xinput = SDL_IsEmscriptenJoystickXInput(gamepadEvent->index); +#ifdef SDL_JOYSTICK_HIDAPI + if (HIDAPI_IsDeviceSupported(vendor, product, 0, "")) { + SDL_RequestWebHIDDevice(vendor, product); + printf("HIDAPI_IsDeviceSupported\n"); + SDL_free(item); + goto done; + } +#endif + // Use a generic VID/PID representing an XInput controller if (!vendor && !product && is_xinput) { vendor = USB_VENDOR_MICROSOFT; diff --git a/src/joystick/hidapi/SDL_hidapi_ps4.c b/src/joystick/hidapi/SDL_hidapi_ps4.c index 62c9ff6273..dbdcdae3f2 100644 --- a/src/joystick/hidapi/SDL_hidapi_ps4.c +++ b/src/joystick/hidapi/SDL_hidapi_ps4.c @@ -1034,6 +1034,7 @@ static void HIDAPI_DriverPS4_HandleStatePacket(SDL_Joystick *joystick, SDL_hid_d SDL_SendJoystickTouchpad(timestamp, joystick, 0, 1, touchpad_down, touchpad_x * TOUCHPAD_SCALEX, touchpad_y * TOUCHPAD_SCALEY, touchpad_down ? 1.0f : 0.0f); } + printf("PS4 Packet usb1 %d\n", packet->rgucButtonsHatAndCounter[0]); if (ctx->last_state.rgucButtonsHatAndCounter[0] != packet->rgucButtonsHatAndCounter[0]) { { Uint8 data = (packet->rgucButtonsHatAndCounter[0] >> 4); @@ -1295,7 +1296,9 @@ static bool HIDAPI_DriverPS4_UpdateDevice(SDL_HIDAPI_Device *device) #ifdef DEBUG_PS4_PROTOCOL HIDAPI_DumpPacket("PS4 packet: size = %d", data, size); #endif + printf("PS4 Packet start\n"); if (!HIDAPI_DriverPS4_IsPacketValid(ctx, data, size)) { + printf("PS4 Packet invalid\n"); continue; } @@ -1303,11 +1306,13 @@ static bool HIDAPI_DriverPS4_UpdateDevice(SDL_HIDAPI_Device *device) ctx->last_packet = now; if (!joystick) { + printf("PS4 Packet no joystick\n"); continue; } switch (data[0]) { case k_EPS4ReportIdUsbState: + printf("PS4 Packet usb, packet[1]=%d\n", +data[1]); HIDAPI_DriverPS4_HandleStatePacket(joystick, device->dev, ctx, (PS4StatePacket_t *)&data[1], size - 1); break; case k_EPS4ReportIdBluetoothState1: diff --git a/src/joystick/hidapi/SDL_hidapijoystick.c b/src/joystick/hidapi/SDL_hidapijoystick.c index 81587b0a33..8fa0fc66e9 100644 --- a/src/joystick/hidapi/SDL_hidapijoystick.c +++ b/src/joystick/hidapi/SDL_hidapijoystick.c @@ -342,7 +342,7 @@ static SDL_GamepadType SDL_GetJoystickGameControllerProtocol(const char *name, U return type; } -static bool HIDAPI_IsDeviceSupported(Uint16 vendor_id, Uint16 product_id, Uint16 version, const char *name) +bool HIDAPI_IsDeviceSupported(Uint16 vendor_id, Uint16 product_id, Uint16 version, const char *name) { int i; SDL_GamepadType type = SDL_GetJoystickGameControllerProtocol(name, vendor_id, product_id, -1, 0, 0, 0); diff --git a/src/joystick/hidapi/SDL_hidapijoystick_c.h b/src/joystick/hidapi/SDL_hidapijoystick_c.h index 59c62f6fe5..9e7be455de 100644 --- a/src/joystick/hidapi/SDL_hidapijoystick_c.h +++ b/src/joystick/hidapi/SDL_hidapijoystick_c.h @@ -180,6 +180,8 @@ extern SDL_HIDAPI_DeviceDriver SDL_HIDAPI_DriverZUIKI; (((Uint32)(C)) << 16) | \ (((Uint32)(D)) << 24)) +extern bool HIDAPI_IsDeviceSupported(Uint16 vendor_id, Uint16 product_id, Uint16 version, const char *name); + // Return true if a HID device is present and supported as a joystick of the given type extern bool HIDAPI_IsDeviceTypePresent(SDL_GamepadType type);