From 73372a1c8c2a60ac6b4feaa01c952d15cf401b81 Mon Sep 17 00:00:00 2001 From: ruevs Date: Mon, 23 Jun 2025 18:02:58 +0300 Subject: [PATCH 1/2] WIP: WebHID support through Emscripten Unfinished. --- CMakeLists.txt | 19 ++- config.h.in | 1 + examples/web/CMakeLists.txt | 36 ++++++ examples/web/test.c | 68 +++++++++++ examples/web/test.html | 44 +++++++ src/webhidapi.h | 233 ++++++++++++++++++++++++++++++++++++ 6 files changed, 399 insertions(+), 2 deletions(-) create mode 100644 examples/web/CMakeLists.txt create mode 100644 examples/web/test.c create mode 100644 examples/web/test.html create mode 100644 src/webhidapi.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 23d910d..69b0854 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,12 +20,22 @@ cmake_minimum_required(VERSION 3.1...3.19) project( libspnavdev VERSION 0.1 - LANGUAGES C) +) if(WIN32) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") endif() +if(EMSCRIPTEN) + set (CMAKE_CXX_STANDARD 11) + set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") + SET_SOURCE_FILES_PROPERTIES( + src/serdev.c + src/spnavdev.c + src/usbdev.c + PROPERTIES LANGUAGE CXX ) +endif() + add_library(spnavdev src/serdev.c src/spnavdev.c @@ -42,12 +52,15 @@ else() ) endif() -if(WIN32 OR APPLE OR EMSCRIPTEN) +if(WIN32 OR APPLE) set(HAVE_HIDAPI TRUE) set(BUILD_SHARED_LIBS FALSE) add_subdirectory(extlib/hidapi) target_link_libraries(spnavdev PRIVATE hidapi::hidapi) target_include_directories(spnavdev PRIVATE ${CMAKE_BINARY_DIR}/extlib/hidapi/hidapi) +elseif(EMSCRIPTEN) + set(HAVE_WEBHIDAPI TRUE) + set(BUILD_SHARED_LIBS FALSE) else() find_package(hidapi) if(hidapi_FOUND) @@ -66,6 +79,8 @@ if(CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR) # We are our own project, not a subproject if(WIN32) add_subdirectory(examples/win32) + elseif(EMSCRIPTEN) + add_subdirectory(examples/web) else() add_subdirectory(examples/unix) endif() diff --git a/config.h.in b/config.h.in index 25ee887..3610acd 100644 --- a/config.h.in +++ b/config.h.in @@ -18,4 +18,5 @@ along with this program. If not, see . #ifndef SPNAVDEV_CONFIG_H_ #define SPNAVDEV_CONFIG_H_ #cmakedefine HAVE_HIDAPI +#cmakedefine HAVE_WEBHIDAPI #endif \ No newline at end of file diff --git a/examples/web/CMakeLists.txt b/examples/web/CMakeLists.txt new file mode 100644 index 0000000..c07333e --- /dev/null +++ b/examples/web/CMakeLists.txt @@ -0,0 +1,36 @@ +# libspnavdev - direct 6dof device handling library +# Copyright (C) 2021 Collabora, Ltd. +# +# SPDX-License-Identifier: GPL-3-or-later +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +add_executable(test test.c) +target_link_libraries(test PRIVATE spnavdev) + +if(EMSCRIPTEN) + set(M_LIBRARY "" CACHE STRING "libm (not necessary)" FORCE) + +# set(CMAKE_EXECUTABLE_SUFFIX ".html") + + set(SHELL ${CMAKE_CURRENT_SOURCE_DIR}/test.html) + + set_target_properties(test PROPERTIES LINK_FLAGS + "--bind + --no-heap-copy -s ALLOW_MEMORY_GROWTH=1 -s WASM=1 -s ASYNCIFY=1 + -s DYNCALLS=1 -s ASSERTIONS=1 + -s EXPORTED_RUNTIME_METHODS=ccall,cwrap + -s EXPORTED_FUNCTIONS=_test,_main + -s TOTAL_STACK=33554432 -s TOTAL_MEMORY=134217728") +endif() diff --git a/examples/web/test.c b/examples/web/test.c new file mode 100644 index 0000000..87b402e --- /dev/null +++ b/examples/web/test.c @@ -0,0 +1,68 @@ +#include +#include +#include +#include +#include +#include "spnavdev.h" + +static struct spndev *dev; +static int quit = 0; + +extern int test() +{ + int fd; + union spndev_event ev; + const char *s; + int led=0; + + if(!(dev = spndev_open(0))) { + fprintf(stderr, "Failed to open 6dof device\n"); + return 1; + } + fd = spndev_fd(dev); + + printf("Monitoring device, ctrl-c to quit\n"); + + return 0; + while(!quit) { + + if (spndev_process(dev, &ev)) { + switch (ev.type) { + case SPNDEV_MOTION: + printf("motion: T[%+6d %+6d %+6d] R[%+6d %+6d %+6d]\n", + ev.mot.v[0], ev.mot.v[1], ev.mot.v[2], ev.mot.v[3], + ev.mot.v[4], ev.mot.v[5]); + break; + + case SPNDEV_BUTTON: + if ((s = spndev_button_name(dev, ev.bn.num))) { + printf("button %d (\"%s\") ", ev.bn.num, s); + } else { + printf("button %d ", ev.bn.num); + } + puts(ev.bn.press ? "pressed" : "released"); + + if (ev.bn.press) { + spndev_set_led(dev, led); + spndev_write_lcd(dev, 0xAA); + spndev_set_lcd_bl(dev, led); + led = !led; + } + + break; + + default: + break; + } + } + + } + + spndev_close(dev); + return 0; +} + +int main(int argc, char** argv) { + printf("Press the button to test\n"); + return 0; +} diff --git a/examples/web/test.html b/examples/web/test.html new file mode 100644 index 0000000..0a83076 --- /dev/null +++ b/examples/web/test.html @@ -0,0 +1,44 @@ + + + + + +Emscripten-Generated Code + + + + + + +
+
+
Downloading...
+ + +Resize canvas +Lock/hide mouse pointer     + + + + +
+ +
+
+ + +
+ + + + + + + \ No newline at end of file diff --git a/src/webhidapi.h b/src/webhidapi.h new file mode 100644 index 0000000..bfc443f --- /dev/null +++ b/src/webhidapi.h @@ -0,0 +1,233 @@ +// emcmake cmake ..\git -DCMAKE_BUILD_TYPE=Release +// ninja +// emrun examples\web\test.html + +#include +#include +#include +#include +#include +#include + +using namespace emscripten; + +struct hid_device_; +typedef struct hid_device_ hid_device; /**< opaque hidapi structure */ + +/** @brief HID underlying bus types. + + @ingroup API +*/ +typedef enum { + /** Unknown bus type */ + HID_API_BUS_UNKNOWN = 0x00, + + /** USB bus + Specifications: + https://usb.org/hid */ + HID_API_BUS_USB = 0x01, + + /** Bluetooth or Bluetooth LE bus + Specifications: + https://www.bluetooth.com/specifications/specs/human-interface-device-profile-1-1-1/ + https://www.bluetooth.com/specifications/specs/hid-service-1-0/ + https://www.bluetooth.com/specifications/specs/hid-over-gatt-profile-1-0/ */ + HID_API_BUS_BLUETOOTH = 0x02, + + /** I2C bus + Specifications: + https://docs.microsoft.com/previous-versions/windows/hardware/design/dn642101(v=vs.85) */ + HID_API_BUS_I2C = 0x03, + + /** SPI bus + Specifications: + https://www.microsoft.com/download/details.aspx?id=103325 */ + HID_API_BUS_SPI = 0x04, +} hid_bus_type; + +/** hidapi info structure */ +struct hid_device_info { + /** Platform-specific device path */ + char* path; + /** Device Vendor ID */ + unsigned short vendor_id; + /** Device Product ID */ + unsigned short product_id; + /** Serial Number */ + wchar_t* serial_number; + /** Device Release Number in binary-coded decimal, + also known as Device Version Number */ + unsigned short release_number; + /** Manufacturer String */ + wchar_t* manufacturer_string; + /** Product string */ + wchar_t* product_string; + /** Usage Page for this Device/Interface + (Windows/Mac/hidraw only) */ + unsigned short usage_page; + /** Usage for this Device/Interface + (Windows/Mac/hidraw only) */ + unsigned short usage; + /** The USB interface which this logical device + represents. + + Valid only if the device is a USB HID device. + Set to -1 in all other cases. + */ + int interface_number; + + /** Pointer to the next device */ + struct hid_device_info* next; + + /** Underlying bus type + Since version 0.13.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 13, 0) + */ + hid_bus_type bus_type; +}; + +/* PAR@@@: Not really needed but anyway */ +#define HID_API_EXPORT /**< API export macro */ +#define HID_API_CALL /**< API call macro */ + +/* https://wicg.github.io/webhid/#requestdevice-method + https://wicg.github.io/webhid/#dom-hid-getdevices + https://web.dev/hid/ + https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#embind-val-guide + https://emscripten.org/docs/api_reference/val.h.html + https://web.dev/articles/emscripten-embedding-js-snippets + https://emscripten.org/docs/porting/asyncify.html + https://emscripten.org/docs/tools_reference/emcc.html#emcc-s-option-value */ + +struct hid_device_info HID_API_EXPORT* HID_API_CALL hid_enumerate(unsigned short vendor_id, unsigned short product_id) +{ +// val device = val::global("Navigator.hid").call("requestDevice", std::string("{filters: [{ vendorId: 0x046d }, { vendorId: 0x256f }]}")); +// val webhid = val::global("Navigator.hid"); + val::global("console").call("log", std::string("hid_enumerate")); + val navigator = val::global("navigator"); + val hid = navigator["hid"]; + if (hid.isUndefined()) { +// if (!hid.as()) { + printf("WebHID API not supported. Check https://caniuse.com/webhid for a list of browsers that support it.\n"); + return 0; + } else { +// val hid = webhid.new_(); +// val device = hid.call("requestDevice", "{filters: [{ vendorId: 0x046d }, { vendorId: 0x256f }]}"); + //val reqd = hid["requestDevice"].await(); +// val devices = hid.call("requestDevice", std::string("{filters: [{ vendorId: 0x046d }, { vendorId: 0x256f }]}")).await(); + + + // Define the filters + val filters = val::array(); + val filter1 = val::object(); + filter1.set("vendorId", "0x046d"); + filters.call("push", filter1); + val filter2 = val::object(); + filter2.set("vendorId", "0x256f"); + filters.call("push", filter2); + + // Create options object with filters + val options = val::object(); + options.set("filters", filters); + + val::global("console").call("log", options); + + val devices = hid.call("requestDevice", options).await(); + + val::global("console").call("log", devices); +// std::cout << "Devices: " << devices.as() << std::endl; + if (0 == devices["length"].as()) { + return 0; + } + else { + hid_device_info* devs = new hid_device_info; + devs->path = (char*)(new val(devices[0])); + devs->product_string = (wchar_t*)calloc(devices[0]["productName"].as().length(), sizeof(char)); + wcscpy(devs->product_string, (wchar_t*)(devices[0]["productName"].as().c_str())); + devs->product_id = devices[0]["productId"].as(); + devs->vendor_id = devices[0]["vendorId"].as(); + + return devs; + } + } +} + +void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info* devs) +{ + val::global("console").call("log", std::string("hid_free_enumeration")); + + while (devs) { + hid_device_info* current = devs; + devs = devs->next; + delete current->path; + delete current->serial_number; + delete current->manufacturer_string; + delete current->product_string; + delete current; + } +} + +static void hid_handleInputReport(val e) { + val::global("console").call("log", std::string("hid_handleInputReport")); + val::global("console").call("log", e); + e.call("preventDefault"); // don't interfere with regular mouse cursor + e.call("stopImmediatePropagation"); // don't interfere with regular mouse cursor + +} + +EMSCRIPTEN_BINDINGS(my_module) { + function("hid_handleInputReport", &hid_handleInputReport); +} + +HID_API_EXPORT hid_device* HID_API_CALL hid_open_path(const char* path) +{ + val::global("console").call("log", std::string("hid_open_path")); + + val* device = (val *)path; + val::global("console").call("log", *device); + device->call("open").await(); + val::global("console").call("log", *device); + if (true == (*device)["opened"].as()) { + device->call("addEventListener", std::string("inputreport"), val::module_property("hid_handleInputReport")); +/* device->call("addEventListener", "inputreport", function(val event) { + var data = new Uint8Array(event.data.buffer); + Module._cppInputReportHandler(data.byteOffset, data.byteLength); + });*/ + return (hid_device*)path; + } + + return 0; +} + +int HID_API_EXPORT HID_API_CALL hid_write(hid_device* dev, const unsigned char* data, size_t length) +{ + val::global("console").call("log", std::string("hid_write")); + + return -1; +} + +int HID_API_EXPORT HID_API_CALL hid_read(hid_device* dev, unsigned char* data, size_t length) +{ + val::global("console").call("log", std::string("hid_read")); + + return -1; +} + +int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device* dev, int nonblock) +{ + val::global("console").call("log", std::string("hid_set_nonblocking")); + + return 0; +} + +int HID_API_EXPORT HID_API_CALL hid_send_feature_report(hid_device* dev, const unsigned char* data, size_t length) +{ + val::global("console").call("log", std::string("hid_send_feature_report")); + + return -1; +} + +void HID_API_EXPORT HID_API_CALL hid_close(hid_device* dev) +{ + val::global("console").call("log", std::string("hid_close")); + +} From 4a91bd2057f9341ef6d34332f3aa15c2f6436473 Mon Sep 17 00:00:00 2001 From: ruevs Date: Fri, 25 Sep 2026 22:45:29 +0300 Subject: [PATCH 2/2] Add WebHID support through Emscripten Fully working with an asynchronous API. The browser has to support the WebHID API. Currently these are only the Chromium based browsers https://caniuse.com/webhid . --- CMakeLists.txt | 4 +- README.md | 49 ++++- examples/web/CMakeLists.txt | 14 +- examples/web/test.c | 80 ++++---- examples/web/test.html | 82 ++++---- src/dev.h | 3 + src/serdev.c | 3 +- src/spnavdev.c | 10 + src/spnavdev.h | 4 + src/usbdev.c | 67 ++++++- src/webhidapi.h | 369 ++++++++++++++++++++++++++---------- 11 files changed, 480 insertions(+), 205 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 69b0854..1322664 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,8 +27,8 @@ if(WIN32) endif() if(EMSCRIPTEN) - set (CMAKE_CXX_STANDARD 11) - set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") + set (CMAKE_CXX_STANDARD 17) + set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17") SET_SOURCE_FILES_PROPERTIES( src/serdev.c src/spnavdev.c diff --git a/README.md b/README.md index dcb0554..0555958 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,16 @@ It is easily portable to embedded systems, robot controllers, non-UNIX systems e The goal of spnavdev is to handle all USB and serial 6DOF devices, across multiple platforms such as: all UNIX systems, windows, DOS, and possibly even bare metal on certain systems. +The library also works in [web browsers](#building-for-web) that support the [WebHID][webhid] api. + +[webhid]: https://caniuse.com/webhid + ### Supported devices The library supports all USB devices from 3Dconnexion and should support all serial devices as well. The following devices are tested and fully working - 6DOF input, buttons, LEDs (if present): -#### USB devices (all by 3Dconnexion): +#### Tested USB devices (all by 3Dconnexion): Device | VID | PID | Note ------ | --- | --- | ---- @@ -29,7 +33,7 @@ SpaceExplorer | 0x046d | 0xc627 | "New" devices (with VID = 0x256f) should work but I have not personally tested them. -#### Serial devices: +#### Tested Serial devices: Device | Manufacturer | Note ------ | ------------ | ---- @@ -71,7 +75,7 @@ sudo dnf install git gcc-c++ cmake Before building, [check out the project and the necessary submodules](#Building). -After that, build libspnavdev as following: +After that, build `libspnavdev` as following: ```sh mkdir build @@ -122,7 +126,7 @@ Open `build\libspnavdev.sln` with Visual C++ and build it. #### Building with MinGW -It is also possible to build libspnavdev using [MinGW][mingw]. +It is also possible to build `libspnavdev` using [MinGW][mingw]. First, ensure that git and gcc are in your `$PATH`. Then, run the following in bash: @@ -137,10 +141,45 @@ make [cmakewin]: http://www.cmake.org/download/#latest [mingw]: http://www.mingw.org/ +### Building for web + +You will need the usual build tools, cmake and [Emscripten][]. On a Debian derivative (e.g. Ubuntu) dependencies other than Emscripten can be installed with: + +```sh +apt-get install git build-essential cmake +``` + +First, install and prepare `emsdk`: + +```sh +git clone https://github.com/emscripten-core/emsdk +cd emsdk +./emsdk install latest +./emsdk activate latest +source ./emsdk_env.sh +cd .. +``` + +Before building, [check out the project and the necessary submodules](#Building). + +After that, build `libspnavdev` as following: + +```sh +mkdir build +cd build +emcmake cmake .. -DCMAKE_BUILD_TYPE=Release +make +``` + +The example application is built as three files in the `build/examples/web` directory with names +starting with `test`. It can be run locally with `emrun build/examples/web/test.html`. + +[emscripten]: https://emscripten.org/ + History ------- John Tsiombikas (the author of [spacenavd](https://github.com/FreeSpacenav/spacenavd)) [started writing this library](https://github.com/FreeSpacenav/libspnavdev) -in November 2020 with the inntention to: +in November 2020 with the intention to: > eventually replace the > device handling code in spacenavd v2, and will also be usable as a standalone diff --git a/examples/web/CMakeLists.txt b/examples/web/CMakeLists.txt index c07333e..d4e40e0 100644 --- a/examples/web/CMakeLists.txt +++ b/examples/web/CMakeLists.txt @@ -22,15 +22,15 @@ target_link_libraries(test PRIVATE spnavdev) if(EMSCRIPTEN) set(M_LIBRARY "" CACHE STRING "libm (not necessary)" FORCE) -# set(CMAKE_EXECUTABLE_SUFFIX ".html") - - set(SHELL ${CMAKE_CURRENT_SOURCE_DIR}/test.html) - set_target_properties(test PROPERTIES LINK_FLAGS - "--bind - --no-heap-copy -s ALLOW_MEMORY_GROWTH=1 -s WASM=1 -s ASYNCIFY=1 + "--shell-file=${CMAKE_CURRENT_SOURCE_DIR}/test.html + --bind + -s ALLOW_MEMORY_GROWTH=1 -s WASM=1 -s ASYNCIFY=1 -s DYNCALLS=1 -s ASSERTIONS=1 -s EXPORTED_RUNTIME_METHODS=ccall,cwrap -s EXPORTED_FUNCTIONS=_test,_main - -s TOTAL_STACK=33554432 -s TOTAL_MEMORY=134217728") + -s TOTAL_STACK=33554432 -s TOTAL_MEMORY=134217728" + SUFFIX ".html") + set_property(TARGET test APPEND PROPERTY LINK_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/test.html") endif() diff --git a/examples/web/test.c b/examples/web/test.c index 87b402e..0344ee6 100644 --- a/examples/web/test.c +++ b/examples/web/test.c @@ -8,57 +8,53 @@ static struct spndev *dev; static int quit = 0; -extern int test() -{ - int fd; - union spndev_event ev; - const char *s; - int led=0; - - if(!(dev = spndev_open(0))) { - fprintf(stderr, "Failed to open 6dof device\n"); - return 1; - } - fd = spndev_fd(dev); +void usercallback(union spndev_event ev, void* uptr) { + static int led = 0; + const char* s; + struct spndev* dev = (struct spndev*)uptr; - printf("Monitoring device, ctrl-c to quit\n"); + switch (ev.type) { + case SPNDEV_MOTION: + printf("motion: T[%+6d %+6d %+6d] R[%+6d %+6d %+6d]\n", + ev.mot.v[0], ev.mot.v[1], ev.mot.v[2], ev.mot.v[3], + ev.mot.v[4], ev.mot.v[5]); + break; - return 0; - while(!quit) { + case SPNDEV_BUTTON: + if ((s = spndev_button_name(dev, ev.bn.num))) { + printf("button %d (\"%s\") ", ev.bn.num, s); + } + else { + printf("button %d ", ev.bn.num); + } + puts(ev.bn.press ? "pressed" : "released"); - if (spndev_process(dev, &ev)) { - switch (ev.type) { - case SPNDEV_MOTION: - printf("motion: T[%+6d %+6d %+6d] R[%+6d %+6d %+6d]\n", - ev.mot.v[0], ev.mot.v[1], ev.mot.v[2], ev.mot.v[3], - ev.mot.v[4], ev.mot.v[5]); - break; + if (ev.bn.press) { + spndev_set_led(dev, led); + spndev_write_lcd(dev, 0xAA); + spndev_set_lcd_bl(dev, led); + led = !led; + } - case SPNDEV_BUTTON: - if ((s = spndev_button_name(dev, ev.bn.num))) { - printf("button %d (\"%s\") ", ev.bn.num, s); - } else { - printf("button %d ", ev.bn.num); - } - puts(ev.bn.press ? "pressed" : "released"); + break; - if (ev.bn.press) { - spndev_set_led(dev, led); - spndev_write_lcd(dev, 0xAA); - spndev_set_lcd_bl(dev, led); - led = !led; - } + default: + break; + } +} - break; +extern int test() +{ + if(!(dev = spndev_open(0))) { + fprintf(stderr, "Failed to open 6dof device\n"); + return 1; + } - default: - break; - } - } + spndev_set_userptr(dev, (void*)dev); + spndev_set_usercallback(dev, usercallback); - } + printf("Monitoring device, ctrl-c to quit\n"); - spndev_close(dev); return 0; } diff --git a/examples/web/test.html b/examples/web/test.html index 0a83076..82d6888 100644 --- a/examples/web/test.html +++ b/examples/web/test.html @@ -1,44 +1,46 @@ - - + + - - -Emscripten-Generated Code - + + + libspnavdev WebHID test + + - - - -
-
-
Downloading...
- - -Resize canvas -Lock/hide mouse pointer     - - - - -
- -
-
- - -
- - - - - +

