A Morse code (CW) trainer designed for the R36S handheld game console running ROCKNIX. Built with Python and pygame.
Learn CW the way you'd learn a language — through drills, listening, and practice on a device that fits in your pocket.
![]() |
![]() |
![]() |
| Main Menu | Koch Trainer | Band Explorer |
![]() |
![]() |
|
| Straight Key | Glossary |
Straight Key — Practice sending with the A button as a straight key. Hear your sidetone, see your dits and dahs decoded in real time.
Iambic Keyer — Plug in a CW paddle via the 3.5mm jack (or use L1/R1 on the gamepad) for iambic A/B keying with adjustable speed.
Koch Trainer — Receive practice using the Koch method with spaced repetition. Characters you struggle with appear more often. Toggle hints (D-pad Up) to see dit/dah patterns while learning. Progress persists between sessions.
Band Explorer — Simulated HF waterfall display. Tune across a 40m CW band segment with D-pad, hear stations calling CQ and having QSOs. Realistic band noise with atmospheric fading and static.
Vocabulary Quiz — Timed drills on common CW abbreviations and Q-codes. Listen to a CW phrase, pick the correct meaning.
QSO Procedure Trainer — Practice full QSO exchanges: CQ calls, signal reports, and common ragchew elements. Hear the expected exchange, then key your response.
Glossary — Browse common CW abbreviations, prosigns, Q-codes, and typical QSO phrases. Press R1 to hear any term played as CW.
Profiles — Multiple user profiles so you can share the device. Each profile tracks its own Koch progress independently.
Settings — Adjust sidetone frequency, character speed (WPM), Farnsworth spacing, master volume, and key sound style.
| Button | Function |
|---|---|
| D-pad | Navigate menus, tune waterfall, adjust WPM |
| A | Confirm, straight key, start round |
| B | Answer choice (Koch) |
| X | Answer choice (Koch), delete (callsign editor) |
| Y | Answer choice (Koch), edit callsign |
| L1/R1 | Iambic paddle (dit/dah), play/hear audio |
| L2 | Take screenshot |
| Select | Back to menu |
| Select+Start | Quit to EmulationStation |
| Key | Function |
|---|---|
| Arrow keys | D-pad equivalent |
| Space | Straight key |
| W/D/S/A | X/A/B/Y face buttons |
| R | Replay / play audio |
| H | Toggle hints (Koch) |
| F12 | Take screenshot |
| Escape | Back to menu |
| Q | Quit |
- Python 3.10+
- pygame (or pygame-ce) >= 2.5
- numpy >= 1.24
git clone https://github.com/DavidClawson/cw-dojo.git
cd cw-dojoWith uv:
uv run python main.pyWith pip:
pip install -r requirements.txt
python main.py- R36S running ROCKNIX
- WiFi dongle connected and configured
- SSH access to the device
-
Install Python packages on the device:
# From your computer, download aarch64 wheels pip download --only-binary=:all: --platform manylinux2014_aarch64 \ --python-version 3.11 --implementation cp pygame numpy -d /tmp/wheels/ # Copy to device scp /tmp/wheels/*.whl root@<device-ip>:/tmp/ # SSH in and install ssh root@<device-ip> mkdir -p /storage/lib/python3.11/site-packages cd /tmp python3 -c " import zipfile site = '/storage/lib/python3.11/site-packages' for whl in ['pygame-*.whl', 'numpy-*.whl']: import glob for f in glob.glob(whl): print(f'Installing {f}...') zipfile.ZipFile(f).extractall(site) "
Then symlink system SDL2 over the bundled versions (ROCKNIX's SDL2 has the correct display drivers):
cd /storage/lib/python3.11/site-packages/pygame.libs/ ln -sf /usr/lib/libSDL2-2.0.so.0 libSDL2-*.so.* ln -sf /usr/lib/libSDL2_image-2.0.so.0 libSDL2_image-*.so.* ln -sf /usr/lib/libSDL2_mixer-2.0.so.0 libSDL2_mixer-*.so.* ln -sf /usr/lib/libSDL2_ttf-2.0.so.0 libSDL2_ttf-*.so.*
-
Copy CW Dojo to the device:
scp -r cw-dojo/ root@<device-ip>:/storage/roms/ports/morse_trainer/
-
Create the launch script:
ssh root@<device-ip> cat > '/storage/roms/ports/CW Dojo.sh' << 'EOF' #!/bin/bash . /etc/profile export PYTHONPATH=/storage/lib/python3.11/site-packages cd /storage/roms/ports/morse_trainer python3 main.py > /tmp/cwdojo.log 2>&1 EOF chmod +x '/storage/roms/ports/CW Dojo.sh'
-
Restart EmulationStation — CW Dojo will appear under Ports.
cw-dojo/
main.py Entry point and scene dispatcher
audio.py Sidetone and CW character playback (numpy + pygame)
band.py Simulated HF band with CW stations
buttons.py R36S button mapping constants
glossary.py CW abbreviations, Q-codes, and prosigns
keyer.py Iambic keyer logic (Mode A/B)
koch.py Koch method trainer with spaced repetition
morse.py Morse code table, decoder, and Koch character order
profiles.py Multi-user profile management
progress.py Persistent Koch training progress
qso_scripts.py QSO exchange templates and scripts
scenes.py All scene classes (menu, straight key, Koch, etc.)
settings.py Persistent user settings
sounds.py UI sound effects
ui.py Display rendering for all screens
vocab_quiz.py Vocabulary quiz scene
waterfall.py Waterfall display and band explorer scene
assets/ Fonts, background images, and sound effects
The portmaster/ directory contains a standalone build of CW Dojo packaged for PortMaster. This copy is currently behind the main source and needs to be rebuilt from the latest code. A GitHub Action to automate this is planned.
To run CW Dojo on an R36S, you'll need:
| Item | Notes |
|---|---|
| R36S handheld | The RK3326 model running ROCKNIX. ~$35 on Amazon. |
| USB WiFi dongle | Required for SSH deploy and future features. Must be Linux-compatible — the TP-Link TL-WN725N works well with ROCKNIX. |
| CW paddle or straight key (optional) | Any paddle/key with a 3.5mm TRS plug, or use a 3.5mm cable to wire your own. |
| 3.5mm TRS audio cable (optional) | For connecting a CW key to the hardware mod. |
A 3.5mm stereo jack (PJ-307) can be wired to the L1/R1 shoulder button pads to accept a CW straight key or iambic paddle. When nothing is plugged in, the shoulder buttons work normally (the jack's internal switches are normally closed). When a key is plugged in, the switches open and the key contacts take over. The keyer.py module handles iambic timing and mode switching.
- Noto Sans (
assets/NotoSans.ttf) — Google's Noto Sans font, licensed under the SIL Open Font License 1.1.
MIT — see LICENSE for details.
Issues and pull requests welcome. This project is in early development — feedback from CW operators is especially appreciated.
If you're adapting CW Dojo for a different handheld or Linux device, the main things to change are buttons.py (button mappings) and the display resolution in main.py.
If CW Dojo helped you learn CW or you just think it's cool, consider sponsoring the project on GitHub. It helps keep development going.
73 de CW Dojo




