Skip to main content

USB Iambic Key Adapter in a Day: YD-RP2040, a Printed Case and a Koch Morse Trainer

Β· 13 min read
UR3PKI
Software Engineer

I own an iambic paddle but had nothing to practise with: switching on the transceiver for that is a chore, and browser trainers expect keyboard presses. One day later there were three things: an adapter that turns the paddle into a USB keyboard, a case for it with my callsign, and a trainer that understands the paddle itself.

The principle is simple: an RP2040 board reads the two paddle levers and sends the computer the keys A and S. The computer sees an ordinary keyboard, no drivers needed. On top of that I added what I was missing: a case and a trainer of my own.

The case model, the firmware and the trainer were made together with an AI assistant (Claude Code). There is a separate two-millimetre lesson about that below.

Hardware​

PartWhat I used
BoardYD-RP2040 (VCC-GND Studio), USB-C, 23.2 Γ— 53.5 mm; AliExpress link
Jack3.5 mm stereo, board-mount, from the front panel of an old PC case
Wiresthree, 4–5 cm each

The jack has five pins, three of them matter: sleeve, tip and ring. The other two are switch contacts that open when a plug is inserted. The easiest way to find the right ones is a multimeter through the cable: plug in the jack, one probe on a contact of the plug at the other end of the cable, the other probe walks along the pins.

The wiring:

Plug contactBoard padKeyIn the trainer
Sleeve (common)GND between 13 and 14β€”common
Tip14Sdit
Ring15Adah

All three pads sit together at the end of the board opposite the USB connector. The wire colours in the diagram are arbitrary; what matters is that each plug contact lands on its own pad.

Firmware​

The board runs CircuitPython 10.3.2. The usual choice for a keyboard is the adafruit_hid library; I did without it, because a keyboard report is eight bytes that can be sent directly.

code.py
import time
import board
import digitalio
import usb_hid

KEY_GP14 = 0x16 # HID code of the S key
KEY_GP15 = 0x04 # HID code of the A key


def make_input(pin):
p = digitalio.DigitalInOut(pin)
p.switch_to_input(pull=digitalio.Pull.UP)
return p


pins = ((make_input(board.GP14), KEY_GP14), (make_input(board.GP15), KEY_GP15))
kbd = next(d for d in usb_hid.devices if d.usage_page == 0x01 and d.usage == 0x06)

report = bytearray(8)
last = None
while True:
state = tuple(not p.value for p, _ in pins)
if state != last:
report[2] = pins[0][1] if state[0] else 0
report[3] = pins[1][1] if state[1] else 0
try:
kbd.send_report(report)
last = state
except OSError:
pass # the host is not ready yet, retry on the next loop
time.sleep(0.002)

The second file, boot.py, hides the CIRCUITPY drive so that the adapter is nothing but a keyboard. If you hold the lever on 14 closed while plugging in, the drive comes back and the code can be edited.

boot.py
import board
import digitalio
import storage

pin = digitalio.DigitalInOut(board.GP14)
pin.switch_to_input(pull=digitalio.Pull.UP)
if pin.value: # the lever is not pressed
storage.disable_usb_drive()
pin.deinit()

The order is: hold BOOT, plug in the board, copy the CircuitPython file to the RPI-RP2 drive, then put code.py on the CIRCUITPY drive. Check with tweezers that shorting 14 to GND types the letter s, and only then copy boot.py: if the code has a mistake, it is easier to fix while the drive is still visible.

One trap on Linux: if USBGuard is running, the board changes its "face" three times during flashing (bootloader, drive with keyboard, keyboard only), and each one has to be allowed separately.

Case​

The case is described by a FreeCAD script rather than drawn with a mouse. The board, jack and clearance dimensions sit at the top of the file as numbers; after a change the script rebuilds the base, the lid and both STL files on its own. That came in handy sooner than I expected.

What the case has:

  • a USB-C cutout in one end and a hole for the jack nose in the opposite one;
  • ledges under the board edges, leaving three millimetres beneath it for the solder joints;
  • a cradle in which the jack lies with its pins facing up;
  • a snap-on lid with a hole for the BOOT button;
  • the site logo and the callsign, recessed by 0.6 mm to be filled with paint.

You can turn the model right here. The slider lifts the lid and shows how the board and the jack sit inside:

Drag the model to rotate it.

Files for printing:

  • case_base.stl β€” the base;
  • case_lid.stl β€” the lid with the logo and the UR3PKI callsign, already flipped face down;
  • case.py β€” the FreeCAD script both parts are built from. The board and jack dimensions, the text and the logo are changed there; run it with freecadcmd case.py.

Both parts print without supports.

The logo is taken from the site's SVG: each outline becomes a separate pocket, so the red, blue and yellow paint do not mix. The first version of the logo was 21 mm across and its traces came out 0.75 mm wide. For a 0.4 mm nozzle that is borderline, so I enlarged the logo to 28 mm and widened the case to 33 mm to fit it.

