Morse Paddle → vendor HID (raw)
July 2, 2026 · View on GitHub
Part of the hamtools suite — see the hub for how cwsd, xlog2 and usb-paddles combine into an integrated (remote) station.
Firmware for an STM32F411 "Black Pill" (WeAct F411CEU6) that turns two Morse
paddle contacts into a vendor-defined (raw) USB HID device. Unlike a
keyboard, a raw HID device on a vendor usage page is not interpreted by any OS
input subsystem, so it never types into the focused window — only software that
knows the report format reads it. The companion reader is xlog2
(HidPaddleInput).
Branches.
masteris this STM32F411 + Adafruit TinyUSB build. The olderraw-hidbranch targets the STM32F103 "Blue Pill" (maple core +USBComposite) — use it only if you have that board. The on-wire USB identity and report format are identical across both, so the xlog2 host reader is unchanged either way.
| Paddle | Pin |
|---|---|
| dit | PA0 |
| dash | PA1 |
Protocol
- Identity: USB vendor
0x1EAF, product id0x0024(1eaf:0024), manufacturer"YO6SSW"+ product"USB paddles"(shown as "YO6SSW USB paddles"), vendor HID usage page0xFFC0(RAWHID_USAGE_PAGE). Discover by matching the vendor and the usage-page bytes06 C0 FFin the report descriptor. - Report (device → host), 2 bytes, sent on every debounced change and once
at connect for the baseline:
byte0:bit0= dit closed,bit1= dash closedbyte1: sequence counter (increments per report; lets the host spot a drop)
- Latency: leading-edge debounce (acts on the first edge) + a 1 ms HID poll interval ⇒ a paddle edge reaches the host in ~1 ms worst case.
Wiring
Each paddle contact connects its pin to GND (internal pull-ups; LOW = closed). No external resistors.
PA0 ──[ dit contact ]── GND
PA1 ──[ dash contact ]── GND
Build & flash
Built with PlatformIO (board blackpill_f411ce, official ststm32 Arduino
core + the Adafruit TinyUSB library, pulled automatically via lib_deps).
Flashing is over an ST-Link/V2 on SWD (SWDIO=PA13, SWCLK=PA14, GND, 3V3) —
no USB bootloader needed.
pio run # build
pio run -t upload # flash via ST-Link
After flashing, unplug/replug the board's USB so the host re-enumerates it.
Host access (udev)
/dev/hidraw* is root-only by default, so a normal user (xlog2) can't open it.
Install the included rule once:
sudo cp udev/60-xlog2-paddle.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm trigger
then unplug/replug the board. It will then be readable by the plugdev group /
the logged-in user. Confirm enumeration with lsusb | grep 1eaf:0024, and
inspect the usage page via
cat /sys/class/hidraw/hidraw*/device/report_descriptor | xxd.
Implementation notes
- Uses the official STM32 Arduino core + Adafruit TinyUSB (
-D USBCON -D USE_TINYUSB). The maple-coreUSBCompositeHIDRawclass used on the F103 is STM32F1-only, so the F411 uses TinyUSB with a hand-written report descriptor that emits the exact same bytes (06 C0 FF …, a 2-byte input + 1-byte output report, no report ID) — wire-identical to the F103'sHIDRaw<2,1>. - HID-only (no CDC serial); there are no configurable keys — the host decides what dit/dash mean.
- USB identity is set after
TinyUSBDevice.begin()(which resets the descriptor to build defaults), then the device is detached/re-attached so the host re-reads descriptors and sees the HID interface — seeCLAUDE.mdfor why that ordering matters. - Enumerates as
1eaf:0024"YO6SSW USB paddles".
Related projects
Part of a small suite of Linux ham-radio tools:
- xlog2 — the host application that reads
this device (
HidPaddleInput). - cwsd — its
remote_keyservice replays paddle keying over the internet.
License
Licensed under the GNU General Public License v3.0 or later
(GPL-3.0-or-later). See LICENSE for the full text.