Very late update: We finished our project! We got full marks on it. We received quite a bit of help from a fellow student who is a better coder. We were also told that this would be easier with a Hash table assigning the morse code to the characters, but it would be memory intensive. Here is the code in its entirety:
#include <LiquidCrystal.h>
#define NOTE_A4 440
#define NOTE_G5 784
// initialize the library by associating any needed LCD interface pin
// with the arduino pin number it is connected to
const int rs = 11, en = 12, d4 = 2, d5 = 3, d6 = 4, d7 = 5; // LCD pin assignments
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);
int operation_mode = 1; // sets operation mode for system, 0 for pushbuttons, 1 for telegraph key
bool demo = true; // enables demo mode for telegraph key
String demoMessage = "GO GAMECOCKS!"; // demo message that user will be taught how to write in morse
// Pin assignments
int ditPin = 6; // Dit button connected to digital pin 6
int dahPin = 7; // Dah button connected to digital pin 7
int endCharPin = 8; // End character button connected to digital pin 8
int speakerPin = 9; // Speaker output connected to digital pin 9
int keyerPin = 10; // Telegraph keyer connected to digital pin 10
int resetPin = 13; // Rest button clears the lcd, connected to digital pin 13
// dit and dah length initialization - change this to change dit and dah length for all modes of operation (200 ms works well)
int ditLength = 200; // amount of time in ms for one dit
int dahLength = 3 * ditLength; // dah length is 3 times dit length
int clicks = 0; // to count if a double click occurs on endChar button
// Variables for debouncing logic
unsigned long lastDebounceTime = 0;
int debounceDelay = 50; // amount of time necessary between inputs for system to recognize two inputs (in ms)
int lastKeyerState = HIGH; // keyer initialized as HIGH due to pullup resistor
int reading;
int keyerState;
unsigned long timePressed; // how long telegraph key is pressed down for
// Variables for release of keyer (dead time)
unsigned long releaseTime; // calculated when telegraph key is disconnected
unsigned long deadTime; // how long telegraph key is disconnecteed (dead time)
// extra variables for statistics and debugging
int retries; // keeps track of how many times the user has to retry a morse input in the demo mode
String unbrokenMorse; // can be printed to show the entire morse string at once
// string to hold mcurrent morse message and decoded message for all modes of operation
String morseCode = ""; // String to store Morse code input
String decodedMessage = ""; // String to store decoded message
// arrays of all english characters and their matching morse inputs
char engChars[] = { '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', '.', ',', '!', '?', '/', '=' };
String morseInputs[] = { ".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---", "-.-", ".-..", "--", "-.", "---", ".--.", "--.-", ".-.", "...", "-", "..-", "...-", ".--", "-..-", "-.--", "--..", ".-.-.-", "--..--", "-.-.--", "..--..", "-..-.", "-...-" };
void setup() {
Serial.begin(9600);
// testCorrectConversion(); // uncomment this to check if morse inputs are correctly matched with their english translation
pinMode(speakerPin, OUTPUT); // initialize output for speaker
pinMode(ditPin, INPUT_PULLUP); // Dit pin will read HIGH unless button is pressed
pinMode(dahPin, INPUT_PULLUP); // Dah pin will read HIGH unless button is pressed
pinMode(endCharPin, INPUT_PULLUP); // endChar pin will read HIGH unless button is pressed
pinMode(keyerPin, INPUT_PULLUP); // keyer pin will read HIGH unless button is pressed
pinMode(resetPin, INPUT_PULLUP); // reset pin will read HIGH unless button is pressed
lcd.begin(16, 2); // initalize size of LCD
lcd.setCursor(0, 1); // set cursor to first column of second row of LCD
}
// compares morseCode input to morseInputs array to find matching english charactes
void decodeMorse() {
for (int i = 0; i < sizeof(morseInputs) / sizeof(morseInputs[0]); i++) {
if (morseCode.equals(morseInputs[i])) {
decodedMessage += engChars[i];
// Serial.print(decodedMessage);
break;
}
}
}
// Slightly altered decoding function for the demo operation
// Decodes the morse input, then checks if it matches the next letter of the demo message
// If it matches, that char is added to the decoded message, if it doesn't match, the character is not added and the user gets to try again
void decodeMorse(int index) {
// compare morseCode input to morseInputs array to find matching english character
for (int i = 0; i < sizeof(morseInputs) / sizeof(morseInputs[0]); i++) {
if (morseCode.equals(morseInputs[i])) {
if (demoMessage[index] == engChars[i]) {
decodedMessage += engChars[i];
// Serial.print(decodedMessage);
break;
} else {
errorTone();
retries += 1;
lcd.setCursor(0, 1);
lcd.print(" ");
lcd.setCursor(0, 1);
lcd.print(English_to_Morse(demoMessage, index));
break;
}
}
}
}
// Used when operation mode is 0
void buttonHandler() {
// Dit button handler
if (digitalRead(ditPin) == LOW) {
clicks = 0; // reset click counter for endChar pin
tone(speakerPin, NOTE_G5, ditLength); // play tone for length of dit
morseCode += "."; // add "." to morse code String
lcd.setCursor(morseCode.length() - 1, 1); // move cursor to end of morse code message
lcd.print("."); // print the dit to the LCD
delay(200);
}
// Dah button handler
if (digitalRead(dahPin) == LOW) {
clicks = 0; // reset click counter for endChar pin
tone(speakerPin, NOTE_G5, dahLength); // play tone for length of dah
morseCode += "-"; // add "-" to morse code string
lcd.setCursor(morseCode.length() - 1, 1); // move cursor to end of morse code message
lcd.print("-"); // print the dah to the LCD
delay(200);
}
// endChar button handler
if (digitalRead(endCharPin) == LOW) {
if (clicks == 0) {
decodeMorse(); // decode morse input to english character
unbrokenMorse += morseCode;
unbrokenMorse += " ";
morseCode = ""; // reset morse input string
lcd.setCursor(0, 0); // move lcd cursor to beginning of first line
lcd.print(decodedMessage); // print new decoded message to lcd
lcd.setCursor(0, 1); // move cursor to beginning of bottom row
lcd.print(" "); // clear the bottom row of morse (16 spaces)
lcd.setCursor(0, 1); // move cursor to beginning of bottom row
delay(200);
clicks += 1; // increment counter for number of time endChar button has been pressed
Serial.println(unbrokenMorse);
// playMorse(unbrokenMorse);
}
// if clicks is 1, second push of endChar button registers as a double push and adds a space to the message
else {
lcd.setCursor(decodedMessage.length() - 1, 0); // move cursor to end of decoded message
decodedMessage += " "; // add a space to decoded message
lcd.setCursor(0, 0);
lcd.print(decodedMessage); // print message
delay(250);
clicks = 0; // reset clicks counter
}
}
}
// Used when operation mode is 1 and demo = false
void telegraphKeyHandler() {
reading = digitalRead(keyerPin);
if (reading != lastKeyerState) {
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > debounceDelay) {
if (reading != keyerState) {
keyerState = reading;
if (digitalRead(keyerPin) == LOW) {
tone(speakerPin, NOTE_G5); // play tone
unsigned long start = millis();
while (digitalRead(keyerPin) == LOW) {
timePressed = millis() - start;
}
if (timePressed < ditLength) { // check if telegraph key was pressed for less than a dit length
morseCode += "."; // add a dit to the morse code string
lcd.setCursor(morseCode.length() - 1, 1);
lcd.print("."); // print the dit to the lcd
} else { // if telegraph key is pressed longer than a dit, it is registered as a dah
morseCode += "-"; // add a dah to the morse code string
lcd.setCursor(morseCode.length() - 1, 1);
lcd.print("-"); // print the dah to the lcd
}
delay(ditLength); // a space of a dit is between each individual morse input
noTone(speakerPin); // turn off tone
} else {
releaseTime = millis();
while (digitalRead(keyerPin) == HIGH) {
deadTime = millis() - releaseTime;
// if deadTime exceeds the length of 3 dits (1 dah), the character is considered finished and is then translated
if (deadTime > dahLength) {
decodeMorse(); // decode morse input to english character
morseCode = ""; // reset morse input string
lcd.setCursor(0, 0); // move lcd cursor to beginning of first line
lcd.print(decodedMessage); // print new decoded message to lcd
lcd.setCursor(0, 1); // move cursor to beginning of bottom row
lcd.print(" "); // clear the bottom row of morse (16 spaces)
lcd.setCursor(0, 1); // move cursor to beginning of bottom row
delay(200);
// if deadTime exceeds the length of 7 dits, it's translated to a space
if (decodedMessage.length() > 0 && deadTime > ditLength * 7) {
lcd.setCursor(decodedMessage.length() - 1, 0);
decodedMessage += " ";
lcd.setCursor(0, 0);
lcd.print(decodedMessage);
break; // breaks out of while loop so only one space is inserted into message no matter how long the deadTime is (facilitates learning morse)
}
}
}
}
}
}
lastKeyerState = reading; // update the last keyer state
}
// Very similar to telegraphKeyHandler, used when operation mode is 0 and demo = true
void demoTelegraphKeyHandler(int index) {
reading = digitalRead(keyerPin);
if (reading != lastKeyerState) {
lastDebounceTime = millis();
}
if ((millis() - lastDebounceTime) > debounceDelay) {
if (reading != keyerState) {
keyerState = reading;
if (keyerState == LOW) {
tone(speakerPin, NOTE_G5); // play tone
unsigned long start = millis();
while (digitalRead(keyerPin) == LOW) {
timePressed = millis() - start;
}
if (timePressed < ditLength) { // check if telegraph key was pressed for less than a dit length
morseCode += "."; // add a dit to the morse code string
lcd.setCursor(morseCode.length() - 1, 1);
lcd.print("."); // print the dit to the lcd
} else { // if telegraph key is pressed longer than a dit, it is registered as a dah
morseCode += "-"; // add a dah to the morse code string
lcd.setCursor(morseCode.length() - 1, 1);
lcd.print("-"); // print the dah to the lcd
}
delay(ditLength); // a space of a dit is between each individual morse input
noTone(speakerPin); // turn off tone
} else {
releaseTime = millis();
while (digitalRead(keyerPin) == HIGH) {
deadTime = millis() - releaseTime;
// if deadTime exceeds the length of 3 dits (1 dah), the character is considered finished and is then translated
if (deadTime > dahLength) {
decodeMorse(index); // decode morse input to english character
morseCode = ""; // reset morse input string
lcd.setCursor(0, 0); // move lcd cursor to beginning of first line
lcd.print(decodedMessage); // print new decoded message to lcd
delay(200);
// no logic for spaces here, doesn't work with how long the demo takes, instead, spaces are handled by tranNewUser()
break;
}
}
}
}
}
lastKeyerState = reading; // update the last keyer state
}
// called when operation mode = 1 and demo = true
// Trains user on a dit, dah, and the demo message with the telegraph key until they get it all right
void trainNewUser() {
while (demo == true) {
if (operation_mode == 1) {
lcd.setCursor(0, 0);
lcd.print("Let's Practice!");
delay(2000);
lcd.clear();
lcd.print("Here's a dit");
lcd.setCursor(0, 1);
lcd.print(".");
tone(speakerPin, NOTE_G5, ditLength);
delay(2000);
lcd.clear();
lcd.print("Try a dit:");
while (morseCode != ".") { // train the user on a dit
telegraphKeyHandler();
if (morseCode == "-") {
morseCode = "";
errorTone();
retries += 1;
lcd.setCursor(0, 0);
lcd.print("Try again...");
delay(2000);
lcd.clear();
lcd.print("Here's a dit");
lcd.setCursor(0, 1);
lcd.print(".");
tone(speakerPin, NOTE_G5, ditLength);
delay(2000);
lcd.clear();
lcd.print("Try a dit:");
}
}
morseCode = "";
lcd.setCursor(0, 0);
lcd.clear();
lcd.print("Nice!");
delay(2000);
lcd.clear();
lcd.print("Now try a dah");
lcd.setCursor(0, 1);
lcd.print("-");
tone(speakerPin, NOTE_G5, dahLength);
delay(2000);
lcd.clear();
lcd.print("Try a dah:");
while (morseCode != "-") { // train the user on a dah
telegraphKeyHandler();
if (morseCode == ".") {
morseCode = "";
errorTone();
retries += 1;
lcd.setCursor(0, 0);
lcd.print("Try again...");
delay(2000);
lcd.clear();
lcd.print("Here's a dah");
lcd.setCursor(0, 1);
lcd.print("-");
tone(speakerPin, NOTE_G5, dahLength);
delay(2000);
lcd.clear();
lcd.print("Try a dah:");
}
}
morseCode = "";
lcd.setCursor(0, 0);
lcd.clear();
lcd.print("Nice!");
delay(2000);
lcd.clear();
lcd.print(demoMessage);
lcd.setCursor(0, 1);
delay(2000);
lcd.clear();
while (decodedMessage != demoMessage) { // train the user with the demo message
for (int i = 0; i < demoMessage.length(); i++) {
lcd.setCursor(i, 0);
lcd.print(demoMessage.charAt(i));
delay(500);
lcd.setCursor(i, 0);
lcd.print(" ");
delay(500);
lcd.setCursor(i, 0);
lcd.print(demoMessage.charAt(i));
delay(500);
lcd.setCursor(0, 1);
lcd.print(English_to_Morse(demoMessage, i));
while (decodedMessage.charAt(i) != demoMessage.charAt(i)) {
demoTelegraphKeyHandler(i);
if (demoMessage.charAt(i) == ' ') {
decodedMessage += ' ';
delay(200);
break;
}
}
lcd.setCursor(0, 1);
lcd.print(" ");
// Serial.println(retries);
}
}
delay(1000);
lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Nice!");
lcd.setCursor(0, 1);
lcd.print("Only ");
lcd.print(retries); // print the number of retries for each user
lcd.print(" retries!");
delay(4000);
lcd.clear();
decodedMessage = "";
retries = 0; // reset retries counter
}
}
}
// Takes in a specific char index of a string and translates it and returns the matching morse string
String English_to_Morse(String inputString, int index) {
String morse_translation;
// compare morseCode input to morseInputs array to find matching english character
for (int i = 0; i < sizeof(engChars) / sizeof(engChars[0]); i++) {
if (inputString.charAt(index) == engChars[i]) {
morse_translation += morseInputs[i];
break;
}
}
return morse_translation;
}
// Plays the tone for dits and dahs in a string of morse, spaces must be included in the string
void playMorse(String Morse) {
for (int i = 0; i < Morse.length(); i++) {
if (Morse.charAt(i) == '.') {
tone(speakerPin, NOTE_G5, ditLength);
delay(ditLength);
}
if (Morse.charAt(i) == '-') {
tone(speakerPin, NOTE_G5, dahLength);
delay(dahLength);
}
if (Morse.charAt(i) == ' ') {
delay(dahLength);
}
delay(ditLength);
}
}
// plays a sound that sounds like you made an error
void errorTone() {
tone(speakerPin, A4, 500);
delay(300);
tone(speakerPin, A4, 500);
}
//Run this when you make an edit to engChars[] or morseInputs[] and you want to ensure that they are accurate
//This should print the character and its Morse equivalent
void testCorrectConversion() {
//This section tests length of both arrays
int engCharsLength = sizeof(engChars) / sizeof(engChars[0]);
int morseInputsLength = sizeof(morseInputs) / sizeof(morseInputs[0]);
for (int i = 0; i < engCharsLength; i++) {
Serial.print(engChars[i]);
Serial.print(" -> ");
Serial.println(morseInputs[i]);
}
delay(1000);
}
// Main loop
void loop() {
// reset button doesn't work with demo mode, only operation mode 0 and operation mode 1 with no demo
if (digitalRead(resetPin) == LOW) {
lcd.clear();
morseCode = "";
decodedMessage = "";
}
// Operation mode 0 uses three buttons as user inputs, the ditPin, the dahPin, and the endCharPin
if (operation_mode == 0) {
buttonHandler();
}
// Operation mode 1 uses the telegraph key as the input, where the length of each press matters and no input (dead time) equals a space
if (operation_mode == 1) {
if (demo == true) {
trainNewUser();
} else {
telegraphKeyHandler();
}
}
}
Thank you all for the help!