Hardware components | ||||||
![]() |
| × | 2 | |||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 |
One thing that is common among new HAM radio operators, is "Mic Fright." That very first time you tune up a radio and talk into the microphone. Who is Listening? How far is your voice traveling? Do you sound stupid? What if you make a mistake? Are they laughing? But soon you get comfortable and you realize it's really no big deal...
Now, multiply "Mic Fright" by a million for your first time trying to send Morse Code...
So I have created a suite of programs that run on an M5 cardputer, that allow people to play games, practice and improve their morse code skills. Now there are already devices out there that can help you practice. The simplist device is called an Oscillator - which simply beeps when you press the key. Using an oscillator you can hear what you sound like when you send.
A Suite of Morse Code Programs
So the first program in the Suite is an oscillator it simply beeps when you press the key. If you are using a paddle the left paddle is a dot, the right paddle is a dash. The is an oscillator for paddles and and oscillator for straight keys.
Ths paddle oscilator also tells you if you pressed dash or dot.
The staight Key Oscillators tells you if the key is up or down.
The next program is a advancement over a simple oscillator. It is called a decoder it will take in the morse you are entering and tell you which letter you entered.
The next program is to practice your own translating it will read a.txt file from the cardputer's SD card and then play that file to you translated to morse, you can then practice listening and translating the morse code back to english.
Then there are a couple of games Morsegame and Morsefalls, which improve your reaction time. The cardputer sends a letter and you have to type in the morse code to elimnate it.
The main program is called Wifi Telegraph. It lets you put two cardputers onto the same wifi network, (after entering each IP) the cardputer will allow you to send each other morse code messages. (You are on your own to translate them :) )
This is not a completed project. There are more games to come, including tic/tac/toe and a first person maze game. Also I am going to use PlatformIO to take advantage of the microphone libraries on the cardputer and the stick c to listen to morse and translate it to letters. (I have an M5 stickC Plus2 which will be come a wearable Morse translator)
Morse _Oscillator2.0
Arduino// Morse Oscillator
// Version: 2.0
// Author: Matthew Sparks N6MMS
// Email: ProfessorSparks@gmail.com
// Platform: M5Cardputer
// License: GNU General Public License v3.0
const int groveG1Pin = 1; // G1 = GPIO1
const int groveG2Pin = 2; // G2 = GPIO2
bool lastG1State = HIGH;
bool lastG2State = HIGH;
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
pinMode(groveG1Pin, INPUT_PULLUP);
pinMode(groveG2Pin, INPUT_PULLUP);
Serial.begin(115200);
Serial.println("M5 Morse Oscillator");
M5.Lcd.setTextSize(2);
M5.Lcd.setCursor(10, 10);
M5.Lcd.println("M5 Morse Oscillator");
}
void loop() {
int g1State = digitalRead(groveG1Pin);
int g2State = digitalRead(groveG2Pin);
// Detect G1 change to LOW (grounded)
if (g1State == LOW && lastG1State == HIGH) {
Serial.println("G1 LOW - Long Beep");
M5.Speaker.tone(1000, 300); // 300 ms long beep
}
// Detect G2 change to LOW (grounded)
if (g2State == LOW && lastG2State == HIGH) {
Serial.println("G2 LOW - Short Beep");
M5.Speaker.tone(1000, 100); // 100 ms short beep
}
// Update screen
M5.Lcd.setCursor(10, 50);
M5.Lcd.fillRect(10, 50, 240, 60, BLACK); // Clear area
M5.Lcd.setTextColor(WHITE);
M5.Lcd.setCursor(10, 50);
M5.Lcd.printf("CW Key: %s\n", g1State == LOW ? "Dash" : " ");
M5.Lcd.setCursor(11, 50);
M5.Lcd.printf("CW Key: %s\n", g2State == LOW ? "Dot" : " ");
// Save last states
lastG1State = g1State;
lastG2State = g2State;
delay(100); // Loop delay
}
// MorseFalls v2.4 for M5Cardputer
// Author: Matthew Sparks N6MMS
// Features: Leveling, scoring, proper Morse input, centered startup, bug fixes
#include <M5Cardputer.h>
#include <M5GFX.h>
M5Canvas canvas(&M5Cardputer.Display);
const char charset[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
char currentChar = 'A';
int charX = 0, charY = 22;
int score = 0, level = 1;
int fallDelay = 80;
int correctStreak = 0;
int errorStreak = 0;
unsigned long lastFall = 0;
char morse[10] = "";
int morseLen = 0;
unsigned long lastInputTime = 0;
bool dotPressed = false, dashPressed = false;
const int dotPin = 2;
const int dashPin = 1;
void showStartupScreen() {
canvas.deleteSprite();
canvas.createSprite(132, 239);
canvas.fillScreen(BLACK);
canvas.setTextColor(GREEN);
canvas.setTextSize(1);
canvas.setTextDatum(middle_center);
canvas.drawString("MorseFalls", 66, 80);
canvas.drawString("Version 2.4", 66, 110);
canvas.drawString("Matthew Sparks N6MMS", 66, 140);
canvas.pushSprite(0, 0);
delay(3000);
canvas.deleteSprite();
}
void newChar() {
currentChar = charset[random(0, sizeof(charset) - 1)];
charX = random(2, 132 - 16);
charY = 22;
}
void drawScreen() {
canvas.fillScreen(BLACK);
// Borders
canvas.drawFastHLine(0, 0, 132, GREEN); // Top
canvas.drawFastHLine(0, 20, 132, GREEN); // Bottom of top box
canvas.drawFastHLine(0, 218, 132, RED); // Top of bottom box
canvas.drawFastHLine(0, 238, 132, RED); // Bottom
canvas.drawFastVLine(0, 0, 239, GREEN); // Left
canvas.drawFastVLine(131, 0, 239, GREEN); // Right
// Score and level
canvas.setTextColor(GREEN);
canvas.setTextSize(1);
canvas.setCursor(4, 4);
canvas.printf("L%d", level);
canvas.setCursor(90, 4);
canvas.setTextSize(1.5);
canvas.printf("%d", score);
// Falling letter
canvas.setTextSize(2);
canvas.setTextDatum(top_left);
canvas.drawString(String(currentChar), charX, charY);
// Morse code input (larger and centered)
canvas.setTextSize(2);
canvas.setTextDatum(middle_center);
canvas.drawString(morse, 66, 228);
canvas.pushSprite(0, 0);
}
void flashBox(uint16_t color, int yStart, int yEnd, int count = 1) {
for (int i = 0; i < count; i++) {
canvas.fillRect(1, yStart, 130, yEnd - yStart, color);
canvas.pushSprite(0, 0);
delay(150);
drawScreen();
delay(100);
}
}
String decodeMorse(const char *m) {
if (strcmp(m, ".-") == 0) return "A";
else if (strcmp(m, "-...") == 0) return "B";
else if (strcmp(m, "-.-.") == 0) return "C";
else if (strcmp(m, "-..") == 0) return "D";
else if (strcmp(m, ".") == 0) return "E";
else if (strcmp(m, "..-.") == 0) return "F";
else if (strcmp(m, "--.") == 0) return "G";
else if (strcmp(m, "....") == 0) return "H";
else if (strcmp(m, "..") == 0) return "I";
else if (strcmp(m, ".---") == 0) return "J";
else if (strcmp(m, "-.-") == 0) return "K";
else if (strcmp(m, ".-..") == 0) return "L";
else if (strcmp(m, "--") == 0) return "M";
else if (strcmp(m, "-.") == 0) return "N";
else if (strcmp(m, "---") == 0) return "O";
else if (strcmp(m, ".--.") == 0) return "P";
else if (strcmp(m, "--.-") == 0) return "Q";
else if (strcmp(m, ".-.") == 0) return "R";
else if (strcmp(m, "...") == 0) return "S";
else if (strcmp(m, "-") == 0) return "T";
else if (strcmp(m, "..-") == 0) return "U";
else if (strcmp(m, "...-") == 0) return "V";
else if (strcmp(m, ".--") == 0) return "W";
else if (strcmp(m, "-..-") == 0) return "X";
else if (strcmp(m, "-.--") == 0) return "Y";
else if (strcmp(m, "--..") == 0) return "Z";
else if (strcmp(m, ".----") == 0) return "1";
else if (strcmp(m, "..---") == 0) return "2";
else if (strcmp(m, "...--") == 0) return "3";
else if (strcmp(m, "....-") == 0) return "4";
else if (strcmp(m, ".....") == 0) return "5";
else if (strcmp(m, "-....") == 0) return "6";
else if (strcmp(m, "--...") == 0) return "7";
else if (strcmp(m, "---..") == 0) return "8";
else if (strcmp(m, "----.") == 0) return "9";
else if (strcmp(m, "-----") == 0) return "0";
else return "";
}
void setup() {
auto cfg = M5.config();
M5Cardputer.begin(cfg);
M5Cardputer.Display.setRotation(0);
pinMode(dotPin, INPUT_PULLUP);
pinMode(dashPin, INPUT_PULLUP);
canvas.createSprite(132, 239);
showStartupScreen();
canvas.createSprite(132, 239);
newChar();
}
void loop() {
M5Cardputer.update();
bool dot = digitalRead(dotPin) == LOW;
bool dash = digitalRead(dashPin) == LOW;
unsigned long now = millis();
if (dot && !dotPressed) {
strcat(morse, ".");
morseLen++;
lastInputTime = now;
}
if (dash && !dashPressed) {
strcat(morse, "-");
morseLen++;
lastInputTime = now;
}
dotPressed = dot;
dashPressed = dash;
if (morseLen > 0 && now - lastInputTime > 1000) {
String decoded = decodeMorse(morse);
if (decoded == "") {
flashBox(RED, 218, 238);
score -= 2;
errorStreak++;
correctStreak = 0;
} else if (decoded[0] == currentChar) {
flashBox(GREEN, 22, 218, 2);
score += 10;
correctStreak++;
errorStreak = 0;
if (correctStreak % 5 == 0) {
level++;
fallDelay = max(20, fallDelay - 10);
canvas.fillScreen(BLACK);
canvas.setTextColor(GREEN);
canvas.setTextSize(2);
canvas.setTextDatum(middle_center);
canvas.drawString("Level " + String(level), 66, 120);
canvas.pushSprite(0, 0);
delay(1500);
}
newChar();
} else {
score -= 5;
correctStreak = 0;
errorStreak++;
}
morse[0] = '\0';
morseLen = 0;
}
if (now - lastFall > fallDelay) {
charY += 3;
lastFall = now;
if (charY > 205) {
score -= 10;
flashBox(RED, 218, 238);
newChar();
errorStreak++;
correctStreak = 0;
}
}
if (errorStreak >= 5) {
canvas.fillScreen(BLACK);
canvas.setTextColor(RED);
canvas.setTextSize(2);
canvas.setTextDatum(middle_center);
canvas.drawString("Game Over", 66, 100);
canvas.drawString("Score: " + String(score), 66, 140);
canvas.drawString("Level: " + String(level), 66, 170);
canvas.pushSprite(0, 0);
while (true) delay(1000);
}
drawScreen();
}
MorseFalls 2.7 for straight key
Arduino// MorseFalls SK v2.7
// Fixes: top box offset, longer build wait, 6-error game over
// New: On-screen WPM indicator
// Author: Matthew Sparks N6MMS
// Platform: M5Cardputer
#include <M5Cardputer.h>
#include <M5GFX.h>
M5Canvas canvas(&M5Cardputer.Display);
// ---------------- Game state ----------------
const char charset[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
char currentChar = 'A';
int charX = 0, charY = 30; // shifted down
int score = 0, level = 1;
int fallDelay = 80;
int correctStreak = 0;
int errorStreak = 0;
unsigned long lastFall = 0;
char morse[16] = "";
int morseLen = 0;
// ---------------- Straight key + timing ----------------
const int keyPin = 1; // G1
bool keyDown = false;
unsigned long pressStartMs = 0;
unsigned long lastChangeMs = 0;
unsigned long lastElementEnd = 0;
const int debounceMs = 10;
// WPM timing
float measuredWPM = 8.0f;
float ditMs, dotDashThresholdMs, letterGapMs;
const float WPM_MIN = 5.0f, WPM_MAX = 25.0f;
// ---------------- UI helpers ----------------
void showStartupScreen() {
canvas.deleteSprite();
canvas.createSprite(132, 239);
canvas.fillScreen(BLACK);
canvas.setTextColor(GREEN);
canvas.setTextSize(1);
canvas.setTextDatum(middle_center);
canvas.drawString("MorseFalls", 66, 80);
canvas.drawString("SK Version 2.7", 66, 110);
canvas.drawString("Matthew Sparks N6MMS", 66, 140);
canvas.pushSprite(0, 0);
delay(3000);
canvas.deleteSprite();
}
void newChar() {
currentChar = charset[random(0, sizeof(charset) - 1)];
charX = random(2, 132 - 16);
charY = 30; // start below header box
}
void drawScreen() {
canvas.fillScreen(BLACK);
// Borders
canvas.drawFastHLine(0, 0, 132, GREEN);
canvas.drawFastHLine(0, 20, 132, GREEN); // bottom of top bar
canvas.drawFastHLine(0, 218, 132, RED);
canvas.drawFastHLine(0, 238, 132, RED);
canvas.drawFastVLine(0, 0, 239, GREEN);
canvas.drawFastVLine(131, 0, 239, GREEN);
// Score, level, WPM
canvas.setTextColor(GREEN);
canvas.setTextSize(1);
canvas.setCursor(4, 4);
canvas.printf("L%d", level);
canvas.setCursor(70, 2);
canvas.setTextSize(1.5);
canvas.printf("%d", score);
canvas.setCursor(70, 12);
canvas.setTextSize(1);
canvas.printf("WPM:%2.0f", measuredWPM);
// Falling letter
canvas.setTextSize(2);
canvas.setTextDatum(top_left);
canvas.drawString(String(currentChar), charX, charY);
// Morse code input
canvas.setTextSize(2);
canvas.setTextDatum(middle_center);
canvas.drawString(morse, 66, 228);
canvas.pushSprite(0, 0);
}
void flashBox(uint16_t color, int yStart, int yEnd, int count = 1) {
for (int i = 0; i < count; i++) {
canvas.fillRect(1, yStart, 130, yEnd - yStart, color);
canvas.pushSprite(0, 0);
delay(150);
drawScreen();
delay(100);
}
}
// ---------------- Morse decode ----------------
String decodeMorse(const char *m) {
if (strcmp(m, ".-") == 0) return "A";
else if (strcmp(m, "-...") == 0) return "B";
else if (strcmp(m, "-.-.") == 0) return "C";
else if (strcmp(m, "-..") == 0) return "D";
else if (strcmp(m, ".") == 0) return "E";
else if (strcmp(m, "..-.") == 0) return "F";
else if (strcmp(m, "--.") == 0) return "G";
else if (strcmp(m, "....") == 0) return "H";
else if (strcmp(m, "..") == 0) return "I";
else if (strcmp(m, ".---") == 0) return "J";
else if (strcmp(m, "-.-") == 0) return "K";
else if (strcmp(m, ".-..") == 0) return "L";
else if (strcmp(m, "--") == 0) return "M";
else if (strcmp(m, "-.") == 0) return "N";
else if (strcmp(m, "---") == 0) return "O";
else if (strcmp(m, ".--.") == 0) return "P";
else if (strcmp(m, "--.-") == 0) return "Q";
else if (strcmp(m, ".-.") == 0) return "R";
else if (strcmp(m, "...") == 0) return "S";
else if (strcmp(m, "-") == 0) return "T";
else if (strcmp(m, "..-") == 0) return "U";
else if (strcmp(m, "...-") == 0) return "V";
else if (strcmp(m, ".--") == 0) return "W";
else if (strcmp(m, "-..-") == 0) return "X";
else if (strcmp(m, "-.--") == 0) return "Y";
else if (strcmp(m, "--..") == 0) return "Z";
else if (strcmp(m, ".----") == 0) return "1";
else if (strcmp(m, "..---") == 0) return "2";
else if (strcmp(m, "...--") == 0) return "3";
else if (strcmp(m, "....-") == 0) return "4";
else if (strcmp(m, ".....") == 0) return "5";
else if (strcmp(m, "-....") == 0) return "6";
else if (strcmp(m, "--...") == 0) return "7";
else if (strcmp(m, "---..") == 0) return "8";
else if (strcmp(m, "----.") == 0) return "9";
else if (strcmp(m, "-----") == 0) return "0";
else return "";
}
// ---------------- WPM logic ----------------
void applyWPM(float wpm) {
measuredWPM = constrain(wpm, WPM_MIN, WPM_MAX);
ditMs = 1200.0f / measuredWPM;
dotDashThresholdMs = 2.0f * ditMs;
letterGapMs = 6.0f * ditMs; // longer idle gap for character finalize
}
// ---------------- Input handling ----------------
void handleKey(unsigned long now, bool currentDown) {
if (!keyDown && currentDown && (now - lastChangeMs > debounceMs)) {
keyDown = true;
lastChangeMs = now;
pressStartMs = now;
M5Cardputer.Speaker.tone(700);
}
if (keyDown && !currentDown && (now - lastChangeMs > debounceMs)) {
keyDown = false;
lastChangeMs = now;
unsigned long pressDur = now - pressStartMs;
M5Cardputer.Speaker.stop();
char symbol = (pressDur < dotDashThresholdMs) ? '.' : '-';
if (morseLen < 15) {
morse[morseLen++] = symbol;
morse[morseLen] = '\0';
}
lastElementEnd = now;
}
}
void pollWPMKeys() {
if (M5Cardputer.Keyboard.isChange() && M5Cardputer.Keyboard.isPressed()) {
auto ks = M5Cardputer.Keyboard.keysState();
for (auto ch : ks.word) {
if (ch == ';') applyWPM(measuredWPM + 1.0f);
if (ch == '.') applyWPM(measuredWPM - 1.0f);
}
}
}
// ---------------- Setup ----------------
void setup() {
auto cfg = M5.config();
M5Cardputer.begin(cfg);
M5Cardputer.Display.setRotation(0);
pinMode(keyPin, INPUT_PULLUP);
M5Cardputer.Speaker.setVolume(70);
canvas.createSprite(132, 239);
showStartupScreen();
canvas.createSprite(132, 239);
applyWPM(measuredWPM);
newChar();
}
// ---------------- Loop ----------------
void loop() {
M5Cardputer.update();
pollWPMKeys();
unsigned long now = millis();
bool currentDown = (digitalRead(keyPin) == LOW);
handleKey(now, currentDown);
// Decode after idle gap
if (morseLen > 0 && lastElementEnd > 0 && now - lastElementEnd > letterGapMs) {
String decoded = decodeMorse(morse);
if (decoded == "") {
flashBox(RED, 218, 238);
score -= 2;
errorStreak++;
correctStreak = 0;
} else if (decoded[0] == currentChar) {
flashBox(GREEN, 22, 218, 2);
score += 10;
correctStreak++;
errorStreak = 0;
if (correctStreak % 5 == 0) {
level++;
fallDelay = max(20, fallDelay - 10);
canvas.fillScreen(BLACK);
canvas.setTextColor(GREEN);
canvas.setTextSize(2);
canvas.setTextDatum(middle_center);
canvas.drawString("Level " + String(level), 66, 120);
canvas.pushSprite(0, 0);
delay(1500);
}
newChar();
} else {
score -= 5;
correctStreak = 0;
errorStreak++;
}
morse[0] = '\0';
morseLen = 0;
lastElementEnd = 0;
}
// Falling logic
if (now - lastFall > fallDelay) {
charY += 3;
lastFall = now;
if (charY > 205) {
score -= 10;
flashBox(RED, 218, 238);
newChar();
errorStreak++;
correctStreak = 0;
}
}
// Game over at 6 errors
if (errorStreak >= 6) {
canvas.fillScreen(BLACK);
canvas.setTextColor(RED);
canvas.setTextSize(2);
canvas.setTextDatum(middle_center);
canvas.drawString("Game Over", 66, 100);
canvas.drawString("Score: " + String(score), 66, 140);
canvas.drawString("Level: " + String(level), 66, 170);
canvas.drawString("Press ENTER", 66, 200);
canvas.pushSprite(0, 0);
// Wait for ENTER to restart
while (true) {
M5Cardputer.update();
if (M5Cardputer.Keyboard.isChange() && M5Cardputer.Keyboard.isPressed()) {
auto ks = M5Cardputer.Keyboard.keysState();
if (ks.enter) {
// Reset game state
score = 0;
level = 1;
fallDelay = 80;
correctStreak = 0;
errorStreak = 0;
morse[0] = '\0';
morseLen = 0;
lastFall = millis();
newChar();
return;
}
}
delay(100);
}
}
drawScreen();
}
Wifi Telegraph
Arduino// WiFi_Telegraph
// Version: 1.6 (official M5Cardputer keyboard input)
// Author: Matthew Sparks N6MMS
// Platform: M5Cardputer
// License: GNU GPL v3.0
#include <M5Cardputer.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <SPIFFS.h>
WiFiUDP udp;
IPAddress remoteIP;
const int udpPort = 4210;
const int groveG1Pin = 1; // Dash
const int groveG2Pin = 2; // Dot
bool lastG1State = HIGH;
bool lastG2State = HIGH;
int wpm = 15;
int ditLen = 1200 / 15;
int txCursorX = 10;
int txCursorY = 30;
unsigned long lastReceiveTime = 0;
String morseLine = "";
void drawMessageLabel() {
M5Cardputer.Display.setTextSize(2);
M5Cardputer.Display.setTextColor(WHITE, BLACK);
M5Cardputer.Display.setCursor(10, 10);
M5Cardputer.Display.print("Message:");
}
void clearMessageArea() {
M5Cardputer.Display.fillScreen(BLACK);
drawMessageLabel();
txCursorX = 10;
txCursorY = 30;
}
void showLocalMorse(char symbol) {
if (txCursorX > 220) {
txCursorX = 10;
txCursorY += 20;
}
if (txCursorY > 120) {
clearMessageArea();
}
M5Cardputer.Display.setCursor(txCursorX, txCursorY);
M5Cardputer.Display.setTextColor(WHITE, BLACK);
M5Cardputer.Display.setTextSize(2);
M5Cardputer.Display.print(symbol);
txCursorX += 12;
}
void sendMorse(char symbol) {
udp.beginPacket(remoteIP, udpPort);
udp.write(symbol);
udp.endPacket();
}
void receiveMorse() {
int packetSize = udp.parsePacket();
if (packetSize > 0) {
char symbol;
udp.read(&symbol, 1);
unsigned long now = millis();
if (now - lastReceiveTime > ditLen * 7 && morseLine.length() > 0) {
txCursorX = 10;
txCursorY += 20;
if (txCursorY > 120) {
clearMessageArea();
}
morseLine = "";
}
morseLine += symbol;
lastReceiveTime = now;
if (symbol == '.') {
M5Cardputer.Speaker.tone(1000, 100);
} else if (symbol == '-') {
M5Cardputer.Speaker.tone(1000, 300);
}
showLocalMorse(symbol);
}
}
String getStoredPassword(const String& ssid) {
String path = "/" + ssid;
if (SPIFFS.exists(path)) {
File file = SPIFFS.open(path, "r");
if (file) {
String pw = file.readStringUntil('\n');
file.close();
return pw;
}
}
return "";
}
void storePassword(const String& ssid, const String& password) {
String path = "/" + ssid;
File file = SPIFFS.open(path, "w");
if (file) {
file.println(password);
file.close();
}
}
void connectWiFi() {
M5Cardputer.Display.fillScreen(BLACK);
M5Cardputer.Display.setTextColor(WHITE);
M5Cardputer.Display.setTextSize(1);
M5Cardputer.Display.setCursor(10, 10);
M5Cardputer.Display.println("Scanning networks...");
int n = WiFi.scanNetworks();
if (n == 0) {
M5Cardputer.Display.println("No networks found.");
while (true);
}
std::vector<String> ssids;
for (int i = 0; i < n; i++) {
ssids.push_back(WiFi.SSID(i));
}
int selected = 0;
bool confirmed = false;
while (!confirmed) {
M5Cardputer.Display.fillScreen(BLACK);
M5Cardputer.Display.setCursor(10, 10);
M5Cardputer.Display.println("Select WiFi:");
for (int i = 0; i < ssids.size(); i++) {
M5Cardputer.Display.setCursor(20, 30 + i * 15);
if (i == selected) M5Cardputer.Display.print("> ");
else M5Cardputer.Display.print(" ");
M5Cardputer.Display.print(ssids[i]);
}
M5Cardputer.update();
if (M5Cardputer.Keyboard.isChange() && M5Cardputer.Keyboard.isPressed()) {
auto keys = M5Cardputer.Keyboard.keysState();
for (char c : keys.word) {
if (c == '.') selected = (selected + 1) % ssids.size();
if (c == ';') selected = (selected - 1 + ssids.size()) % ssids.size();
}
if (keys.enter) confirmed = true;
}
delay(100);
}
String chosenSSID = ssids[selected];
String storedPW = getStoredPassword(chosenSSID);
String enteredPW = storedPW;
bool done = false;
while (!done) {
M5Cardputer.Display.fillScreen(BLACK);
M5Cardputer.Display.setCursor(10, 10);
M5Cardputer.Display.print("SSID: ");
M5Cardputer.Display.println(chosenSSID);
M5Cardputer.Display.setCursor(10, 40);
M5Cardputer.Display.println("Enter Password:");
M5Cardputer.Display.setCursor(10, 70);
M5Cardputer.Display.print("> ");
M5Cardputer.Display.print(enteredPW);
M5Cardputer.update();
if (M5Cardputer.Keyboard.isChange() && M5Cardputer.Keyboard.isPressed()) {
auto keys = M5Cardputer.Keyboard.keysState();
for (char c : keys.word) {
if (isPrintable(c) && enteredPW.length() < 32) enteredPW += c;
}
if (keys.del && enteredPW.length() > 0) enteredPW.remove(enteredPW.length() - 1);
if (keys.enter) done = true;
}
delay(50);
}
if (enteredPW != storedPW) {
storePassword(chosenSSID, enteredPW);
}
WiFi.begin(chosenSSID.c_str(), enteredPW.c_str());
M5Cardputer.Display.fillScreen(BLACK);
M5Cardputer.Display.setCursor(10, 30);
M5Cardputer.Display.println("Connecting...");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
M5Cardputer.Display.print(".");
}
M5Cardputer.Display.println("\nConnected!");
delay(1000);
clearMessageArea();
}
void promptForRemoteIP() {
IPAddress local = WiFi.localIP();
int base1 = local[0];
int base2 = local[1];
int base3 = local[2];
String input = "";
bool confirmed = false;
M5Cardputer.Display.fillScreen(BLACK);
M5Cardputer.Display.setTextColor(WHITE);
M5Cardputer.Display.setTextSize(2);
M5Cardputer.Display.setCursor(10, 10);
M5Cardputer.Display.printf("My IP: %d.%d.%d.%d", base1, base2, base3, local[3]);
M5Cardputer.Display.setCursor(10, 50);
M5Cardputer.Display.print("Remote IP Octet:");
M5Cardputer.Display.setCursor(10, 85);
M5Cardputer.Display.print("Press ENTER when done");
M5Cardputer.Display.setCursor(10, 120);
M5Cardputer.Display.print("> ");
while (!confirmed) {
M5Cardputer.update();
if (M5Cardputer.Keyboard.isChange() && M5Cardputer.Keyboard.isPressed()) {
auto keys = M5Cardputer.Keyboard.keysState();
for (char c : keys.word) {
if (isdigit(c) && input.length() < 3) input += c;
}
if (keys.del && input.length() > 0) input.remove(input.length() - 1);
if (keys.enter && input.length() > 0) {
int lastOctet = input.toInt();
remoteIP = IPAddress(base1, base2, base3, lastOctet);
confirmed = true;
}
M5Cardputer.Display.fillRect(40, 120, 180, 24, BLACK);
M5Cardputer.Display.setCursor(40, 120);
M5Cardputer.Display.print(input);
}
}
M5Cardputer.Display.fillScreen(BLACK);
M5Cardputer.Display.setCursor(10, 60);
M5Cardputer.Display.setTextColor(GREEN);
M5Cardputer.Display.printf("Remote IP: %d.%d.%d.%d",
remoteIP[0], remoteIP[1], remoteIP[2], remoteIP[3]);
delay(1500);
clearMessageArea();
}
void setup() {
auto cfg = M5.config();
M5Cardputer.begin(cfg);
SPIFFS.begin(true);
pinMode(groveG1Pin, INPUT_PULLUP);
pinMode(groveG2Pin, INPUT_PULLUP);
Serial.begin(115200);
clearMessageArea();
connectWiFi();
promptForRemoteIP();
udp.begin(udpPort);
}
void loop() {
M5Cardputer.update();
int g1State = digitalRead(groveG1Pin);
int g2State = digitalRead(groveG2Pin);
if (g1State == LOW && lastG1State == HIGH) {
M5Cardputer.Speaker.tone(1000, 300);
showLocalMorse('-');
sendMorse('-');
}
if (g2State == LOW && lastG2State == HIGH) {
M5Cardputer.Speaker.tone(1000, 100);
showLocalMorse('.');
sendMorse('.');
}
receiveMorse();
lastG1State = g1State;
lastG2State = g2State;
delay(100);
}
Gettysburg.txt
ArduinoFour score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal.
Now we are engaged in a great civil war, testing whether that nation, or any nation so conceived and so dedicated, can long endure. We are met on a great battle-field of that war. We have come to dedicate a portion of that field, as a final resting place for those who here gave their lives that that nation might live. It is altogether fitting and proper that we should do this.
But, in a larger sense, we can not dedicatewe can not consecratewe can not hallowthis ground. The brave men, living and dead, who struggled here, have consecrated it, far above our poor power to add or detract. The world will little note, nor long remember what we say here, but it can never forget what they did here. It is for us the living, rather, to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced. It is rather for us to be here dedicated to the great task remaining before usthat from these honored dead we take increased devotion to that cause for which they gave the last full measure of devotionthat we here highly resolve that these dead shall not have died in vainthat this nation, under God, shall have a new birth of freedomand that government of the people, by the people, for the people, shall not perish from the earth.
A. Lincoln 1863
CW_Straight-Key_Oscillator2.1
Arduino// M5 CW Straight-Key Oscillator
// Version: 2.1
// Author: Matthew Sparks N6MMS
// Platform: M5Cardputer (M5Unified + M5Cardputer 1.0.3)
// License: GPLv3
//
// 2.1 fixes:
// - Append logic no longer clears before every new dot/dash.
// - On first element after WPM screen, set firstAppend=false so stream persists.
// - On truncation, refresh interior without calling resetCWStream() (which re-set firstAppend=true).
//
// Controls:
// ';' -> WPM up (5..25)
// '.' -> WPM down
//
// Idle behavior:
// - Insert a single space at 3 dits (inter-letter).
// - Clear the CW box only after 6 dits idle *and* a space was already inserted.
#include <M5Unified.h>
#include <M5Cardputer.h>
const int groveG1Pin = 1; // Straight key input (active LOW)
const int sidetoneHz = 700; // Sidetone frequency
const int debounceMs = 10; // Debounce delay
// Mutable WPM and derived timing
float measuredWPM = 8.0f;
float ditMs, dotDashThresholdMs, letterGapMs, resetGapMs;
const float WPM_MIN = 5.0f;
const float WPM_MAX = 25.0f;
bool keyDown = false;
unsigned long lastChangeMs = 0;
unsigned long pressStartMs = 0;
unsigned long lastElementEndMs = 0;
// UI box
const int BOX_X = 8;
const int BOX_Y = 90;
const int BOX_W = 225;
const int BOX_H = 40;
String cwLine;
const int CW_MAX_CHARS = 36;
bool showingWPM = true; // CW box currently shows "CW: <WPM> WPM"
bool firstAppend = true; // clear box on first Morse char, then set false
bool letterSpaceInserted = false;
// ---------- UI helpers ----------
void drawTitle() {
M5.Lcd.clear(BLACK);
M5.Lcd.setTextSize(2);
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.setCursor(10, 10); M5.Lcd.println("Straight-Key");
M5.Lcd.setCursor(10, 35); M5.Lcd.print(" Oscillator");
M5.Lcd.setCursor(10, 55); M5.Lcd.print("By N6MMS");
}
void drawCWFrameOnce() {
M5.Lcd.drawRect(BOX_X, BOX_Y, BOX_W, BOX_H, GREEN);
}
void clearCWInterior() {
M5.Lcd.fillRect(BOX_X + 2, BOX_Y + 2, BOX_W - 4, BOX_H - 4, BLACK);
}
void showLabelCW() {
M5.Lcd.setCursor(BOX_X + 6, BOX_Y + 10);
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.print("CW:");
}
void showWPMBox() {
clearCWInterior();
showLabelCW();
M5.Lcd.setCursor(BOX_X + 6 + 34, BOX_Y + 10); // after "CW:"
M5.Lcd.printf("%.0f WPM", measuredWPM);
showingWPM = true;
firstAppend = true;
letterSpaceInserted = false;
}
void startCWStreamIfNeeded() {
if (showingWPM || firstAppend) {
// Switch from WPM notice to live stream
clearCWInterior();
showLabelCW();
showingWPM = false;
firstAppend = false; // IMPORTANT: allow subsequent appends to persist
letterSpaceInserted = false;
}
}
void printCWLine() {
M5.Lcd.setCursor(BOX_X + 6 + 34, BOX_Y + 10); // after "CW:"
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.print(cwLine);
}
void truncateAndRefresh() {
// Trim to last CW_MAX_CHARS and refresh interior without resetting firstAppend
if ((int)cwLine.length() > CW_MAX_CHARS) {
cwLine = cwLine.substring(cwLine.length() - CW_MAX_CHARS);
clearCWInterior();
showLabelCW();
}
}
void appendCWChar(char c) {
startCWStreamIfNeeded();
cwLine += c;
truncateAndRefresh();
printCWLine();
}
// ---------- Timing ----------
void applyWPM(float wpm) {
measuredWPM = wpm;
if (measuredWPM < WPM_MIN) measuredWPM = WPM_MIN;
if (measuredWPM > WPM_MAX) measuredWPM = WPM_MAX;
ditMs = 1200.0f / measuredWPM;
dotDashThresholdMs = 2.0f * ditMs; // dot vs dash split
letterGapMs = 3.0f * ditMs; // inter-letter
resetGapMs = 6.0f * ditMs; // clear after long idle (post-space)
showWPMBox(); // immediately reflect WPM in CW box
}
// ---------- Straight-key handling ----------
void handleKeyTransitions(unsigned long now, bool currentDown) {
// Debounced press
if (!keyDown && currentDown && (now - lastChangeMs > debounceMs)) {
keyDown = true;
lastChangeMs = now;
pressStartMs = now;
M5.Speaker.tone(sidetoneHz);
}
// Debounced release
if (keyDown && !currentDown && (now - lastChangeMs > debounceMs)) {
keyDown = false;
lastChangeMs = now;
unsigned long pressDur = now - pressStartMs;
M5.Speaker.stop();
appendCWChar((float)pressDur < dotDashThresholdMs ? '.' : '-');
lastElementEndMs = now;
letterSpaceInserted = false; // new element => same letter
}
}
// ---------- Keyboard (M5Cardputer 1.0.3 API) ----------
void pollWPMKeys() {
if (M5Cardputer.Keyboard.isChange() && M5Cardputer.Keyboard.isPressed()) {
Keyboard_Class::KeysState ks = M5Cardputer.Keyboard.keysState();
for (auto ch : ks.word) {
if (ch == ';') { applyWPM(measuredWPM + 1.0f); }
if (ch == '.') { applyWPM(measuredWPM - 1.0f); }
}
}
}
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
M5Cardputer.begin(cfg, true);
pinMode(groveG1Pin, INPUT_PULLUP);
M5.Speaker.setVolume(70);
applyWPM(measuredWPM);
drawTitle();
drawCWFrameOnce();
showWPMBox();
}
void loop() {
M5.update();
M5Cardputer.update();
pollWPMKeys();
unsigned long now = millis();
bool currentDown = (digitalRead(groveG1Pin) == LOW);
handleKeyTransitions(now, currentDown);
// Inter-letter space + clear logic while key is up
if (!currentDown && lastElementEndMs != 0) {
unsigned long idle = now - lastElementEndMs;
// Insert a single space after 3 dits (separates letters in-stream)
if (!showingWPM && cwLine.length() > 0 && !letterSpaceInserted && idle >= (unsigned long)letterGapMs) {
appendCWChar(' ');
letterSpaceInserted = true;
}
// Clear only if we've already separated letters AND idle >= 6 dits
if (!showingWPM && cwLine.length() > 0 && letterSpaceInserted && idle >= (unsigned long)resetGapMs) {
// Clear the CW area and keep streaming mode (don't revert to WPM)
cwLine = "";
clearCWInterior();
showLabelCW();
// Keep firstAppend=false so next elements continue appending
lastElementEndMs = now; // avoid repeated clears
}
}
}
M5_morse3.0
Arduino// Program Name: CW Paddle Decoder
// Version : 3.0
// Date : 2025-06-06
// Platform : M5Cardputer
// Author : Matthew Sparks N6MMS
// EMail : ProfessorSparks@gmail.com
// Description : Morse code paddle decoder for use on an M5Cardputer, that uses dot/dash paddles on G1/G2,
// plays tones, and displays decoded letters, punctuation and prosigns.
// Includes WPM input and error handling.
// Notes : - Enter WPM 520, default is 15 if none entered.
// - Uses M5GFX and M5Cardputer libraries.
/* License : GNU General Public License v3.0
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include <M5Cardputer.h>
#include <M5GFX.h>
const int dotPin = 2; // Grove G1
const int dashPin = 1; // Grove G2
const char* version = "3.0"; // Version variable
M5Canvas canvas(&M5Cardputer.Display);
char morsestring[10] = "";
char morseletter[8] = "";
int morselen = 0;
int wpm = 15;
int ditLen = 0;
unsigned long lastInputTime = 0;
bool dotState = false;
bool dashState = false;
bool isProsign = false;
void playTone(int freq, int dur) {
M5Cardputer.Speaker.tone(freq, dur);
delay(dur);
M5Cardputer.Speaker.end();
}
void drawMorseInput() {
canvas.fillSprite(BLACK);
canvas.setCursor(0, 0);
canvas.setTextColor(GREEN, BLACK);
canvas.setTextSize(1.25); // Set as requested
canvas.println("Morse:");
canvas.print(" ");
canvas.println(morsestring);
canvas.pushSprite(0, 0);
}
void decodeMorse() {
strcpy(morseletter, "");
isProsign = false;
bool foundmatch = false;
// Letters
if (strcmp(morsestring, ".-") == 0) strcpy(morseletter, "A");
else if (strcmp(morsestring, "-...") == 0) strcpy(morseletter, "B");
else if (strcmp(morsestring, "-.-.") == 0) strcpy(morseletter, "C");
else if (strcmp(morsestring, "-..") == 0) strcpy(morseletter, "D");
else if (strcmp(morsestring, ".") == 0) strcpy(morseletter, "E");
else if (strcmp(morsestring, "..-.") == 0) strcpy(morseletter, "F");
else if (strcmp(morsestring, "--.") == 0) strcpy(morseletter, "G");
else if (strcmp(morsestring, "....") == 0) strcpy(morseletter, "H");
else if (strcmp(morsestring, "..") == 0) strcpy(morseletter, "I");
else if (strcmp(morsestring, ".---") == 0) strcpy(morseletter, "J");
else if (strcmp(morsestring, "-.-") == 0) strcpy(morseletter, "K");
else if (strcmp(morsestring, ".-..") == 0) strcpy(morseletter, "L");
else if (strcmp(morsestring, "--") == 0) strcpy(morseletter, "M");
else if (strcmp(morsestring, "-.") == 0) strcpy(morseletter, "N");
else if (strcmp(morsestring, "---") == 0) strcpy(morseletter, "O");
else if (strcmp(morsestring, ".--.") == 0) strcpy(morseletter, "P");
else if (strcmp(morsestring, "--.-") == 0) strcpy(morseletter, "Q");
else if (strcmp(morsestring, ".-.") == 0) strcpy(morseletter, "R");
else if (strcmp(morsestring, "...") == 0) strcpy(morseletter, "S");
else if (strcmp(morsestring, "-") == 0) strcpy(morseletter, "T");
else if (strcmp(morsestring, "..-") == 0) strcpy(morseletter, "U");
else if (strcmp(morsestring, "...-") == 0) strcpy(morseletter, "V");
else if (strcmp(morsestring, ".--") == 0) strcpy(morseletter, "W");
else if (strcmp(morsestring, "-..-") == 0) strcpy(morseletter, "X");
else if (strcmp(morsestring, "-.--") == 0) strcpy(morseletter, "Y");
else if (strcmp(morsestring, "--..") == 0) strcpy(morseletter, "Z");
// Numbers
else if (strcmp(morsestring, ".----") == 0) strcpy(morseletter, "1");
else if (strcmp(morsestring, "..---") == 0) strcpy(morseletter, "2");
else if (strcmp(morsestring, "...--") == 0) strcpy(morseletter, "3");
else if (strcmp(morsestring, "....-") == 0) strcpy(morseletter, "4");
else if (strcmp(morsestring, ".....") == 0) strcpy(morseletter, "5");
else if (strcmp(morsestring, "-....") == 0) strcpy(morseletter, "6");
else if (strcmp(morsestring, "--...") == 0) strcpy(morseletter, "7");
else if (strcmp(morsestring, "---..") == 0) strcpy(morseletter, "8");
else if (strcmp(morsestring, "----.") == 0) strcpy(morseletter, "9");
else if (strcmp(morsestring, "-----") == 0) strcpy(morseletter, "0");
// Punctuation
else if (strcmp(morsestring, "-.-.--") == 0 || strcmp(morsestring, "..--.") == 0) strcpy(morseletter, "!");
else if (strcmp(morsestring, "...-..-") == 0) strcpy(morseletter, "$");
else if (strcmp(morsestring, ".-...") == 0) strcpy(morseletter, "&");
else if (strcmp(morsestring, ".----.") == 0) strcpy(morseletter, "'");
else if (strcmp(morsestring, "-.--.") == 0) strcpy(morseletter, "(");
else if (strcmp(morsestring, "-.--.-") == 0) strcpy(morseletter, ")");
else if (strcmp(morsestring, ".-.-.") == 0) strcpy(morseletter, "+");
else if (strcmp(morsestring, "--..--") == 0) strcpy(morseletter, ",");
else if (strcmp(morsestring, "-....-") == 0) strcpy(morseletter, "-");
else if (strcmp(morsestring, ".-.-.-") == 0) strcpy(morseletter, ".");
else if (strcmp(morsestring, "-..-.") == 0) strcpy(morseletter, "/");
else if (strcmp(morsestring, "---:") == 0) strcpy(morseletter, ":");
else if (strcmp(morsestring, "-.-.-.") == 0) strcpy(morseletter, ";");
else if (strcmp(morsestring, "..--..") == 0) strcpy(morseletter, "?");
else if (strcmp(morsestring, ".--.-.") == 0) strcpy(morseletter, "@");
else if (strcmp(morsestring, "..--.-") == 0) strcpy(morseletter, "_");
// Prosigns
else if (strcmp(morsestring, ".-.-") == 0) { strcpy(morseletter, "AA"); isProsign = true; }
// else if (strcmp(morsestring, ".-.-.") == 0) { strcpy(morseletter, "AR"); isProsign = true; }// this prosign is the same CW as the +.
else if (strcmp(morsestring, "-...-.-") == 0) { strcpy(morseletter, "BK"); isProsign = true; }
else if (strcmp(morsestring, "-...-") == 0) { strcpy(morseletter, "BT"); isProsign = true; }
else if (strcmp(morsestring, "-.-..-..") == 0) { strcpy(morseletter, "CL"); isProsign = true; }
else if (strcmp(morsestring, "-.-.-") == 0) { strcpy(morseletter, "CT"); isProsign = true; }
else if (strcmp(morsestring, "-.--.") == 0) { strcpy(morseletter, "KN"); isProsign = true; }
else if (strcmp(morsestring, "...-.-") == 0) { strcpy(morseletter, "SK"); isProsign = true; }
else if (strcmp(morsestring, "...-.") == 0) { strcpy(morseletter, "SN"); isProsign = true; }
else if (strcmp(morsestring, "...---...") == 0) { strcpy(morseletter, "SOS"); isProsign = true; }
else if (strcmp(morsestring, "-.-.--.-") == 0) { strcpy(morseletter, "CQ"); isProsign = true; }
else if (strcmp(morsestring, "........") == 0) { strcpy(morseletter, "Err"); isProsign = true; } // also shown as HH
if (strlen(morseletter) == 0) {
M5Cardputer.Display.fillScreen(RED);
M5Cardputer.Display.setTextColor(BLACK);
M5Cardputer.Display.setTextFont(&fonts::FreeSerifBoldItalic18pt7b);
M5Cardputer.Display.setTextSize(2);
int16_t centerX = (M5Cardputer.Display.width() / 2) - 12;
int16_t centerY = (M5Cardputer.Display.height() / 2) - 20;
M5Cardputer.Display.setCursor(centerX, centerY);
M5Cardputer.Display.print("?");
playTone(300, ditLen * 3);
delay(ditLen * 2);
M5Cardputer.Display.fillScreen(BLACK);
strcpy(morsestring, "");
drawMorseInput();
return;
}
M5Cardputer.Display.fillScreen(BLACK);
M5Cardputer.Display.setTextColor(GREEN);
M5Cardputer.Display.setTextFont(&fonts::FreeSerifBoldItalic18pt7b);
M5Cardputer.Display.setTextSize(2);
int16_t x = (M5Cardputer.Display.width() / 2) - 32;
int16_t y = (M5Cardputer.Display.height() / 2) - 24;
if (isProsign) {
M5Cardputer.Display.fillRect(x - 8, y - 20, 120, 8, GREEN); // Thick line
}
M5Cardputer.Display.setCursor(x, y);
M5Cardputer.Display.print(morseletter);
for (int i = 0; i < strlen(morsestring); i++) {
playTone(1000, morsestring[i] == '.' ? ditLen : ditLen * 3);
delay(ditLen);
}
delay(ditLen * 2);
strcpy(morsestring, "");
drawMorseInput();
}
void promptWPM() {
canvas.setTextSize(0.65);
canvas.fillSprite(BLACK);
canvas.setTextColor(GREEN, BLACK);
canvas.setCursor(0, 0);
canvas.printf("CWPaddle v%s\n", version);
canvas.println("(M5Cardputer)");
canvas.println("");
canvas.println("Enter WPM (5-20):");
canvas.pushSprite(0, 0);
String input = "> ";
bool confirmed = false;
while (!confirmed) {
M5Cardputer.update();
if (M5Cardputer.Keyboard.isChange() && M5Cardputer.Keyboard.isPressed()) {
auto keys = M5Cardputer.Keyboard.keysState();
for (auto c : keys.word) {
if (isdigit(c)) input += c;
}
if (keys.del && input.length() > 2) {
input.remove(input.length() - 1);
}
if (keys.enter) {
if (input.length() <= 2) {
// User pressed enter with no digits use default
wpm = 15;
confirmed = true;
} else {
int val = input.substring(2).toInt();
if (val >= 5 && val <= 20) {
wpm = val;
confirmed = true;
} else {
input = "> ";
}
}
}
canvas.fillSprite(BLACK);
canvas.setCursor(0, 0);
canvas.setTextSize(0.65);
canvas.printf("CWPaddle v%s\n", version);
canvas.println("(M5Cardputer)");
canvas.println("");
canvas.println("Enter WPM (520):");
canvas.println(input);
canvas.pushSprite(0, 0);
}
}
ditLen = 1200 / wpm;
}
void setup() {
auto cfg = M5.config();
M5Cardputer.begin(cfg, true);
M5Cardputer.Display.setRotation(1);
canvas.createSprite(M5Cardputer.Display.width(), M5Cardputer.Display.height());
canvas.setTextFont(&fonts::FreeSerifBoldItalic18pt7b);
canvas.setTextSize(0.5);
canvas.setTextColor(GREEN);
canvas.fillSprite(BLACK);
canvas.pushSprite(0, 0);
pinMode(dotPin, INPUT_PULLUP);
pinMode(dashPin, INPUT_PULLUP);
promptWPM();
drawMorseInput();
}
void loop() {
M5Cardputer.update();
dotState = digitalRead(dotPin) == LOW;
dashState = digitalRead(dashPin) == LOW;
if (dotState) {
strcat(morsestring, ".");
playTone(1000, ditLen);
drawMorseInput();
delay(ditLen);
lastInputTime = millis();
} else if (dashState) {
strcat(morsestring, "-");
playTone(1000, ditLen * 3);
drawMorseInput();
lastInputTime = millis();
}
if (strlen(morsestring) > 0 && millis() - lastInputTime > ditLen * 5) {
decodeMorse();
delay(ditLen * 2);
}
}
CW_Straight_key_Decoder 2.8
Arduino// M5 Straight-Key CW Decoder
// Version: 2.8
// Author: Matthew Sparks N6MMS
// Platform: M5Cardputer (M5Unified + M5Cardputer 1.0.3)
// License: GPLv3
//
// Whats new in 2.8
// - Revert to compact screen-safe text (setTextSize(2)) like v2.6.
// - Add numbers & punctuation decoding (AZ, 09, common symbols).
// - Keep decoded output visible until next keypress or 7-dits idle.
// - Decode idle gap = 4 dits; clear BOTH boxes after 8 dits.
//
// Controls
// - Straight key on G1 (active LOW).
// - ; to increase WPM, . to decrease WPM (5..25).
#include <M5Unified.h>
#include <M5Cardputer.h>
// --- Hardware / timing ---
const int groveG1Pin = 1; // Straight key input (active LOW)
const int sidetoneHz = 700; // Sidetone frequency
const int debounceMs = 10; // Debounce delay
float measuredWPM = 8.0f;
float ditMs, dotDashThresholdMs, letterGapMs, wordGapMs, resetGapMs;
const float WPM_MIN = 5.0f, WPM_MAX = 25.0f;
bool keyDown = false;
unsigned long lastChangeMs = 0;
unsigned long pressStartMs = 0;
unsigned long lastElementEndMs = 0;
// --- UI layout ---
int16_t DISP_W = 240, DISP_H = 135;
int BOX_X = 8, BOX_Y = 50, BOX_W = 224, BOX_H = 40; // CW box (green)
int DECODED_BOX_Y = 100; // Decoded box (red)
// --- State ---
String seq; // current dot/dash sequence
String lastDecoded = ""; // what is currently shown in decoded box
bool showingWPM = true; // box shows "CW: <WPM> WPM"
bool firstAppend = true; // first element switches to stream
bool letterSpaceInserted = false; // used for visual reset timing
const int SEQ_MAX_CHARS = 16;
// ---------------- UI helpers ----------------
void computeLayout() {
DISP_W = M5.Display.width();
DISP_H = M5.Display.height();
BOX_X = 8;
BOX_W = DISP_W - 2 * BOX_X; // leave small margins
BOX_H = 40;
BOX_Y = (DISP_H >= 135) ? 48 : max(10, DISP_H / 4);
DECODED_BOX_Y = BOX_Y + BOX_H + 12;
if (DECODED_BOX_Y + BOX_H > DISP_H - 4) {
DECODED_BOX_Y = max(BOX_Y + BOX_H + 4, DISP_H - BOX_H - 4);
}
}
void drawTitle() {
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextSize(2);
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.setCursor(10, 10); M5.Lcd.println("Straight-Key");
M5.Lcd.setCursor(10, 30); M5.Lcd.print("Decoder");
M5.Lcd.setCursor(10, 50); M5.Lcd.print("By N6MMS");
}
void drawFramesOnce() {
// CW box (green)
M5.Lcd.drawRect(BOX_X, BOX_Y, BOX_W, BOX_H, GREEN);
// Decoded box (red)
M5.Lcd.drawRect(BOX_X, DECODED_BOX_Y, BOX_W, BOX_H, RED);
}
void clearCWInterior() {
M5.Lcd.fillRect(BOX_X + 2, BOX_Y + 2, BOX_W - 4, BOX_H - 4, BLACK);
}
void clearDecodedBox() {
M5.Lcd.fillRect(BOX_X + 2, DECODED_BOX_Y + 2, BOX_W - 4, BOX_H - 4, BLACK);
}
void showLabelCW() {
M5.Lcd.setCursor(BOX_X + 6, BOX_Y + 10);
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.print("CW:");
}
void showWPMBox() {
clearCWInterior();
showLabelCW();
M5.Lcd.setCursor(BOX_X + 40, BOX_Y + 10); // after "CW:"
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.printf("%.0f WPM", measuredWPM);
showingWPM = true;
firstAppend = true;
letterSpaceInserted = false;
// Initialize/refresh decoded box header
clearDecodedBox();
M5.Lcd.setCursor(BOX_X + 6, DECODED_BOX_Y + 10);
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.print("Decoded:");
lastDecoded = "";
}
void startCWStreamIfNeeded() {
if (showingWPM || firstAppend) {
clearCWInterior();
showLabelCW();
showingWPM = false;
firstAppend = false;
letterSpaceInserted = false;
}
}
void resetCWBoxOnly() {
clearCWInterior();
showLabelCW();
}
void printSeqInBox() {
M5.Lcd.setCursor(BOX_X + 40, BOX_Y + 10);
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.print(seq);
}
void printDecodedText(const String& text) {
clearDecodedBox();
M5.Lcd.setCursor(BOX_X + 6, DECODED_BOX_Y + 10);
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.print("Decoded: ");
M5.Lcd.setTextColor(GREEN, BLACK);
M5.Lcd.print(text);
lastDecoded = text;
}
// --------------- Decoder (letters, numbers, punctuation) ---------------
bool decodeSymbol(const String& s, String& out) {
// Letters AZ
if (s == ".-") out = "A";
else if (s == "-...") out = "B";
else if (s == "-.-.") out = "C";
else if (s == "-..") out = "D";
else if (s == ".") out = "E";
else if (s == "..-.") out = "F";
else if (s == "--.") out = "G";
else if (s == "....") out = "H";
else if (s == "..") out = "I";
else if (s == ".---") out = "J";
else if (s == "-.-") out = "K";
else if (s == ".-..") out = "L";
else if (s == "--") out = "M";
else if (s == "-.") out = "N";
else if (s == "---") out = "O";
else if (s == ".--.") out = "P";
else if (s == "--.-") out = "Q";
else if (s == ".-.") out = "R";
else if (s == "...") out = "S";
else if (s == "-") out = "T";
else if (s == "..-") out = "U";
else if (s == "...-") out = "V";
else if (s == ".--") out = "W";
else if (s == "-..-") out = "X";
else if (s == "-.--") out = "Y";
else if (s == "--..") out = "Z";
// Numbers 09
else if (s == "-----") out = "0";
else if (s == ".----") out = "1";
else if (s == "..---") out = "2";
else if (s == "...--") out = "3";
else if (s == "....-") out = "4";
else if (s == ".....") out = "5";
else if (s == "-....") out = "6";
else if (s == "--...") out = "7";
else if (s == "---..") out = "8";
else if (s == "----.") out = "9";
// Punctuation (common)
else if (s == ".-.-.-") out = ".";
else if (s == "--..--") out = ",";
else if (s == "..--..") out = "?";
else if (s == ".----.") out = "'";
else if (s == "-.-.--") out = "!";
else if (s == "-..-.") out = "/";
else if (s == "-.--.") out = "(";
else if (s == "-.--.-") out = ")";
else if (s == ".-...") out = "&";
else if (s == "---...") out = ":";
else if (s == "-.-.-.") out = ";";
else if (s == "-...-") out = "=";
else if (s == ".-.-.") out = "+";
else if (s == "-....-") out = "-";
else if (s == "..--.-") out = "_";
else if (s == ".--.-.") out = "@";
else return false;
return true;
}
// ---------------- Timing / WPM ----------------
void applyWPM(float wpm) {
measuredWPM = constrain(wpm, WPM_MIN, WPM_MAX);
ditMs = 1200.0f / measuredWPM;
dotDashThresholdMs = 2.0f * ditMs; // dot/dash split
letterGapMs = 4.0f * ditMs; // decode letter after this idle
wordGapMs = 7.0f * ditMs; // keep decoded visible until this idle
resetGapMs = 8.0f * ditMs; // clear BOTH boxes after longer idle
showWPMBox();
}
// -------- Straight-key handling (G1) --------
void handleKeyTransitions(unsigned long now, bool currentDown) {
// Debounced press
if (!keyDown && currentDown && (now - lastChangeMs > debounceMs)) {
keyDown = true;
lastChangeMs = now;
pressStartMs = now;
M5.Speaker.tone(sidetoneHz);
// If a decoded symbol is shown, hide it as we begin a new letter
if (lastDecoded.length() > 0) {
clearDecodedBox();
M5.Lcd.setCursor(BOX_X + 6, DECODED_BOX_Y + 10);
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.print("Decoded:");
lastDecoded = "";
}
}
// Debounced release
if (keyDown && !currentDown && (now - lastChangeMs > debounceMs)) {
keyDown = false;
lastChangeMs = now;
unsigned long pressDur = now - pressStartMs;
M5.Speaker.stop();
startCWStreamIfNeeded();
// Append element
seq += ((float)pressDur < dotDashThresholdMs) ? '.' : '-';
if ((int)seq.length() > SEQ_MAX_CHARS) {
seq = seq.substring(seq.length() - SEQ_MAX_CHARS);
}
resetCWBoxOnly();
printSeqInBox();
lastElementEndMs = now;
letterSpaceInserted = false; // still within a letter
}
}
// --------------- Keyboard (WPM adjust) ---------------
void pollWPMKeys() {
if (M5Cardputer.Keyboard.isChange() && M5Cardputer.Keyboard.isPressed()) {
auto ks = M5Cardputer.Keyboard.keysState();
for (auto ch : ks.word) {
if (ch == ';') applyWPM(measuredWPM + 1.0f);
if (ch == '.') applyWPM(measuredWPM - 1.0f);
}
}
}
// --------------- Decode on idle ---------------
void maybeDecodeOnIdle(unsigned long now, bool currentDown) {
if (currentDown || lastElementEndMs == 0) return;
unsigned long idle = now - lastElementEndMs;
// Decode at letter gap (4 dits)
if (!showingWPM && seq.length() > 0 && idle >= (unsigned long)letterGapMs) {
String sym;
if (decodeSymbol(seq, sym)) {
printDecodedText(sym);
} else {
printDecodedText("Error");
}
// Prepare for next letter
seq = "";
resetCWBoxOnly();
letterSpaceInserted = true; // we've separated letters
}
// If a decoded symbol is visible and we reach a word gap (7 dits), clear it
if (lastDecoded.length() > 0 && idle >= (unsigned long)wordGapMs) {
clearDecodedBox();
M5.Lcd.setCursor(BOX_X + 6, DECODED_BOX_Y + 10);
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.print("Decoded:");
lastDecoded = "";
}
// Cosmetic clear after longer idle (8 dits): clear both boxes
if (!showingWPM && letterSpaceInserted && idle >= (unsigned long)resetGapMs) {
resetCWBoxOnly();
clearDecodedBox();
M5.Lcd.setCursor(BOX_X + 6, DECODED_BOX_Y + 10);
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.print("Decoded:");
lastElementEndMs = now;
}
}
// ---------------- Setup / Loop ----------------
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
M5Cardputer.begin(cfg, true);
pinMode(groveG1Pin, INPUT_PULLUP);
M5.Speaker.setVolume(70);
computeLayout();
drawTitle();
drawFramesOnce();
// Initialize decoded box label
clearDecodedBox();
M5.Lcd.setCursor(BOX_X + 6, DECODED_BOX_Y + 10);
M5.Lcd.setTextColor(WHITE, BLACK);
M5.Lcd.print("Decoded:");
applyWPM(measuredWPM); // also shows WPM in the CW box
}
void loop() {
M5.update();
M5Cardputer.update();
pollWPMKeys();
unsigned long now = millis();
bool currentDown = (digitalRead(groveG1Pin) == LOW);
handleKeyTransitions(now, currentDown);
maybeDecodeOnIdle(now, currentDown);
}
CW_FilePlayer2.5
Arduino// CW_FilePlayer
// Version: 2.5 (Fix: file selection with ;/. and ENTER)
// Author: Matthew Sparks N6MMS
// Platform: M5Cardputer
// License: GNU GPL v3.0
#include <M5Cardputer.h>
#include <M5GFX.h>
#include <SD.h>
#include <vector>
int wpm = 15;
float ditlen = 1200.0 / wpm;
std::vector<String> fileList;
int selectedFileIndex = 0;
bool paused = false;
bool stopped = false;
const char* getMorseCodeForChar(char c) {
switch (toupper(c)) {
case 'A': return ".-"; case 'B': return "-..."; case 'C': return "-.-.";
case 'D': return "-.."; case 'E': return "."; case 'F': return "..-.";
case 'G': return "--."; case 'H': return "...."; case 'I': return "..";
case 'J': return ".---"; case 'K': return "-.-"; case 'L': return ".-..";
case 'M': return "--"; case 'N': return "-."; case 'O': return "---";
case 'P': return ".--."; case 'Q': return "--.-"; case 'R': return ".-.";
case 'S': return "..."; case 'T': return "-"; case 'U': return "..-";
case 'V': return "...-"; case 'W': return ".--"; case 'X': return "-..-";
case 'Y': return "-.--"; case 'Z': return "--..";
case '0': return "-----"; case '1': return ".----"; case '2': return "..---";
case '3': return "...--"; case '4': return "....-"; case '5': return ".....";
case '6': return "-...."; case '7': return "--..."; case '8': return "---..";
case '9': return "----.";
case '.': return ".-.-.-"; case ',': return "--..--"; case '?': return "..--..";
case '!': return "-.-.--"; case '-': return "-....-"; case '/': return "-..-.";
default: return "";
}
}
bool hasChar(const std::vector<char>& word, char target) {
for (char c : word) if (c == target) return true;
return false;
}
void centerText(const String& text, int y, float size = 1.0) {
M5Cardputer.Display.setTextSize(size);
int w = text.length() * 6 * size;
int x = (M5Cardputer.Display.width() - w) / 2;
M5Cardputer.Display.setCursor(x, y);
M5Cardputer.Display.print(text);
}
void showTitleScreen() {
M5Cardputer.Display.fillScreen(BLACK);
M5Cardputer.Display.setTextColor(WHITE);
centerText("CW FilePlayer", 25, 2.0);
centerText("by N6MMS", 60, 1.5);
delay(3000);
}
void showWPMScreen() {
M5Cardputer.Display.fillScreen(BLACK);
centerText("Enter WPM (5-25,", 20, 2);
centerText("default = 10):", 45, 1.7);
centerText(">", 85, 2);
String input = "";
bool confirmed = false;
while (!confirmed) {
M5Cardputer.update();
if (M5Cardputer.Keyboard.isChange()) {
auto keys = M5Cardputer.Keyboard.keysState();
for (char c : keys.word) {
if (isdigit(c) && input.length() < 2) input += c;
else if (c == '\b' && input.length() > 0) input.remove(input.length() - 1);
}
if (keys.enter) confirmed = true;
M5Cardputer.Display.fillRect(10, 110, 300, 20, BLACK);
M5Cardputer.Display.setCursor((M5Cardputer.Display.width() - input.length() * 6) / 2, 110);
M5Cardputer.Display.print(input);
}
delay(10);
}
int entered = input.toInt();
wpm = (entered >= 5 && entered <= 25) ? entered : 10;
ditlen = 1200.0 / wpm;
}
void listTextFiles() {
File root = SD.open("/");
fileList.clear();
File file = root.openNextFile();
while (file) {
String name = file.name();
name.toUpperCase();
if (!file.isDirectory() && name.endsWith(".TXT")) fileList.push_back(name);
file = root.openNextFile();
}
}
void showFileSelection() {
bool confirmed = false;
while (!confirmed) {
M5Cardputer.update();
if (M5Cardputer.Keyboard.isChange()) {
auto keys = M5Cardputer.Keyboard.keysState();
if (hasChar(keys.word, ';')) {
selectedFileIndex = (selectedFileIndex - 1 + fileList.size()) % fileList.size();
delay(150);
} else if (hasChar(keys.word, '.')) {
selectedFileIndex = (selectedFileIndex + 1) % fileList.size();
delay(150);
} else if (keys.enter) {
confirmed = true;
break;
}
}
M5Cardputer.Display.fillScreen(BLACK);
centerText("Select File", 10, 1.5);
int y = 40;
for (int i = 0; i < fileList.size(); i++) {
M5Cardputer.Display.setCursor(10, y);
M5Cardputer.Display.setTextColor(i == selectedFileIndex ? YELLOW : WHITE);
M5Cardputer.Display.print(fileList[i]);
y += 18;
}
delay(20);
}
}
void playMorse(const char* code) {
for (int i = 0; code[i] != '\0'; i++) {
int duration = (code[i] == '.') ? ditlen : ditlen * 3;
M5Cardputer.Speaker.tone(800);
delay(duration);
M5Cardputer.Speaker.end();
delay(ditlen);
}
}
void playChar(char c) {
c = toupper(c);
const char* morse = getMorseCodeForChar(c);
M5Cardputer.Display.fillScreen(BLACK);
M5Cardputer.Display.setTextColor(WHITE);
M5Cardputer.Display.setCursor(5, 0);
M5Cardputer.Display.setTextSize(1.0);
M5Cardputer.Display.printf("CW Player v2.4 WPM = %d", wpm);
M5Cardputer.Display.setTextSize(6.0);
M5Cardputer.Display.setTextColor(GREEN);
int x = (M5Cardputer.Display.width() - 24) / 2;
M5Cardputer.Display.setCursor(x-1, 40);
M5Cardputer.Display.print(c);
M5Cardputer.Display.setTextColor(WHITE);
playMorse(morse);
delay(ditlen * 3);
}
void playFile(String filename) {
File file = SD.open("/" + filename);
if (!file) return;
stopped = false;
paused = false;
while (file.available() && !stopped) {
M5Cardputer.update();
if (M5Cardputer.Keyboard.isChange()) {
auto keys = M5Cardputer.Keyboard.keysState();
for (char c : keys.word) {
if (c == ' ') { paused = !paused; delay(200); }
if (c == ';') { wpm = min(25, wpm + 1); ditlen = 1200.0 / wpm; }
if (c == '.') { wpm = max(5, wpm - 1); ditlen = 1200.0 / wpm; }
}
if (keys.enter) { stopped = true; break; }
}
if (paused) { delay(100); continue; }
char c = file.read();
if (c >= 32 && c <= 126) playChar(c);
}
file.close();
}
void setup() {
auto cfg = M5.config();
M5Cardputer.begin(cfg);
M5Cardputer.Display.setRotation(1);
M5Cardputer.Display.setBrightness(128);
M5Cardputer.Display.setFont(&fonts::Font0);
M5Cardputer.Display.setTextColor(WHITE);
M5Cardputer.Display.setTextSize(1.0);
M5Cardputer.Display.setTextWrap(false);
M5Cardputer.Speaker.setVolume(165);
if (!SD.begin()) {
M5Cardputer.Display.fillScreen(BLACK);
M5Cardputer.Display.setTextColor(RED);
centerText("SD Card Init Failed", 50, 1.5);
while (1);
}
showTitleScreen();
showWPMScreen();
listTextFiles();
showFileSelection();
if (fileList.size() > 0) playFile(fileList[selectedFileIndex]);
M5Cardputer.Display.fillScreen(BLACK);
centerText("Playback complete", 50, 1.5);
}
void loop() {}
// CW Paddle Quiz
// Version: 4.22
// Author: Matthew Sparks N6MMS
// Email: ProfessorSparks@gmail.com
// Platform: M5Cardputer
// License: GNU General Public License v3.0
#include <M5Cardputer.h>
#include <M5GFX.h>
const int dotPin = 2;
const int dashPin = 1;
const char* version = "4.22";
const int maxRounds = 25;
M5Canvas canvas(&M5Cardputer.Display);
char morsestring[10] = "";
char morseletter[8] = "";
char currentPrompt[4] = "";
bool includeLetters = true;
bool includeNumbers = true;
bool includePunctuation = false;
int wpm = 15;
int ditLen = 0;
unsigned long lastInputTime = 0;
unsigned long promptStartTime = 0;
unsigned long entryStartTime = 0;
bool dotState = false;
bool dashState = false;
bool startedSending = false;
int totalScore = 0;
int roundScore = 0;
int roundsPlayed = 0;
const struct { const char* ch; const char* code; } morseLookup[] = {
{"A",".-"}, {"B","-..."}, {"C","-.-."}, {"D","-.."}, {"E","."}, {"F","..-."}, {"G","--."},
{"H","...."}, {"I",".."}, {"J",".---"}, {"K","-.-"}, {"L",".-.."}, {"M","--"}, {"N","-."},
{"O","---"}, {"P",".--."}, {"Q","--.-"}, {"R",".-."}, {"S","..."}, {"T","-"}, {"U","..-"},
{"V","...-"}, {"W",".--"}, {"X","-..-"}, {"Y","-.--"}, {"Z","--.."},
{"1",".----"}, {"2","..---"}, {"3","...--"}, {"4","....-"}, {"5","....."},
{"6","-...."}, {"7","--..."}, {"8","---.."}, {"9","----."}, {"0","-----"},
{"!", "-.-.--"}, {"$", "...-..-"}, {"&", ".-..."}, {"'", ".----."}, {"(", "-.--."},
{")", "-.--.-"}, {"+", ".-.-."}, {",", "--..--"}, {"-", "-....-"}, {".", ".-.-.-"},
{"/", "-..-."}, {":", "---..."}, {";", "-.-.-."}, {"?", "..--.."}, {"@", ".--.-."},
{"_", "..--.-"}
};
const int morseCount = sizeof(morseLookup) / sizeof(morseLookup[0]);
void playTone(int freq, int dur) {
M5Cardputer.Speaker.tone(freq, dur);
delay(dur);
M5Cardputer.Speaker.end();
}
void centerText(const char* text, int y, float size = 2.0) {
canvas.setTextSize(size);
int charWidth = 6 * size;
int textWidth = strlen(text) * charWidth;
int x = (canvas.width() - textWidth) / 2;
canvas.setCursor(x > 0 ? x : 0, y);
canvas.println(text);
}
void titleScreen() {
canvas.fillSprite(BLACK);
centerText("CW Paddle Quiz", 20, 2.0);
centerText("Version 4.22", 60, 2);
centerText("by N6MMS", 85, 3);
canvas.pushSprite(0, 0);
delay(2500);
}
void instructionScreen() {
canvas.fillSprite(BLACK);
canvas.setTextSize(1.5);
centerText("Key Morse to match", 20);
centerText("the character shown.", 50);
centerText("25 rounds or 8 dots ends game.", 90, 1.25);
centerText("Press ENTER to begin.", 120, 1.25);
canvas.pushSprite(0, 0);
while (true) {
M5Cardputer.update();
if (M5Cardputer.Keyboard.isChange() && M5Cardputer.Keyboard.isPressed()) {
if (M5Cardputer.Keyboard.keysState().enter) break;
}
}
}
void promptInclusion() {
struct { const char* label; bool* flag; bool defaultYes; } prompts[] = {
{"Letters? (Y/N)", &includeLetters, true},
{"Numbers? (Y/N)", &includeNumbers, true},
{"Punctuation? (Y/N)", &includePunctuation, false},
};
for (auto& p : prompts) {
*(p.flag) = p.defaultYes;
bool decided = false;
while (!decided) {
canvas.fillSprite(BLACK);
centerText(p.label, 30, 2);
centerText("ENTER = ", 70, 2);
centerText(p.defaultYes ? "YES" : "NO", 95, 2);
canvas.pushSprite(0, 0);
M5Cardputer.update();
if (M5Cardputer.Keyboard.isChange() && M5Cardputer.Keyboard.isPressed()) {
auto keys = M5Cardputer.Keyboard.keysState();
if (keys.enter) { *(p.flag) = p.defaultYes; decided = true; }
else if (keys.word[0] == 'y' || keys.word[0] == 'Y') { *(p.flag) = true; decided = true; }
else if (keys.word[0] == 'n' || keys.word[0] == 'N') { *(p.flag) = false; decided = true; }
}
}
}
}
void promptWPM() {
canvas.fillSprite(BLACK);
centerText("WPM (5 - 20)", 30, 2.0);
centerText("ENTER = 15 WPM", 60, 2);
canvas.pushSprite(0, 0);
String input = "> ";
bool confirmed = false;
while (!confirmed) {
M5Cardputer.update();
if (M5Cardputer.Keyboard.isChange() && M5Cardputer.Keyboard.isPressed()) {
auto keys = M5Cardputer.Keyboard.keysState();
for (auto c : keys.word) {
if (isdigit(c)) input += c;
}
if (keys.del && input.length() > 2) input.remove(input.length() - 1);
if (keys.enter) {
int val = input.substring(2).toInt();
wpm = (val >= 5 && val <= 20) ? val : 15;
confirmed = true;
}
canvas.fillSprite(BLACK);
centerText("WPM (5 - 20)", 30, 2.0);
centerText("ENTER = 15 WPM", 60, 1.75);
centerText(input.c_str(), 100, 1.5);
canvas.pushSprite(0, 0);
}
}
ditLen = 1200 / wpm;
}
void decodeMorse() {
strcpy(morseletter, "?");
for (int i = 0; i < morseCount; i++) {
if (strcmp(morsestring, morseLookup[i].code) == 0) {
strcpy(morseletter, morseLookup[i].ch);
break;
}
}
if (strcmp(morseletter, "?") == 0) {
canvas.fillSprite(RED);
canvas.setTextColor(WHITE);
centerText("?", canvas.height() / 2 - 10, 3.0);
canvas.setTextColor(GREEN);
canvas.pushSprite(0, 0);
delay(ditLen * 5);
}
}
void calculateScore() {
bool correct = strcasecmp(morseletter, currentPrompt) == 0;
roundScore = correct ? 100 : -50;
totalScore += roundScore;
roundsPlayed++;
}
void showResultsScreen() {
canvas.fillSprite(BLACK);
canvas.setTextColor(GREEN);
char buffer[64];
snprintf(buffer, sizeof(buffer), "Expected: %s", currentPrompt);
centerText(buffer, 20, 1.5);
snprintf(buffer, sizeof(buffer), "You: %s", morseletter);
centerText(buffer, 50, 1.5);
snprintf(buffer, sizeof(buffer), "Round: %d / %d", roundsPlayed, maxRounds);
centerText(buffer, 80, 1.5);
snprintf(buffer, sizeof(buffer), "Score: %d", totalScore);
centerText(buffer, 110, 1.5);
canvas.pushSprite(0, 0);
delay(1000);
}
void gameOverScreen() {
canvas.fillSprite(BLACK);
centerText("Game Over!", 30, 2.0);
char buf[32];
snprintf(buf, sizeof(buf), "Final Score: %d", totalScore);
centerText(buf, 70, 1.75);
centerText("Press ENTER to restart", 110, 1.25);
canvas.pushSprite(0, 0);
while (true) {
M5Cardputer.update();
if (M5Cardputer.Keyboard.isChange() && M5Cardputer.Keyboard.isPressed()) {
if (M5Cardputer.Keyboard.keysState().enter) {
fullRestart();
break;
}
}
}
}
void newPrompt() {
int tries = 0;
while (tries++ < 50) {
const char* pick = morseLookup[random(morseCount)].ch;
bool isLetter = isalpha(pick[0]);
bool isDigit = isdigit(pick[0]);
bool isPunct = !isLetter && !isDigit;
if ((isLetter && includeLetters) || (isDigit && includeNumbers) || (isPunct && includePunctuation)) {
strcpy(currentPrompt, pick);
break;
}
}
canvas.fillSprite(BLACK);
canvas.setTextSize(4.5);
int charWidth = 6 * 6.0;
int x = (canvas.width() - strlen(currentPrompt) * charWidth) / 2;
canvas.setCursor(x > 0 ? x : 0, 20);
canvas.println(currentPrompt);
canvas.setTextSize(1.25);
if (strcmp(currentPrompt, "0") == 0)
centerText("(ZERO)", 100, 2);
else if (strcmp(currentPrompt, "O") == 0)
centerText("(LETTER O)", 100, 2);
canvas.pushSprite(0, 0);
promptStartTime = millis();
startedSending = false;
strcpy(morsestring, "");
strcpy(morseletter, "");
}
void fullRestart() {
totalScore = 0;
roundsPlayed = 0;
promptWPM();
promptInclusion();
newPrompt();
}
void setup() {
auto cfg = M5.config();
M5Cardputer.begin(cfg, true);
M5Cardputer.Display.setRotation(1);
canvas.createSprite(M5Cardputer.Display.width(), M5Cardputer.Display.height());
canvas.setTextColor(GREEN);
pinMode(dotPin, INPUT_PULLUP);
pinMode(dashPin, INPUT_PULLUP);
titleScreen();
instructionScreen();
promptWPM();
promptInclusion();
randomSeed(millis());
newPrompt();
}
void loop() {
M5Cardputer.update();
dotState = digitalRead(dotPin) == LOW;
dashState = digitalRead(dashPin) == LOW;
if ((dotState || dashState) && !startedSending) {
entryStartTime = millis();
startedSending = true;
}
if (dotState) {
strcat(morsestring, ".");
playTone(1000, ditLen);
delay(ditLen);
lastInputTime = millis();
} else if (dashState) {
strcat(morsestring, "-");
playTone(1000, ditLen * 3);
lastInputTime = millis();
}
if (strlen(morsestring) >= 8 && strcmp(morsestring, "........") == 0) {
gameOverScreen();
return;
}
if (strlen(morsestring) > 0 && millis() - lastInputTime > ditLen * 5) {
decodeMorse();
calculateScore();
showResultsScreen();
if (roundsPlayed >= maxRounds) {
gameOverScreen();
return;
}
newPrompt();
}
}
Comments