When is comes to ham radio operators that use Morse code keys each has their favorite and none have just one. Morse keys can differ in many ways, but the bug key is typically the most expensive and the most difficult to learn. Real bugs mechanically (not electronically) "automate" the DITs while the DAHs are still produced manually. It's a throwback to the times when effortless high speed code was sent without the use of fancy electronics.
-----
That was then and this is now because with fancy electronics we can simulate things. BTW, if you are a SKCC member this isn't a 100% manual key so would not qualify as a bug for a Triple Key Award, but it gives a real bug key feel at price well under a six pack. This allows more OPs the experiment or practice with a bug key before having to lay out real folding money for the experience.
-----
We cobbled this together in a few hours with "stuff in the drawer". A C3 uController was used but any Arduino based device could work.
----
Video of our first try (we could benefit from a little bug practice):
Throw the whole enchilada in a 3D Printed case and it looks like this:
-----
To create your own, wire it up like this and upload the code below to the uController via the Arduino IDE. Presto, instant bug! Note; safety first: We used a PC817 to completely isolate and protect the ham radio rig from having any external voltages sent into it.
We also designed in a potentiometer to adjust DIT WPM speed, but it can be eliminated if you just want to hardcode the WPM speed into the code.
-----
/*
* Bug Keyer Simulator with Smoothed WPM Control and Live Pot Adjustment
* Arduino IDE Board Settings: ESP32 Dev Module, 240MHz (WiFi/BT), 921600
* APRIL2026
* Project details at: whiskeytangohotel.com
*/
// Define C3 pins
const int ditPin = 2; // From key
const int dahPin = 3; // From key
const int keyOutPin = 8; // To rig
const int potPin = A0; // Bug Speed adjust
// Define some variables. Feel free to adjust minWPM and maxWPM to suit needs
int wpm;
int targetWpm;
int ditLength;
int minWPM = 10;
int maxWPM = 40;
float smoothedWpm = 20;
void setup() {
pinMode(ditPin, INPUT_PULLUP);
pinMode(dahPin, INPUT_PULLUP);
pinMode(keyOutPin, OUTPUT);
digitalWrite(keyOutPin, LOW);
ditLength = 1200 / wpm;
}
void loop() {
// --- Read pot and update WPM (live knob response) ---
updateWPM();
bool ditPressed = digitalRead(ditPin) == LOW;
bool dahPressed = digitalRead(dahPin) == LOW;
// --- DIT side (automatic repeating like bug) ---
The Flex Radio 6400
is an amazing state of the art ham radio transceiver with so many
modern bells and whistles than no normal human could ever get around to
using them all. However, this can also put off many olde tyme hams that resist changes from the Golden Age of Radio. In an effort to ease this transition we had to create the Drifty Flex and Rotary Phone Interface application. As helpful as this has been, there was still the common
complaint that "Dammit, real radios have knobs and buttons!". So, to assist again
with the transition to Flex Radio we launched another project.
-----
The Stream Deck is a relatively common Flex Radio add on, but those things can cost over a hundred bucks and that money could be better spent on old J38 Morse keys, etc. In all seriousness, we don't miss the buttons and knobs at all on the Flex. However, we did find moving the mouse quickly to click the small RIT and XIT adjustment buttons in the Smart SDR interface a burden. After seeing this ESP32 Module with onboard 2.8 inch touchscreen 2.8 and WiFi (also called a CYD for "Cheap Yellow Display") for only $9 on AliExpress we got an idea.
-----
The code below splits the CYD touchscreen into four sections to adjust for RIT-, RIT+, XIT-, and XIT+. Adjustment are made in 10Hz steps. If no change is made for 60 minutes the values revert to 0. The rig connects to the Flex Radio over WiFi so only 5VDC power is needed. The IP address of the CYD is displayed in yellow in the bottom right.
-----
It works great. Here is a short video of the rig in action:
-----
Want to build your own? It's easy and here is the code to make it happen. As with all programs written today AI both sped up and slowed down progress at times:
/* * "CYD_StreamDeck" uses ESP32 with touchscreen "Cheap Yellow Display" to provide s * RIT and XIT control to a FLEXRADIO. If no change for 60 both RIT and XIT * reset to "0"; edit AUTO_RESET_TIMEOUT variable to adjust.
* Arduino IDE Board Setting: ESP32 Dev Module, 240MHz (WiFi/BT), 921600
#define TFT_BL 21 // Backlight Original = 21 #define TFT_BACKLIGHT_ON HIGH
#define SPI_FREQUENCY 40000000
#define LOAD_GLCD // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH #define LOAD_FONT2 // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters #define LOAD_FONT4 // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters #define LOAD_FONT6 // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm #define LOAD_FONT7 // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:. #define LOAD_FONT8 // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-. #define LOAD_GFXFF // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 #define SMOOTH_FONT * * */
// Debounce unsigned long lastTouchTime = 0; const unsigned long touchDebounce = 250;
// Auto-reset timers (moved to global scope so parseStatus can see them) static unsigned long lastRitChange = 0; static unsigned long lastXitChange = 0;
// Timeout for auto-reset RIT and XIT to "0" if not changed const unsigned long AUTO_RESET_TIMEOUT = 60UL * 60 * 1000; // 60UL = 60 mins, 30UL = 30 mins, etc.
// --- Display offsets centered on x-axis, narrow boxes for 4 chars, + for positive --- void drawOffsets() { tft.setTextColor(TFT_CYAN); tft.setTextSize(2); tft.setTextDatum(MC_DATUM); // Middle-center alignment
// Narrow rectangle just for 4 chars const int rectWidth = 48; const int rectHeight = 30;
// Horizontal center of the screen int centerX = tft.width() / 2; // 160
// RIT - same vertical center int ritY = 16;
// Format RIT: add "+" if positive String ritStr = (current_rit > 0) ? "+" + String(current_rit) : String(current_rit);
// WiFi connect WiFi.begin(ssid, password); esp_wifi_set_ps(WIFI_PS_NONE); // no power saving at all while (WiFi.status() != WL_CONNECTED) { delay(500); }
// Auto-reset both RIT and XIT to zero after 60 min of no change unsigned long now = millis(); if ((now - lastRitChange >= AUTO_RESET_TIMEOUT) && (now - lastXitChange >= AUTO_RESET_TIMEOUT)) { if (current_rit != 0 || current_xit != 0) { current_rit = 0; current_xit = 0; sendCommand("slice s 0 rit_on=0 rit_freq=0"); sendCommand("slice s 0 xit_on=0 xit_freq=0"); Serial.println("Auto-reset: RIT and XIT set to 0 (no change for 60 min)"); drawOffsets(); lastRitChange = now; lastXitChange = now; } }
if (!flexClient.connected()) { connectToFlex(); }
// Optional: refresh IP every 5 mins = 300 seconds static unsigned long lastIPRefresh = 0; if (millis() - lastIPRefresh > 300000) { drawIP(); lastIPRefresh = millis(); } }