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