-
Notifications
You must be signed in to change notification settings - Fork 465
RFC: Add hid_read_interrupt API for thread-safe read cancellation #799
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| cmake_minimum_required(VERSION 3.8...3.25 FATAL_ERROR) | ||
| project(hid_read_test CXX) | ||
|
|
||
| if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) | ||
| # built as a standalone project | ||
| if(POLICY CMP0074) | ||
| cmake_policy(SET CMP0074 NEW) | ||
| endif() | ||
|
|
||
| find_package(hidapi 0.16 REQUIRED) | ||
| message(STATUS "Using HIDAPI: ${hidapi_VERSION}") | ||
| else() | ||
| message(STATUS "Building hid_read_test") | ||
| endif() | ||
|
|
||
| find_package(Threads REQUIRED) | ||
|
|
||
| set(HIDAPI_HID_READ_TEST_TARGETS) | ||
| if(NOT WIN32 AND NOT APPLE AND CMAKE_SYSTEM_NAME MATCHES "Linux") | ||
| if(TARGET hidapi::hidraw) | ||
| add_executable(hid_read_test_hidraw main.cpp) | ||
| target_compile_features(hid_read_test_hidraw PRIVATE cxx_std_11) | ||
| target_link_libraries(hid_read_test_hidraw PRIVATE hidapi::hidraw Threads::Threads) | ||
| list(APPEND HIDAPI_HID_READ_TEST_TARGETS hid_read_test_hidraw) | ||
| endif() | ||
| if(TARGET hidapi::libusb) | ||
| add_executable(hid_read_test_libusb main.cpp) | ||
| target_compile_features(hid_read_test_libusb PRIVATE cxx_std_11) | ||
| target_compile_definitions(hid_read_test_libusb PRIVATE USING_HIDAPI_LIBUSB) | ||
| target_link_libraries(hid_read_test_libusb PRIVATE hidapi::libusb Threads::Threads) | ||
| list(APPEND HIDAPI_HID_READ_TEST_TARGETS hid_read_test_libusb) | ||
| endif() | ||
| else() | ||
| add_executable(hid_read_test main.cpp) | ||
| target_compile_features(hid_read_test PRIVATE cxx_std_11) | ||
| target_link_libraries(hid_read_test PRIVATE hidapi::hidapi Threads::Threads) | ||
| list(APPEND HIDAPI_HID_READ_TEST_TARGETS hid_read_test) | ||
| endif() | ||
|
|
||
| install(TARGETS ${HIDAPI_HID_READ_TEST_TARGETS} | ||
| RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| /******************************************************* | ||
| HIDAPI - hid_read_test | ||
|
|
||
| A small C++11 cmd-line tool that opens a HID device by | ||
| VID/PID, spawns a read thread that prints input reports | ||
| as hex with timestamps, and waits for Enter or Ctrl+C | ||
| to gracefully interrupt the read and exit. | ||
|
|
||
| The contents of this file may be used by anyone for any | ||
| reason without any conditions and may be used as a | ||
| starting point for your own applications which use HIDAPI. | ||
| ********************************************************/ | ||
|
|
||
| #include <hidapi.h> | ||
|
|
||
| #include <atomic> | ||
| #include <chrono> | ||
| #include <csignal> | ||
| #include <cstdlib> | ||
| #include <ctime> | ||
| #include <iomanip> | ||
| #include <iostream> | ||
| #include <sstream> | ||
| #include <string> | ||
| #include <thread> | ||
|
|
||
| namespace { | ||
|
|
||
| std::atomic<hid_device*> g_dev{nullptr}; | ||
|
|
||
| std::string timestamp_now() | ||
| { | ||
| using namespace std::chrono; | ||
| auto now = system_clock::now(); | ||
| auto t = system_clock::to_time_t(now); | ||
| auto ms = duration_cast<milliseconds>(now.time_since_epoch()) % 1000; | ||
|
|
||
| std::tm tm_buf{}; | ||
| #ifdef _WIN32 | ||
| localtime_s(&tm_buf, &t); | ||
| #else | ||
| localtime_r(&t, &tm_buf); | ||
| #endif | ||
|
|
||
| std::ostringstream ss; | ||
| ss << std::put_time(&tm_buf, "%Y-%m-%d %H:%M:%S") | ||
| << '.' << std::setfill('0') << std::setw(3) << ms.count(); | ||
| return ss.str(); | ||
| } | ||
|
|
||
| extern "C" void on_signal(int) | ||
| { | ||
| if (hid_device *d = g_dev.load(std::memory_order_acquire)) | ||
| hid_read_interrupt(d); | ||
| } | ||
|
Comment on lines
+51
to
+55
|
||
|
|
||
| void read_thread_fn(hid_device *dev) | ||
| { | ||
| unsigned char buf[4096]; | ||
| const int max_retries = 3; | ||
| int errors = 0; | ||
|
|
||
| for (;;) { | ||
| int n = hid_read_timeout(dev, buf, sizeof(buf), -1); | ||
| if (n < 0) { | ||
| std::cout << '[' << timestamp_now() << "] read returned -1"; | ||
| const wchar_t *err = hid_read_error(dev); | ||
| if (err) std::wcout << L": " << err; | ||
| std::cout << std::endl; | ||
|
|
||
| if (hid_is_read_interrupted(dev)) | ||
| break; | ||
|
|
||
| if (++errors > max_retries) { | ||
| std::cout << '[' << timestamp_now() << "] giving up after " | ||
| << max_retries << " consecutive errors" << std::endl; | ||
| break; | ||
| } | ||
| continue; | ||
| } | ||
|
|
||
| errors = 0; /* reset on a successful read */ | ||
|
|
||
| if (n == 0) continue; | ||
|
|
||
| std::cout << '[' << timestamp_now() << "] "; | ||
| std::cout << std::hex << std::setfill('0'); | ||
| for (int i = 0; i < n; ++i) | ||
| std::cout << std::setw(2) << static_cast<unsigned>(buf[i]) << ' '; | ||
| std::cout << std::dec << std::endl; | ||
| } | ||
| } | ||
|
|
||
| unsigned short parse_hex_u16(const char *s) | ||
| { | ||
| return static_cast<unsigned short>(std::strtoul(s, nullptr, 16)); | ||
| } | ||
|
|
||
| } // anonymous namespace | ||
|
|
||
| int main(int argc, char **argv) | ||
| { | ||
| if (argc < 3) { | ||
| std::cerr << "Usage: " << argv[0] << " <vid> <pid>\n" | ||
| << " vid, pid: hex (e.g. 04d8 003f)\n"; | ||
| return 1; | ||
| } | ||
|
|
||
| unsigned short vid = parse_hex_u16(argv[1]); | ||
| unsigned short pid = parse_hex_u16(argv[2]); | ||
|
|
||
| if (hid_init() != 0) { | ||
| std::cerr << "hid_init failed\n"; | ||
| return 1; | ||
| } | ||
|
|
||
| hid_device *dev = hid_open(vid, pid, nullptr); | ||
| if (!dev) { | ||
| std::cerr << "hid_open failed for VID=" << std::hex << std::setw(4) | ||
| << std::setfill('0') << vid << " PID=" << std::setw(4) << pid | ||
| << std::dec << '\n'; | ||
| hid_exit(); | ||
| return 1; | ||
| } | ||
| g_dev.store(dev, std::memory_order_release); | ||
|
|
||
| std::signal(SIGINT, on_signal); | ||
| #ifdef SIGTERM | ||
| std::signal(SIGTERM, on_signal); | ||
| #endif | ||
|
|
||
| std::cout << "Reading from VID=" << std::hex << std::setw(4) << std::setfill('0') | ||
| << vid << " PID=" << std::setw(4) << pid << std::dec | ||
| << " (press Enter or Ctrl+C to exit)" << std::endl; | ||
|
|
||
| std::thread reader(read_thread_fn, dev); | ||
|
|
||
| std::cin.get(); /* Enter, EOF, or POSIX-EINTR from a signal */ | ||
|
|
||
| hid_read_interrupt(dev); /* idempotent — also safe if signal handler ran first */ | ||
| reader.join(); | ||
|
|
||
| hid_close(dev); | ||
| hid_exit(); | ||
| return 0; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -423,6 +423,90 @@ extern "C" { | |||||||||||||
| */ | ||||||||||||||
| int HID_API_EXPORT HID_API_CALL hid_set_nonblocking(hid_device *dev, int nonblock); | ||||||||||||||
|
|
||||||||||||||
| /** @brief Asynchronously interrupt blocking hid_read/hid_read_timeout call. | ||||||||||||||
|
|
||||||||||||||
| Since version 0.16.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 16, 0) | ||||||||||||||
|
|
||||||||||||||
| Thread-safely interrupts a blocking hid_read()/hid_read_timeout() call | ||||||||||||||
| currently in progress (or about to start) on @p dev. The interrupted | ||||||||||||||
| call returns -1 immediately; hid_read_error(dev) returns a string | ||||||||||||||
| indicating the read was interrupted. | ||||||||||||||
|
|
||||||||||||||
| Once interrupted, subsequent calls to hid_read()/ | ||||||||||||||
| hid_read_timeout() also return -1 immediately, until @ref | ||||||||||||||
| hid_read_clear_interrupt is called. | ||||||||||||||
|
|
||||||||||||||
| A hid_read*() call that observes data already buffered or | ||||||||||||||
| queued before the interrupt may return that data; otherwise it | ||||||||||||||
| returns -1. Eventually (within at most one further call) hid_read*() | ||||||||||||||
| will return -1. | ||||||||||||||
|
|
||||||||||||||
| Only the read pipeline is affected: hid_write(), hid_get_input_report(), | ||||||||||||||
| feature/output report functions, and all other operations on @p dev | ||||||||||||||
| continue to work normally. | ||||||||||||||
|
|
||||||||||||||
| This is the only function on hid_device that may be called concurrently | ||||||||||||||
| with another function operating on the same device. The call is | ||||||||||||||
| idempotent and is safe to invoke when no read is in flight. | ||||||||||||||
|
|
||||||||||||||
| Recommended usage to cleanly shut down a dedicated read thread: | ||||||||||||||
| @code | ||||||||||||||
| hid_read_interrupt(dev); | ||||||||||||||
| // join the read thread | ||||||||||||||
| hid_close(dev); | ||||||||||||||
| @endcode | ||||||||||||||
|
|
||||||||||||||
| @ingroup API | ||||||||||||||
| @param dev A device handle returned from hid_open(). | ||||||||||||||
|
|
||||||||||||||
| @returns | ||||||||||||||
| This function returns 0 on success and -1 on error. | ||||||||||||||
| Call hid_error(dev) to get the failure reason. | ||||||||||||||
| */ | ||||||||||||||
| int HID_API_EXPORT HID_API_CALL hid_read_interrupt(hid_device *dev); | ||||||||||||||
|
|
||||||||||||||
| /** @brief Query whether the read pipeline is currently interrupted. | ||||||||||||||
|
|
||||||||||||||
| Since version 0.16.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 16, 0) | ||||||||||||||
|
|
||||||||||||||
| Returns the current interrupt state set by @ref hid_read_interrupt | ||||||||||||||
| and @ref hid_read_clear_interrupt. Suitable for cross-thread | ||||||||||||||
| observation (read with acquire semantics). | ||||||||||||||
|
Comment on lines
+473
to
+474
|
||||||||||||||
| and @ref hid_read_clear_interrupt. Suitable for cross-thread | |
| observation (read with acquire semantics). | |
| and @ref hid_read_clear_interrupt. This reports the library's | |
| current interrupt flag for @p dev, but does not by itself provide | |
| any additional cross-thread synchronization or memory-ordering | |
| guarantees. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CMake minimum version here (3.8) is higher than the repository’s top-level minimum (3.1.3). Enabling
HIDAPI_BUILD_HID_READ_TESTwill therefore make the overall build require a newer CMake than advertised. Consider aligning thiscmake_minimum_required()with the top-level requirement unless this tool genuinely needs newer CMake features.