Send keyboard, mouse, gamepad, touchscreen, touchpad, pen, and media-key input from a PC to any USB host through a microcontroller board (Raspberry Pi Pico RP2040 / RP2350, Nordic nRF52840, STM32F4, or ESP32-S3) connected over a high-speed UART link. The target machine sees a standard USB composite HID device and needs no driver, no app, and no agent. Linux, Windows, and macOS hosts; usable from C, C++, Python, C#, and Rust. MIT licensed.
Version 0.2.1. Changes are listed in CHANGELOG.md.
libmimhid is a C++20 library with a stable C ABI. You describe the USB device
identity and each HID interface (ranges, bit widths, Report IDs, Usages,
endpoints); the library validates the descriptor against the USB HID 1.11, HUT
1.7, and target Linux parser rules, streams the configuration over UART to the
board, and keeps the report state machines (multi-touch contacts, key rollover,
pointer deltas, pen tool transitions) bit-exact. The board holds its USB data
lines at 0 V until your first configuration is committed, so no generic vendor
or bootloader device ever appears on the target bus.
Sister library of libaoahid, which
sends the same kind of input to an Android device over a plain USB cable using
Android Open Accessory 2.0, with no microcontroller board needed. Both libraries
share the same Spec / Node model and profile state machines.
- Not a source of defaults. Every
*_initfunction zeroes its structure and setsstruct_sizeonly. Any required USB, transport, endpoint, or profile field left zero is rejected withMIMHID_ERR_UNSET_FIELD(mimhid_last_errornames the exact field). - Not a scheduler. The library generates no key repeat, debounce, or timed motion, and stores no timeout on a handle: every wait is passed to the call that waits.
- Not reflashed per device. One firmware image per MCU serves any USB identity, interface layout, and report descriptor sent at runtime.
- Not hardware-verified. No release has been verified on a board; see Verification status.
Requirements: a C11/C++20 compiler and CMake 3.20 or later; no external host
libraries are needed. To talk to a real board you also need one supported MCU
flashed with mimhid_firmware and a 3.3 V USB-UART adapter
(QUICKSTART.md).
- Release archive. The Releases page publishes exactly 18 uploaded assets per release: one
libmimhid-X.Y.Z-<platform>-<arch>[-ubuntu22.04]archive per platform (Linux, Windows, macOS) carrying both the shared and static libraries, the headers, and the CMake andpkg-configfiles, alongside the prebuilt firmware images, source archive, SPDX 2.3 SBOM, and SHA-256 checksums (RELEASES.md). - CMake.
find_package(mimhid 0.2 CONFIG REQUIRED COMPONENTS shared)orCOMPONENTS static(or both), then linkmimhid::mimhidormimhid::mimhid_static(ormimhid::cpp/mimhid::cpp_staticfor the C++20 header). A0.2request is satisfied by a0.2.xinstall only. - pkg-config.
pkg-config --cflags --libs mimhidfor the shared library, orpkg-config --static --cflags --libs mimhid-staticfor the static archive. - vcpkg. Not needed: the host library has zero external dependencies and uses operating-system serial APIs (
termioson Linux and macOS, Win32 on Windows) directly. - From source.
cmake -S . -B build -DMIMHID_BUILD_SHARED=ON -DMIMHID_BUILD_STATIC=ON && cmake --build build --config Release && cmake --install build --prefix staging --config Release. Every CMake option, the presets, the firmware builds, and the sanitizer and coverage commands are in BUILDING.md.
Pressing and releasing the a key on a board on /dev/ttyUSB0. Every value the
library needs is set, including transport.log_level: a zero there is refused
like any other unset field. See
examples/c/basic_keyboard_mouse.c for port
discovery.
#define _POSIX_C_SOURCE 200809L /* nanosleep */
#include "mimhid.h"
#include <stdio.h>
#ifdef _WIN32
#include <windows.h>
static void pause_ms(unsigned ms) { Sleep(ms); }
#else
#include <time.h>
static void pause_ms(unsigned ms) {
const struct timespec t = {0, (long)ms * 1000000L};
nanosleep(&t, NULL);
}
#endif
static int failed(const char* call, mimhid_result result) {
/* Read the record first: mimhid_result_name is a call too and replaces it. */
const mimhid_error_detail* detail = mimhid_last_error();
const char* field = detail->field != NULL ? detail->field : "-";
const char* reason = detail->reason;
fprintf(stderr, "%s: %s (%s: %s)\n", call, mimhid_result_name(result), field, reason);
return 1;
}
int main(void) {
mimhid_device_config dev_cfg;
mimhid_node_config node_cfg;
mimhid_commit_timeouts waits;
mimhid_keyboard_options kb_opts;
mimhid_device_config_init(&dev_cfg);
mimhid_node_config_init(&node_cfg);
mimhid_commit_timeouts_init(&waits);
mimhid_keyboard_options_init(&kb_opts);
dev_cfg.transport.baud_rate = MIMHID_BAUD_FASTEST;
dev_cfg.transport.handshake_timeout_ms = 200;
dev_cfg.transport.maximum_report_bytes = 64;
dev_cfg.transport.linux_hid = (mimhid_linux_hid_policy){4096, 256, 4, 256, 12288, 65528};
dev_cfg.transport.log_level = MIMHID_LOG_DISABLED; /* zero is refused as unset */
dev_cfg.usb_device.bcd_usb = 0x0200;
dev_cfg.usb_device.ep0_max_packet_size = 64;
dev_cfg.usb_device.vendor_id = 0x1209; /* pid.codes test IDs: in-house testing only */
dev_cfg.usb_device.product_id = 0x0001;
dev_cfg.usb_device.language_id = 0x0409;
dev_cfg.usb_config.configuration_value = 1;
dev_cfg.usb_config.bm_attributes = 0x80;
dev_cfg.usb_config.bmax_power = 50;
node_cfg.iface.class_code = 0x03;
node_cfg.iface.bcd_hid = 0x0111;
node_cfg.ep_in = (mimhid_usb_endpoint){0x81, 1, 64};
waits.control_timeout_ms = 500;
waits.reconnect_timeout_ms = 3000;
kb_opts.usage_minimum = 0x04;
kb_opts.usage_maximum = 0xDD;
mimhid_device* dev = NULL;
mimhid_spec* spec = NULL;
mimhid_node* kb = NULL;
mimhid_result r = mimhid_device_open("/dev/ttyUSB0", &dev_cfg, &dev);
if (r != MIMHID_OK)
return failed("mimhid_device_open", r);
int status = 1;
if ((r = mimhid_spec_create_keyboard(&kb_opts, &spec)) != MIMHID_OK) {
failed("mimhid_spec_create_keyboard", r);
} else if ((r = mimhid_node_open(dev, spec, &node_cfg, &waits, &kb)) != MIMHID_OK) {
failed("mimhid_node_open", r);
} else if ((r = mimhid_device_wait_ready(dev, 5000)) != MIMHID_OK) {
failed("mimhid_device_wait_ready", r);
} else if ((r = mimhid_keyboard_key(kb, 0x04, 1)) != MIMHID_OK || /* Keyboard a */
(r = mimhid_node_submit(kb, 500)) != MIMHID_OK ||
(r = mimhid_keyboard_key(kb, 0x04, 0)) != MIMHID_OK ||
(r = mimhid_node_submit(kb, 500)) != MIMHID_OK) {
failed("typing a", r);
} else {
status = 0;
}
/* Taking the device off the target drops reports still queued on the board, so give the
* target, which polls every bInterval (1 ms here), time to read them first. */
pause_ms(50);
if (kb != NULL && (r = mimhid_node_close(kb, &waits, 1000)) != MIMHID_OK)
status = failed("mimhid_node_close", r);
if (spec != NULL)
mimhid_spec_release(spec);
if ((r = mimhid_device_close(dev, 500, 1000)) != MIMHID_OK)
status = failed("mimhid_device_close", r);
return status;
}On Linux, from the directory of a release archive that holds include/ and
lib/ (the static library needs the C++ runtime; examples/README.md
has the other link lines):
cc -std=c11 -Iinclude first.c lib/libmimhid_static.a -lstdc++ -lpthread -o firstQUICKSTART.md walks through wiring a board, setting serial permissions, and running the watchable demos under examples/c/verify/.
| Profile | Factory | Input | Output / Feature |
|---|---|---|---|
| Keyboard (NKRO bitmap) | mimhid_spec_create_keyboard |
8 modifiers + one bit per Usage from usage_minimum (at least 0x04) to usage_maximum |
— |
| Mouse (relative pointer) | mimhid_spec_create_mouse |
1..65535 buttons, X/Y, optional Wheel and AC Pan |
— |
| Toggle controls | mimhid_spec_create_toggle |
1..65535 one-bit controls from an allow-list on a page of the caller's choice (Consumer 0x0C, Generic Desktop 0x01, Telephony 0x0B, Camera Control 0x90, ...) |
— |
| Gamepad | mimhid_spec_create_gamepad |
1..65535 buttons, at least two caller-declared axes (X and Y among them), optional hat or 4-button D-pad |
— |
| Touchscreen (multi-touch) | mimhid_spec_create_touchscreen |
1..16 contact slots (X, Y, optional pressure, width, height, azimuth), optional Scan Time |
Optional Contact Count Maximum Feature report |
| Touchpad | mimhid_spec_create_touchpad |
1..16 contact slots + 0..65535 buttons |
Optional Contact Count Maximum Feature report |
| Pen (direct screen) | mimhid_spec_create_pen |
In Range, Tip, optional Invert (eraser), up to two barrel switches, X/Y, optional pressure, tilt, twist |
— |
| Digitizer | mimhid_spec_create_digitizer |
The Pen report under Digitizers / Digitizer (0x0D/0x01) |
— |
| Raw descriptor | mimhid_spec_create_raw / mimhid_node_open_raw |
Any Input report (mimhid_raw_submit checks it against the Spec; mimhid_report_send does not) |
Output reports and Feature writes arrive through transport.event_sink; Feature reads are answered with mimhid_node_set_feature_response |
Any generated profile can also include a battery-strength Input report. Full descriptor layouts and state-machine rules are in PROFILES.md.
Both libraries share the profile Spec factories, the report state machines,
and the input calls (aoahid_keyboard_key and mimhid_keyboard_key, and so
on). They differ where the hardware differs: libmimhid has no Context
handle, takes a serial port name and the full USB and endpoint descriptors
(mimhid_device_config, mimhid_node_config), uses mimhid_commit_timeouts on
calls that re-enumerate the board, and carries two-way HID traffic (Output
reports, Feature answers, bus connect and disconnect, endpoint 0 control
transfer routing). Three differences change code that is moved across:
- Initializers.
libaoahid's*_initfunctions fill in values that work with a current Android phone, and its header has the timeout constantsAOAHID_CONTROL_TIMEOUT_MS,AOAHID_SEND_TIMEOUT_MSandAOAHID_DRAIN_TIMEOUT_MS.libmimhid's*_initfunctions only zero the structure and setstruct_size, there are no timeout constants, and a required field or timeout left at zero is refused withMIMHID_ERR_UNSET_FIELD. - Closing.
libaoahidhasAOAHID_CLOSE_PENDING(15): a transfer of the object is still in flight and the caller calls again later. Inlibmimhidwrites to the serial port finish within the call, so a close never leaves anything pending and code 15 is not used. - Result code 4. It is
AOAHID_ERR_NOT_AOAinlibaoahidandMIMHID_ERR_PROTOCOL(firmware of another major or minor version) here.
The full comparison is in API.md.
- Hardware: no release is hardware-verified. CHANGELOG.md records for 0.1.0 that an earlier firmware ran on a Raspberry Pi Pico and that the 0.1.0 firmware has not, and for 0.2.0 that none of it has run on a board yet.
- One latency measurement is recorded in LATENCY.md, as it was written.
- Automated CI: unit, scripted-board (
tests/scripted), and simulated-board (tests/sim, the real C firmware core over a pseudo-terminal) suites, also under ASan/UBSan and ThreadSanitizer, and an 86% line / 73% branch coverage floor onsrc/(theCoveragejob). - Details and commands: VERIFICATION.md has the feature-by-layer matrix and the commands to reproduce each check.
| Document | Contents |
|---|---|
| QUICKSTART.md | Wiring a board, flashing firmware, permissions, and running the verification demos |
| BUILDING.md | Host requirements, CMake options and presets, firmware builds, package consumption, bindings |
| API.md | C and C++ API reference: handles, structs, rules, every public function with its results, result codes |
| PROFILES.md | Descriptor and state-machine contract of all nine profiles and the battery report |
| EXAMPLES.md | Walkthrough of the C, C++, Python, C#, and Rust example programs |
| ARCHITECTURE.md | System structure, threading, data paths, and design decisions |
| INTERNALS.md | Contributor guide: source map, handle internals, locking, teardown, design decisions |
| PROTOCOL.md | Binary UART wire protocol (frames, CRC-16, commands, events, line-rate handshake) |
| LIMITS.md | Every numeric limit with its USB, HID, chip, or firmware source |
| LATENCY.md | Where time goes, the one recorded latency measurement, and how to measure |
| PORTING.md | Adding a new MCU port (the 12 HAL functions) or a new host OS serial backend |
| TARGET_MATRIX.md | Build and verification status per MCU board, host OS, and target |
| VERIFICATION.md | Feature × test-layer verification matrix, coverage numbers, and reproduction commands |
| RELEASES.md | The 18 release assets, archive layout, platform baselines, and component licenses |
| CHANGELOG.md | Release history and hardware verification notes |
libmimhid, its headers, examples, language bindings, and portable firmware
core are MIT licensed (LICENSE). Prebuilt firmware images link
TinyUSB (MIT) and vendor HALs under BSD-3-Clause and Apache-2.0; see
RELEASES.md and
THIRD_PARTY_NOTICES.md.
Most of the code, tests, and documentation in this repository were written by an AI coding assistant. Architecture and design decisions, hardware verification steps, debugging, and review of the AI's output were done by a human. The hardware status is in TARGET_MATRIX.md; review the code before relying on it for anything security- or safety-relevant.