Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ if(HIDAPI_BUILD_HIDTEST)
add_subdirectory(hidtest)
endif()

option(HIDAPI_BUILD_HID_READ_TEST "Build hid_read_test cmd-line tool" OFF)
if(HIDAPI_BUILD_HID_READ_TEST)
add_subdirectory(hid_read_test)
endif()

if(HIDAPI_ENABLE_ASAN)
if(NOT MSVC)
# MSVC doesn't recognize those options, other compilers - requiring it
Expand Down
42 changes: 42 additions & 0 deletions hid_read_test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
cmake_minimum_required(VERSION 3.8...3.25 FATAL_ERROR)
Copy link

Copilot AI Apr 26, 2026

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_TEST will therefore make the overall build require a newer CMake than advertised. Consider aligning this cmake_minimum_required() with the top-level requirement unless this tool genuinely needs newer CMake features.

Suggested change
cmake_minimum_required(VERSION 3.8...3.25 FATAL_ERROR)
cmake_minimum_required(VERSION 3.1.3 FATAL_ERROR)

Copilot uses AI. Check for mistakes.
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}"
)
146 changes: 146 additions & 0 deletions hid_read_test/main.cpp
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
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on_signal() calls hid_read_interrupt() directly from a signal handler. That is not async-signal-safe on POSIX (e.g. mac/libusb implementations take a pthread mutex/condvar), and can deadlock or crash if the signal interrupts code holding the same lock. Consider making the handler only set an atomic flag (or write to a self-pipe) and have the main thread (or a dedicated signal-wait thread using sigwait) call hid_read_interrupt() from a safe context.

Copilot uses AI. Check for mistakes.

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;
}
84 changes: 84 additions & 0 deletions hidapi/hidapi.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment promises cross-thread observation with acquire semantics, but at least the Linux and NetBSD backends implement the interrupt state as a plain int/volatile int without atomics or locking. Either strengthen the implementations to actually provide atomic/acquire semantics everywhere, or relax/reword this guarantee in the public header to match what is implemented.

Suggested change
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.

Copilot uses AI. Check for mistakes.

@ingroup API
@param dev A device handle returned from hid_open().

@returns
1 if the read pipeline is interrupted, 0 if not, -1 on error.
Call hid_error(dev) to get the failure reason.
*/
int HID_API_EXPORT HID_API_CALL hid_is_read_interrupted(hid_device *dev);

/** @brief Clear the read-interrupt state, allowing reads to resume.

Since version 0.16.0, @ref HID_API_VERSION >= HID_API_MAKE_VERSION(0, 16, 0)

After this call, subsequent hid_read()/hid_read_timeout() calls
operate normally. Any data that the device buffered during the
interrupted period may be returned by subsequent reads, subject
to a (backend-specific) buffer capacity. For a fresh-start
behavior, the caller may drain the buffered data with a loop of
hid_read_timeout(dev, ..., 0) calls before resuming the normal
read loop.

Recommended use: call only when no hid_read*() call is in flight
on @p dev. The timing of an in-flight read returning -1 versus
a concurrent clear-interrupt taking effect is undefined.

@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_clear_interrupt(hid_device *dev);

/** @brief Send a Feature report to the device.

Feature reports are sent over the Control endpoint as a
Expand Down
46 changes: 44 additions & 2 deletions libusb/hid.c
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ struct hid_device_ {
/* Read thread objects */
hidapi_thread_state thread_state;
int shutdown_thread;
int read_interrupted;
int transfer_loop_finished;
struct libusb_transfer *transfer;

Expand Down Expand Up @@ -1681,14 +1682,23 @@ int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t
goto ret;
}

if (dev->read_interrupted) {
bytes_read = -1;
register_read_error(dev, "hid_read(_timeout): operation interrupted");
goto ret;
}

if (milliseconds == -1) {
/* Blocking */
while (!dev->input_reports && !dev->shutdown_thread) {
while (!dev->input_reports && !dev->shutdown_thread && !dev->read_interrupted) {
hidapi_thread_cond_wait(&dev->thread_state);
}
if (dev->input_reports) {
bytes_read = return_data(dev, data, length);
}
else if (dev->read_interrupted) {
register_read_error(dev, "hid_read(_timeout): operation interrupted");
}
else {
/* Woken up by shutdown_thread without data. */
register_read_error(dev, "hid_read(_timeout): read thread terminated");
Expand All @@ -1701,13 +1711,17 @@ int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t
hidapi_thread_gettime(&ts);
hidapi_thread_addtime(&ts, milliseconds);

while (!dev->input_reports && !dev->shutdown_thread) {
while (!dev->input_reports && !dev->shutdown_thread && !dev->read_interrupted) {
res = hidapi_thread_cond_timedwait(&dev->thread_state, &ts);
if (res == 0) {
if (dev->input_reports) {
bytes_read = return_data(dev, data, length);
break;
}
if (dev->read_interrupted) {
register_read_error(dev, "hid_read(_timeout): operation interrupted");
break;
}
if (dev->shutdown_thread) {
register_read_error(dev, "hid_read(_timeout): read thread terminated");
break;
Expand Down Expand Up @@ -1763,6 +1777,34 @@ int HID_API_EXPORT hid_set_nonblocking(hid_device *dev, int nonblock)
}


int HID_API_EXPORT hid_read_interrupt(hid_device *dev)
{
hidapi_thread_mutex_lock(&dev->thread_state);
dev->read_interrupted = 1;
hidapi_thread_cond_broadcast(&dev->thread_state);
hidapi_thread_mutex_unlock(&dev->thread_state);
return 0;
}


int HID_API_EXPORT hid_is_read_interrupted(hid_device *dev)
{
hidapi_thread_mutex_lock(&dev->thread_state);
int v = dev->read_interrupted;
hidapi_thread_mutex_unlock(&dev->thread_state);
return v;
}


int HID_API_EXPORT hid_read_clear_interrupt(hid_device *dev)
{
hidapi_thread_mutex_lock(&dev->thread_state);
dev->read_interrupted = 0;
hidapi_thread_mutex_unlock(&dev->thread_state);
return 0;
}


int HID_API_EXPORT hid_send_feature_report(hid_device *dev, const unsigned char *data, size_t length)
{
int res = -1;
Expand Down
Loading
Loading