Overview
Morsewurst Keyer is a custom ESP32-S3-based device designed for practical Morse training. It reads a straight key or an iambic paddle, produces a headphone sidetone, shows basic information on an OLED display, and sends raw timing telemetry to Morsewurst running on a computer.
The key idea is that the ESP32-S3 measures key press timings and sends them as JSON telemetry. Morsewurst can then analyse rhythm, timing, gaps, speed, accuracy and problem characters much more precisely than simple text input.
Input
Straight key and iambic paddle support.
Telemetry
USB CDC Serial JSON timing events measured in microseconds.
Feedback
Morsewurst analyses timing, accuracy, cleanliness, WPM and weak characters.
USB HID keyboard
The firmware may also support USB HID keyboard text output.
Required parts
Electronics
- Adafruit ESP32-S3 Feather with STEMMA QT 8MB, or a similar ESP32-S3 board with native USB.
- 128x64 I2C OLED display, for example SSD1306 or SH1106 compatible.
- KY-040 or similar rotary encoder with push button.
- 3.5 mm stereo jack for the iambic paddle.
- 3.5 mm stereo jack for the straight key.
- 3.5 mm stereo headphone jack, preferably a switched model if headphone detection is wanted.
- 10 kΩ logarithmic mono potentiometer for volume.
- Two 1 kΩ metal film resistors.
- One 2.2 kΩ or 10 kΩ metal film resistor for the headphone output pull-down.
- Wire in several colours.
- Optional Wago connectors, ground rail or another neat way to join GND wires during prototyping.
Tools and mechanical parts
- Soldering iron and solder.
- Wire strippers and side cutters.
- Small drill or hand drill for cleaning up holes.
- 3D printer and PETG filament.
- Screws or other fasteners suitable for the enclosure.
Arduino IDE settings
The firmware is intended for an ESP32-S3 board with native USB. Use a real USB data cable, not a charge-only cable.
USB Mode: USB-OTG or TinyUSB
USB CDC On Boot: Enabled
Upload Mode: USB-OTG CDC or TinyUSB
If USB CDC On Boot is not enabled, Morsewurst may not find the telemetry serial port correctly. If USB-OTG or TinyUSB mode is not enabled, USB HID Keyboard mode may not work.
Arduino sketch folder
Arduino sketches should be stored so that the folder and the main .ino file have the same name.
For example:
morsewurst_keyer_1_0/
└── morsewurst_keyer_1_0.ino
Bootloader mode
If the computer does not recognise the ESP32-S3 board for programming, try this sequence:
- Hold the BOOT button down.
- Press the RESET button.
- Release RESET.
- Release BOOT.
Display
The project uses a 128x64 I2C OLED display. In practice, many OLED displays can work as long as the correct U8g2 driver is selected for the display controller chip.
U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(
U8G2_R2,
U8X8_PIN_NONE
);
Although a display may be sold as SSD1306, the SH1106 driver may work better in some setups. If the image is shifted, partly wrong or shows artefacts, try another U8g2 driver.
| Display | ESP32-S3 Feather |
|---|---|
| GND | GND |
| VCC | 3.3V |
| SCL | SCL |
| SDA | SDA |
U8G2_R2 rotates the image 180 degrees. This is useful if the display is physically installed upside down
in the enclosure.
Pinout
| Function | ESP32 pin |
|---|---|
| Sidetone audio out | GPIO11 |
| Straight key | GPIO12 |
| Iambic DIT | GPIO9 |
| Iambic DAH | GPIO10 |
| Rotary encoder CLK or A | GPIO5 |
| Rotary encoder DT or B | GPIO6 |
| Rotary encoder pushbutton | GPIO13 |
| Headphone detect | A3 |
All key and encoder inputs use the internal INPUT_PULLUP resistor. This means that the button or key connects
the signal pin to ground when pressed.
Keys and encoder
Straight key
| Jack part | Connection |
|---|---|
| TIP | GPIO12 |
| RING | Not used |
| SLEEVE | GND |
const int STRAIGHT_KEY_PIN = 12;
Iambic paddle
| Jack part | Connection |
|---|---|
| TIP | GPIO9, DIT |
| RING | GPIO10, DAH |
| SLEEVE | GND |
const int DIT_PIN = 9;
const int DAH_PIN = 10;
If DIT and DAH are reversed, fix it by swapping the wires in the jack or by using the firmware’s paddle swap setting.
Rotary encoder
| Encoder pin | Connection |
|---|---|
| Pushbutton pin 1 | GPIO13 |
| Pushbutton pin 2 | GND |
| CLK or A | GPIO5 |
| Common | GND |
| DT or B | GPIO6 |
const int ENC_CLK_PIN = 5;
const int ENC_DT_PIN = 6;
const int ENC_SW_PIN = 13;
If the rotation direction is reversed, swap GPIO5 and GPIO6.
Sidetone and headphones
The sidetone is produced from ESP32 GPIO11 as a PWM square wave. It is intended for headphone-level output, not for driving a speaker directly.
const int AUDIO_PIN = 11;
Potentiometer wiring
| Potentiometer terminal | Connection |
|---|---|
| Left outer terminal | GPIO11 |
| Right outer terminal | GND |
| Middle terminal, wiper | Headphone output junction |
Resistors from the middle terminal
| From middle terminal | To |
|---|---|
| 1 kΩ resistor | Headphone jack TIP |
| 1 kΩ resistor | Headphone jack RING |
| 2.2 kΩ or 10 kΩ resistor | GND |
GPIO11 -> potentiometer outer terminal
GND -> other potentiometer outer terminal
Potentiometer middle terminal
├── 1 kΩ -> headphone jack TIP
├── 1 kΩ -> headphone jack RING
└── 2.2 kΩ or 10 kΩ -> GND
Headphone jack SLEEVE -> GND
Headphone detection, optional
The project reserves support for a switched 3.5 mm headphone jack. The idea is that the jack can pull the A3
pin to ground when headphones are connected.
const int HEADPHONE_DETECT_PIN = A3;
bool headphonesConnected() {
return digitalRead(HEADPHONE_DETECT_PIN) == LOW;
}
This feature is mainly a reserved extension option. If you use a normal headphone jack without detection, you can leave it unused.
3D-printable enclosure
The Morsewurst Keyer enclosure is supplied as two separate STL files: the lower box and the lid. Print both parts separately. The design is intended for PETG, and small differences in displays, jacks, encoders or printer calibration may require minor cleanup or adjustment after printing.
| File | Description | Download |
|---|---|---|
| morsewurst_keyer_box.stl Download STL | Lower box / main body part of the enclosure. | |
| morsewurst_keyer_lid.stl Download STL | Lid / top cover part of the enclosure. |
- Material: PETG.
- Layer height: 0.2 mm works, but 0.10 to 0.15 mm is recommended for a cleaner result.
- Infill: about 15 to 30 percent.
- Walls: 2 to 3 perimeters.
- Printer: Prusa i3 MK3S Plus or similar.
If holes are too tight, clean them carefully by hand. Do not use too much force, because the enclosure may crack.
Telemetry for Morsewurst
The device sends timing events over USB CDC Serial in JSON format. Morsewurst reads these lines and uses them to analyse timing, rhythm and decoding during practice.
Straight key tone event
{
"v": 1,
"type": "tone",
"src": "straight",
"t0": 123456789,
"t1": 123556789,
"dur": 100000
}
Iambic tone event
{
"v": 1,
"type": "tone",
"src": "iambic",
"el": ".",
"t0": 123456789,
"t1": 123516789,
"dur": 60000,
"unit": 60000,
"wpm": 20.0
}
The important point is that t0, t1 and dur are measured by the ESP32 itself.
USB and Python latency affect when the event appears on the computer, but they do not change the timestamps already measured
by the device.
Measurement accuracy
uint64_t nowTime() {
return (uint64_t)esp_timer_get_time();
}
Timestamps are stored in microseconds, but practical accuracy is not exactly one microsecond. With the current polling-based implementation, realistic practical accuracy in good conditions is about 50 to 200 microseconds.
Serial API for compatible devices
Morsewurst’s most important hardware interface is line-based JSON telemetry sent over a USB CDC Serial connection. The interface is effectively one-way. The device sends timing data to Morsewurst, and Morsewurst does not need to send commands back to the device.
Serial connection
115200 baud
one JSON object per line
line ending: \n
Device identification
Useful message types for identification are hello, heartbeat and tone.
A compatible device should send a hello message after startup and heartbeat messages regularly when idle.
Hello message
{"v":1,"type":"hello","app":"morsewurst","device":"Morsewurst Keyer","fw":"1.0","mode":"raw_timing"}
Heartbeat message
{"v":1,"type":"heartbeat","app":"morsewurst","device":"Morsewurst Keyer","fw":"1.0","mode":"raw_timing","uptime":5000000,"wpm":20,"telemetry":true}
Tone message fields
| Field | Required | Meaning |
|---|---|---|
v | Yes | Protocol version. Current version is 1. |
type | Yes | Message type. For timing events, use tone. |
src | Yes | Event source, for example straight or iambic. |
t0 | Yes | Start time in microseconds. |
t1 | Yes | End time in microseconds. |
dur | Yes | Duration in microseconds, usually t1 - t0. |
el | No | Iambic element hint, either . or -. |
unit | No | Length of one dit unit in microseconds. |
wpm | No | Device speed in words per minute. |
Practical minimum implementation
- Open a USB CDC Serial connection at 115200 baud.
- Send a
hellomessage when the connection is available. - Send a
heartbeatmessage regularly. - Measure the start time of the key press in microseconds.
- Measure the release time of the key press in microseconds.
- Calculate the duration in microseconds.
- Send one
tonemessage for each press.
{"v":1,"type":"hello","app":"morsewurst","device":"Morsewurst Keyer","fw":"1.0","mode":"raw_timing"}
{"v":1,"type":"heartbeat","app":"morsewurst","device":"Morsewurst Keyer","fw":"1.0","mode":"raw_timing","uptime":5000000,"wpm":20,"telemetry":true}
{"v":1,"type":"tone","src":"straight","t0":6000000,"t1":6100000,"dur":100000}
t0, t1 and dur.
Build order
- Load the Arduino sketch into the Arduino IDE.
- Select the correct ESP32-S3 board and USB settings.
- Test ESP32 programming with a USB-C data cable.
- Connect the OLED display and make sure the image appears correctly.
- Connect the rotary encoder and test menu control.
- Connect the straight key to GPIO12 and GND.
- Connect the iambic paddle to GPIO9, GPIO10 and GND.
- Build the sidetone circuit with the potentiometer and resistors.
- Test the headphone output at low volume.
- Test USB serial telemetry in Morsewurst.
- Test USB keyboard mode if needed.
- Print the enclosure from PETG.
- Clean up the holes carefully.
- Solder the final wiring.
- Install the parts in the enclosure.
- Do a final test before closing the enclosure.
Safety and reliability notes
- Use 3.3 volt logic.
- Do not feed 5 volts into ESP32 GPIO pins.
- Do not drive a speaker directly from GPIO11.
- Start headphone tests at low volume.
- Make the final wiring as short and mechanically solid as possible.
- Avoid loose connectors in a tight enclosure.
- Clearly mark GND wires.
- Test each part separately before final assembly.
This guide describes the practical build path for the current Morsewurst Keyer. Details may change as the hardware and firmware evolve.