From ad2242d81277a3caeb109870a67f41c602d908fb Mon Sep 17 00:00:00 2001 From: Jorge Guzman Date: Thu, 30 Jul 2026 15:15:54 -0300 Subject: [PATCH 1/6] system/kbd: add a keyboard dump that reads any keyboard The hidkbd and keyboard examples do the same thing for one kind of keyboard each: hidkbd reads a USB HID keyboard as a byte stream, and keyboard reads an upper half keyboard as events. Neither works with the other, so bringing up a new keyboard means picking the right example first, and there is no answer for somebody whose keyboard is neither. Every keyboard registered with keyboard_register() is read the same way, so one tool covers them all: USB HID, matrix, simulator, virtio, VNC. The payload follows INPUT_KEYBOARD_BYTESTREAM rather than a switch of its own. An application has no business knowing what hardware is behind the device, and a build cannot mix the two formats anyway. The two examples stay for now. They are what the in-tree configurations still name, and removing them has to wait until those configurations have been moved over. Signed-off-by: Jorge Guzman --- system/kbd/CMakeLists.txt | 35 +++++ system/kbd/Kconfig | 38 ++++++ system/kbd/Make.defs | 25 ++++ system/kbd/Makefile | 32 +++++ system/kbd/kbd_main.c | 265 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 395 insertions(+) create mode 100644 system/kbd/CMakeLists.txt create mode 100644 system/kbd/Kconfig create mode 100644 system/kbd/Make.defs create mode 100644 system/kbd/Makefile create mode 100644 system/kbd/kbd_main.c diff --git a/system/kbd/CMakeLists.txt b/system/kbd/CMakeLists.txt new file mode 100644 index 00000000000..8ee13e65b0b --- /dev/null +++ b/system/kbd/CMakeLists.txt @@ -0,0 +1,35 @@ +# ############################################################################## +# apps/system/kbd/CMakeLists.txt +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more contributor +# license agreements. See the NOTICE file distributed with this work for +# additional information regarding copyright ownership. The ASF licenses this +# file to you under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# +# ############################################################################## + +if(CONFIG_SYSTEM_KBD) + nuttx_add_application( + NAME + ${CONFIG_SYSTEM_KBD_PROGNAME} + PRIORITY + ${CONFIG_SYSTEM_KBD_PRIORITY} + STACKSIZE + ${CONFIG_SYSTEM_KBD_STACKSIZE} + MODULE + ${CONFIG_SYSTEM_KBD} + SRCS + kbd_main.c) +endif() diff --git a/system/kbd/Kconfig b/system/kbd/Kconfig new file mode 100644 index 00000000000..d1030ab0915 --- /dev/null +++ b/system/kbd/Kconfig @@ -0,0 +1,38 @@ +# +# For a description of the syntax of this configuration file, +# see the file kconfig-language.txt in the NuttX tools repository. +# + +config SYSTEM_KBD + tristate "Keyboard dump" + default n + depends on INPUT_KEYBOARD + ---help--- + Dump what a keyboard reports. This reads any keyboard registered + with keyboard_register(), whatever the hardware behind it is, so it + is the tool to reach for when bringing up a new keyboard. + + Replaces the hidkbd and keyboard examples, which did the same thing + for one kind of keyboard each. + +if SYSTEM_KBD + +config SYSTEM_KBD_PROGNAME + string "Program name" + default "kbd" + +config SYSTEM_KBD_PRIORITY + int "Task priority" + default 100 + +config SYSTEM_KBD_STACKSIZE + int "Task stack size" + default DEFAULT_TASK_STACKSIZE + +config SYSTEM_KBD_DEVPATH + string "Keyboard device path" + default "/dev/kbd0" + ---help--- + The keyboard to read when none is named on the command line. + +endif diff --git a/system/kbd/Make.defs b/system/kbd/Make.defs new file mode 100644 index 00000000000..f80780936d9 --- /dev/null +++ b/system/kbd/Make.defs @@ -0,0 +1,25 @@ +############################################################################ +# apps/system/kbd/Make.defs +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +ifneq ($(CONFIG_SYSTEM_KBD),) +CONFIGURED_APPS += $(APPDIR)/system/kbd +endif diff --git a/system/kbd/Makefile b/system/kbd/Makefile new file mode 100644 index 00000000000..97c5dad1969 --- /dev/null +++ b/system/kbd/Makefile @@ -0,0 +1,32 @@ +############################################################################ +# apps/system/kbd/Makefile +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +include $(APPDIR)/Make.defs + +PROGNAME = $(CONFIG_SYSTEM_KBD_PROGNAME) +PRIORITY = $(CONFIG_SYSTEM_KBD_PRIORITY) +STACKSIZE = $(CONFIG_SYSTEM_KBD_STACKSIZE) +MODULE = $(CONFIG_SYSTEM_KBD) + +MAINSRC = kbd_main.c + +include $(APPDIR)/Application.mk diff --git a/system/kbd/kbd_main.c b/system/kbd/kbd_main.c new file mode 100644 index 00000000000..a736dd85c57 --- /dev/null +++ b/system/kbd/kbd_main.c @@ -0,0 +1,265 @@ +/**************************************************************************** + * apps/system/kbd/kbd_main.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/* Dump what a keyboard reports. + * + * Every keyboard registered with keyboard_register() is read the same way, + * whatever the hardware behind it is: a USB HID keyboard, a matrix, the + * simulator, virtio. So this works with all of them, and it is the place + * to look when bringing up a new one. + * + * The device delivers struct keyboard_event_s events unless the kernel was + * built with INPUT_KEYBOARD_BYTESTREAM, in which case it delivers the byte + * stream that the keyboard codec defines. This follows that setting rather + * than having a switch of its own. + */ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#ifdef CONFIG_INPUT_KEYBOARD_BYTESTREAM +# include +# include +#endif + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define KBD_READ_SIZE 64 + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: kbd_typename + ****************************************************************************/ + +static FAR const char *kbd_typename(uint32_t type) +{ + switch (type) + { + case KEYBOARD_PRESS: + return "press"; + + case KEYBOARD_RELEASE: + return "release"; + + case KEYBOARD_SPECPRESS: + return "specpress"; + + case KEYBOARD_SPECREL: + return "specrel"; + + default: + return "unknown"; + } +} + +/**************************************************************************** + * Name: kbd_show + * + * Description: + * Print one key. A normal key carries a character, a special key carries + * a value from enum kbd_keycode_e, and the type is what says which. + * + ****************************************************************************/ + +static void kbd_show(uint32_t code, uint32_t type) +{ + if (type == KEYBOARD_PRESS || type == KEYBOARD_RELEASE) + { + printf("%-9s code %3" PRIu32 " '%c'\n", kbd_typename(type), code, + isprint(code) ? (int)code : '.'); + } + else + { + printf("%-9s keycode %" PRIu32 "\n", kbd_typename(type), code); + } + + fflush(stdout); +} + +/**************************************************************************** + * Name: kbd_dump + * + * Description: + * Print everything in one read. + * + * Returned Value: + * The number of keys printed. + * + ****************************************************************************/ + +#ifdef CONFIG_INPUT_KEYBOARD_BYTESTREAM +static size_t kbd_dump(FAR char *buffer, size_t nbytes) +{ + struct lib_meminstream_s stream; + struct kbd_getstate_s state; + uint8_t ch; + size_t nkeys = 0; + int ret; + + lib_meminstream(&stream, buffer, nbytes); + memset(&state, 0, sizeof(state)); + + for (; ; ) + { + ret = kbd_decode(&stream.common, &state, &ch); + if (ret == KBD_ERROR) + { + break; + } + + kbd_show(ch, ret); + nkeys++; + } + + return nkeys; +} +#else +static size_t kbd_dump(FAR char *buffer, size_t nbytes) +{ + FAR struct keyboard_event_s *key = (FAR struct keyboard_event_s *)buffer; + size_t nkeys = 0; + + if ((nbytes % sizeof(struct keyboard_event_s)) != 0) + { + fprintf(stderr, "kbd: short read of %zu bytes, ignoring\n", nbytes); + return 0; + } + + while (nbytes >= sizeof(struct keyboard_event_s)) + { + kbd_show(key->code, key->type); + + nbytes -= sizeof(struct keyboard_event_s); + key++; + nkeys++; + } + + return nkeys; +} +#endif + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: main + ****************************************************************************/ + +int main(int argc, FAR char *argv[]) +{ + FAR const char *devpath = CONFIG_SYSTEM_KBD_DEVPATH; + char buffer[KBD_READ_SIZE]; + size_t nkeys = 0; + size_t limit = 0; + ssize_t nbytes; + int fd; + + if (argc > 1) + { + devpath = argv[1]; + } + + if (argc > 2) + { + limit = strtoul(argv[2], NULL, 10); + } + + /* Wait for the device. A USB keyboard appears when it is plugged in, so + * this is the normal way to start rather than an error path. + */ + + for (; ; ) + { + fd = open(devpath, O_RDONLY); + if (fd >= 0) + { + break; + } + + printf("kbd: waiting for %s: %d\n", devpath, errno); + fflush(stdout); + sleep(3); + } + + printf("kbd: reading %s, %s\n", devpath, +#ifdef CONFIG_INPUT_KEYBOARD_BYTESTREAM + "byte stream" +#else + "keyboard events" +#endif + ); + fflush(stdout); + + for (; ; ) + { + nbytes = read(fd, buffer, sizeof(buffer)); + if (nbytes < 0) + { + if (errno == EINTR) + { + continue; + } + + fprintf(stderr, "kbd: read %s failed: %d\n", devpath, errno); + break; + } + else if (nbytes == 0) + { + /* The keyboard is gone */ + + break; + } + + nkeys += kbd_dump(buffer, nbytes); + + if (limit > 0 && nkeys >= limit) + { + break; + } + } + + close(fd); + printf("kbd: %zu keys\n", nkeys); + return EXIT_SUCCESS; +} From 720caf03db1463aa72c37e63085ad708fb4e5b43 Mon Sep 17 00:00:00 2001 From: Jorge Guzman Date: Thu, 30 Jul 2026 15:15:54 -0300 Subject: [PATCH 2/6] games/NXDoom: handle the special key events i_get_event() handled KEYBOARD_PRESS and KEYBOARD_RELEASE and dropped everything else, so every key reported as KEYBOARD_SPECPRESS went nowhere. On the simulator that is already every arrow key, since sim_keyboard reports them that way: the game is unplayable and says nothing about why. Handle all four types. A special key carries a value from enum kbd_keycode_e rather than a character, so it gets its own translation into doomkeys.h and contributes no printable character to the event. The map covers what the game binds by default, which now includes Ctrl, Shift and Alt. Fire, run and strafe are on the modifiers, so a keyboard that reports them is the difference between playing and walking around. Enter needed folding onto KEY_ENTER as well. A driver reports it as the character that it produces, which is the line feed, so it never arrived as KEYCODE_ENTER and never matched the carriage return that the game binds the menu to. Confirmed on hardware with a USB keyboard. Signed-off-by: Jorge Guzman --- games/NXDoom/src/i_input.c | 158 ++++++++++++++++++++++++++++++++----- games/NXDoom/src/i_video.c | 16 +--- 2 files changed, 143 insertions(+), 31 deletions(-) diff --git a/games/NXDoom/src/i_input.c b/games/NXDoom/src/i_input.c index 8cb3a863019..a7e61b8f8b7 100644 --- a/games/NXDoom/src/i_input.c +++ b/games/NXDoom/src/i_input.c @@ -192,6 +192,38 @@ static int init_kbd_dev(struct keyboard_dev *dev) ****************************************************************************/ static int translate_key(uint32_t keycode) +{ + /* A keyboard driver reports Enter as the character that it produces, so + * it never arrives as KEYCODE_ENTER. The game binds the menu to + * KEY_ENTER, which is the carriage return, so the line feed that a + * driver actually sends has to be folded onto it. + */ + + if (keycode == '\n') + { + return KEY_ENTER; + } + + return keycode; +} + +/**************************************************************************** + * Name: translate_speckey + * + * Description: + * Translates a value from enum kbd_keycode_e into one from doomkeys.h. + * + * A special key arrives with a KEYBOARD_SPECPRESS or KEYBOARD_SPECREL + * event rather than as a character: the keycodes overlap the character + * range, so the event type is what tells an arrow key from the character + * that shares its value. + * + * Return: + * The translated key code, or zero if the key has no meaning to the game. + * + ****************************************************************************/ + +static int translate_speckey(uint32_t keycode) { switch (keycode) { @@ -203,10 +235,76 @@ static int translate_key(uint32_t keycode) return KEY_UPARROW; case KEYCODE_DOWN: return KEY_DOWNARROW; + + /* Drivers disagree on Enter: a USB HID keyboard reports the line feed + * that the key produces, while the simulator reports KEYCODE_ENTER. + * Both have to land on KEY_ENTER, so both are handled, here and in + * translate_key(). + */ + case KEYCODE_ENTER: return KEY_ENTER; + + case KEYCODE_BACKDEL: + return KEY_BACKSPACE; + case KEYCODE_FWDDEL: + return KEY_DEL; + case KEYCODE_INSERT: + return KEY_INS; + case KEYCODE_HOME: + return KEY_HOME; + case KEYCODE_END: + return KEY_END; + case KEYCODE_PAGEUP: + return KEY_PGUP; + case KEYCODE_PAGEDOWN: + return KEY_PGDN; + case KEYCODE_PAUSE: + return KEY_PAUSE; + case KEYCODE_CAPSLOCK: + return KEY_CAPSLOCK; + + /* Fire, run and strafe. The game binds these by default, so a + * keyboard that does not report its modifiers cannot play it. + */ + + case KEYCODE_LCTRL: + case KEYCODE_RCTRL: + return KEY_RCTRL; + case KEYCODE_LSHIFT: + case KEYCODE_RSHIFT: + return KEY_RSHIFT; + case KEYCODE_LALT: + case KEYCODE_RALT: + return KEY_RALT; + + case KEYCODE_F1: + return KEY_F1; + case KEYCODE_F2: + return KEY_F2; + case KEYCODE_F3: + return KEY_F3; + case KEYCODE_F4: + return KEY_F4; + case KEYCODE_F5: + return KEY_F5; + case KEYCODE_F6: + return KEY_F6; + case KEYCODE_F7: + return KEY_F7; + case KEYCODE_F8: + return KEY_F8; + case KEYCODE_F9: + return KEY_F9; + case KEYCODE_F10: + return KEY_F10; + case KEYCODE_F11: + return KEY_F11; + case KEYCODE_F12: + return KEY_F12; + default: - return keycode; + return 0; } } @@ -319,24 +417,53 @@ void i_handle_keyboard_event(struct keyboard_event_s *kevent) */ event_t event; + bool press; switch (kevent->type) { case KEYBOARD_PRESS: - event.type = ev_keydown; + case KEYBOARD_RELEASE: + press = (kevent->type == KEYBOARD_PRESS); event.data1 = translate_key(kevent->code); - event.data2 = get_localized_key(kevent->code); - event.data3 = get_typed_char(kevent->code); + break; - if (event.data1 != 0) - { - d_post_event(&event); - } + case KEYBOARD_SPECPRESS: + case KEYBOARD_SPECREL: + press = (kevent->type == KEYBOARD_SPECPRESS); + event.data1 = translate_speckey(kevent->code); break; - case KEYBOARD_RELEASE: + default: + return; + } + + if (event.data1 == 0) + { + return; + } + + if (press) + { + event.type = ev_keydown; + + /* A special key has no printable character, so it contributes + * nothing to data2 and data3. + */ + + if (kevent->type == KEYBOARD_PRESS) + { + event.data2 = get_localized_key(kevent->code); + event.data3 = get_typed_char(kevent->code); + } + else + { + event.data2 = 0; + event.data3 = 0; + } + } + else + { event.type = ev_keyup; - event.data1 = translate_key(kevent->code); /* data2/data3 are initialized to zero for ev_keyup. * For ev_keydown it's the shifted Unicode character @@ -347,16 +474,9 @@ void i_handle_keyboard_event(struct keyboard_event_s *kevent) event.data2 = 0; event.data3 = 0; - - if (event.data1 != 0) - { - d_post_event(&event); - } - break; - - default: - break; } + + d_post_event(&event); } void i_start_text_input(int x1, int y1, int x2, int y2) diff --git a/games/NXDoom/src/i_video.c b/games/NXDoom/src/i_video.c index cf3c837802a..d9a76a65144 100644 --- a/games/NXDoom/src/i_video.c +++ b/games/NXDoom/src/i_video.c @@ -505,19 +505,11 @@ static void i_get_event(void) while ((err = get_kbd_event(&kbdevent)) == 0) { - switch (kbdevent.type) - { - case KEYBOARD_PRESS: - - /* deliberate fall-though */ - - case KEYBOARD_RELEASE: - i_handle_keyboard_event(&kbdevent); - break; + /* All four event types are handled: dropping the special ones here + * would silently lose the arrow keys. + */ - default: - break; - } + i_handle_keyboard_event(&kbdevent); } #endif } From f462f825d3148404a18023ed37b6335b3bc1a539 Mon Sep 17 00:00:00 2001 From: Jorge Guzman Date: Thu, 30 Jul 2026 16:55:35 -0300 Subject: [PATCH 3/6] examples/lvglterm: read any keyboard through one input path The terminal had three input sources to choose from, and its own help text explained why: the physical keyboard options "differ in the data the keyboard device delivers on read(), so pick the one that matches the hardware". That is the abstraction leaking. A user had to know that the keyboard was USB rather than a matrix in order to compile the terminal, and swapping one for the other meant rebuilding. There are two sources now, touch and physical, and the physical one reads any keyboard registered with keyboard_register(). Which format arrives is decided by INPUT_KEYBOARD_BYTESTREAM, a property of the build rather than of the hardware, so the terminal no longer asks. Cursor keys reported as special events scroll the terminal, which is what a driver following the current contract sends. The out of band codes that the M5Stack Cardputer reports as ordinary presses are still honoured, so that board keeps working until its driver is converted. Signed-off-by: Jorge Guzman --- examples/lvglterm/Kconfig | 32 +++----- examples/lvglterm/lvglterm_kbd.c | 137 +++++++++++++++---------------- 2 files changed, 77 insertions(+), 92 deletions(-) diff --git a/examples/lvglterm/Kconfig b/examples/lvglterm/Kconfig index ca393e845b1..7c4a706834a 100644 --- a/examples/lvglterm/Kconfig +++ b/examples/lvglterm/Kconfig @@ -17,9 +17,7 @@ choice default EXAMPLES_LVGLTERM_INPUT_TOUCH ---help--- Select how the terminal receives keystrokes. Only one input source - is built at a time. The physical-keyboard options differ in the data - the keyboard device delivers on read(), so pick the one that matches - the hardware. + is built at a time. config EXAMPLES_LVGLTERM_INPUT_TOUCH bool "On-screen keyboard (touch)" @@ -28,37 +26,29 @@ config EXAMPLES_LVGLTERM_INPUT_TOUCH command line is typed and submitted with Enter. This is the default and matches the original behaviour. -config EXAMPLES_LVGLTERM_INPUT_KBD_MATRIX - bool "Matrix / upper-half keyboard (keyboard events)" +config EXAMPLES_LVGLTERM_INPUT_KBD + bool "Physical keyboard" depends on INPUT_KEYBOARD ---help--- - Physical keyboard registered through the INPUT_KEYBOARD upper half, - whose read() returns struct keyboard_event_s events (for example the - M5Stack Cardputer matrix keyboard on /dev/kbd0). Fn Up/Down scroll - the terminal. - -config EXAMPLES_LVGLTERM_INPUT_KBD_USB - bool "USB HID keyboard (byte stream)" - depends on USBHOST_HIDKBD - select LIBC_KBDCODEC - ---help--- - USB HID keyboard (for example on /dev/kbda with CONFIG_USBHOST_HIDKBD). - read() returns a byte stream that is decoded with the keyboard codec: - normal keys go to the shell and, when the driver is built with - CONFIG_HIDKBD_ENCODED, the Up/Down cursor keys scroll the terminal. + Any keyboard registered with keyboard_register(): USB HID, a matrix, + the simulator, virtio. The terminal does not need to know which one + it is. Cursor Up and Down scroll the terminal, everything else goes + to the shell. endchoice config EXAMPLES_LVGLTERM_KBD_DEV string "Keyboard device path" - default "/dev/kbda" if EXAMPLES_LVGLTERM_INPUT_KBD_USB default "/dev/kbd0" - depends on EXAMPLES_LVGLTERM_INPUT_KBD_MATRIX || EXAMPLES_LVGLTERM_INPUT_KBD_USB + depends on EXAMPLES_LVGLTERM_INPUT_KBD ---help--- Keyboard device the terminal reads from. Can also be overridden at run time by passing the path as the first command-line argument (useful when more than one keyboard is present). + Note that the USB HID driver names its devices /dev/kbda onwards, + since they come and go as keyboards are plugged in. + choice prompt "LVGL Terminal font" default EXAMPLES_LVGLTERM_FONT_UNSCII_16 diff --git a/examples/lvglterm/lvglterm_kbd.c b/examples/lvglterm/lvglterm_kbd.c index 08ae83612d8..daea4c50bcf 100644 --- a/examples/lvglterm/lvglterm_kbd.c +++ b/examples/lvglterm/lvglterm_kbd.c @@ -26,15 +26,13 @@ * a keyboard driver; the device defaults to CONFIG_EXAMPLES_LVGLTERM_KBD_DEV * and can be overridden by the first command-line argument. * - * Two device flavours are supported, selected at build time: + * Any keyboard registered with keyboard_register() works: USB HID, a + * matrix, the simulator, virtio. Which one it is makes no difference here. * - * - Upper-half keyboards (INPUT_KEYBOARD, e.g. the M5Stack Cardputer - * matrix on /dev/kbd0) deliver struct keyboard_event_s events; the Fn - * Up/Down keys are handled locally as scroll requests. - * - USB HID keyboards (EXAMPLES_LVGLTERM_INPUT_KBD_USB, e.g. /dev/kbda) - * deliver a byte stream decoded with the NuttX keyboard codec: normal - * keys are forwarded to the shell and, when the driver is built with - * CONFIG_HIDKBD_ENCODED, the Up/Down cursor keys scroll the terminal. + * The device delivers struct keyboard_event_s events unless the kernel was + * built with INPUT_KEYBOARD_BYTESTREAM, in which case it delivers the byte + * stream that the keyboard codec defines. That is a property of the build + * rather than of the hardware, so it is what selects the reader below. */ /**************************************************************************** @@ -50,11 +48,9 @@ #include #include -#ifdef CONFIG_EXAMPLES_LVGLTERM_INPUT_KBD_MATRIX -# include -#endif +#include -#ifdef CONFIG_EXAMPLES_LVGLTERM_INPUT_KBD_USB +#ifdef CONFIG_INPUT_KEYBOARD_BYTESTREAM # include # include #endif @@ -67,17 +63,6 @@ * Pre-processor Definitions ****************************************************************************/ -/* Out-of-band key codes for the Fn + navigation cluster (cursor keys), - * reported by keyboard drivers that follow this convention (for example the - * M5Stack Cardputer). Up/Down scroll the terminal instead of going to the - * shell; drivers that do not emit these codes keep normal behaviour. - */ - -#define KEY_UP 0x80 -#define KEY_DOWN 0x81 -#define KEY_LEFT 0x82 -#define KEY_RIGHT 0x83 - #define SCROLL_STEP 24 /* Pixels scrolled per Up/Down keypress */ /**************************************************************************** @@ -134,6 +119,41 @@ static void scroll_terminal(bool up) lv_obj_scroll_by(g_output, 0, dy, LV_ANIM_OFF); } +/**************************************************************************** + * Name: handle_key + * + * Description: + * Act on one key press. Cursor Up and Down scroll the terminal, anything + * else goes to the shell. + * + * Input Parameters: + * code - The character, or a value from enum kbd_keycode_e + * special - True if the code is a keycode rather than a character. The + * two ranges overlap, so this is what tells them apart. + * + ****************************************************************************/ + +static void handle_key(uint32_t code, bool special) +{ + if (special) + { + if (code == KEYCODE_UP) + { + scroll_terminal(true); + } + else if (code == KEYCODE_DOWN) + { + scroll_terminal(false); + } + + /* Any other special key has no meaning to a terminal */ + + return; + } + + feed_char(code); +} + /**************************************************************************** * Public Functions ****************************************************************************/ @@ -171,25 +191,21 @@ void lvglterm_input_create(int argc, FAR char *argv[]) void lvglterm_input_poll(void) { - if (g_kfd < 0) - { - return; - } - -#ifdef CONFIG_EXAMPLES_LVGLTERM_INPUT_KBD_USB - /* USB HID keyboard: read() returns a byte stream that is decoded with the - * keyboard codec. Normal keys are fed to the shell; the Up/Down cursor - * keys (only emitted when the driver is built with CONFIG_HIDKBD_ENCODED) - * scroll the terminal. On a plain-ASCII stream every byte simply decodes - * to a normal key press, so this also works without encoding. - */ - - struct lib_meminstream_s stream; - struct kbd_getstate_s state; char buf[64]; ssize_t nread; +#ifdef CONFIG_INPUT_KEYBOARD_BYTESTREAM + struct lib_meminstream_s stream; + struct kbd_getstate_s state; uint8_t ch; int ret; +#else + FAR struct keyboard_event_s *evt; +#endif + + if (g_kfd < 0) + { + return; + } nread = read(g_kfd, buf, sizeof(buf)); if (nread <= 0) @@ -197,12 +213,13 @@ void lvglterm_input_poll(void) return; } +#ifdef CONFIG_INPUT_KEYBOARD_BYTESTREAM memset(&state, 0, sizeof(state)); lib_meminstream(&stream, buf, nread); for (; ; ) { - ret = kbd_decode((FAR struct lib_instream_s *)&stream, &state, &ch); + ret = kbd_decode(&stream.common, &state, &ch); if (ret == KBD_ERROR) { break; @@ -210,51 +227,29 @@ void lvglterm_input_poll(void) if (ret == KBD_PRESS) { - feed_char((char)ch); + handle_key(ch, false); } else if (ret == KBD_SPECPRESS) { - if (ch == KEYCODE_UP) - { - scroll_terminal(true); - } - else if (ch == KEYCODE_DOWN) - { - scroll_terminal(false); - } + handle_key(ch, true); } } #else - /* Upper-half keyboard: read() returns keyboard_event_s events */ - - struct keyboard_event_s evt; + evt = (FAR struct keyboard_event_s *)buf; - while (read(g_kfd, &evt, sizeof(evt)) == (ssize_t)sizeof(evt)) + while (nread >= (ssize_t)sizeof(struct keyboard_event_s)) { - if (evt.type != KEYBOARD_PRESS) + if (evt->type == KEYBOARD_PRESS) { - continue; + handle_key(evt->code, false); } - - /* Fn navigation keys are handled locally: Up/Down scroll the terminal; - * Left/Right are reserved and simply swallowed for now. - */ - - if (evt.code >= KEY_UP && evt.code <= KEY_RIGHT) + else if (evt->type == KEYBOARD_SPECPRESS) { - if (evt.code == KEY_UP) - { - scroll_terminal(true); - } - else if (evt.code == KEY_DOWN) - { - scroll_terminal(false); - } - - continue; + handle_key(evt->code, true); } - feed_char((char)evt.code); + nread -= sizeof(struct keyboard_event_s); + evt++; } #endif } From b8a7b2cc9b9752be7686ffc956e0e5f8990ca285 Mon Sep 17 00:00:00 2001 From: Jorge Guzman Date: Thu, 30 Jul 2026 18:46:11 -0300 Subject: [PATCH 4/6] system/kbd: inject keys into a uinput keyboard Somebody porting a board has to work on the application side before the keyboard driver exists, and somebody reviewing that work often does not have the board at hand at all. With -i the tool goes the other way and writes into a uinput keyboard, either what it reads from its own stdin or every key of another keyboard. So an application reading /dev/ukeyboard is driven from the serial console, or from whatever is on the far end of it, and a real keyboard and an injected one can drive it at the same time, which neither can do on its own since an application opens a single device. Nothing in the application changes: it is reading a keyboard like any other, which is the point. Validated on a Linum STM32H753BI, forwarding a USB HID keyboard and the serial console into the same virtual keyboard, with the LVGL terminal reading it. 24 press and release pairs survived the crossing with no duplicate, no orphan and three keys held at once. Signed-off-by: Jorge Guzman --- system/kbd/kbd_main.c | 221 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 216 insertions(+), 5 deletions(-) diff --git a/system/kbd/kbd_main.c b/system/kbd/kbd_main.c index a736dd85c57..20851077bb4 100644 --- a/system/kbd/kbd_main.c +++ b/system/kbd/kbd_main.c @@ -31,6 +31,13 @@ * built with INPUT_KEYBOARD_BYTESTREAM, in which case it delivers the byte * stream that the keyboard codec defines. This follows that setting rather * than having a switch of its own. + * + * With -i it goes the other way and injects into a uinput keyboard, either + * what it reads from its own stdin or every key of another keyboard. So an + * application can be driven from a serial console with no keyboard hardware + * at all, and a real keyboard and an injected one can drive it at the same + * time, which neither can do on its own since an application opens a single + * device. */ /**************************************************************************** @@ -45,6 +52,7 @@ #include #include #include +#include #include #include #include @@ -178,6 +186,157 @@ static size_t kbd_dump(FAR char *buffer, size_t nbytes) } #endif +/**************************************************************************** + * Name: kbd_inject + * + * Description: + * Turn each byte read from stdin into a key press and release on the + * given device. The device has to be a uinput keyboard: a real one has + * nothing to inject into. + * + * Note that this always writes struct keyboard_event_s. Injection goes + * through the lower half write method, which is not affected by + * INPUT_KEYBOARD_BYTESTREAM. + * + * Returned Value: + * The number of keys injected. + * + ****************************************************************************/ + +static bool kbd_emit(int fd, uint32_t code, uint32_t type) +{ + struct keyboard_event_s key; + + key.code = code; + key.type = type; + + if (write(fd, &key, sizeof(key)) != (ssize_t)sizeof(key)) + { + fprintf(stderr, "kbd: inject failed: %d\n", errno); + return false; + } + + return true; +} + +static size_t kbd_inject(int fd) +{ + unsigned char buf[KBD_READ_SIZE]; + uint32_t code; + ssize_t nread; + ssize_t i; + size_t nkeys = 0; + + for (; ; ) + { + nread = read(STDIN_FILENO, buf, sizeof(buf)); + if (nread <= 0) + { + break; + } + + for (i = 0; i < nread; i++) + { + /* A terminal sends a carriage return where a keyboard would + * send a line feed. + */ + + code = (buf[i] == '\r') ? '\n' : buf[i]; + + if (!kbd_emit(fd, code, KEYBOARD_PRESS)) + { + return nkeys; + } + + kbd_emit(fd, code, KEYBOARD_RELEASE); + nkeys++; + } + } + + return nkeys; +} + +/**************************************************************************** + * Name: kbd_forward + * + * Description: + * Copy every key from one keyboard onto another. Pointing an + * application at the destination lets a real keyboard and an injected one + * drive it at the same time, which neither can do on its own since an + * application opens a single device. + * + * Returned Value: + * The number of keys forwarded. + * + ****************************************************************************/ + +static size_t kbd_forward(int fd, int srcfd) +{ + char buf[KBD_READ_SIZE]; + ssize_t nread; + size_t nkeys = 0; +#ifdef CONFIG_INPUT_KEYBOARD_BYTESTREAM + struct lib_meminstream_s stream; + struct kbd_getstate_s state; + uint8_t ch; + int ret; +#else + FAR struct keyboard_event_s *key; +#endif + + for (; ; ) + { + nread = read(srcfd, buf, sizeof(buf)); + if (nread <= 0) + { + break; + } + +#ifdef CONFIG_INPUT_KEYBOARD_BYTESTREAM + + /* The source speaks the byte stream and the destination only takes + * events, so this decodes rather than copies. The decoder returns + * the event type directly. + */ + + memset(&state, 0, sizeof(state)); + lib_meminstream(&stream, buf, nread); + + for (; ; ) + { + ret = kbd_decode(&stream.common, &state, &ch); + if (ret == KBD_ERROR) + { + break; + } + + if (!kbd_emit(fd, ch, ret)) + { + return nkeys; + } + + nkeys++; + } +#else + key = (FAR struct keyboard_event_s *)buf; + + while (nread >= (ssize_t)sizeof(struct keyboard_event_s)) + { + if (!kbd_emit(fd, key->code, key->type)) + { + return nkeys; + } + + nread -= sizeof(struct keyboard_event_s); + key++; + nkeys++; + } +#endif + } + + return nkeys; +} + /**************************************************************************** * Public Functions ****************************************************************************/ @@ -189,20 +348,41 @@ static size_t kbd_dump(FAR char *buffer, size_t nbytes) int main(int argc, FAR char *argv[]) { FAR const char *devpath = CONFIG_SYSTEM_KBD_DEVPATH; + FAR const char *srcpath = NULL; char buffer[KBD_READ_SIZE]; + bool inject = false; size_t nkeys = 0; size_t limit = 0; ssize_t nbytes; + int argi = 1; + int srcfd; int fd; - if (argc > 1) + if (argi < argc && strcmp(argv[argi], "-i") == 0) + { + inject = true; + argi++; + } + + if (argi < argc) { - devpath = argv[1]; + devpath = argv[argi++]; } - if (argc > 2) + if (argi < argc) { - limit = strtoul(argv[2], NULL, 10); + /* When injecting, the extra argument is the keyboard to forward from + * rather than a count: there is nothing to count. + */ + + if (inject) + { + srcpath = argv[argi]; + } + else + { + limit = strtoul(argv[argi], NULL, 10); + } } /* Wait for the device. A USB keyboard appears when it is plugged in, so @@ -211,7 +391,7 @@ int main(int argc, FAR char *argv[]) for (; ; ) { - fd = open(devpath, O_RDONLY); + fd = open(devpath, (inject ? O_WRONLY : O_RDONLY) | O_CLOEXEC); if (fd >= 0) { break; @@ -222,6 +402,36 @@ int main(int argc, FAR char *argv[]) sleep(3); } + if (inject) + { + if (srcpath == NULL) + { + printf("kbd: injecting into %s, type to send, Ctrl-D to stop\n", + devpath); + fflush(stdout); + + nkeys = kbd_inject(fd); + } + else + { + srcfd = open(srcpath, O_RDONLY | O_CLOEXEC); + if (srcfd < 0) + { + fprintf(stderr, "kbd: open %s failed: %d\n", srcpath, errno); + close(fd); + return EXIT_FAILURE; + } + + printf("kbd: forwarding %s into %s\n", srcpath, devpath); + fflush(stdout); + + nkeys = kbd_forward(fd, srcfd); + close(srcfd); + } + + goto done; + } + printf("kbd: reading %s, %s\n", devpath, #ifdef CONFIG_INPUT_KEYBOARD_BYTESTREAM "byte stream" @@ -259,6 +469,7 @@ int main(int argc, FAR char *argv[]) } } +done: close(fd); printf("kbd: %zu keys\n", nkeys); return EXIT_SUCCESS; From b0a472154f8387ebc30fa5980db34d3b72bedc89 Mon Sep 17 00:00:00 2001 From: Jorge Guzman Date: Thu, 30 Jul 2026 18:10:21 -0300 Subject: [PATCH 5/6] examples/hidkbd: follow the byte stream option rename The example gated its decoding on HIDKBD_ENCODED, which no longer exists: the USB HID keyboard driver reports through the keyboard upper half now, and what produces the byte stream is INPUT_KEYBOARD_BYTESTREAM. Left as it was, the option could never be satisfied and the example would quietly stop decoding special keys. Reaching the option again brings hidkbd_decode() back into the build after a spell of being unreachable, and it uses isprint() without including ctype.h. That is an error rather than a warning under the -Werror the CI builds with, in the nine configurations that enable EXAMPLES_HIDKBD, so add the include here rather than in a later commit. Signed-off-by: Jorge Guzman --- examples/hidkbd/Kconfig | 2 +- examples/hidkbd/hidkbd_main.c | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/hidkbd/Kconfig b/examples/hidkbd/Kconfig index c80d4094372..8b938de37ab 100644 --- a/examples/hidkbd/Kconfig +++ b/examples/hidkbd/Kconfig @@ -36,7 +36,7 @@ config EXAMPLES_HIDKBD_DEVNAME config EXAMPLES_HIDKBD_ENCODED bool "Encode Special Keys" default y - depends on HIDKBD_ENCODED && LIBC_KBDCODEC + depends on INPUT_KEYBOARD_BYTESTREAM ---help--- Decode special key press events in the user buffer. In this case, the example coded will use the interfaces defined in diff --git a/examples/hidkbd/hidkbd_main.c b/examples/hidkbd/hidkbd_main.c index 1515d65261b..d2608213652 100644 --- a/examples/hidkbd/hidkbd_main.c +++ b/examples/hidkbd/hidkbd_main.c @@ -35,6 +35,8 @@ #include #ifdef CONFIG_EXAMPLES_HIDKBD_ENCODED +# include + # include # include #endif @@ -55,7 +57,8 @@ # define CONFIG_EXAMPLES_HIDKBD_DEVNAME "/dev/kbda" #endif -#if !defined(CONFIG_HIDKBD_ENCODED) || !defined(CONFIG_LIBC_KBDCODEC) +#if !defined(CONFIG_INPUT_KEYBOARD_BYTESTREAM) || \ + !defined(CONFIG_LIBC_KBDCODEC) # undef CONFIG_EXAMPLES_HIDKBD_ENCODED #endif From 4f211f6d582dca971ec6089a92feff7a3f0f3211 Mon Sep 17 00:00:00 2001 From: Jorge Guzman Date: Fri, 31 Jul 2026 00:17:44 -0300 Subject: [PATCH 6/6] nshlib, graphics/microwindows: require the keyboard byte stream Both read a keyboard as a stream of characters: NSH uses a USB HID keyboard for stdin, and the raw mode of the Microwindows keyboard driver decodes the stream with the codec. That stream now comes from INPUT_KEYBOARD_BYTESTREAM rather than from the USB HID driver itself. Say so, so that Kconfig refuses a combination that cannot work instead of letting the application read events and treat them as text. No in-tree configuration selects either option. Signed-off-by: Jorge Guzman --- graphics/microwindows/Kconfig | 2 +- nshlib/Kconfig | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/graphics/microwindows/Kconfig b/graphics/microwindows/Kconfig index 8463d791fb0..9d5f0cb8c43 100644 --- a/graphics/microwindows/Kconfig +++ b/graphics/microwindows/Kconfig @@ -33,7 +33,7 @@ config MICROWINDOWS_KBD_EVENT config MICROWINDOWS_KBD_RAW bool "Raw-mode keyboard driver" - depends on LIBC_KBDCODEC + depends on INPUT_KEYBOARD_BYTESTREAM ---help--- Reads raw byte stream from a character device (e.g., /dev/kbda) and decodes escape sequences via the kbd_codec library. Suitable diff --git a/nshlib/Kconfig b/nshlib/Kconfig index 8f0cd36ee67..fbbb08d3b39 100644 --- a/nshlib/Kconfig +++ b/nshlib/Kconfig @@ -1090,6 +1090,7 @@ config NSH_USBKBD bool "Use USB keyboard input" default n depends on NSH_CONSOLE && USBHOST_HIDKBD && !NSH_USBCONSOLE + depends on INPUT_KEYBOARD_BYTESTREAM ---help--- Normally NSH uses the same device for stdin, stdout, and stderr. By default, that device is /dev/console. If this option is selected, @@ -1098,6 +1099,9 @@ config NSH_USBKBD interface) and the data from the keyboard will drive NSH. NSH output (stdout and stderr) will still go to /dev/console. + NSH reads the keyboard as a stream of characters, which is what + INPUT_KEYBOARD_BYTESTREAM makes a keyboard device deliver. + if NSH_USBKBD config NSH_USBKBD_DEVNAME