libspnavdev WebHID test

+

Connect a supported SpaceMouse, press the button below, then grant access to it in the browser prompt.

+ + + {{{ SCRIPT }}} - \ No newline at end of file + diff --git a/src/dev.h b/src/dev.h index 33af30c..75c47f0 100644 --- a/src/dev.h +++ b/src/dev.h @@ -40,10 +40,13 @@ struct spndev { int lcdbl; void *uptr, *drvdata; + spndev_callback *callback; void (*close)(struct spndev*); int (*read)(struct spndev*, union spndev_event*); + int (*setcallback)(struct spndev*, spndev_callback callback); + void (*setled)(struct spndev*, int led); int (*getled)(struct spndev*); diff --git a/src/serdev.c b/src/serdev.c index 1a33182..ba38daf 100644 --- a/src/serdev.c +++ b/src/serdev.c @@ -57,7 +57,7 @@ static struct { {"Magellan SpaceMouse", 11, {"1", "2", "3", "4", "5", "6", "7", "8", "*", "+", "-"}}, {"Spaceball 5000FLX", 12, {"1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C"}}, {"CadMan", 4, {"1", "2", "3", "4"}}, - {"Space Explorer", 14, {"1", "2", "T", "L", "R", "F", "ALT", "ESC", "SHIFT", "CTRL", "Fit", "Panel", "+", "-", "2D"}}, + {"Space Explorer", 15, {"1", "2", "T", "L", "R", "F", "ALT", "ESC", "SHIFT", "CTRL", "Fit", "Panel", "+", "-", "2D"}}, // 3DX-700027 {0, 0, {0}} }; @@ -102,6 +102,7 @@ int spndev_ser_open(struct spndev *dev, const char *devstr) dev->drvdata = sb; dev->close = close_dev_serial; dev->read = read_dev_serial; + dev->setcallback = 0; dev->setled = 0; dev->getled = 0; dev->setlcdbl = 0; diff --git a/src/spnavdev.c b/src/spnavdev.c index 5d5fbbe..db60f52 100644 --- a/src/spnavdev.c +++ b/src/spnavdev.c @@ -17,6 +17,7 @@ along with this program. If not, see . */ #include #include +#include #include "spnavdev.h" #include "dev.h" @@ -29,6 +30,7 @@ struct spndev *spndev_open(const char *devstr) perror("spndev_open: failed to allocate device structure"); return 0; } + memset(dev, 0, sizeof *dev); if(!devstr || sscanf(devstr, "%hx:%hx", &vendor, &product) == 2) { if(spndev_usb_open(dev, devstr, vendor, product) == -1) { @@ -62,6 +64,14 @@ void *spndev_get_userptr(struct spndev *dev) return dev->uptr; } +int spndev_set_usercallback(struct spndev* dev, spndev_callback callback) { + dev->callback = callback; + if (!dev->setcallback) { + return -1; + } + return dev->setcallback(dev, callback); +} + /* device information */ const char *spndev_name(struct spndev *dev) { diff --git a/src/spnavdev.h b/src/spnavdev.h index 365e1a7..4f51e09 100644 --- a/src/spnavdev.h +++ b/src/spnavdev.h @@ -49,6 +49,8 @@ union spndev_event { struct spndev_event_button bn; }; +typedef void (spndev_callback)(union spndev_event ev, void *uptr); + /* possible devstr parameters: * - null: auto-detect (USB only) * - device file (e.g. COM6 or /dev/ttyS0 or /dev/input/event42) @@ -60,6 +62,8 @@ void spndev_close(struct spndev *dev); void spndev_set_userptr(struct spndev *dev, void *uptr); void *spndev_get_userptr(struct spndev *dev); +int spndev_set_usercallback(struct spndev* dev, spndev_callback callback); + /* device information */ const char *spndev_name(struct spndev *dev); const char *spndev_path(struct spndev *dev); diff --git a/src/usbdev.c b/src/usbdev.c index ac5e3fe..d215892 100644 --- a/src/usbdev.c +++ b/src/usbdev.c @@ -50,6 +50,7 @@ static const int axis_deadz = 0; typedef struct spnav_hid { unsigned char btn[max_num_btn]; unsigned char buf[max_buf_size]; + int motion[6]; size_t reportsize; } tspnav_hid; @@ -95,6 +96,7 @@ static void usbdev_close(struct spndev* dev); static int usbdev_read(struct spndev* dev, union spndev_event* evt); static int usbdev_parse_O(struct spndev* dev, union spndev_event* evt); static int usbdev_parse_N(struct spndev* dev, union spndev_event* evt); +static int usbdev_setcallback(struct spndev* dev, spndev_callback callback); static void usbdev_setled(struct spndev* dev, int led); static int usbdev_getled(struct spndev* dev); static inline void usbdev_parsebuttons(const struct spndev* dev, union spndev_event* evt, const unsigned char* report); @@ -260,6 +262,7 @@ static int usbdev_init(struct spndev* dev, const unsigned type) } else { // WTF? } + dev->setcallback = usbdev_setcallback; dev->getled = usbdev_getled; dev->setled = usbdev_setled; if((dev->usb_vendor == devinfo[5].vid) && (dev->usb_product == devinfo[5].pid)) { // ToDo: The constant 5 here is an ugly hack @@ -283,7 +286,6 @@ static void usbdev_close(struct spndev *dev) { } - // Read and parse positon and rotation data static int usbdev_read(struct spndev* dev, union spndev_event* evt) { @@ -301,11 +303,40 @@ static int usbdev_read(struct spndev* dev, union spndev_event* evt) return evt->type; } +static void usbdev_callback(unsigned char* data, size_t length, void* uptr) { + struct spndev* dev = (struct spndev*)uptr; + union spndev_event evt; + tspnav_hid* hid = (tspnav_hid*)dev->drvdata; + size_t copy_length; + + evt.type = SPNDEV_NONE; + if (!hid) { + return; + } + + copy_length = length < hid->reportsize ? length : hid->reportsize; + memset(hid->buf, 0, hid->reportsize); + memcpy(hid->buf, data, copy_length); + + if (oldposrotreportsize == hid->reportsize) { + usbdev_parse_O(dev, &evt); + } + else { + usbdev_parse_N(dev, &evt); + } + + if (evt.type != SPNDEV_NONE && dev->callback) { + dev->callback(evt, dev->uptr); + } +} + + // Parse "Old" (two reports) style positon and rotation data static int usbdev_parse_O(struct spndev* dev, union spndev_event* evt) { evt->type = SPNDEV_NONE; - unsigned char *buffer = ((tspnav_hid*)dev->drvdata)->buf; + tspnav_hid* hid = (tspnav_hid*)dev->drvdata; + unsigned char *buffer = hid->buf; switch (buffer[0]) { case 0: @@ -317,17 +348,19 @@ static int usbdev_parse_O(struct spndev* dev, union spndev_event* evt) case 1: // Translation evt->type = SPNDEV_MOTION; for (int i = 0; i < 3; ++i) { - evt->mot.v[i] = *(int16_t*)(buffer + 1 + 2 * i); - checkrange(dev, evt->mot.v[i]); + hid->motion[i] = *(int16_t*)(buffer + 1 + 2 * i); + checkrange(dev, hid->motion[i]); } + memcpy(evt->mot.v, hid->motion, sizeof hid->motion); break; case 2: // Rotation evt->type = SPNDEV_MOTION; for (int i = 0; i < 3; ++i) { - evt->mot.v[i + 3] = *(int16_t*)(buffer + 1 + 2 * i); - checkrange(dev, evt->mot.v[i + 3]); + hid->motion[i + 3] = *(int16_t*)(buffer + 1 + 2 * i); + checkrange(dev, hid->motion[i + 3]); } + memcpy(evt->mot.v, hid->motion, sizeof hid->motion); break; case 3: // Buttons usbdev_parsebuttons(dev, evt, buffer); @@ -346,7 +379,8 @@ static int usbdev_parse_O(struct spndev* dev, union spndev_event* evt) static int usbdev_parse_N(struct spndev* dev, union spndev_event* evt) { evt->type = SPNDEV_NONE; - unsigned char* buffer = ((tspnav_hid*)dev->drvdata)->buf; + tspnav_hid* hid = (tspnav_hid*)dev->drvdata; + unsigned char* buffer = hid->buf; switch (buffer[0]) { case 0: @@ -358,9 +392,10 @@ static int usbdev_parse_N(struct spndev* dev, union spndev_event* evt) case 1: // Translation & Rotation evt->type = SPNDEV_MOTION; for (int i = 0; i < 6; ++i) { - evt->mot.v[i] = *(int16_t*)(buffer + 1 + 2 * i); - checkrange(dev, evt->mot.v[i]); + hid->motion[i] = *(int16_t*)(buffer + 1 + 2 * i); + checkrange(dev, hid->motion[i]); } + memcpy(evt->mot.v, hid->motion, sizeof hid->motion); break; case 3: // Buttons @@ -376,6 +411,18 @@ static int usbdev_parse_N(struct spndev* dev, union spndev_event* evt) return evt->type; } +static int usbdev_setcallback(struct spndev* dev, spndev_callback callback) { +#if defined(HAVE_WEBHIDAPI) + (void)callback; + return hid_set_callback((hid_device*)dev->handle, usbdev_callback, (void *)dev); +#else + (void)dev; + (void)callback; + return -1; +#endif + +} + static void usbdev_setled(struct spndev *dev, int led) { /* The LED report is 4. The second byte is the LED state. It is a bit mask. @@ -455,7 +502,7 @@ static inline void checkrange(const struct spndev* dev, const int val) { */ static void SpacePilotLCDStartPos(struct spndev* dev, unsigned char column, unsigned char row) { /* https://forum.3dconnexion.com/viewtopic.php?f=19&t=1095 - Post by jwick » Wed Aug 29, 2007 8 : 14 am + Post by jwick » Wed Aug 29, 2007 8 : 14 am Hello mew, There was a small problem in an earlier version of the SP firmware where requests to diff --git a/src/webhidapi.h b/src/webhidapi.h index bfc443f..af214ee 100644 --- a/src/webhidapi.h +++ b/src/webhidapi.h @@ -1,13 +1,19 @@ -// emcmake cmake ..\git -DCMAKE_BUILD_TYPE=Release -// ninja -// emrun examples\web\test.html +/* + * Minimal hidapi-style wrapper for WebHID, used by the Emscripten build. + */ +#ifndef WEBHIDAPI_H_ +#define WEBHIDAPI_H_ -#include -#include -#include #include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include using namespace emscripten; @@ -17,7 +23,7 @@ typedef struct hid_device_ hid_device; /**< opaque hidapi structure */ /** @brief HID underlying bus types. @ingroup API -*/ + */ typedef enum { /** Unknown bus type */ HID_API_BUS_UNKNOWN = 0x00, @@ -85,149 +91,316 @@ struct hid_device_info { hid_bus_type bus_type; }; -/* PAR@@@: Not really needed but anyway */ -#define HID_API_EXPORT /**< API export macro */ -#define HID_API_CALL /**< API call macro */ +#define HID_API_EXPORT +#define HID_API_CALL -/* https://wicg.github.io/webhid/#requestdevice-method - https://wicg.github.io/webhid/#dom-hid-getdevices - https://web.dev/hid/ - https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html#embind-val-guide - https://emscripten.org/docs/api_reference/val.h.html - https://web.dev/articles/emscripten-embedding-js-snippets - https://emscripten.org/docs/porting/asyncify.html - https://emscripten.org/docs/tools_reference/emcc.html#emcc-s-option-value */ +typedef void (hid_input_report_callback)(unsigned char* data, size_t length, void* uptr); -struct hid_device_info HID_API_EXPORT* HID_API_CALL hid_enumerate(unsigned short vendor_id, unsigned short product_id) +struct hid_device_ { + val* js_device; + hid_input_report_callback* input_report_callback; + void* input_report_user_data; + int nonblocking; + std::queue > reports; +}; + +static std::map webhid_device_registry; +static unsigned long webhid_next_path_id = 1; + +void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info* devs); + +static char* webhid_strdup(const std::string& str) { -// val device = val::global("Navigator.hid").call("requestDevice", std::string("{filters: [{ vendorId: 0x046d }, { vendorId: 0x256f }]}")); -// val webhid = val::global("Navigator.hid"); - val::global("console").call("log", std::string("hid_enumerate")); - val navigator = val::global("navigator"); - val hid = navigator["hid"]; - if (hid.isUndefined()) { -// if (!hid.as()) { - printf("WebHID API not supported. Check https://caniuse.com/webhid for a list of browsers that support it.\n"); - return 0; + size_t len = str.size() + 1; + char* copy = (char*)malloc(len); + + if(copy) { + memcpy(copy, str.c_str(), len); + } + return copy; +} + +static wchar_t* webhid_wcsdup_ascii(const std::string& str) +{ + size_t len = str.size(); + wchar_t* copy = (wchar_t*)calloc(len + 1, sizeof(wchar_t)); + + if(copy) { + for(size_t i = 0; i < len; ++i) { + copy[i] = (unsigned char)str[i]; + } + } + return copy; +} + +static std::string webhid_get_string_prop(const val& obj, const char* prop) +{ + val v = obj[prop]; + + if(v.isUndefined() || v.isNull()) { + return std::string(); + } + return v.as(); +} + +static val webhid_build_filters(unsigned short vendor_id, unsigned short product_id) +{ + val filters = val::array(); + + if(vendor_id) { + val filter = val::object(); + filter.set("vendorId", vendor_id); + if(product_id) { + filter.set("productId", product_id); + } + filters.call("push", filter); } else { -// val hid = webhid.new_(); -// val device = hid.call("requestDevice", "{filters: [{ vendorId: 0x046d }, { vendorId: 0x256f }]}"); - //val reqd = hid["requestDevice"].await(); -// val devices = hid.call("requestDevice", std::string("{filters: [{ vendorId: 0x046d }, { vendorId: 0x256f }]}")).await(); + static const unsigned short vendors[] = {0x046d, 0x256f}; + for(size_t i = 0; i < sizeof vendors / sizeof vendors[0]; ++i) { + val filter = val::object(); + filter.set("vendorId", vendors[i]); + filters.call("push", filter); + } + } + return filters; +} - // Define the filters - val filters = val::array(); - val filter1 = val::object(); - filter1.set("vendorId", "0x046d"); - filters.call("push", filter1); - val filter2 = val::object(); - filter2.set("vendorId", "0x256f"); - filters.call("push", filter2); +static struct hid_device_info* webhid_append_device_info(struct hid_device_info** tail, const val& device) +{ + struct hid_device_info* info; + std::string path_key; + std::string manufacturer; + std::string product; + val usage_page; + val usage; + + if(!(info = (struct hid_device_info*)calloc(1, sizeof *info))) { + return 0; + } + + path_key = std::string("webhid:") + std::to_string(webhid_next_path_id++); + webhid_device_registry[path_key] = new val(device); + + info->path = webhid_strdup(path_key); + info->vendor_id = device["vendorId"].as(); + info->product_id = device["productId"].as(); + manufacturer = webhid_get_string_prop(device, "manufacturerName"); + product = webhid_get_string_prop(device, "productName"); + info->manufacturer_string = webhid_wcsdup_ascii(manufacturer); + info->product_string = webhid_wcsdup_ascii(product); + info->interface_number = -1; + info->bus_type = HID_API_BUS_USB; + usage_page = device["usagePage"]; + usage = device["usage"]; + if(!usage_page.isUndefined() && !usage_page.isNull()) { + info->usage_page = usage_page.as(); + } + if(!usage.isUndefined() && !usage.isNull()) { + info->usage = usage.as(); + } - // Create options object with filters - val options = val::object(); - options.set("filters", filters); + (*tail)->next = info; + *tail = info; + return info; +} - val::global("console").call("log", options); +struct hid_device_info HID_API_EXPORT* HID_API_CALL hid_enumerate(unsigned short vendor_id, unsigned short product_id) +{ + struct hid_device_info head; + struct hid_device_info* tail = &head; + val navigator = val::global("navigator"); + val hid = navigator["hid"]; + val devices; + val options; + size_t count; - val devices = hid.call("requestDevice", options).await(); + memset(&head, 0, sizeof head); - val::global("console").call("log", devices); -// std::cout << "Devices: " << devices.as() << std::endl; - if (0 == devices["length"].as()) { + if(hid.isUndefined() || hid.isNull()) { + printf("WebHID API not supported. Check https://caniuse.com/webhid for a list of browsers that support it.\n"); + return 0; + } + + options = val::object(); + options.set("filters", webhid_build_filters(vendor_id, product_id)); + devices = hid.call("requestDevice", options).await(); + count = devices["length"].as(); + + for(size_t i = 0; i < count; ++i) { + if(!webhid_append_device_info(&tail, devices[i])) { + hid_free_enumeration(head.next); return 0; } - else { - hid_device_info* devs = new hid_device_info; - devs->path = (char*)(new val(devices[0])); - devs->product_string = (wchar_t*)calloc(devices[0]["productName"].as().length(), sizeof(char)); - wcscpy(devs->product_string, (wchar_t*)(devices[0]["productName"].as().c_str())); - devs->product_id = devices[0]["productId"].as(); - devs->vendor_id = devices[0]["vendorId"].as(); - - return devs; - } } + + return head.next; } -void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info* devs) +void HID_API_EXPORT HID_API_CALL hid_free_enumeration(struct hid_device_info* devs) { - val::global("console").call("log", std::string("hid_free_enumeration")); + while(devs) { + struct hid_device_info* current = devs; - while (devs) { - hid_device_info* current = devs; devs = devs->next; - delete current->path; - delete current->serial_number; - delete current->manufacturer_string; - delete current->product_string; - delete current; + free(current->path); + free(current->serial_number); + free(current->manufacturer_string); + free(current->product_string); + free(current); } } -static void hid_handleInputReport(val e) { - val::global("console").call("log", std::string("hid_handleInputReport")); - val::global("console").call("log", e); - e.call("preventDefault"); // don't interfere with regular mouse cursor - e.call("stopImmediatePropagation"); // don't interfere with regular mouse cursor +static void hid_handleInputReport(val event) +{ + val target = event["currentTarget"]; + val handle_value = target["__spnav_handle"]; + hid_device* handle; + val data_view; + size_t length; + std::vector report; + + if(handle_value.isUndefined() || handle_value.isNull()) { + return; + } + + handle = (hid_device*)handle_value.as(); + if(!handle) { + return; + } + data_view = event["data"]; + length = data_view["byteLength"].as(); + report.resize(length + 1); + report[0] = event["reportId"].as(); + + for(size_t i = 0; i < length; ++i) { + report[i + 1] = data_view.call("getUint8", (unsigned)i); + } + + if(handle->input_report_callback) { + handle->input_report_callback(report.data(), report.size(), handle->input_report_user_data); + } else { + handle->reports.push(report); + } } -EMSCRIPTEN_BINDINGS(my_module) { +EMSCRIPTEN_BINDINGS(webhidapi_module) { function("hid_handleInputReport", &hid_handleInputReport); } HID_API_EXPORT hid_device* HID_API_CALL hid_open_path(const char* path) { - val::global("console").call("log", std::string("hid_open_path")); + std::map::const_iterator it; + hid_device* handle; + + if(!path) { + return 0; + } + + it = webhid_device_registry.find(path); + if(it == webhid_device_registry.end()) { + return 0; + } + + handle = new hid_device(); + handle->js_device = new val(*it->second); + handle->input_report_callback = 0; + handle->input_report_user_data = 0; + handle->nonblocking = 1; - val* device = (val *)path; - val::global("console").call("log", *device); - device->call("open").await(); - val::global("console").call("log", *device); - if (true == (*device)["opened"].as()) { - device->call("addEventListener", std::string("inputreport"), val::module_property("hid_handleInputReport")); -/* device->call("addEventListener", "inputreport", function(val event) { - var data = new Uint8Array(event.data.buffer); - Module._cppInputReportHandler(data.byteOffset, data.byteLength); - });*/ - return (hid_device*)path; + handle->js_device->call("open").await(); + if(!(*handle->js_device)["opened"].as()) { + delete handle->js_device; + delete handle; + return 0; } + handle->js_device->set("__spnav_handle", val((uintptr_t)handle)); + handle->js_device->call("addEventListener", std::string("inputreport"), val::module_property("hid_handleInputReport")); + return handle; +} + +HID_API_EXPORT int HID_API_CALL hid_set_callback(hid_device* dev, + hid_input_report_callback input_report_callback, void* uptr) +{ + if(!dev || !dev->js_device || !(*dev->js_device)["opened"].as()) { + return -1; + } + + dev->input_report_callback = input_report_callback; + dev->input_report_user_data = uptr; return 0; } -int HID_API_EXPORT HID_API_CALL hid_write(hid_device* dev, const unsigned char* data, size_t length) +int HID_API_EXPORT HID_API_CALL hid_write(hid_device* dev, const unsigned char* data, size_t length) { - val::global("console").call("log", std::string("hid_write")); + if(!dev || !dev->js_device || !data || length == 0) { + return -1; + } - return -1; + dev->js_device->call("sendReport", (unsigned)data[0], + val(typed_memory_view(length - 1, data + 1))).await(); + return (int)length; } -int HID_API_EXPORT HID_API_CALL hid_read(hid_device* dev, unsigned char* data, size_t length) +int HID_API_EXPORT HID_API_CALL hid_read(hid_device* dev, unsigned char* data, size_t length) { - val::global("console").call("log", std::string("hid_read")); + std::vector report; + size_t copy_len; + + if(!dev || !data || !length) { + return -1; + } - return -1; + if(dev->reports.empty()) { + return 0; + } + + report = dev->reports.front(); + dev->reports.pop(); + copy_len = length < report.size() ? length : report.size(); + memcpy(data, report.data(), copy_len); + return (int)copy_len; } -int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device* dev, int nonblock) +int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device* dev, int nonblock) { - val::global("console").call("log", std::string("hid_set_nonblocking")); + if(!dev) { + return -1; + } + dev->nonblocking = nonblock; return 0; } int HID_API_EXPORT HID_API_CALL hid_send_feature_report(hid_device* dev, const unsigned char* data, size_t length) { - val::global("console").call("log", std::string("hid_send_feature_report")); + if(!dev || !dev->js_device || !data || length == 0) { + return -1; + } - return -1; + dev->js_device->call("sendFeatureReport", (unsigned)data[0], + val(typed_memory_view(length - 1, data + 1))).await(); + return (int)length; } void HID_API_EXPORT HID_API_CALL hid_close(hid_device* dev) { - val::global("console").call("log", std::string("hid_close")); + if(!dev) { + return; + } + if(dev->js_device) { + if((*dev->js_device)["opened"].as()) { + dev->js_device->call("removeEventListener", std::string("inputreport"), + val::module_property("hid_handleInputReport")); + dev->js_device->call("close").await(); + } + delete dev->js_device; + } + + delete dev; } + +#endif /* WEBHIDAPI_H_ */