Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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)
Comment on lines +95 to +97
endif()
Comment on lines +95 to +98

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)
Comment thread
Youw marked this conversation as resolved.
Outdated
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 thread
Youw marked this conversation as resolved.

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);
Comment on lines +130 to +133
#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.
Comment on lines +448 to +450

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 thread
Youw marked this conversation as resolved.

@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.
Comment on lines +480 to +481
*/
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