Skip to content
This repository was archived by the owner on Jun 4, 2026. It is now read-only.

Commit 5438fb5

Browse files
committed
create HID report protocol compatibility tester application
1 parent 9d39f27 commit 5438fb5

11 files changed

Lines changed: 617 additions & 18 deletions

File tree

.vscode/tasks.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,29 @@
126126
"problemMatcher": [
127127
"$gcc"
128128
]
129+
},
130+
{
131+
"label": "Build hid-rp-compat-tester for nrf52840dk",
132+
"group": {
133+
"kind": "build",
134+
"isDefault": true
135+
},
136+
"options": {
137+
"cwd": "${workspaceFolder}"
138+
},
139+
"type": "process",
140+
"command": "west",
141+
"args": [
142+
"build",
143+
"--build-dir",
144+
"${cwd}/hid-rp-compat-tester/build/nrf52840dk",
145+
"${cwd}/hid-rp-compat-tester",
146+
"--board",
147+
"nrf52840dk/nrf52840"
148+
],
149+
"problemMatcher": [
150+
"$gcc"
151+
]
129152
}
130153
]
131154
}

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ command:
1313
west init -m https://github.com/IntergatedCircuits/c2usb-zephyr-examples c2usb-workspace
1414
# update Zephyr modules
1515
cd c2usb-workspace
16-
west update
17-
west patch
16+
west update && west patch
1817
```
1918

2019
## Application Index
@@ -26,6 +25,11 @@ A straightforward BLE HID keyboard. UART shell access is needed to complete BLE
2625
Use the button on the board to trigger a caps lock press,
2726
and observe as the host changes the caps lock state on the board's LED.
2827

28+
### hid-rp-compat-tester
29+
30+
A device that performs a sequence of USB enumerations, testing whether the host OS is capable
31+
of interpreting various HID report descriptor variations of an HID keyboard.
32+
2933
### usb-keyboard
3034

3135
A straightforward USB HID keyboard. Use the button on the board to trigger a caps lock press,
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
3+
cmake_minimum_required(VERSION 3.20.0)
4+
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
5+
project(hid-rp-compat-tester)
6+
7+
target_sources(app PRIVATE src/main.cpp)
8+
9+
# link the application to c2usb
10+
target_link_libraries(app PRIVATE
11+
c2usb
12+
c2usb-example-hid
13+
)

hid-rp-compat-tester/Kconfig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
source "Kconfig.zephyr"
2+
rsource "../Kconfig"

hid-rp-compat-tester/prj.conf

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
CONFIG_LOG=y
2+
#CONFIG_UDC_DRIVER_LOG_LEVEL_INF=y
3+
#CONFIG_C2USB_UDC_MAC_LOG_LEVEL_DBG=y
4+
5+
CONFIG_INPUT=y
6+
CONFIG_INPUT_MODE_SYNCHRONOUS=y
7+
CONFIG_HWINFO=y
8+
9+
CONFIG_SHELL=y
10+
CONFIG_SHELL_BACKEND_C2USB=y
11+
# CONFIG_SHELL_BACKEND_SERIAL=y
12+
CONFIG_SHELL_LOG_BACKEND=y
13+
CONFIG_SHELL_MINIMAL=n
14+
# needs to be ~100 bytes more than default
15+
CONFIG_SHELL_STACK_SIZE=1536
16+
17+
CONFIG_C2USB_UDC_MAC=y
18+
# RAM optimization:
19+
# the buffer pool size can be cut down, as it's only used for control transfers
20+
# CONFIG_UDC_BUF_POOL_SIZE=optimize based on your application (and check asserts)
21+
# CONFIG_UDC_BUF_COUNT=3 + maximal used endpoint count in a configuration
22+
23+
# needed as at suspend the msgq is flooded otherwise
24+
# CONFIG_C2USB_UDC_MAC_MSGQ_SIZE=32
25+
26+
CONFIG_DEBUG=y
27+
CONFIG_DEBUG_OPTIMIZATIONS=y
28+
CONFIG_DEBUG_THREAD_INFO=y
29+
30+
# don't use picolibc in debug builds as only its module version can print verbose assert() logs
31+
# CONFIG_PICOLIBC_VERBOSE_ASSERT=y
32+
# but that's conflicting with the chosen C++ standard library
33+
CONFIG_NEWLIB_LIBC=y
34+
35+
CONFIG_USE_SEGGER_RTT=n
36+
37+
CONFIG_DEMO_MANUFACTURER="github.com/IntergatedCircuits/c2usb-zephyr-examples"
38+
CONFIG_DEMO_PRODUCT="HID RP compatibility tester"
39+
# use reserved VID/PID that won't conflict with real devices, in case of host OS blacklisting
40+
CONFIG_DEMO_MANUFACTURER_ID=0x335e
41+
CONFIG_DEMO_PRODUCT_ID=0x1cff

hid-rp-compat-tester/sample.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
sample:
2+
name: HID RP Compatibility Tester
3+
common:
4+
harness: button
5+
filter: dt_alias_exists("sw0") and dt_alias_exists("led0")
6+
depends_on:
7+
- gpio
8+
platform_allow:
9+
- nrf52840dk/nrf52840

hid-rp-compat-tester/src/main.cpp

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
#include "iolib.h"
2+
#include <zephyr/drivers/hwinfo.h>
3+
#include <zephyr/input/input.h>
4+
#include <zephyr/logging/log.h>
5+
6+
#include <bitset>
7+
#include "testcases.hpp"
8+
#include <port/zephyr/message_queue.hpp>
9+
#include <port/zephyr/udc_mac.hpp>
10+
#include <port/zephyr/usb_shell.hpp>
11+
#include <usb/df/class/hid.hpp>
12+
#include <usb/df/device.hpp>
13+
14+
LOG_MODULE_REGISTER(main, LOG_LEVEL_INF);
15+
16+
using namespace magic_enum::bitwise_operators;
17+
using namespace std::chrono_literals;
18+
19+
static std::bitset<TESTCASE_COUNT> test_results{};
20+
static std::size_t testcase_index{};
21+
static bool boot_protocol_host{};
22+
23+
auto& host_feedback_msgq()
24+
{
25+
static os::zephyr::message_queue_instance<bool, 2> msgq;
26+
return msgq;
27+
}
28+
29+
auto& keyboard_app()
30+
{
31+
static simple_keyboard<TEST_REPORT_ID> keyb{
32+
[](const simple_keyboard<TEST_REPORT_ID>::kb_leds_report& report)
33+
{
34+
auto led_state = report.leds.test(hid::page::leds::CAPS_LOCK);
35+
LOG_INF("LED report received: %d", (int)led_state);
36+
host_feedback_msgq().post(led_state);
37+
}};
38+
return keyb;
39+
}
40+
41+
void send_keypress()
42+
{
43+
LOG_INF("Sending key press");
44+
keyboard_app().send_key(hid::page::keyboard_keypad::KEYBOARD_CAPS_LOCK, 1);
45+
k_msleep(50);
46+
keyboard_app().send_key(hid::page::keyboard_keypad::KEYBOARD_CAPS_LOCK, 0);
47+
k_msleep(50);
48+
}
49+
50+
auto& device()
51+
{
52+
static constexpr usb::product_info product_info{
53+
CONFIG_DEMO_MANUFACTURER_ID, CONFIG_DEMO_MANUFACTURER, CONFIG_DEMO_PRODUCT_ID,
54+
CONFIG_DEMO_PRODUCT, usb::version("1.0")};
55+
static usb::zephyr::udc_mac mac{DEVICE_DT_GET(DT_NODELABEL(zephyr_udc0))};
56+
static usb::df::device_instance<usb::speed::FULL> device{mac, product_info};
57+
return device;
58+
}
59+
60+
bool advance_test()
61+
{
62+
constexpr auto speed = usb::speed::FULL;
63+
static usb::df::hid::function usb_kb{keyboard_app(), "keyboard",
64+
usb::hid::boot_protocol_mode::KEYBOARD};
65+
66+
static const auto hid_config = usb::df::config::make_config(
67+
usb::df::config::header(usb::df::config::power::bus(500), "HID config"),
68+
usb::df::hid::config(usb_kb, speed, usb::endpoint::address(0x81), 1
69+
#if 0 // TODO: kconfig flag?
70+
, usb::endpoint::address(0x01), 10
71+
#endif
72+
));
73+
74+
static const auto shell_config = usb::df::config::make_config(
75+
usb::df::config::header(usb::df::config::power::bus(500), "shell config"),
76+
usb::df::cdc::config(
77+
usb::zephyr::usb_shell::handle(), speed, usb::endpoint::address(0x01),
78+
usb::endpoint::address(0x81),
79+
usb::endpoint::address(0x82) // note that notification endpoint is unused here
80+
));
81+
82+
if (device().is_open())
83+
{
84+
// soft disconnect from host
85+
device().close();
86+
k_msleep(1000);
87+
88+
testcase_index++;
89+
}
90+
91+
if (boot_protocol_host)
92+
{
93+
// host is in boot protocol mode, skip further tests
94+
}
95+
else if (testcase_index < TESTCASE_COUNT)
96+
{
97+
auto& tc = testcases()[testcase_index];
98+
keyboard_app().set_report_protocol(tc.protocol);
99+
LOG_INF("Starting test %u/%u: %s", testcase_index + 1, TESTCASE_COUNT, tc.description);
100+
101+
device().set_config(hid_config);
102+
device().open();
103+
return true;
104+
}
105+
// else
106+
{
107+
LOG_INF("All tests done: %u/%u passed", test_results.count(),
108+
boot_protocol_host ? 1 : TESTCASE_COUNT);
109+
device().set_config(shell_config);
110+
device().open();
111+
return false;
112+
}
113+
}
114+
115+
static k_tid_t main_thread_id{};
116+
117+
int main(void)
118+
{
119+
main_thread_id = k_current_get();
120+
device().set_power_event_delegate(
121+
[](usb::df::device& dev, usb::df::device::event ev)
122+
{
123+
if (ev == usb::df::device::event::CONFIGURATION_CHANGE)
124+
{
125+
LOG_INF("USB configured: %d", (int)dev.configured());
126+
if (dev.configured())
127+
{
128+
k_wakeup(main_thread_id);
129+
}
130+
}
131+
});
132+
133+
while (advance_test())
134+
{
135+
// wait until device is configured
136+
k_sleep(K_FOREVER);
137+
138+
auto testcase_start_time = os::zephyr::tick_timer::now();
139+
140+
std::array<std::optional<bool>, 2> replies{};
141+
auto* reply = &replies[0];
142+
143+
// wait for initial LED state from host
144+
*reply = host_feedback_msgq().try_get_for(500ms);
145+
if (!reply->has_value())
146+
{
147+
LOG_WRN("No initial LED state from host");
148+
}
149+
150+
// send key press and check that host changes LED state
151+
send_keypress();
152+
reply++;
153+
*reply = host_feedback_msgq().try_get_for(500ms);
154+
if (!reply->has_value())
155+
{
156+
LOG_ERR("No host response to key press");
157+
continue;
158+
}
159+
if ((reply - 1)->has_value() && (reply->value() == (reply - 1)->value()))
160+
{
161+
LOG_ERR("Host did not change LED state on key press");
162+
continue;
163+
}
164+
165+
// restore original state
166+
send_keypress();
167+
168+
test_results.set(testcase_index);
169+
LOG_INF("Test #%u passed in %u ms", testcase_index,
170+
unsigned(std::chrono::duration_cast<std::chrono::milliseconds>(
171+
os::zephyr::tick_timer::now() - testcase_start_time)
172+
.count()));
173+
174+
if (keyboard_app().get_protocol() == hid::protocol::BOOT)
175+
{
176+
boot_protocol_host = true;
177+
LOG_INF("Host uses boot protocol mode, skipping further tests");
178+
break;
179+
}
180+
}
181+
182+
// visual indication that tests are done
183+
iolib_set_led(0, 1);
184+
185+
// TODO: or simply return?
186+
while (true)
187+
{
188+
k_sleep(K_FOREVER);
189+
}
190+
}
191+
192+
static int cmd_test_results(const ::shell* sh, [[maybe_unused]] size_t argc,
193+
[[maybe_unused]] char* argv[])
194+
{
195+
auto index = testcase_index;
196+
auto results = test_results;
197+
auto pass_count = results.count();
198+
if (index < (boot_protocol_host ? 1 : TESTCASE_COUNT))
199+
{
200+
shell_warn(sh, "Tests are still in progress, results are incomplete");
201+
}
202+
if (boot_protocol_host)
203+
{
204+
shell_print(sh, pass_count ? "Results: test passed" : "Results: test failed");
205+
return 0;
206+
}
207+
208+
shell_print(sh, "Results: %u/%u test passed", pass_count, index);
209+
if (pass_count == index)
210+
{
211+
return 0;
212+
}
213+
shell_print(sh, "Failed test cases:");
214+
for (std::size_t i = 0; i < index; ++i)
215+
{
216+
if (!results.test(i))
217+
{
218+
shell_print(sh, " %u. %s", i + 1, testcases()[i].description);
219+
}
220+
}
221+
return 0;
222+
}
223+
224+
SHELL_STATIC_SUBCMD_SET_CREATE(sub_test,
225+
//
226+
SHELL_CMD(results, NULL, "Print available results",
227+
&cmd_test_results),
228+
SHELL_SUBCMD_SET_END);
229+
230+
SHELL_CMD_REGISTER(test, &sub_test, "Test control", NULL);

0 commit comments

Comments
 (0)