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)
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
54 changes: 54 additions & 0 deletions hid_read_test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
cmake_minimum_required(VERSION 3.1.3...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)
set_target_properties(hid_read_test_hidraw PROPERTIES
CXX_STANDARD 11
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF
)
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)
set_target_properties(hid_read_test_libusb PROPERTIES
CXX_STANDARD 11
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF
)
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)
set_target_properties(hid_read_test PROPERTIES
CXX_STANDARD 11
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF
)
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}"
)
159 changes: 159 additions & 0 deletions hid_read_test/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*******************************************************
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>

#ifndef _WIN32
#include <signal.h>
#endif

namespace {

std::atomic<bool> g_terminate{false};

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)
{
/* async-signal-safe: atomic store only. Main thread will call
hid_read_interrupt() once cin.get() returns from EINTR. */
g_terminate.store(true, std::memory_order_release);
Comment on lines +55 to +59
}
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;
}
#ifdef _WIN32
std::signal(SIGINT, on_signal);
#ifdef SIGTERM
std::signal(SIGTERM, on_signal);
Comment on lines +130 to +133
#endif
#else
/* Use sigaction without SA_RESTART so cin.get() returns on signal. */
struct sigaction sa;
sa.sa_handler = on_signal;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, nullptr);
sigaction(SIGTERM, &sa, nullptr);
#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
Loading
Loading