Inital commit

This commit is contained in:
2026-06-14 19:18:06 +02:00
commit 4d15897b87
21 changed files with 1450 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
[*]
end_of_line = LF
charset = utf-8
indent_style = space
indent_size = 4
insert_final_newline = true
[*.{yml,json,lua,md,html}]
charset = utf-8
indent_style = space
indent_size = 2
[Makefile]
indent_style = tab
+16
View File
@@ -0,0 +1,16 @@
# CMake
build
build-*
cmake-build-*
coverage
# Editors
.vscode
.idea
.ccls
.ccls-cache
.cache
compile_commands.json
# macOS
.DS_Store
+146
View File
@@ -0,0 +1,146 @@
# AGENTS.md
## Project Overview
`stk` is a C23 library for reading EdgeTx radio joystick input via
HID. Wired for TDD using Unity and CMock.
## Build System
- **Generator:** Ninja
- **CMake minimum:** 3.21
- **C standard:** C23
### Commands
Configure (default):
```sh
cmake -S . -B build -G Ninja
```
Configure with coverage:
```sh
cmake -S . -B build-cov -G Ninja -DENABLE_COVERAGE=ON
```
Build:
```sh
ninja -C build
```
Run tests (full Unity output, colored):
```sh
ninja -C build check
```
Run tests (CTest summary only):
```sh
ninja -C build test
```
Run tests and generate coverage HTML:
```sh
ninja -C build-cov coverage
```
Run (Linux/macOS):
```sh
./build/main
```
Run (Windows):
```sh
./build/main.exe
```
### Dependencies
Dependencies are managed via custom `Find*.cmake` scripts in `deps/`.
These scripts use `FetchContent` under the hood to download and build
libraries automatically.
To add a new dependency:
1. Add the corresponding `Find<name>.cmake` to `deps/`
2. Add `find_package(<name> REQUIRED)` to `CMakeLists.txt`
3. Link with `<name>::<name>` in `target_link_libraries()`
### CMake Module Path
`deps/` is added to `CMAKE_MODULE_PATH` so `find_package()` resolves
to the custom scripts instead of system-installed packages.
## Coding Conventions
- **Language:** C23
- **4-space indentation**
- **Naming:** `snake_case` for variables, functions, and types
- **Naming:** `SCREAMING_SNAKE_CASE` only for macros and constants
- `<>` includes only for system headers (std, OS, etc.)
- `""` includes for project headers
- Include order:
1. C standard library headers (`<stdlib.h>`, `<string.h>`, etc.)
2. *(blank line)*
3. OS-specific headers (Windows API, POSIX, etc.)
4. *(blank line)*
5. Third-party dependencies
6. *(blank line)*
7. Local/project headers
## Shell Scripts
- Always use `#!/bin/sh` shebang for shell scripts
- Scripts must be POSIX compliant (no bashisms)
- When providing commands to users:
- Windows/PowerShell: use `` ` `` for line continuation
- Unix/Linux/macOS: use `\` for line continuation
## Commit Messages
- Follow the 50/72 rule:
- Subject line: max 50 characters
- Body lines: wrapped at 72 characters
- Use conventional commit prefixes (`feat:`, `fix:`, `docs:`, `chore:`,
etc.)
- Separate subject from body with a blank line
- Do **not** add yourself as a co-author (`Co-Authored-By:` trailers are
forbidden)
## Documentation (Markdown)
- Wrap normal text and lists at **max 80 columns** (for readability in
terminals and editors).
- **Exceptions**: Tables and code blocks (```` ``` ````) can exceed 80
columns when formatting requires it (e.g. trees, alignment).
- Use standard Markdown: `**bold**`, `` `inline code` ``, `##` headings,
`-` or numbered lists, fenced code blocks with language hints.
- Keep examples concise, up-to-date, and self-documenting.
- Do not use em dashes (`—`). Use a colon or rewrite the sentence.
- Each shell command gets its own fenced code block; do **not** combine
multiple commands into one block. Precede each block with a short
plain-text label describing what the command does.
- This file (`AGENTS.md`) follows its own rules.
## Source Layout
```text
stk/
stk.h / stk_win.c EdgeTX HID radio reader (stub for now)
src/
main.c Entry point (hello world)
tests/
test_stk.c Unity tests for stk
deps/
FindUnity.cmake Fetches Unity v2.6.1 via ZIP
FindCMock.cmake Fetches CMock v2.6.0 via ZIP
Platform.cmake Detects OS and compiler
Flags.cmake Warning flags per compiler
Coverage.cmake gcovr coverage support
Sanitizers.cmake AddressSanitizer support
IDE.cmake VS/Xcode folder grouping
```
## Platform Support
The project supports Windows, Linux, macOS, Emscripten, and Android via
`Platform.cmake` and `Flags.cmake` in `deps/`.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+35
View File
@@ -0,0 +1,35 @@
cmake_minimum_required(VERSION 3.21)
project(stk VERSION 0.1.0 LANGUAGES C)
# CMake configuration
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/deps")
# Platform flags
include(Platform)
include(Flags)
include(Coverage)
# STK library
add_subdirectory(stk)
# Main executable
add_executable(main src/main.c)
target_include_directories(main PRIVATE .)
target_compile_features(main PRIVATE c_std_23)
target_link_libraries(main PRIVATE stkshid)
# HID discovery tool (debug only)
if(WIN32)
add_executable(hid_discover tools/hid_discover.c)
target_compile_features(hid_discover PRIVATE c_std_23)
target_link_libraries(hid_discover PRIVATE setupapi hid)
endif()
# Testing
enable_testing()
add_subdirectory(tests)
# IDE configuration
include(IDE)
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+140
View File
@@ -0,0 +1,140 @@
# STK Library
A C23 library for reading EdgeTX radio joystick (HID) input on Windows.
Reads raw HID input reports to access 4 analog axes and 24 buttons.
Wired for test-driven development using
[Unity](https://github.com/ThrowTheSwitch/Unity) and
[CMock](https://github.com/ThrowTheSwitch/CMock).
All dependencies are fetched automatically via CMake `FetchContent` — no
manual installation required beyond the tools listed below.
## Requirements
| Tool | Purpose |
| ------------- | ----------------------------------------------------- |
| CMake >= 3.21 | Build system |
| Ninja | Build backend |
| C23 compiler | GCC 14+, Clang 18+ |
| Ruby >= 3.0 | CMock mock generation |
| gcovr >= 6.0 | Coverage reports — optional (`uv tool install gcovr`) |
## Build
Configure:
```sh
cmake -S . -B build -G Ninja
```
Build:
```sh
ninja -C build
```
## Test
Full Unity output with colors:
```sh
ninja -C build check
```
CTest summary only:
```sh
ninja -C build test
```
`check` builds all suites and runs CTest with `--output-on-failure`,
so assertion-level detail appears on any failure without running
binaries by hand.
## Coverage
Configure with coverage instrumentation:
```sh
cmake -S . -B build-cov -G Ninja -DENABLE_COVERAGE=ON
```
Generate the HTML report:
```sh
ninja -C build-cov coverage
```
Open `build-cov/coverage/index.html` in a browser to view results.
Only `stk/` source files are measured. Unity, CMock, and generated
mock files are excluded. Requires GCC or Clang with gcov support, and
`gcovr` on `PATH`.
> **Windows note:** requires GCC (e.g. `scoop install gcc`) or a Clang
> build that includes compiler-rt. A custom Clang without compiler-rt
> will fail at link time.
## Run
Windows:
```sh
./build/main.exe
```
## API
```c
#include <stk/stk.h>
// Open with default VID/PID
stk_config_t config = {
.vendor_id = STK_DEFAULT_VID,
.product_id = STK_DEFAULT_PID,
};
if (stk_open(&config) != 0) {
// handle error
}
// Read state (non-blocking)
stk_state_t state;
stk_read(&state);
// Named accessors for the 4 active axes
int16_t rx = stk_right_x(&state);
int16_t ry = stk_right_y(&state);
int16_t throttle = stk_throttle(&state);
int16_t lx = stk_left_x(&state);
// Raw axes array (indices 0-7, only 1/2/4/5 active)
// state.axes[0..7] — values in range STK_AXIS_MIN..MAX (0..2047)
// Button check
if (stk_button_pressed(&state, 3)) {
// switch 4 is on
}
// Close the device
stk_close();
```
## Radio Profile
EdgeTX radio (VID:PID 1209:4F54) HID input report:
| Field | Size | Details |
| ------- | ---------- | --------------------------------------------------- |
| Axes | 4 x 16-bit | Range 0-2047 (HID usage 0x30-0x37, indices 1/2/4/5) |
| Buttons | 24 bits | Switches, rockers, paddles |
| Total | 20 bytes | No report ID |
## Current Status
- [x] Build system (CMake + Ninja)
- [x] Raw HID device enumeration and opening
- [x] Reading 4 axes and 24 buttons
- [ ] Linux support
- [ ] macOS support
+56
View File
@@ -0,0 +1,56 @@
# ==============================================================================
# Coverage
# ==============================================================================
# gcov-based coverage via --coverage, reported by gcovr.
# Works with GCC and Clang on Linux and macOS out of the box.
# Windows requires GCC (e.g. MinGW via scoop install gcc) or a Clang build
# that includes compiler-rt (clang_rt.profile).
#
# Requires: gcovr on PATH (install via: uv tool install gcovr)
# Usage: cmake -DENABLE_COVERAGE=ON ... then ninja coverage
# Report: build/coverage/index.html
# ==============================================================================
option(ENABLE_COVERAGE "Build with coverage instrumentation" OFF)
if (ENABLE_COVERAGE)
if (NOT CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
message(FATAL_ERROR "ENABLE_COVERAGE requires GCC or Clang")
endif()
find_program(GCOVR_EXE gcovr REQUIRED)
if (CMAKE_C_COMPILER_ID MATCHES "Clang")
# gcovr needs a single-token gcov executable. Wrap llvm-cov gcov in a
# script placed in the build dir (guaranteed no spaces in path).
# AppleClang ships llvm-cov inside Xcode, reached only via xcrun.
if (WIN32)
find_program(LLVM_COV_EXE llvm-cov REQUIRED)
set(GCOV_EXECUTABLE "${CMAKE_BINARY_DIR}/llvm-gcov.bat")
file(WRITE "${GCOV_EXECUTABLE}" "@echo off\r\n\"${LLVM_COV_EXE}\" gcov %*\r\n")
elseif (CMAKE_C_COMPILER_ID STREQUAL "AppleClang")
find_program(XCRUN_EXE xcrun REQUIRED)
set(GCOV_EXECUTABLE "${CMAKE_BINARY_DIR}/llvm-gcov.sh")
file(WRITE "${GCOV_EXECUTABLE}" "#!/bin/sh\nexec xcrun llvm-cov gcov \"$@\"\n")
file(CHMOD "${GCOV_EXECUTABLE}"
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE)
else()
find_program(LLVM_COV_EXE llvm-cov REQUIRED)
set(GCOV_EXECUTABLE "${CMAKE_BINARY_DIR}/llvm-gcov.sh")
file(WRITE "${GCOV_EXECUTABLE}" "#!/bin/sh\nexec \"${LLVM_COV_EXE}\" gcov \"$@\"\n")
file(CHMOD "${GCOV_EXECUTABLE}"
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE)
endif()
message(STATUS "Coverage: enabled (gcovr: ${GCOVR_EXE}, gcov: llvm-cov gcov)")
else()
set(GCOV_EXECUTABLE "gcov")
message(STATUS "Coverage: enabled (gcovr: ${GCOVR_EXE})")
endif()
add_compile_options(--coverage -O0 -g)
add_link_options(--coverage)
endif()
+92
View File
@@ -0,0 +1,92 @@
# ==============================================================================
# Find CMock
# ==============================================================================
# This module fetches the CMock mocking framework (depends on Unity).
#
# Targets provided:
# CMock::CMock - The CMock library target
#
# Variables set:
# CMock_FOUND - TRUE if CMock is available
# CMock_LIBRARIES - The CMock library target (CMock::CMock)
# CMock_INCLUDE_DIR - Include directories for CMock
# CMock_VERSION - Version of CMock (if available)
#
# Generator variables (set when Ruby is found):
# CMOCK_SCRIPT - Path to lib/cmock.rb
# RUBY_EXECUTABLE - Path to ruby interpreter
# ==============================================================================
if (DEFINED _FINDCMOCK_INCLUDED)
return()
endif()
set(_FINDCMOCK_INCLUDED TRUE)
find_package(Unity REQUIRED)
if (DEFINED CMock_FIND_VERSION AND NOT CMock_FIND_VERSION STREQUAL "")
set(CMOCK_VERSION "${CMock_FIND_VERSION}")
else()
set(CMOCK_VERSION "2.6.0")
endif()
message(STATUS "Fetching CMock ${CMOCK_VERSION}")
include(FetchContent)
FetchContent_Declare(
cmock
URL https://github.com/ThrowTheSwitch/CMock/archive/refs/tags/v${CMOCK_VERSION}.zip
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
# CMock uses Meson — bypass its build system and compile src/cmock.c directly.
# FetchContent_MakeAvailable cannot be used here (no CMakeLists.txt in CMock),
# so we call FetchContent_Populate directly and opt into the old policy.
cmake_policy(PUSH)
cmake_policy(SET CMP0169 OLD)
FetchContent_GetProperties(cmock)
if (NOT cmock_POPULATED)
FetchContent_Populate(cmock)
endif()
cmake_policy(POP)
# The Ruby generator expects vendor/unity/auto/type_sanitizer.rb — populate
# it from the Unity source we already have rather than needing a git submodule
set(_cmock_vendor_auto "${cmock_SOURCE_DIR}/vendor/unity/auto")
if (NOT EXISTS "${_cmock_vendor_auto}/type_sanitizer.rb")
file(MAKE_DIRECTORY "${_cmock_vendor_auto}")
file(COPY "${unity_SOURCE_DIR}/auto/" DESTINATION "${_cmock_vendor_auto}")
endif()
if (NOT TARGET cmock)
add_library(cmock STATIC "${cmock_SOURCE_DIR}/src/cmock.c")
target_include_directories(cmock PUBLIC
"${cmock_SOURCE_DIR}/src"
"${unity_SOURCE_DIR}/src"
)
target_link_libraries(cmock PUBLIC unity)
endif()
if (NOT TARGET CMock::CMock)
add_library(CMock::CMock ALIAS cmock)
endif()
set(CMock_FOUND TRUE)
set(CMock_LIBRARIES CMock::CMock)
set(CMock_VERSION "${CMOCK_VERSION}")
set(CMock_INCLUDE_DIR "${cmock_SOURCE_DIR}/src")
set(CMOCK_SCRIPT "${cmock_SOURCE_DIR}/lib/cmock.rb" CACHE FILEPATH "Path to CMock Ruby generator")
if (CMock_INCLUDE_DIR AND TARGET cmock)
set_target_properties(cmock PROPERTIES
INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "${CMock_INCLUDE_DIR}"
)
endif()
find_program(RUBY_EXECUTABLE ruby)
if (NOT RUBY_EXECUTABLE)
message(WARNING "Ruby not found — CMock code generation unavailable")
endif()
set(CMOCK_LICENSE_FILE "${cmock_SOURCE_DIR}/LICENSE.txt" CACHE FILEPATH "Path to CMock license file")
+67
View File
@@ -0,0 +1,67 @@
# ==============================================================================
# Find Unity
# ==============================================================================
# This module fetches the Unity unit testing framework.
#
# Targets provided:
# Unity::Unity - The Unity library target
#
# Variables set:
# Unity_FOUND - TRUE if Unity is available
# Unity_LIBRARIES - The Unity library target (Unity::Unity)
# Unity_INCLUDE_DIR - Include directories for Unity
# Unity_VERSION - Version of Unity (if available)
# ==============================================================================
if (DEFINED _FINDUNITY_INCLUDED)
return()
endif()
set(_FINDUNITY_INCLUDED TRUE)
if (DEFINED Unity_FIND_VERSION AND NOT Unity_FIND_VERSION STREQUAL "")
set(UNITY_VERSION "${Unity_FIND_VERSION}")
else()
set(UNITY_VERSION "2.6.1")
endif()
message(STATUS "Fetching Unity ${UNITY_VERSION}")
include(FetchContent)
FetchContent_Declare(
unity
URL https://github.com/ThrowTheSwitch/Unity/archive/refs/tags/v${UNITY_VERSION}.zip
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
FetchContent_MakeAvailable(unity)
# Unity sets INTERFACE_SYSTEM_INCLUDE_DIRECTORIES to a path inside the build
# tree, which CMake rejects on newer versions. The path stays in
# INTERFACE_INCLUDE_DIRECTORIES so headers are still found.
if (TARGET unity)
set_target_properties(unity PROPERTIES INTERFACE_SYSTEM_INCLUDE_DIRECTORIES "")
endif()
if (NOT TARGET Unity::Unity)
if (TARGET unity)
add_library(Unity::Unity ALIAS unity)
else()
message(FATAL_ERROR "Could not fetch Unity; no target unity or Unity::Unity available")
endif()
endif()
set(Unity_FOUND TRUE)
set(Unity_LIBRARIES Unity::Unity)
set(Unity_VERSION "${UNITY_VERSION}")
set(Unity_INCLUDE_DIR "${unity_SOURCE_DIR}/src")
if (TARGET unity)
target_compile_definitions(unity PUBLIC
UNITY_OUTPUT_COLOR
UNITY_INCLUDE_PRINT_FORMATTED
)
endif()
set(UNITY_LICENSE_FILE "${unity_SOURCE_DIR}/LICENSE.txt" CACHE FILEPATH "Path to Unity license file")
+27
View File
@@ -0,0 +1,27 @@
# ==============================================================================
# Compiler Flags
# ==============================================================================
# Sets BASE_OPTIONS (warning flags) and BASE_DEFINITIONS.
# Apply per-target via target_compile_options / target_compile_definitions
# to avoid polluting fetched dependencies.
#
# Requires: Platform.cmake (for IS_CLANG_OR_GCC / IS_MSVC)
# ==============================================================================
set(BASE_DEFINITIONS "")
set(BASE_LIBRARIES "")
set(BASE_OPTIONS "")
if (IS_CLANG_OR_GCC)
set(BASE_OPTIONS
"-Wall"
"-Wextra"
"-Werror"
)
elseif (IS_MSVC)
set(BASE_OPTIONS
"/W4"
"/WX"
"/utf-8"
)
endif()
+17
View File
@@ -0,0 +1,17 @@
# ==============================================================================
# IDE Integration
# ==============================================================================
# Groups dependency targets into folders for Visual Studio / Xcode.
# Has no effect on the build.
# ==============================================================================
function(set_target_folder target folder)
if (TARGET ${target})
set_target_properties(${target} PROPERTIES FOLDER ${folder})
endif()
endfunction()
if (CMAKE_GENERATOR MATCHES "Visual Studio" OR CMAKE_GENERATOR MATCHES "Xcode")
set_target_folder(unity deps)
set_target_folder(cmock deps)
endif()
+64
View File
@@ -0,0 +1,64 @@
# ==============================================================================
# Platform Detection
# ==============================================================================
# This module detects the current platform and compiler, setting IS_* variables
# that can be used throughout the build system for conditional logic.
#
# Compiler flags set:
# IS_CLANG_OR_GCC - TRUE if using Clang or GCC compiler
# IS_MSVC - TRUE if using Microsoft Visual C++ compiler
#
# Platform flags set:
# IS_WINDOWS - TRUE if building for Windows
# IS_LINUX - TRUE if building for Linux
# IS_MACOS - TRUE if building for macOS
# IS_IOS - TRUE if building for iOS
# IS_ANDROID - TRUE if building for Android
# IS_EMSCRIPTEN - TRUE if building for WebAssembly via Emscripten
# ==============================================================================
# ------------------------------------------------------------------------------
# Compiler Detection
# ------------------------------------------------------------------------------
set(IS_CLANG_OR_GCC FALSE)
set(IS_MSVC FALSE)
if(MSVC)
set(IS_MSVC TRUE)
elseif(CMAKE_C_COMPILER_ID MATCHES "Clang|GNU")
set(IS_CLANG_OR_GCC TRUE)
endif()
# ------------------------------------------------------------------------------
# Platform Detection
# ------------------------------------------------------------------------------
set(IS_WINDOWS FALSE)
set(IS_LINUX FALSE)
set(IS_MACOS FALSE)
set(IS_IOS FALSE)
set(IS_ANDROID FALSE)
set(IS_EMSCRIPTEN FALSE)
if(EMSCRIPTEN)
message(STATUS "Platform: Emscripten")
set(IS_EMSCRIPTEN TRUE)
elseif(ANDROID)
message(STATUS "Platform: Android")
set(IS_ANDROID TRUE)
elseif(APPLE)
if(IOS)
message(STATUS "Platform: iOS")
set(IS_IOS TRUE)
else()
message(STATUS "Platform: macOS")
set(IS_MACOS TRUE)
endif()
elseif(WIN32)
message(STATUS "Platform: Windows")
set(IS_WINDOWS TRUE)
elseif(UNIX)
message(STATUS "Platform: Linux")
set(IS_LINUX TRUE)
else()
message(FATAL_ERROR "Unknown platform!")
endif()
+28
View File
@@ -0,0 +1,28 @@
# ==============================================================================
# Sanitizers
# ==============================================================================
# AddressSanitizer (ASan) support.
# Works with GCC/Clang on Linux and macOS, Clang on Windows (requires
# compiler-rt with sanitizers), and MSVC on Windows (/fsanitize=address).
#
# Usage: cmake -DENABLE_ASAN=ON ...
# ==============================================================================
option(ENABLE_ASAN "Build with AddressSanitizer" OFF)
if (ENABLE_ASAN)
if (ENABLE_COVERAGE)
message(FATAL_ERROR "ENABLE_ASAN and ENABLE_COVERAGE cannot be used together")
endif()
if (MSVC)
add_compile_options(/fsanitize=address)
message(STATUS "ASan: enabled (MSVC)")
elseif (CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
add_link_options(-fsanitize=address)
message(STATUS "ASan: enabled (${CMAKE_C_COMPILER_ID})")
else()
message(FATAL_ERROR "ENABLE_ASAN: unsupported compiler ${CMAKE_C_COMPILER_ID}")
endif()
endif()
+36
View File
@@ -0,0 +1,36 @@
#include <stdio.h>
#include <stdint.h>
#include <windows.h>
#include "stk/stk.h"
int main(void) {
stk_config_t config = {
.vendor_id = STK_DEFAULT_VID,
.product_id = STK_DEFAULT_PID,
};
if (stk_open(&config) != 0) {
fprintf(stderr, "warning: stk_open failed (no device connected)\n");
return 1;
}
printf("device opened, reading state... (Ctrl+C to exit)\n");
printf("move sticks and flip switches on the radio\n\n");
for (int i = 0; i < 500; i++) {
stk_state_t state;
if (stk_read(&state) == 0) {
printf("right_x=%4d right_y=%4d throttle=%4d left_x=%4d "
"buttons=0x%08X\n",
stk_right_x(&state),
stk_right_y(&state),
stk_throttle(&state),
stk_left_x(&state),
state.buttons);
}
Sleep(100);
}
stk_close();
return 0;
}
+4
View File
@@ -0,0 +1,4 @@
add_library(stkshid STATIC platform/win/stk_win.c)
target_include_directories(stkshid PUBLIC "${CMAKE_SOURCE_DIR}")
target_compile_features(stkshid PRIVATE c_std_23)
target_link_libraries(stkshid PRIVATE setupapi hid)
+124
View File
@@ -0,0 +1,124 @@
#include "stk/stk.h"
#include <windows.h>
#include <setupapi.h>
#include <hidsdi.h>
#include <stdint.h>
#include <stdio.h>
// GUID_DEVINTERFACE_HID from devguid.h
static const GUID GUID_DEVINTERFACE_HID = {
0x4d1e55b2, 0xf16f, 0x11cf,
{ 0x88, 0xcb, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }
};
// ---------------------------------------------------------------------------
// Internal state
// ---------------------------------------------------------------------------
static HANDLE g_handle = INVALID_HANDLE_VALUE;
// ---------------------------------------------------------------------------
// Platform backend
// ---------------------------------------------------------------------------
int stk_open(stk_config_t const* config) {
if (!config)
return -1;
HDEVINFO info = SetupDiGetClassDevsW(&GUID_DEVINTERFACE_HID, NULL,
NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
if (info == INVALID_HANDLE_VALUE)
return -1;
for (DWORD i = 0; ; i++) {
SP_DEVICE_INTERFACE_DATA iface = { .cbSize = sizeof(iface) };
if (!SetupDiEnumDeviceInterfaces(info, NULL,
&GUID_DEVINTERFACE_HID, i, &iface))
break;
DWORD buflen = 0;
SetupDiGetDeviceInterfaceDetailW(info, &iface, NULL, 0, &buflen, NULL);
if (buflen == 0)
continue;
PSP_DEVICE_INTERFACE_DETAIL_DATA_W detail =
(PSP_DEVICE_INTERFACE_DETAIL_DATA_W)HeapAlloc(GetProcessHeap(),
0, buflen);
detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W);
if (!SetupDiGetDeviceInterfaceDetailW(info, &iface, detail, buflen,
&buflen, NULL)) {
HeapFree(GetProcessHeap(), 0, detail);
continue;
}
HANDLE handle = CreateFileW(detail->DevicePath,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, 0,
NULL);
HeapFree(GetProcessHeap(), 0, detail);
if (handle == INVALID_HANDLE_VALUE)
continue;
HIDD_ATTRIBUTES attrs = { .Size = sizeof(attrs) };
if (!HidD_GetAttributes(handle, &attrs)) {
CloseHandle(handle);
continue;
}
if (attrs.VendorID == config->vendor_id &&
attrs.ProductID == config->product_id) {
g_handle = handle;
SetupDiDestroyDeviceInfoList(info);
return 0;
}
CloseHandle(handle);
}
SetupDiDestroyDeviceInfoList(info);
fprintf(stderr, "stk: device 0x%04X:0x%04X not found\n",
config->vendor_id, config->product_id);
return -1;
}
int stk_read(stk_state_t* state) {
if (g_handle == INVALID_HANDLE_VALUE)
return -1;
uint8_t buf[64];
DWORD bytes = 0;
if (!ReadFile(g_handle, buf, sizeof(buf), &bytes, NULL))
return -1;
if (bytes < 20)
return -1;
// Parse axes (little-endian 16-bit values, offset by 2 bytes)
for (int i = 0; i < STK_NUM_AXES; i++) {
size_t off = 2 + (size_t)i * 2;
uint16_t val = (uint16_t)buf[off] |
((uint16_t)buf[off + 1] << 8);
state->axes[i] = (int16_t)val;
}
// Parse buttons (byte 1 + bytes 18-19 = 24 bits)
state->buttons = (uint32_t)buf[1] |
((uint32_t)buf[18] << 8) |
((uint32_t)buf[19] << 16);
return 0;
}
void stk_close(void) {
if (g_handle != INVALID_HANDLE_VALUE) {
CloseHandle(g_handle);
g_handle = INVALID_HANDLE_VALUE;
}
}
+124
View File
@@ -0,0 +1,124 @@
/**
* @file stk.h
* @brief EdgeTX RC library interface for HID joystick mode.
*
* Reads raw HID input reports from the EdgeTX radio and exposes
* 4 analog axes (sticks + throttle) and 24 digital buttons
* (switches, rockers, paddles).
*
* @par Report layout (20 bytes):
* @code
* byte 0 : report ID (0x00)
* byte 1 : buttons bits 0..7
* bytes 2..17 : 8 axes x 16-bit LE (HID usage 0x30..0x37)
* bytes 18..19 : buttons bits 8..23
* @endcode
*
* @par Active axes:
* | Index | Usage | Physical control | Rest value |
* |-------|-------|-----------------|------------|
* | 0 | — | unused | always 0 |
* | 1 | RX | right stick X | ~1024 |
* | 2 | RY | right stick Y | ~1024 |
* | 3 | — | unused | always 0 |
* | 4 | Z | left stick Y | 0..2047 |
* | 5 | VR | left stick X | ~1024 |
* | 6..7 | — | unused | always 0 |
*/
#pragma once
#include <stdint.h>
// ---------------------------------------------------------------------------
// Axis range (10-bit ADC on the radio)
// ---------------------------------------------------------------------------
#define STK_AXIS_MIN 0
#define STK_AXIS_MAX 2047
#define STK_AXIS_MID 1024
// ---------------------------------------------------------------------------
// Default device IDs (EdgeTX radio in HID joystick mode)
// ---------------------------------------------------------------------------
#define STK_DEFAULT_VID 0x1209
#define STK_DEFAULT_PID 0x4F54
// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------
typedef struct {
uint16_t vendor_id;
uint16_t product_id;
} stk_config_t;
// ---------------------------------------------------------------------------
// Radio state
//
// axes[] holds all 8 HID axes (indices 0-7). Only indices 1, 2, 4, 5
// are wired to physical controls. The rest stay at 0.
//
// buttons is a 24-bit bitmask: bit N set = switch/button N+1 pressed.
// ---------------------------------------------------------------------------
#define STK_NUM_AXES 8
#define STK_NUM_BUTTONS 24
typedef struct {
int16_t axes[STK_NUM_AXES];
uint32_t buttons;
} stk_state_t;
// ---------------------------------------------------------------------------
// Convenience accessors for the 4 active axes
// ---------------------------------------------------------------------------
static inline int16_t stk_right_x(stk_state_t const* s) {
return s->axes[1];
}
static inline int16_t stk_right_y(stk_state_t const* s) {
return s->axes[2];
}
static inline int16_t stk_throttle(stk_state_t const* s) {
return s->axes[4];
}
static inline int16_t stk_left_x(stk_state_t const* s) {
return s->axes[5];
}
// ---------------------------------------------------------------------------
// Button bitmask helpers (bit 0 = first switch, bit 23 = last)
// ---------------------------------------------------------------------------
static inline bool stk_button_pressed(stk_state_t const* s,
uint8_t index) {
if (index >= STK_NUM_BUTTONS)
return false;
return (s->buttons >> index) & 1;
}
// ---------------------------------------------------------------------------
// API
// ---------------------------------------------------------------------------
/**
* Open the HID radio device.
* config must remain valid for the lifetime of the open session.
* Returns 0 on success, -1 on failure.
*/
int stk_open(stk_config_t const* config);
/**
* Read the current radio state (non-blocking).
* Returns 0 on success, -1 on failure or no new data.
*/
int stk_read(stk_state_t* state);
/**
* Close the radio device. Safe to call multiple times.
*/
void stk_close(void);
+48
View File
@@ -0,0 +1,48 @@
find_package(Unity REQUIRED)
find_package(CMock REQUIRED)
set(TEST_TARGETS "")
# STK tests
add_executable(test_stk test_stk.c)
target_include_directories(test_stk PRIVATE "${CMAKE_SOURCE_DIR}")
target_link_libraries(test_stk PRIVATE stkshid Unity::Unity)
target_compile_features(test_stk PRIVATE c_std_23)
add_test(NAME test_stk COMMAND test_stk)
list(APPEND TEST_TARGETS test_stk)
# 'check' builds all suites and runs CTest with full Unity output.
# USES_TERMINAL keeps ANSI colors alive through Ninja's output buffering.
add_custom_target(check
COMMAND ${CMAKE_CTEST_COMMAND}
--test-dir "${CMAKE_BINARY_DIR}"
--output-on-failure
--progress
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
USES_TERMINAL
DEPENDS ${TEST_TARGETS}
)
if (ENABLE_COVERAGE)
set(COVERAGE_DIR "${CMAKE_BINARY_DIR}/coverage")
add_custom_target(coverage
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
COMMAND ${CMAKE_COMMAND} -E make_directory "${COVERAGE_DIR}"
COMMAND ${GCOVR_EXE}
--gcov-executable "${GCOV_EXECUTABLE}"
--root "${CMAKE_SOURCE_DIR}"
--filter "${CMAKE_SOURCE_DIR}/stk/"
--exclude "${CMAKE_SOURCE_DIR}/tests/"
--exclude ".*Mock.*"
--exclude ".*unity.*"
--exclude ".*cmock.*"
--html-details "${COVERAGE_DIR}/index.html"
--txt
--print-summary
"${CMAKE_BINARY_DIR}"
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}"
USES_TERMINAL
DEPENDS ${TEST_TARGETS}
COMMENT "Coverage report: ${COVERAGE_DIR}/index.html"
)
endif()
+156
View File
@@ -0,0 +1,156 @@
#include "unity.h"
#include "stk/stk.h"
void setUp(void) {}
void tearDown(void) {}
// ---------------------------------------------------------------------------
// stk_config_t
// ---------------------------------------------------------------------------
void test_config_has_correct_fields(void) {
stk_config_t cfg = {
.vendor_id = STK_DEFAULT_VID,
.product_id = STK_DEFAULT_PID,
};
TEST_ASSERT_EQUAL_UINT16(STK_DEFAULT_VID, cfg.vendor_id);
TEST_ASSERT_EQUAL_UINT16(STK_DEFAULT_PID, cfg.product_id);
}
void test_config_defaults_match_vid_pid(void) {
TEST_ASSERT_EQUAL_UINT16(0x1209, STK_DEFAULT_VID);
TEST_ASSERT_EQUAL_UINT16(0x4F54, STK_DEFAULT_PID);
}
// ---------------------------------------------------------------------------
// stk_state_t
// ---------------------------------------------------------------------------
void test_state_defaults_to_zero(void) {
stk_state_t state = { 0 };
for (int i = 0; i < STK_NUM_AXES; i++)
TEST_ASSERT_EQUAL_INT16(0, state.axes[i]);
TEST_ASSERT_EQUAL_UINT32(0, state.buttons);
}
void test_state_has_expected_size(void) {
// 8 axes * 2 bytes + 4 bytes buttons = 20 bytes
TEST_ASSERT_EQUAL_INT(20, (int)sizeof(stk_state_t));
}
// ---------------------------------------------------------------------------
// Axis range constants
// ---------------------------------------------------------------------------
void test_axis_range_is_valid(void) {
TEST_ASSERT_TRUE(STK_AXIS_MIN < STK_AXIS_MID);
TEST_ASSERT_TRUE(STK_AXIS_MID < STK_AXIS_MAX);
}
void test_axis_mid_is_center(void) {
TEST_ASSERT_EQUAL_INT(
(STK_AXIS_MIN + STK_AXIS_MAX + 1) / 2,
STK_AXIS_MID);
}
void test_axis_range_values(void) {
TEST_ASSERT_EQUAL_INT(0, STK_AXIS_MIN);
TEST_ASSERT_EQUAL_INT(2047, STK_AXIS_MAX);
TEST_ASSERT_EQUAL_INT(1024, STK_AXIS_MID);
}
void test_axis_count(void) {
TEST_ASSERT_EQUAL_INT(8, STK_NUM_AXES);
}
void test_button_count(void) {
TEST_ASSERT_EQUAL_INT(24, STK_NUM_BUTTONS);
}
// ---------------------------------------------------------------------------
// Named axis accessors
// ---------------------------------------------------------------------------
void test_named_accessors_map_correct_indices(void) {
stk_state_t state = { 0 };
state.axes[1] = 111;
state.axes[2] = 222;
state.axes[4] = 333;
state.axes[5] = 444;
TEST_ASSERT_EQUAL_INT16(111, stk_right_x(&state));
TEST_ASSERT_EQUAL_INT16(222, stk_right_y(&state));
TEST_ASSERT_EQUAL_INT16(333, stk_throttle(&state));
TEST_ASSERT_EQUAL_INT16(444, stk_left_x(&state));
}
// ---------------------------------------------------------------------------
// Button helper
// ---------------------------------------------------------------------------
void test_button_pressed_returns_false_for_unset_bits(void) {
stk_state_t state = { .buttons = 0 };
for (int i = 0; i < STK_NUM_BUTTONS; i++)
TEST_ASSERT_FALSE(stk_button_pressed(&state, (uint8_t)i));
}
void test_button_pressed_returns_true_for_set_bits(void) {
stk_state_t state = { .buttons = (1u << 3) | (1u << 17) };
TEST_ASSERT_TRUE(stk_button_pressed(&state, 3));
TEST_ASSERT_TRUE(stk_button_pressed(&state, 17));
TEST_ASSERT_FALSE(stk_button_pressed(&state, 0));
TEST_ASSERT_FALSE(stk_button_pressed(&state, 23));
}
void test_button_pressed_out_of_range_returns_false(void) {
stk_state_t state = { .buttons = 0xFFFFFFFFu };
TEST_ASSERT_FALSE(stk_button_pressed(&state, 24));
TEST_ASSERT_FALSE(stk_button_pressed(&state, 31));
}
// ---------------------------------------------------------------------------
// API error handling
// ---------------------------------------------------------------------------
void test_open_with_null_config_fails(void) {
TEST_ASSERT_NOT_EQUAL_INT(0, stk_open(NULL));
}
void test_read_with_null_state_fails(void) {
TEST_ASSERT_NOT_EQUAL_INT(0, stk_read(NULL));
}
void test_read_without_open_fails(void) {
stk_state_t state = { 0 };
TEST_ASSERT_NOT_EQUAL_INT(0, stk_read(&state));
}
void test_close_is_safe_without_open(void) {
stk_close(); // should not crash
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_config_has_correct_fields);
RUN_TEST(test_config_defaults_match_vid_pid);
RUN_TEST(test_state_defaults_to_zero);
RUN_TEST(test_state_has_expected_size);
RUN_TEST(test_axis_range_is_valid);
RUN_TEST(test_axis_mid_is_center);
RUN_TEST(test_axis_range_values);
RUN_TEST(test_axis_count);
RUN_TEST(test_button_count);
RUN_TEST(test_named_accessors_map_correct_indices);
RUN_TEST(test_button_pressed_returns_false_for_unset_bits);
RUN_TEST(test_button_pressed_returns_true_for_set_bits);
RUN_TEST(test_button_pressed_out_of_range_returns_false);
RUN_TEST(test_open_with_null_config_fails);
RUN_TEST(test_read_with_null_state_fails);
RUN_TEST(test_read_without_open_fails);
RUN_TEST(test_close_is_safe_without_open);
return UNITY_END();
}
+234
View File
@@ -0,0 +1,234 @@
// hid_discover.c — enumerate HID devices, find EdgeTx radio, dump caps
//
// Build via CMake (target: hid_discover)
//
// Usage:
// hid_discover.exe (lists all HID devices)
// hid_discover.exe 1209 4F54 (find EdgeTx radio, dump input caps)
#include <windows.h>
#include <setupapi.h>
#include <hidsdi.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
// ENUM_GUID from devguid.h — used to enumerate HID class devices
static const GUID GUID_DEVINTERFACE_HID = {
0x4d1e55b2, 0xf16f, 0x11cf,
{ 0x88, 0xcb, 0x00, 0x11, 0x11, 0x00, 0x00, 0x30 }
};
#define ENUM_GUID GUID_DEVINTERFACE_HID
// ---------------------------------------------------------------------------
// Capability listing (works even when the raw report descriptor IOCTL fails)
// ---------------------------------------------------------------------------
static const char* usage_page_name(USAGE page) {
switch (page) {
case 0x01: return "Generic Desktop";
case 0x02: return "Simulation Controls";
case 0x09: return "Button";
default: return "Unknown";
}
}
static void print_value_caps(PHIDP_PREPARSED_DATA preparsed,
const HIDP_CAPS* caps) {
USHORT count = caps->NumberInputValueCaps;
if (count == 0)
return;
PHIDP_VALUE_CAPS vcaps =
HeapAlloc(GetProcessHeap(), 0, count * sizeof(HIDP_VALUE_CAPS));
if (!vcaps)
return;
if (HidP_GetValueCaps(HidP_Input, vcaps, &count, preparsed) == HIDP_STATUS_SUCCESS) {
printf("\nInput value caps (%u):\n", count);
for (USHORT i = 0; i < count; i++) {
const HIDP_VALUE_CAPS* v = &vcaps[i];
if (v->IsRange) {
printf(" [%u] UsagePage=0x%02X (%s) Usage=0x%02X..0x%02X "
"ReportID=%u BitSize=%u ReportCount=%u "
"LogicalMin=%ld LogicalMax=%ld\n",
i, v->UsagePage, usage_page_name(v->UsagePage),
v->Range.UsageMin, v->Range.UsageMax,
v->ReportID, v->BitSize, v->ReportCount,
(long)v->LogicalMin, (long)v->LogicalMax);
} else {
printf(" [%u] UsagePage=0x%02X (%s) Usage=0x%02X "
"ReportID=%u BitSize=%u ReportCount=%u "
"LogicalMin=%ld LogicalMax=%ld\n",
i, v->UsagePage, usage_page_name(v->UsagePage),
v->NotRange.Usage,
v->ReportID, v->BitSize, v->ReportCount,
(long)v->LogicalMin, (long)v->LogicalMax);
}
}
} else {
fprintf(stderr, "HidP_GetValueCaps failed\n");
}
HeapFree(GetProcessHeap(), 0, vcaps);
}
static void print_button_caps(PHIDP_PREPARSED_DATA preparsed,
const HIDP_CAPS* caps) {
USHORT count = caps->NumberInputButtonCaps;
if (count == 0)
return;
PHIDP_BUTTON_CAPS bcaps =
HeapAlloc(GetProcessHeap(), 0, count * sizeof(HIDP_BUTTON_CAPS));
if (!bcaps)
return;
if (HidP_GetButtonCaps(HidP_Input, bcaps, &count, preparsed) == HIDP_STATUS_SUCCESS) {
printf("\nInput button caps (%u):\n", count);
for (USHORT i = 0; i < count; i++) {
const HIDP_BUTTON_CAPS* b = &bcaps[i];
if (b->IsRange) {
printf(" [%u] UsagePage=0x%02X (%s) Usage=0x%02X..0x%02X "
"ReportID=%u\n",
i, b->UsagePage, usage_page_name(b->UsagePage),
b->Range.UsageMin, b->Range.UsageMax, b->ReportID);
} else {
printf(" [%u] UsagePage=0x%02X (%s) Usage=0x%02X ReportID=%u\n",
i, b->UsagePage, usage_page_name(b->UsagePage),
b->NotRange.Usage, b->ReportID);
}
}
} else {
fprintf(stderr, "HidP_GetButtonCaps failed\n");
}
HeapFree(GetProcessHeap(), 0, bcaps);
}
// ---------------------------------------------------------------------------
// Device probe
// ---------------------------------------------------------------------------
static int device_num = 0;
static int probe_device(HANDLE handle, const wchar_t* path,
uint16_t filter_vid, uint16_t filter_pid) {
HIDD_ATTRIBUTES attrs = { .Size = sizeof(attrs) };
if (!HidD_GetAttributes(handle, &attrs))
return 0;
if (filter_vid && filter_pid) {
if (attrs.VendorID != filter_vid || attrs.ProductID != filter_pid) {
fprintf(stderr, "Skipping VID:PID 0x%04X:0x%04X\n", attrs.VendorID, attrs.ProductID);
return 0; // not our target
}
}
// Get HID caps via preparsed data
HIDP_CAPS caps = { 0 };
PHIDP_PREPARSED_DATA preparsed = NULL;
BOOL has_caps = FALSE;
if (HidD_GetPreparsedData(handle, &preparsed)) {
if (HidP_GetCaps(preparsed, &caps) == HIDP_STATUS_SUCCESS)
has_caps = TRUE;
}
device_num++;
printf("\n=== Device %d ===\n", device_num);
printf("Path: %ls\n", path);
printf("VID:PID: 0x%04X:0x%04X Version: 0x%04X\n",
attrs.VendorID, attrs.ProductID, attrs.VersionNumber);
if (has_caps) {
printf("Input report length: %u\n", caps.InputReportByteLength);
printf("Output report length: %u\n", caps.OutputReportByteLength);
printf("Feature report length: %u\n", caps.FeatureReportByteLength);
printf("Input buttons: %u\n", caps.NumberInputButtonCaps);
printf("Input values: %u\n", caps.NumberInputValueCaps);
print_value_caps(preparsed, &caps);
print_button_caps(preparsed, &caps);
}
if (preparsed)
HidD_FreePreparsedData(preparsed);
return 1;
}
// ---------------------------------------------------------------------------
// Enumeration
// ---------------------------------------------------------------------------
static int enumerate_hid_devices(uint16_t filter_vid, uint16_t filter_pid) {
HDEVINFO info = SetupDiGetClassDevsW(&ENUM_GUID, NULL, NULL,
DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
if (info == INVALID_HANDLE_VALUE) {
fprintf(stderr, "SetupDiGetClassDevs failed: %lu\n", GetLastError());
return -1;
}
int found = 0;
for (DWORD i = 0; ; i++) {
SP_DEVICE_INTERFACE_DATA iface = { .cbSize = sizeof(iface) };
BOOL ok = SetupDiEnumDeviceInterfaces(info, NULL, &ENUM_GUID, i, &iface);
if (!ok) {
fprintf(stderr, "Enumerated %lu interfaces (last error: %lu)\n",
(unsigned long)i, GetLastError());
break;
}
DWORD buflen = 0;
SetupDiGetDeviceInterfaceDetailW(info, &iface, NULL, 0, &buflen, NULL);
if (buflen == 0) continue;
PSP_DEVICE_INTERFACE_DETAIL_DATA_W detail =
(PSP_DEVICE_INTERFACE_DETAIL_DATA_W)HeapAlloc(GetProcessHeap(), 0, buflen);
detail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W);
if (!SetupDiGetDeviceInterfaceDetailW(info, &iface, detail, buflen,
&buflen, NULL)) {
HeapFree(GetProcessHeap(), 0, detail);
continue;
}
HANDLE handle = CreateFileW(detail->DevicePath,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, 0,
NULL);
if (handle != INVALID_HANDLE_VALUE) {
if (probe_device(handle, detail->DevicePath, filter_vid, filter_pid))
found++;
CloseHandle(handle);
if (filter_vid && filter_pid && found > 0) break; // found our target
} else {
if (filter_vid && filter_pid)
fprintf(stderr, "CreateFile failed for %ls: %lu\n",
detail->DevicePath, GetLastError());
}
HeapFree(GetProcessHeap(), 0, detail);
}
SetupDiDestroyDeviceInfoList(info);
return found;
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
int main(int argc, char* argv[]) {
uint16_t vid = 0, pid = 0;
if (argc >= 3) {
vid = (uint16_t)strtoul(argv[1], NULL, 16);
pid = (uint16_t)strtoul(argv[2], NULL, 16);
printf("Looking for VID:PID 0x%04X:0x%04X\n", vid, pid);
} else {
printf("Listing all HID devices (pass VID PID to filter)\n");
}
return enumerate_hid_devices(vid, pid) == 0 ? 1 : 0;
}