Printed on a FlyingBear Reborn 2, PLA, 0.2 mm layers, three perimeters. The lid lies face down, so two first-layer settings matter for it: line width 100 % instead of the factory 130 %, and elephant's foot compensation of βˆ’0.12 mm. Without them the thin grooves fill in.

Two millimetres​

I gave the assistant the board dimensions as photos of a calliper. It read them as 21 Γ— 51.5 mm. The scale actually showed 23.2 Γ— 53.5.

Nobody noticed until the base was printed. Fifty minutes of printing, and the board sits on top of the guide instead of between the wall and the guide.

I trimmed the first base by hand. In the model the dimensions are fixed and now checked two ways: from the scale on an enlarged photo, and from the board's hole grid, where twenty holes at 2.54 mm pitch give a known 48.26 mm.

The conclusion is simple and applies beyond AI: a number read off a photograph has to be said out loud and checked before it goes into a model. The parametric script saved the day here: fixing three numbers and rebuilding the case took a minute.

The trainer​

The trainer lives at cw.cyberdev.space/en/. It is a single page with nothing to install; progress is kept in the browser. It also works without the adapter: the keys A and S or the arrow keys serve as the paddles.

The trainer is not tied to my adapter. If you have already built the iambic key adapter described by so1der on the UARO site, it works as is: it sends the same A and S keys. If dit and dah turn out swapped, there is a "swap paddles" switch in the settings.

Learning is built on two well-known methods. With the Koch method the characters sound at working speed from the start, and a new character is added only once the previous ones are copied at 90 %. With Farnsworth timing the characters themselves are fast while the gaps between them are stretched so that a beginner has time to think. The defaults: characters at 20 words per minute, effective speed 15.

A short guide follows. Each screenshot shows the dark theme on the left and the light theme on the right; the theme is switched with a button in the header. The screenshots show the Ukrainian interface; the English one is the same page with the flag in the header switched.

1. The course​

At the top is the current lesson: the new character, its code in dots and dashes, buttons to hear each character of the lesson, and a chart of recent attempts with the threshold line. Below it is the course drawn as a printed circuit board with 40 lessons. The first lesson gives K and M, every next one adds a single character.

Each lesson has three rings: blue for receiving, red for sending, cyan for the radiogram. Once all three modes are passed at 90 %, the rings merge into a single gold one and the next lesson unlocks. The header shows the streak of consecutive days and how many characters you have done today.

2. Receiving​

A character sounds, you press the matching key. The trainer reads physical keys, so the layout does not matter: the K key counts even with a Ukrainian layout active. Space replays the sound. After a mistake the correct character appears on screen with its code.

New characters, and those you get wrong, come up more often than the ones you know.

3. Radiogram​

The classic Koch session: continuous text in groups of five characters for one to five minutes. You write it down by ear, space separates the groups, Enter checks. The main skill here is not to stop at a missed character but to keep writing.

After checking you see what was sent, with mistakes highlighted, and what you wrote. The result is scored two ways, over the whole text and group by group, and the better one counts, so a single missed character does not shift everything after it.

4. Echo on the key​

The mode all of this started for. A character sounds, you repeat it on the paddle, the decoder checks it. The character on screen can be shown or hidden. If your characters fall apart into pieces, lower the "keyer speed" in the settings: the decoder ends a character after a gap of two and a half dits.

There is also "free keying": send anything you like and watch what the decoder makes of it.

5. Words and callsigns​

The next stage after the alphabet. A whole word or callsign is sent with standard spacing, you type it and press Enter. A correct answer adds one word per minute, a mistake takes one off. Words are made only of the characters you have learned; callsigns unlock once the first digit appears among them.

6. Settings and carrying progress over​

The settings hold the character speed and the effective speed, the keyer speed, the tone, the pass threshold and the daily goal. Two switches are worth turning on from about the fifth lesson: random group length and random tone, so that your ear does not get used to one sound.

Below are the accuracy per character, export and import of progress as a file, and sync between devices. It works without an account: the page creates a random key, and whoever enters it on another device carries on from the same place.

What came from LCWO​

Before extending the trainer I compared it with LCWO, the best-known site for learning Morse. Its author, Fabian Kurz, DJ5CW, publishes the source code under the AGPL 3 licence, so how the course works can be read rather than guessed.

The ideas that came from there: the order of the forty lessons, the radiogram as the main kind of session, a bias towards new characters in the text, random group length and tone. The code in the trainer is my own. I checked the timing separately: in a recording from LCWO at 20/15 a dit lasts 59 ms, the gap between characters 339 ms, between groups 789 ms; my calculation gives 60, 338 and 788.

What LCWO does not have, and what the goal was: sending on an iambic paddle with checking.

What it cannot do yet​

  • The trainer has no plain text and no typical QSO, only characters, groups, words and callsigns.
  • The keyer works in a single iambic mode with memory, with no choice between A and B.
  • Word and callsign practice is designed for a keyboard; on a phone only receiving and the key are comfortable.
  • The BOOT button hole in the first lid is off by about a millimetre: a paper clip reaches it, but closer to the edge.

Sources​