Engelbart is a compile-time USB device compiler for embedded C++.
Users describe a USB device with a high-level, strongly typed C++ API. Engelbart generates USB descriptors, HID report descriptors, endpoint layouts, interface numbering, composite device configuration, TinyUSB-facing glue, validation results, and human-readable inspection output.
The first target is RP2040 / Raspberry Pi Pico WH with Pico SDK, TinyUSB, CMake, GCC ARM Embedded, and C++20 where the toolchain supports it. The current implementation is intentionally narrow: HID mouse, boot keyboard, and composite mouse + keyboard with separate HID interfaces.
Engelbart is not a USB attack framework, BadUSB toolkit, anti-cheat tool, GUI product, build-system replacement, runtime descriptor interpreter, or general embedded ecosystem. The core project is the typed descriptor compiler and the generated bytes must remain inspectable.
Engelbart is pre-1.0. Public APIs, generated artifact formats, and diagnostic IDs may still change.
Implemented now:
- header-only C++20 core library
- typed device identities and minimal HID device models
- byte-exact device, configuration, string, and HID report descriptors
- deterministic endpoint and interface allocation for the supported devices
- validation with diagnostic IDs
- inspection text for generated descriptors and allocation state
- TinyUSB descriptor callback helpers
- host-side tests and golden descriptor fixtures
- Pico WH / TinyUSB smoke firmware with UF2 output and picotool reset support
Deferred until a later RFC or ADR:
- CDC, MIDI, gamepad, vendor class, and richer HID models
- runtime descriptor reconfiguration
- runtime descriptor interpretation
- CLI, GUI, dashboard, Studio, or host-side Inspector
- broad board support beyond RP2040 / Pico WH
- C++20
- CMake 3.20 or newer
- Pico SDK for the hardware smoke target
- TinyUSB as the year-one USB device stack
- GCC ARM Embedded /
arm-none-eabi-g++for Pico builds picotoolfor the verified USB-only smoke workflow
The project is organized around a static transformation:
typed device description
-> validation
-> descriptor bytes
-> endpoint and interface allocation
-> inspection output
-> TinyUSB-facing callback glue
The public model describes USB devices, HID capabilities, descriptors, endpoints, and interfaces. TinyUSB is the implementation layer used by the Pico target, not the primary authoring model.
Generated descriptors are emitted as bytes rather than host-packed C++ structs. Tests verify byte output, length fields, string descriptor encoding, endpoint allocation, interface numbering, HID report descriptors, and TinyUSB callback routing.
Build the host examples and tests:
cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failureThe host build produces:
engelbart_testsengelbart_mouse_exampleengelbart_keyboard_exampleengelbart_composite_example
Build the Pico WH / TinyUSB smoke firmware when PICO_SDK_PATH and the ARM embedded toolchain are available:
cmake -S examples/pico_tinyusb_smoke -B build-pico-smoke -DPICO_BOARD=pico_w
cmake --build build-pico-smokeThe smoke build emits ELF, UF2, BIN, and HEX outputs through the Pico SDK. The UF2 path is:
build-pico-smoke/engelbart_pico_tinyusb_smoke.uf2
The Pico smoke firmware uses the pid.codes-style test VID/PID 1209:0003. RP2040 BOOTSEL mode uses Raspberry Pi VID/PID 2e8a:0003.
To ask the running smoke firmware to reboot into BOOTSEL:
sudo picotool reboot -u -f --vid 0x1209 --pid 0x0003 || trueOnce the board is in BOOTSEL, use plain picotool commands without the smoke firmware VID/PID filter:
sudo picotool info -a
sudo picotool load -x build-pico-smoke/engelbart_pico_tinyusb_smoke.uf2See hardware testing for Linux permission notes, picotool's serial-tracking behavior, and the SWD/OpenOCD automation path. The reset support in the smoke firmware is development harness infrastructure; it does not make CDC or vendor class a supported Engelbart-generated USB class.
The current API is small and may change before 1.0:
#include <iostream>
#include "engelbart/engelbart.hpp"
using example_identity = engelbart::identity<
0x1209,
0x0001,
engelbart::fixed_string{"Engelbart Example"},
engelbart::fixed_string{"Example HID Mouse"}>;
using example_device = engelbart::hid_mouse<example_identity>;
int main() {
static_assert(engelbart::validate_device<example_device>().ok());
std::cout << engelbart::inspect_device<example_device>();
}Examples use non-impersonating test identities. Do not use real vendor IDs, product IDs, manufacturer strings, or product strings in examples unless they belong to the project using them.
include/engelbart/ Header-only core library
examples/ Host examples
examples/pico_tinyusb_smoke/ Pico WH / TinyUSB smoke firmware
tests/ Host tests and golden descriptor fixtures
docs/adr/ Accepted architecture decisions
docs/rfc/ API and descriptor layout RFCs
docs/design-history/ Public design-history summaries
docs/hardware-testing.md Hardware smoke workflow
The local internal/ directory is ignored by git and is not part of the public project.
Keep changes inside the project identity: a compile-time USB device compiler for embedded C++.
For descriptor changes:
- update or add byte-exact golden tests
- verify descriptor length fields and little-endian encoding
- update inspection expectations when visible output changes
- document compatibility-relevant descriptor byte changes
- avoid runtime descriptor synthesis unless an ADR explicitly allows it
For Pico/TinyUSB changes:
- keep the Engelbart core separate from smoke-test harness conveniences
- do not treat development-only reset support as a generated USB class
- verify the Pico smoke build still emits a UF2
- update hardware testing when the real workflow changes
For scope changes:
- start with an RFC or ADR
- check non-goals
- keep examples free of real-product impersonation
- Prefer compile-time generation and validation where practical.
- Emit descriptor bytes explicitly; do not rely on host struct packing or host endianness.
- Keep generated output deterministic and inspectable.
- Avoid hidden global registration, static constructor side effects, and runtime descriptor trees.
- Keep abstractions zero-cost for generated descriptors.
- Treat TinyUSB as an integration boundary, not the user's main device description language.
- Keep diagnostics actionable and test diagnostic IDs plus key message fragments.
Run the standard host test loop:
cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failureThe test strategy covers:
- pure descriptor generation
- byte-exact golden descriptors
- HID report descriptor bytes
- USB string descriptor encoding and indexes
- endpoint and interface allocation
- validation failures and diagnostic IDs
- TinyUSB callback routing
- inspection output
Pico hardware enumeration remains manual or CI-hardware-dependent. The repository provides a smoke firmware, not a complete hardware lab automation system.
Start with these documents:
- Engineering principles
- Non-goals
- Roadmap
- Testing strategy
- Versioning
- Release checklist
- ADR 0001: Project Identity
- RFC 0001: Minimal HID Device Model
- RFC 0002: Minimal HID Descriptor Layout
- Hardware testing notes
- Design history: scope narrowing
Contributions should make the supported descriptor compiler clearer, smaller, better tested, or more correct. Avoid adding product surfaces or USB classes before the core model needs them. See CONTRIBUTING.md.
Before changing generated bytes, explain why the byte output changes and update the golden tests deliberately. Before adding a new device class or target board, write the design down first.
Engelbart is distributed under the MIT License.