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. master is this STM32F411 + Adafruit TinyUSB build. The older raw-hid branch 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.

PaddlePin
ditPA0
dashPA1

Protocol

  • Identity: USB vendor 0x1EAF, product id 0x0024 (1eaf:0024), manufacturer "YO6SSW" + product "USB paddles" (shown as "YO6SSW USB paddles"), vendor HID usage page 0xFFC0 (RAWHID_USAGE_PAGE). Discover by matching the vendor and the usage-page bytes 06 C0 FF in 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 closed
    • byte1: 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-core USBComposite HIDRaw class 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's HIDRaw<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 — see CLAUDE.md for why that ordering matters.
  • Enumerates as 1eaf:0024 "YO6SSW USB paddles".

Part of a small suite of Linux ham-radio tools:

  • xlog2 — the host application that reads this device (HidPaddleInput).
  • cwsd — its remote_key service 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.