Inital commit
This commit is contained in:
@@ -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
@@ -0,0 +1,16 @@
|
|||||||
|
# CMake
|
||||||
|
build
|
||||||
|
build-*
|
||||||
|
cmake-build-*
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# Editors
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
.ccls
|
||||||
|
.ccls-cache
|
||||||
|
.cache
|
||||||
|
compile_commands.json
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
@@ -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/`.
|
||||||
@@ -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)
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
Vendored
+56
@@ -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()
|
||||||
Vendored
+92
@@ -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")
|
||||||
Vendored
+67
@@ -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")
|
||||||
Vendored
+27
@@ -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()
|
||||||
Vendored
+17
@@ -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()
|
||||||
Vendored
+64
@@ -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()
|
||||||
Vendored
+28
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -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()
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user