/* * My first Arduino program : LM Rev.1 - 5-letter groups * Rev.2 - Speed control * Rev.3 - Mother of All Keyers - MOAK * 1.0.3.1 Pseudo-text generator * keyModes (Electronic Keyer) * (Straight key) * LCD Display * Run-time option selection * Add pseudo-call sign mode * Sending practice mode * 1.0.3.2 Add speed adjustment mode ('Select' button) * Splash screen gets version ID from VERSION * 1.0.3.3 Add bug mode * Simplify 'option select' processing * * Lloyd Milligan (WA4EFS) April 2018 */ #include #include const boolean DEBUG = false; boolean TEST = false; // Canned sentence (all letters and period) boolean incNum = false; // Include numbers in 5-character groups // or in generated pseudo-text const int XMITPIN = 13; // Output - Key CPO or external device d13 int timeConstant = 850; // Empirically determined value int sndSpeed = 9; // Will be revalued as read from Arduino int minSpeed = 5; // Must be > 0 (Prevent ÷ 0 error and extend max speed) int dotTime; // Valued in setSpeed() int dashTime; // dotTime + dotTime + dotTime; int charTime; // dashTime; int wordTime; // dashTime + dashTime + dotTime; // Audio oscillator shield has speed control trimmer potentiometer on A0 // Assign different analog pin to panel mounted speed control potentiometer const int speedControl = 1; // Speed control potentiometer A1 int lastSpeed = sndSpeed; // For detecting change in speed setting // Test String testString = "The quick brown fox jumped over the lazy dog's back."; // Rev.3 MOAK constants and variables const String VERSION = "1.0.3.3"; const int MAXWORDLENGTH = 15; // wlen[] dimension const int GENWORDLENGTH = 9; // This value must not exceed MAXWORDLENGTH. const int NUMALPHA = 26; // Number of alphabetic characters (letters) const int NUMVOWELS = 5; // Number of vowels const int NUMCONSTS = NUMALPHA - NUMVOWELS; const int SDELAY = 500; // Delay between sentences in pseudo-text mode boolean ptMode = true; // Pseudo-text mode boolean keyMode = true; // Overrides other modes if true boolean bugMode = false; // Emulates bug (automatic dots, manual dashes) boolean pcsMode = false; // Pseudo-callsign mode boolean practiceMode = false; // Practice sending by keying-in displayed text boolean incSound = false; // Sound out displayed text in practice mode // Accept and process input from key or paddle boolean straight = false; // Straight key mode // Arrays supporting pseudo-text generation /* // Word lenght weights based on lexical data - long wLen [MAXWORDLENGTH] = {52L, 1751L, 13542L, 38567L, 80482L, 138788L, 205095L, 268755L, 322641L, 363623L, 391129L, 408435L, 418761L, 424608L, 427895L}; */ // Made-up (arbitrary) word length weights long wLen [MAXWORDLENGTH] = {5, 15, 35, 55, 75, 90, 105, 115, 125, 130, 135, 139, 142, 144, 145}; // Based on lexical data - first letter, letter, vowel, and consonant weights long flWeight[NUMALPHA] = {60593L, 126028L, 213114L, 265556L, 301422L, 339834L, 379183L, 421634L, 451726L, 463127L, 487414L, 525509L, 593554L, 618349L, 640923L, 712575L, 717273L, 765491L, 869532L, 920780L, 942190L, 959583L, 988103L, 989754L, 994943L, 1000000L}; long lWeight[NUMALPHA] = {2865L, 3394L, 4586L, 5946L, 10398L, 11254L, 11920L, 13721L, 16418L, 16475L, 16668L, 18118L, 19013L, 21591L, 24314L, 25075L, 25118L, 27356L, 29677L, 32982L, 33955L, 34330L, 34927L, 35011L, 35604L, 35636L}; long vWeight[NUMVOWELS] = {2865L, 7317L, 10014L, 12737L, 13710L}; long cWeight[NUMCONSTS] = {529L, 1721L, 3081L, 3937L, 4603L, 6404L, 6461L, 6654L, 8104L, 8999L, 11577L, 12338L, 12381L, 14619L, 16940L, 20245L, 20620L, 21217L, 21301L, 21894L, 21926L}; char alpha[NUMALPHA] = {'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'}; char vowel[NUMVOWELS] = {'A', 'E', 'I', 'O', 'U'}; char consonant[NUMCONSTS] = {'B', 'C', 'D', 'F','G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z'}; const int CPCT = 80; // Percent of pseudo-words followed by comma const int DPCT = 20; // Percent of pseudo-words followed by dash const int QPCT = 10; // Percent of pseudo-questions [sentences ending in '?'] const int NPCT = 10; // Percent of pseudo-words that are numeric const int MAXNLEN = 5; // Maximum length of numeric pseudo-word (minimum is 1) // Maximum pseudo-sentence length (number of words - minimum sentence length) const int MAXSLEN = 12; const int MINSLEN = 5; // Same for punctuation delimited pseudo-phrases const int MAXPLEN = 4; const int MINPLEN = 2; // Miscellaneous punctuation (convenience aliases) const char PERIOD = '.'; const char QUESTIONMARK = '?'; const char COMMA = ','; const char DASH = '-'; const char SPACE = ' '; // Keyer related - const int DOTPIN = 6; // Dot side of key paddle d6 const int DASHPIN = 7; // Dash side of key =or= straight key d7 // [Serial] LCD support [Analog pins 4 (SDA) and 5 (SCL)] const boolean lcdPresent = true; // Indicates whether hardware includes LCD const int ROWS = 4; // If 16x2 change ROWS to 2 const int COLS = 20; // If 16x2 change COLS to 16 LiquidCrystal_I2C lcd(0x27, COLS, ROWS); // Instantiate // Other LCD-related const int DIM = ROWS * COLS; // One-dimensional equivalent (convenience) const int SPLASHDURATION = 5000; // milliseconds boolean displayCode = false; // Display generated Morse code on LCD long currentCell = 0; // Top left to bottom right display cell // General - // Splash screen gets version ID from the VERSION constant (top of sketch) const String splash21 = " MOAK " + VERSION + " "; const String splash22 = " Welcome "; const String splash41 = " "; // Welcome screen for 4 row display const String splash42 = "Mother Of All Keyers"; const String splash43 = " Version " + VERSION + " "; const String splash44 = " "; // One-dimensional copy of LCD display, upper left to lower right char lcdCells[DIM] = ""; // Scratch array for ticker // Option selection const int OPTBTN = 4; // Option button d4 const int SELBTN = 5; // Select button d5 const int NUMOPT = 10; // Number of options const String optList[NUMOPT] = {"Electronic Key", "Straight Key", "5 Alpha Groups", "5 Alpha-num Grps", "Rand Pseudo-text", "AlpNumPseudo-txt", "Pseudo callsigns", "Sending Practice", "Listen and send", "Emulate Bug" }; int currentOption = 0; // Select button steps through options boolean commandMode = false; // true if and only if Option button pressed // and select button not yet pressed. // Sending practice options String keyinThis = ""; // For display and comparison // Miscellaneous const unsigned long ONESEC = 1000L; const unsigned long TWOSEC = 2000L; void setup() { pinMode(XMITPIN, OUTPUT); pinMode(LED_BUILTIN, OUTPUT); // Visual indicator pinMode(DOTPIN, INPUT); pinMode(DASHPIN, INPUT); digitalWrite(XMITPIN, LOW); // key up digitalWrite(LED_BUILTIN, LOW); // LED off if (lcdPresent) lcd.init(); if (DEBUG) { Serial.begin(9600); Serial.print("Mother of all keyers - Version "); Serial.println(VERSION); Serial.println(); Serial.println("Test mode:"); Serial.println(); debug(); } if (lcdPresent) { lcd.backlight(); displaySplash(); delay(SPLASHDURATION); tickerClear(); } delay(ONESEC); // Dark before displaying current option // Afterthoughts ... if (lcdPresent) { displayCurrentOption(); delay(TWOSEC); myClear(); if (displayCode) { lcd.backlight(); // Turn back on for code display } else lcd.noBacklight(); } else delay(ONESEC); // Random number generator gets re-seeded later, based on button press randomSeed(analogRead(7)); // Initialize random number generator // (floating analog pin) if (keyMode) { setSpeed(); // For startup in keyer mode! // Otherwise keyer defaults to initial sndSpeed } } void loop() { senseButtonPress(); // Test for command button press // Key input has highest priority if (keyMode) { if (straight) { if (digitalRead(DASHPIN) == LOW) { // Key DOWN digitalWrite(LED_BUILTIN, HIGH); // LED on digitalWrite(XMITPIN, HIGH); // transmit on (if different d-pin than LED) } else { // Key up digitalWrite(LED_BUILTIN, LOW); // LED off digitalWrite(XMITPIN, LOW); // transmit off } return; } if (bugMode) { if (digitalRead(DOTPIN) == LOW) { // Automatic dots dot(); } else if (digitalRead(DASHPIN) == LOW) { digitalWrite(LED_BUILTIN, HIGH); // LED on digitalWrite(XMITPIN, HIGH); // transmit on (if different d-pin than LED) } else { // Key up digitalWrite(LED_BUILTIN, LOW); // LED off digitalWrite(XMITPIN, LOW); // transmit off } return; } if (digitalRead(DOTPIN) == LOW) { // Else electronic keyer dot(); } else if (digitalRead(DASHPIN) == LOW) { dash(); } return; // Key mode } // Fall through here if NOT keymode if (not lcdPresent) // Adjust speed without requiring button press setSpeed(); // if (TEST) { outputString(testString); delay(4000); } else if (ptMode) { // Pseudo-text modes if (incNum) outputString(rndpsn()); else outputString(rndps()); delay(SDELAY); tickerAddCharacter(SPACE); // Else no space between sentences } else if (pcsMode) { // Pseudo callsigns outputString(rndpcs() + SPACE); delay(SDELAY); } else if (practiceMode) { // Sending practice modes practiceSending(); } else outputString(fiveCharacterGroup() + SPACE); // Default (original implementation) } String fiveCharacterGroup() { byte b; char c; String s = ""; for (int i=0; i<5; i++) { if (commandMode) break; if (incNum) { b = random(36); // Can be changed to < 36 for partial numbers if (b < 26) b = 65 + b; else b = 22 + b; } else b = 65 + random(26); c = (char) b; s.concat(c); } return s; } void dot() { digitalWrite(LED_BUILTIN, HIGH); // LED on digitalWrite(XMITPIN, HIGH); // key down (if different d-pin from LED) delay(dotTime); digitalWrite(LED_BUILTIN, LOW); // LED off digitalWrite(XMITPIN, LOW); // key up delay(dotTime); } void dash() { digitalWrite(LED_BUILTIN, HIGH); // LED on digitalWrite(XMITPIN, HIGH); // key down delay(dashTime); digitalWrite(LED_BUILTIN, LOW); // LED off digitalWrite(XMITPIN, LOW); // key up delay(dotTime); } void outputString(String str) { char chr; int i; str.toLowerCase(); for (i = 0; i < str.length(); i ++) { if (commandMode) break; chr = str.charAt(i); if (chr == SPACE) { delay(wordTime); tickerAddCharacter(SPACE); } else { outputMorseChar(chr); tickerAddCharacter(chr); senseButtonPress(); } } } void outputMorseChar(char chr) { switch (chr) { case 'a' : dot(); dash(); break; case 'b' : dash(); dot(); dot(); dot(); break; case 'c' : dash(); dot(); dash(); dot(); break; case 'd' : dash(); dot(); dot(); break; case 'e' : dot(); break; case 'f' : dot(); dot(); dash(); dot(); break; case 'g' : dash(); dash(); dot(); break; case 'h' : dot(); dot(); dot(); dot(); break; case 'i' : dot(); dot(); break; case 'j' : dot(); dash(); dash(); dash(); break; case 'k' : dash(); dot(); dash(); break; case 'l' : dot(); dash(); dot(); dot();; break; case 'm' : dash(); dash(); break; case 'n' : dash(); dot(); break; case 'o' : dash(); dash(); dash(); break; case 'p' : dot(); dash(); dash(); dot(); break; case 'q' : dash(); dash(); dot(); dash(); break; case 'r' : dot(); dash(); dot(); break; case 's' : dot(); dot(); dot(); break; case 't' : dash(); break; case 'u' : dot(); dot(); dash(); break; case 'v' : dot(); dot(); dot(); dash(); break; case 'w' : dot(); dash(); dash(); break; case 'x' : dash(); dot(); dot(); dash(); break; case 'y' : dash(); dot(); dash(); dash(); break; case 'z' : dash(); dash(); dot(); dot(); break; case '0' : dash(); dash(); dash(); dash(); dash(); break; case '1' : dot(); dash(); dash(); dash(); dash(); break; case '2' : dot(); dot(); dash(); dash(); dash(); break; case '3' : dot(); dot(); dot(); dash(); dash(); break; case '4' : dot(); dot(); dot(); dot(); dash(); break; case '5' : dot(); dot(); dot(); dot(); dot(); break; case '6' : dash(); dot(); dot(); dot(); dot(); break; case '7' : dash(); dash(); dot(); dot(); dot(); break; case '8' : dash(); dash(); dash(); dot(); dot(); break; case '9' : dash(); dash(); dash(); dash(); dot(); break; case ',' : dash(); dash(); dot(); dot(); dash(); dash(); break; case '.' : dot(); dash(); dot(); dash(); dot(); dash(); break; case '?' : dot(); dot(); dash(); dash(); dot(); dot(); break; case '/' : dash(); dot(); dot(); dash(); dot(); break; default : dash(); dot(); dot(); dot(); dash(); } delay(charTime); } char decodeMorseChar(int hcode) { // Inverse of outputMorseChar() switch (hcode) { case 12 : return 'a'; case 2111 : return 'b'; case 2121 : return 'c'; case 211 : return 'd'; case 1 : return 'e'; case 1121 : return 'f'; case 221 : return 'g'; case 1111 : return 'h'; case 11 : return 'i'; case 1222 : return 'j'; case 212 : return 'k'; case 1211 : return 'l'; case 22 : return 'm'; case 21 : return 'n'; case 222 : return 'o'; case 1221 : return 'p'; case 2212 : return 'q'; case 121 : return 'r'; case 111 : return 's'; case 2 : return 't'; case 112 : return 'u'; case 1112 : return 'v'; case 122 : return 'w'; case 2112 : return 'x'; case 2122 : return 'y'; case 2211 : return 'z'; case 22222 : return '0'; case 12222 : return '1'; case 11222 : return '2'; case 11122 : return '3'; case 11112 : return '4'; case 11111 : return '5'; case 21111 : return '6'; case 22111 : return '7'; case 22211 : return '8'; case 22221 : return '9'; case 21112 : return '-'; case 21121 : return '/'; case 41 : return ','; case 42 : return '.'; case 43 : return '?'; default : return '*'; } } int pHash(String mc) { // Punctuation hash // Supplement to decodeMorseChar() if (mc == "221122") return 41; if (mc == "121212") return 42; if (mc == "112211") return 43; return mc.toInt(); } void setSpeed() { // Based on input from speed potentiometer sndSpeed = analogRead(speedControl) / 32 + minSpeed; // 0-1023 to w.p.m. dotTime = timeConstant / sndSpeed; dashTime = dotTime + dotTime + dotTime; charTime = dashTime; wordTime = dashTime + dashTime + dotTime; if (sndSpeed != lastSpeed) { lastSpeed = sndSpeed; if (lcdPresent) { displayLCD("Setting speed to", (String) sndSpeed + " wpm ..."); delay(TWOSEC); if (not displayCode) lcd.noBacklight(); myClear(); } } } /* * Rev.3 MOAK Utilities */ boolean isVowel(char c) { // true if and only if c is a vowel for (int i=0; i minEOC) { mcph = pHash(mc); mc = ""; c = decodeMorseChar(mcph); if (c == keyinThis.charAt(characterPosition)) { lcd.print(c); if (characterPosition == patternLength - 1) success = true; } else { lcd.print(c); // Display sensed character (or default '*' if not understood) delay(ONESEC); break; } characterPosition++; senseButtonPress(); now = millis(); } senseButtonPress(); if (commandMode) break; } // while character position < length senseButtonPress(); if (commandMode) break; } // while not success (not complete) senseButtonPress(); if (commandMode) return; delay(ONESEC); } // Next added in version 1.0.3.2 - void processSpeedAdjustment() { if (not lcdPresent) return; // Meaningless without LCD while (digitalRead(SELBTN) == HIGH) ; // Wait for button press to complete lcd.backlight(); // Always on in setSpeedMode displayLCD("Speed adjustment"," mode ..."); // This is a tight loop - Do not sense button externally // commandMode disabled until Select button pressed to exit mode while (true) { if (digitalRead(SELBTN) == HIGH) { // End setSpeedMode while (digitalRead(SELBTN) == HIGH) ; // Wait for button press to complete break; // Exit while (true) loop } // End if (digitalRead(SELBTN) == HIGH) delay (100); // Adjust for smooth display specialSetSpeed(); } // end while (true) - tight loop lcd.clear(); if (displayCode) lcd.backlight(); // Conditionally on or off when exiting set speed mode else lcd.noBacklight(); // Might be redundant (check setSpeed()) } void specialSetSpeed() { // Goes with processSpeedAdjustment() // No delay, no conditional setting of LCD display sndSpeed = analogRead(speedControl) / 32 + minSpeed; // 0-1023 to w.p.m. dotTime = timeConstant / sndSpeed; dashTime = dotTime + dotTime + dotTime; charTime = dashTime; wordTime = dashTime + dashTime + dotTime; if (sndSpeed != lastSpeed) { lastSpeed = sndSpeed; lcd.clear(); displayLCD("Setting speed to", (String) sndSpeed + " wpm ..."); } } // Development in progress below this line void debug() { randomSeed(analogRead(7)); // Initialize random number generator if (lcdPresent) { lcd.backlight(); displayLCD("Test mode...",""); } while (true) { ; // Test function here } return; }