/*////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// * * K4ICY CW Decoder * * For use with logic-signal input from key or tone decoder * * by (c) Michael A. Maynard, a.k.a. "K4ICY" Visit: http://www.k4icy.com/ * * Code is Open Source and free to alter and distribute. If copying or using derivitive * of this code, please give proper credit to the original author/programmer. * * Version 1.14r 06/23/20 Questions: mikek4icy@gmail.com * *////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /* * The Morse Code: * * According to the generally agreed upon timing convention, the word 'PARIS', when sent in Morse code (also known as * "CW,") with proper timing equates to 50 units of time, when the total of the duration of all code elements is divided * into 1 minute, this constitutes the Word Per Minute speed. * _ ___ ___ _ _ ___ _ ___ _ _ _ _ _ _ * |_ ___ ___ _ _ ___ _ ___ _ _ _ _ _ _ | * * The millisecond duration of an element unit can be calculated as 60,000 / (WPM * 50) or 1,200 / WPM. * Dots, or 'Dits', which are the shorter pulse bursts, are considered 1 unit while Dashes or 'Dahs', the longer of * the pulse bursts, are considered to be 3 units. These elements are called 'marks'. The 'space' or signal-off time * between each intra-character element are also 1 unit in length. 3 units of space are to follow each character and * seven are to follow each word. * * The actual timing performed by hand without electronic assistance will vary as much as the skill and personality * of the operator themselves. At the least discernible, a 'weighting' ratio, or the relationship between dots and dashes * of 3:2 is passable but harder to copy. 3:1 is preferred but most operators prefer to send and copy a lighter weight, * especially at higher speeds. The typical range for Morse code hand-sending (via CW) is 10 to 25 WPM and 'radiosports' * contesters prefer 30 to 40 sent electronically. 60 WPM is a great maximum for most homebrew projects where the smallest * 'dit' would have a mark duration only 20 ms. A 'dah' at a slow 5 WPM would be 720 ms. * This will be our typical working range which is fine using the millis() function of the Arduino. * * A Secret Decoder Ring Requires at Least Two Parts: * * A CW decoder requires two parts including both a way to 'hear', or discriminate the signal coming from your receiver's * audio as well as an algorithm or process to decode that signal. The tone decoder should be able to operate from the * audio port jack of your radio, should be adjustable for gain and frequency response and should be able to narrow in * on your signal apart from others that may be competing. You'll find the schematic for the tone decoder using the * classic LM567 IC at k4icy.com/cw_decoder/cw_decoder.html The chosen audio tone will be converted to a matching * logic signal which is then used by the second part, an Arduino Uno, Nano (or other model,) to handle the code * deciphering and display of the result to an LCD panel. * * Timing of a CW Morse code signal, especially that of human-sent code does not rigidly follow the 'PARIS' 3:1 timing * convention which makes it harder to deciphyer with a rigid algorithm. While many options for software and * microcontroller-based CW decoding do exist, many performing better than most humans, for our purposes as hams * and Arduino enthusiasts, are not really granted the room or processing speed on a basic Arduino (at 32k of flash) * to include the option for higher-level analysis such as using Bayesian classification and histogram modeling. * My approach isn't the most accurate but it's simple enough and should do well with hand-sent code, quickly adjusting * to any speed up to around 70 WPM. * * The decoding algorithm works as follows: * * The duration of each Morse code element sent, whether long or short, relatively speaking, is sampled. The two are, * of course, not able to be sent at the same time so the best we can do is poll the duration of each pulse in consecutive * order, and if close enough to each other in occurrence will most likely have been sent at the same WPM speed. * When a long pulse follows a short one or vice-versa, it can be assumed that the long pulse is generally more than * 2-times the duration of the short. Since Dashes are generally 3 times the duration of a dot, a difference should be evident. * * When this condition is detected the algorithm can move a reference point in between the the two and use * it to discriminate all future occurrences of any sampled duration, classifying a very likely candidate for either a * dot or a dash for which to decode via a lookup table. * * Two thresholds are established and maintained via a moving average including a geometric mean and an arithmetic mean. * The former is more appropriate for finding the most likely mid-point in between a sampled dot and dash as, in practice, * any variation in duration, especially that being sent by hand, will be in likely propotion to the element's usual duration. * The latter mean is simply a mid-way average and is better suited for determining the space. * * Programming Considerations: * * The decoding algorithm of this sketch is most likely an attempt to "reinvent the wheel" as one or two other Arduino * sketches I've looked into use the geometric mean, but generally, most only sample the dashes to determin the thresholds. * I assume that working more accurately with both known duration values may tend to yield better results. It's now all down * to the limitations of Arduino process timing, the nuances of printing to an LCD screen, desired features and crafting * a usable electronic input signal the sketch which all add to the complexity. This decoder has to do well with hand-sent * code as well as keeping up with CW 'speed demons'. There can certainly be later improvements made to this sketch. * * License: * * The K4ICY CW Decoder sketch is Open Source and free to distribute and implement without permission. * Full license is also given to alter and generally improve upon this code/sketch - BUT - Michael A. Maynard, a.k.a. "K4ICY", * the author, insists that credit and acknowledgement be given to the same, including the web site reference. * Enjoy! * * Notes: * * 1 - Sorry, there is no easy way to tell this sketch arbitrary character dimentions for any LCD panel, the builder will have to go through and * make adjustments to the code. The current setup is for a 20 x 4 LCD using I2C and I don't believe the decoder timing will handle higher * dimensions but yoy're welcome to experiment. * 2 - used this text for testing input using LCWO's Convert Text to CW feature: ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-=,.?"':;!@$ * *////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// Included Libraries /////////////////////// #include // Uncomment if using an LCD Panel with I2C interface #include // Uncomment if using an LCD Panel with I2C interface //#include // Uncomment if using an LCD Panel with standard wiring /// Initialize /////////////////////// // Uncomment if using an LCD Panel with I2C interface, // Set the I2C address to 0x27 (for most LCD's) - Set to use 20 chars and 4 line display, a change to a different type // will require some modification to the function code. LiquidCrystal_I2C lcd(0x27,20,4); // Uncomment if using an LCD Panel with standard wiring, // Pin assignments if using standard wiring --> (RS, E, D4, D5, D6, D7) //LiquidCrystal lcd(7, 6, 5, 4, 3, 2); // POUR AFFICHEUR 2 LIGNES F1AZJ // LiquidCrystal lcd(8, 9, 4, 5, 6, 7); DANS LE PROG PA5HE #define keyLineInput 12 // Morse Key Input Pin #define audioOutputPin 10 // Speaker Output Pin #define indicatorLED 13 // Visual Indication Pin /// SYSTEM VARIABLES //////////////////// bool toneOutput = true; // Allows for control of a sidetone int debounceFactor = 20; // Debouncing of input signal via minimal signal time duration in ms. Default should be 15 ms and no less than 8 bool keyLine = false; // This flag denotes acknowledgement of a active input status on the Morse Key Input Pin /// TIMING VARIABLES //////////////////// unsigned long timeTrack = millis(); // Will be used as a time base for many events long thresholdGeometricMean = 139; // This with be the Geometric Mean between short and long pulse pairs sqrt (s * l) long thresholdArithmeticMean = 160; // This will be the Arithmetic Mean between short and long pulse pairs (s + l) / 2 long keyLineNewEvent = 0; // Record newest key-down timing long keyLinePriorEvent = 0; // Prior key-down timing for comparison long shortEventHistogramList[10]; long longEventHistogramList[10]; long spaceEventHistogramList[10]; int eventHistogramTrack = 0; int wpmHistogramTrack = 0; unsigned long keyLineDuration = 0; // Key-Down duration unsigned long spaceDuration = 0; // Key-Up duration unsigned long oldSpaceDuration = 0; // Historical reference of Key-Up duration unsigned long spaceDurationReference = 0; // Key-Up start moment float wordSpaceTiming = 3.0; // * threshold... usually 7 units typical unsigned long wordSpaceDuration = 0; // Key-Up duration unsigned long wordSpaceDurationReference = 0; // Key-Up start moment /// DECODE VARIABLES //////////////////// float compareFactor = 2.0; // This factor determines the ratio threshold for comparing 'dahs' to 'dits' - assuming arithmetic mean of 3:1 bool characterStep = false; // Lock-step one character at a time for decoding char elementSequence[10]; // This string will hold the 'dahs' and 'dits' to decode byte decodeChar = 0; // Decoded character from code element sequence lookup table String decodeProSign = ""; // Decoded/detected multiple-letter procedural signs can be passed via this variable int decodeProSignLength; // This is useful when printing any prosigns bool wordStep = false; // Add one blank space to display after word space duration has been met int oldWPM = 0; int WPM = 0; // Words Per Minute to display int WPMHistogramList[20]; int WPMHistogramTrack = 0; /// LCD VARIABLES /////////////////////// const int columns = 20; // columns in LCD const int rows = 4; // rows in LCD String row0LCD = ""; String row1LCD = ""; String row2LCD = ""; String row3LCD = ""; String rowFill = " "; // Fill these quotes with the number of columns in blank spaces on the bottom row String rowHalfFill = " "; // Fill these quotes with the half the number of columns in blank spaces on the top row int lcdPos = 0; // position within display active character line at bottom int lcdCodePos = 0; // position within display where an intra-character element by element representation will be writen bool clearCodeBar = false; // to allow clearing of code bar at top bool scrollLCD = false; // this is a cue to perform the LCD text field scrolling durring the main loop to divide up the chore delay time int scrollLCDColumn = 0; // these coordinates help position the main loop scrolling by increment - adjust per LCD model in main loop int scrollLCDRow = 3; // Define Special Character Maps byte umlautA[] = {B01010,B00000,B01110,B10001,B11111,B10001,B10001,B00000}; // Ä byte acuteA[] = {B00111,B00000,B01110,B10001,B11111,B10001,B10001,B00000}; // Á byte capitalAE[] = {B00111,B01100,B10100,B10111,B11100,B10100,B10111,B00000}; // Æ byte cedillaC[] = {B01110,B10001,B10000,B10000,B10001,B01110,B00010,B00110}; // Ç byte acuteE[] = {B00111,B00000,B11111,B10000,B11111,B10000,B11111,B00000}; // É byte umlautO[] = {B01010,B00000,B01110,B10001,B10001,B10001,B01110,B00000}; // Ö byte umlautU[] = {B01010,B00000,B10001,B10001,B10001,B10001,B01110,B00000}; // Ü byte virgulillaN[]= {B01010,B10100,B10001,B11001,B10101,B10011,B10001,B00000}; // Ñ byte unknown[] = {B10101,B01010,B10101,B01010,B10101,B01010,B10101,B00000}; // (Shaded box used to show unknown decode) /// System Setup ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// void setup() { pinMode(keyLineInput, INPUT_PULLUP); // Morse Key Input digitalWrite(indicatorLED, LOW); // turn indicator LED off // LCD Setup ///// lcd.init(); // Initialize the LCD display lcd.createChar(0, umlautA); // Ä lcd.createChar(1, acuteA); // Á lcd.createChar(2, capitalAE); // Æ lcd.createChar(2, cedillaC); // Ç lcd.createChar(3, acuteE); // É lcd.createChar(4, umlautO); // Ö lcd.createChar(5, umlautU); // Ü lcd.createChar(6, virgulillaN); // Ñ lcd.createChar(7, unknown); // (Shaded Box) lcd.clear(); lcd.backlight(); // Activate the backlight - or do this separately if not integrated lcd.begin(columns, rows); // Splash Screen: Credit / Version / Site if (columns == 20 && rows == 4) { lcd.setCursor(0,0); lcd.print(" CW Morse Decoder "); lcd.setCursor(0,1); lcd.print("for Arduino by K4ICY"); lcd.setCursor(0,2); lcd.print(" Version: 1.14r "); lcd.setCursor(0,3); lcd.print(" Visit k4icy.com! "); } else { // if (columns == 16 && rows == 2) { lcd.setCursor(0,0); lcd.print("CW Morse Decoder"); lcd.setCursor(0,1); lcd.print("1.14r k4icy.com"); } delay(4000); lcd.clear(); lcd.setCursor(5,0); lcd.print("wpm"); printWPM(); lcd.setCursor(0,rows - 1); lcd.blink(); // preset moving averages (15 wpm default) for (int i = 0; i < 10; i++ ) { shortEventHistogramList[i] = 80; longEventHistogramList[i] = 240; spaceEventHistogramList[i] = 80; } for (int i = 0; i < 20; i++ ) { WPMHistogramList[i] = 15; } } ////////////////////////////////// Operation ////////////////////////////////////////////////////////////////////////////////////// void loop() { /// Track Current Elapsed Time timeTrack = millis(); /// When the Key is Down - or There is a Signal //////////////////////////////////////// if (digitalRead(keyLineInput) == LOW) { /// KEY DOWN if (keyLine == false) { // do the following only once per key-down event keyLineDuration = timeTrack; // update duration wordSpaceDurationReference = timeTrack; // create starting point to measure for word space (typically 7 dits long) if (clearCodeBar) { // clear the code element display at the top-right of the LCD lcd.noBlink(); lcd.setCursor(columns / 2, 0); lcd.print(rowHalfFill); lcd.setCursor(lcdPos, rows - 1); lcd.blink(); // show cursor lcdCodePos = 0; clearCodeBar = false; } } keyLine = true; digitalWrite(indicatorLED, HIGH); // turn indicator LED on if (toneOutput) { tone(audioOutputPin, 700); } // send tone to speaker port } /// Incremental Scrolling for the Text Field /////////////////////////////////////// if (scrollLCD) { printToFieldLCDIncrement(); } /// When the Key is Up - or There is Not a Signal /////////////////////////////////// if (digitalRead(keyLineInput) == HIGH) { /// KEY UP /// Allow for Time to Debounce the Signal ////////////////////////////////////// if (timeTrack >= (keyLineDuration + debounceFactor) && keyLine) { keyLine = false; // only allow for this section once per detected key-up keyLinePriorEvent = keyLineNewEvent; // Create history of last signal duration event to compare to new one keyLineNewEvent = timeTrack - keyLineDuration; // Get current event duration digitalWrite(indicatorLED, LOW); // turn indicator LED off if (toneOutput) { noTone(audioOutputPin); } // turn off tone to speaker port /// IF the Current Duration Event Compared to the Previous Event appears to be a Dot / Dash pair [ roughly (>2):1 ] ///////////////////////// if ( ( keyLineNewEvent >= keyLinePriorEvent * compareFactor && oldSpaceDuration <= keyLinePriorEvent * compareFactor ) || ( keyLinePriorEvent >= keyLineNewEvent * compareFactor && oldSpaceDuration <= keyLineNewEvent * compareFactor ) ) { /// Find out which one is the Dot and which is the Dash and roll them into a moving average of each ///////////////////////////////////// if ( keyLineNewEvent >= keyLinePriorEvent ) { longEventHistogramList[eventHistogramTrack] = keyLineNewEvent ; shortEventHistogramList[eventHistogramTrack] = keyLinePriorEvent ; } else { longEventHistogramList[eventHistogramTrack] = keyLinePriorEvent ; shortEventHistogramList[eventHistogramTrack] = keyLineNewEvent ; } /// Keep a moving average detected dot/dash pairs //////////////////////// long longEventAverage = longEventHistogramList[0] + longEventHistogramList[1] + longEventHistogramList[2] + longEventHistogramList[3] + longEventHistogramList[4] ; longEventAverage += longEventHistogramList[5] + longEventHistogramList[6] + longEventHistogramList[7] + longEventHistogramList[8] + longEventHistogramList[9] ; longEventAverage /= 10 ; long shortEventAverage = shortEventHistogramList[0] + shortEventHistogramList[1] + shortEventHistogramList[2] + shortEventHistogramList[3] + shortEventHistogramList[4] ; shortEventAverage += shortEventHistogramList[5] + shortEventHistogramList[6] + shortEventHistogramList[7] + shortEventHistogramList[8] + shortEventHistogramList[9] ; shortEventAverage /= 10 ; /// Find threshold means ///////////////////////// thresholdGeometricMean = sqrt(shortEventAverage * longEventAverage) ; // thresholdArithmeticMean = (shortEventAverage + longEventAverage) / 2 ; /// Keep a moving average of the intra-element space duration spaceEventHistogramList[eventHistogramTrack] = oldSpaceDuration ; long spaceEventAverage = spaceEventHistogramList[0] + spaceEventHistogramList[1] + spaceEventHistogramList[2] + spaceEventHistogramList[3] + spaceEventHistogramList[4] ; spaceEventAverage += spaceEventHistogramList[5] + spaceEventHistogramList[6] + spaceEventHistogramList[7] + spaceEventHistogramList[8] + spaceEventHistogramList[9] ; spaceEventAverage /= 10 ; /// Bootstrap threshold values - - - If any are below or above known Dot/Dash pair ranges then move them instantly if ( thresholdGeometricMean < shortEventHistogramList[eventHistogramTrack] || thresholdGeometricMean > longEventHistogramList[eventHistogramTrack] ) { thresholdGeometricMean = sqrt( shortEventHistogramList[eventHistogramTrack] * longEventHistogramList[eventHistogramTrack] ) ; longEventHistogramList[0] = longEventHistogramList[eventHistogramTrack]; longEventHistogramList[1] = longEventHistogramList[eventHistogramTrack]; longEventHistogramList[2] = longEventHistogramList[eventHistogramTrack]; longEventHistogramList[3] = longEventHistogramList[eventHistogramTrack]; longEventHistogramList[4] = longEventHistogramList[eventHistogramTrack]; longEventHistogramList[5] = longEventHistogramList[eventHistogramTrack]; longEventHistogramList[6] = longEventHistogramList[eventHistogramTrack]; longEventHistogramList[7] = longEventHistogramList[eventHistogramTrack]; longEventHistogramList[8] = longEventHistogramList[eventHistogramTrack]; longEventHistogramList[9] = longEventHistogramList[eventHistogramTrack]; shortEventHistogramList[0] = shortEventHistogramList[eventHistogramTrack]; shortEventHistogramList[1] = shortEventHistogramList[eventHistogramTrack]; shortEventHistogramList[2] = shortEventHistogramList[eventHistogramTrack]; shortEventHistogramList[3] = shortEventHistogramList[eventHistogramTrack]; shortEventHistogramList[4] = shortEventHistogramList[eventHistogramTrack]; shortEventHistogramList[5] = shortEventHistogramList[eventHistogramTrack]; shortEventHistogramList[6] = shortEventHistogramList[eventHistogramTrack]; shortEventHistogramList[7] = shortEventHistogramList[eventHistogramTrack]; shortEventHistogramList[8] = shortEventHistogramList[eventHistogramTrack]; shortEventHistogramList[9] = shortEventHistogramList[eventHistogramTrack]; longEventAverage = longEventHistogramList[eventHistogramTrack]; shortEventAverage = shortEventHistogramList[eventHistogramTrack]; WPMHistogramList[wpmHistogramTrack] = ( 6000 / ( longEventHistogramList[eventHistogramTrack] + shortEventHistogramList[eventHistogramTrack] + oldSpaceDuration ) ) ; WPMHistogramList[0] = WPMHistogramList[wpmHistogramTrack]; WPMHistogramList[1] = WPMHistogramList[wpmHistogramTrack]; WPMHistogramList[2] = WPMHistogramList[wpmHistogramTrack]; WPMHistogramList[3] = WPMHistogramList[wpmHistogramTrack]; WPMHistogramList[4] = WPMHistogramList[wpmHistogramTrack]; WPMHistogramList[5] = WPMHistogramList[wpmHistogramTrack]; WPMHistogramList[6] = WPMHistogramList[wpmHistogramTrack]; WPMHistogramList[7] = WPMHistogramList[wpmHistogramTrack]; WPMHistogramList[8] = WPMHistogramList[wpmHistogramTrack]; WPMHistogramList[9] = WPMHistogramList[wpmHistogramTrack]; WPMHistogramList[10] = WPMHistogramList[wpmHistogramTrack]; WPMHistogramList[11] = WPMHistogramList[wpmHistogramTrack]; WPMHistogramList[12] = WPMHistogramList[wpmHistogramTrack]; WPMHistogramList[13] = WPMHistogramList[wpmHistogramTrack]; WPMHistogramList[14] = WPMHistogramList[wpmHistogramTrack]; WPMHistogramList[15] = WPMHistogramList[wpmHistogramTrack]; WPMHistogramList[16] = WPMHistogramList[wpmHistogramTrack]; WPMHistogramList[17] = WPMHistogramList[wpmHistogramTrack]; WPMHistogramList[18] = WPMHistogramList[wpmHistogramTrack]; WPMHistogramList[19] = WPMHistogramList[wpmHistogramTrack]; } /// we will now calculate the WPM... //////////////////////////////////////////// oldWPM = WPM; // used to see when WPM has changed to save on LCD upload/refresh time // WPM = ( 6000 / ( longEventAverage + shortEventAverage + spaceEventAverage ) ) * 1.06 ; // use this instead for faster WPM result ///* WPMHistogramList[wpmHistogramTrack] = ( 6000 / ( longEventAverage + shortEventAverage + spaceEventAverage ) ) ; // use this instead if WPM result is too jittery // // WPM = 20 + WPMHistogramList[0] + WPMHistogramList[1] + WPMHistogramList[2] + WPMHistogramList[3] + WPMHistogramList[4] ; // WPM += WPMHistogramList[5] + WPMHistogramList[6] + WPMHistogramList[7] + WPMHistogramList[8] + WPMHistogramList[9] ; // WPM += WPMHistogramList[10] + WPMHistogramList[11] + WPMHistogramList[12] + WPMHistogramList[13] + WPMHistogramList[14] ; // WPM += WPMHistogramList[15] + WPMHistogramList[16] + WPMHistogramList[17] + WPMHistogramList[18] + WPMHistogramList[19] ; // WPM /= 19.05 ; //WPM /= 20 ; //WPM *= 1.05 ; //*/ if (WPM != oldWPM) { // if WPM has changed then print to LCD printWPM(); } /// set index to build up further moving averages //////////////// eventHistogramTrack ++ ; if ( eventHistogramTrack > 9 ) { eventHistogramTrack = 0 ; } // reset index if needed wpmHistogramTrack ++ ; if ( wpmHistogramTrack > 19 ) { wpmHistogramTrack = 0 ; } // reset index if needed } /// Reset space durations - in Key-Up state /////////// spaceDurationReference = timeTrack; wordSpaceDurationReference = timeTrack; /// Classify and add most likely Dots or Dashes to a string for eventual character decoding if (keyLineNewEvent <= thresholdGeometricMean) { strcat(elementSequence,"."); characterStep = true; wordStep = true; lcd.noBlink(); lcd.setCursor((columns / 2) + lcdCodePos, 0); //lcd.print("."); lcd.write(165); lcd.setCursor(lcdPos, rows - 1); lcd.blink(); // show cursor if (lcdCodePos < ( columns / 2) - 1 ) { lcdCodePos++; } } else { strcat(elementSequence,"-"); characterStep = true; wordStep = true; lcd.noBlink(); lcd.setCursor((columns / 2) + lcdCodePos, 0); //lcd.print("-"); lcd.write(176); lcd.setCursor(lcdPos, rows - 1); lcd.blink(); // show cursor if (lcdCodePos < ( columns / 2) - 1 ) { lcdCodePos++; } } } /// Keep track of Key-Up space timing oldSpaceDuration = spaceDuration; spaceDuration = timeTrack - spaceDurationReference; /// DECODE collected string of elements ///////////////////////////////////////// // check to see if inter-element space duration threshold has been exceeded - then decode // it is assumed that the intra-space is longer than a Dot but shorter than a Dash //if (spaceDuration >= thresholdArithmeticMean) { // using the Arithmetic Mean seems more stable if (spaceDuration >= thresholdGeometricMean) { // but using the Geometric Mean seems more accurate spaceDurationReference = timeTrack; // Key-Up space reset if (characterStep) { morseDecode(); // decode element sequence string if (decodeChar != 7) { //Serial.print((char) decodeChar); printToFieldLCD(decodeChar); } else { decodeProSignLength = decodeProSign.length(); // get the length of the prosign if used if (decodeProSignLength > 0) { //Serial.print("["); Serial.print(decodeProSign); Serial.print("]"); printToFieldLCD(162); // Japanese brackets look less cluttered for (int i = 0; i < decodeProSignLength; i++) { // scan through the prosign string... printToFieldLCD(decodeProSign.charAt(i)); } printToFieldLCD(163); } else { //Serial.print((char) decodeChar); printToFieldLCD(decodeChar); } } decodeChar = 0; elementSequence[0] = '\0'; // clear memory of code elements clearCodeBar = true; // allow for clearing of code bar display at top right when the next element is detected } characterStep = false; } /// Keep track of Key-Up timing and see if a word space is required wordSpaceDuration = timeTrack - wordSpaceDurationReference; if (wordSpaceDuration >= thresholdGeometricMean * wordSpaceTiming) { wordSpaceDurationReference = timeTrack; // word space reset if (wordStep) { //Serial.print((char) 32); printToFieldLCD(32); } wordStep = false; } } } // End Main Loop ///// ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// Print Characters To a Field on the LCD void printToFieldLCD(int charASCII) { lcd.setCursor(lcdPos, rows - 1); // set LCD cursor position to lcdPos, character should be established as clear to use lcd.write(charASCII); // white ASCII character - as stored inside of LCD row3LCD = row3LCD + char(charASCII); // build up lowest row - - - change the string name to suit LCD model lcdPos++ ; // advance position one to the right if (lcdPos >= columns) { // if the cursor position goes off screen... lcdPos = 0; // then reset position to absolute left of screen, and... row1LCD = row2LCD; // move rows up in memory - - - Change numbers of rows based on LCD model row2LCD = row3LCD; row3LCD = ""; // clear memory on bottom row scrollLCD = true; // do the below (commented) in the main loop (and in function following this one,) but gradually scrollLCDRow = 3; scrollLCDColumn = 0; } } /// Incremental Scrolling /////////////////////////////////// void printToFieldLCDIncrement() { if (scrollLCDRow == 3) { lcd.setCursor(scrollLCDColumn, 3); if (scrollLCDColumn < row3LCD.length()) { lcd.write(row3LCD.charAt(scrollLCDColumn)); } else { lcd.write(rowFill.charAt(scrollLCDColumn)); } scrollLCDColumn++; if (scrollLCDColumn >= columns) { scrollLCDColumn = 0; scrollLCDRow = 2; } } else { if (scrollLCDRow == 2) { lcd.setCursor(scrollLCDColumn, 2); if (row2LCD.length() > 0) { lcd.write(row2LCD.charAt(scrollLCDColumn)); } scrollLCDColumn++; if (scrollLCDColumn >= columns) { scrollLCDColumn = 0; scrollLCDRow = 1; } } else { if (scrollLCDRow == 1) { lcd.setCursor(scrollLCDColumn, 1); if (row1LCD.length() > 0) { lcd.write(row1LCD.charAt(scrollLCDColumn)); } scrollLCDColumn++; if (scrollLCDColumn >= columns) { scrollLCDColumn = 0; scrollLCDRow = 3; scrollLCD = false; } } } } } /// Print WPM /////////////////////////////////// void printWPM() { lcd.noBlink(); // hide cursor lcd.setCursor(1, 0); if (WPM >= 100 && WPM < 1000) { lcd.print(WPM); } if (WPM >= 10 && WPM < 100) { lcd.print(" "); lcd.print(WPM); } if (WPM < 10) { lcd.print(" "); lcd.print(WPM); } lcd.setCursor(lcdPos, rows - 1); lcd.blink(); // show cursor } /// Secret Decoder Ring /////////////////////////////////// void morseDecode() { // Here, we check the collected elements against our list to decode for the matching character decodeChar = 7; // ? (shaded box) Default [error] character, no match decodeProSign = ""; if (strcmp(elementSequence,".-") == 0) { decodeChar = 65; } // A Letters if (strcmp(elementSequence,"-...") == 0) { decodeChar = 66; } // B if (strcmp(elementSequence,"-.-.") == 0) { decodeChar = 67; } // C if (strcmp(elementSequence,"-..") == 0) { decodeChar = 68; } // D if (strcmp(elementSequence,".") == 0) { decodeChar = 69; } // E if (strcmp(elementSequence,"..-.") == 0) { decodeChar = 70; } // F if (strcmp(elementSequence,"--.") == 0) { decodeChar = 71; } // G if (strcmp(elementSequence,"....") == 0) { decodeChar = 72; } // H if (strcmp(elementSequence,"..") == 0) { decodeChar = 73; } // I if (strcmp(elementSequence,".---") == 0) { decodeChar = 74; } // J if (strcmp(elementSequence,"-.-") == 0) { decodeChar = 75; } // K if (strcmp(elementSequence,".-..") == 0) { decodeChar = 76; } // L if (strcmp(elementSequence,"--") == 0) { decodeChar = 77; } // M if (strcmp(elementSequence,"-.") == 0) { decodeChar = 78; } // N if (strcmp(elementSequence,"---") == 0) { decodeChar = 79; } // O if (strcmp(elementSequence,".--.") == 0) { decodeChar = 80; } // P if (strcmp(elementSequence,"--.-") == 0) { decodeChar = 81; } // Q if (strcmp(elementSequence,".-.") == 0) { decodeChar = 82; } // R if (strcmp(elementSequence,"...") == 0) { decodeChar = 83; } // S if (strcmp(elementSequence,"-") == 0) { decodeChar = 84; } // T if (strcmp(elementSequence,"..-") == 0) { decodeChar = 85; } // U if (strcmp(elementSequence,"...-") == 0) { decodeChar = 86; } // V if (strcmp(elementSequence,".--") == 0) { decodeChar = 87; } // W if (strcmp(elementSequence,"-..-") == 0) { decodeChar = 88; } // X if (strcmp(elementSequence,"-.--") == 0) { decodeChar = 89; } // Y if (strcmp(elementSequence,"--..") == 0) { decodeChar = 90; } // Z if (strcmp(elementSequence,".----") == 0) { decodeChar = 49; } // 1 Numbers if (strcmp(elementSequence,"..---") == 0) { decodeChar = 50; } // 2 if (strcmp(elementSequence,"...--") == 0) { decodeChar = 51; } // 3 if (strcmp(elementSequence,"....-") == 0) { decodeChar = 52; } // 4 if (strcmp(elementSequence,".....") == 0) { decodeChar = 53; } // 5 if (strcmp(elementSequence,"-....") == 0) { decodeChar = 54; } // 6 if (strcmp(elementSequence,"--...") == 0) { decodeChar = 55; } // 7 if (strcmp(elementSequence,"---..") == 0) { decodeChar = 56; } // 8 if (strcmp(elementSequence,"----.") == 0) { decodeChar = 57; } // 9 if (strcmp(elementSequence,"-----") == 0) { decodeChar = 48; } // 0 if (strcmp(elementSequence,"-.-.--") == 0) { decodeChar = 33; } // ! Punctuation if (strcmp(elementSequence,"..--.") == 0) { decodeChar = 33; } // ! if (strcmp(elementSequence,".-..-.") == 0) { decodeChar = 34; } // " if (strcmp(elementSequence,"...-..-") == 0) { decodeChar = 36; } // $ //if (strcmp(elementSequence,".-...") == 0) { decodeChar = 38; } // & if (strcmp(elementSequence,".----.") == 0) { decodeChar = 39; } // ' //if (strcmp(elementSequence,"-.--.") == 0) { decodeChar = 40; } // ( //if (strcmp(elementSequence,"-.--.-") == 0) { decodeChar = 41; } // ) //if (strcmp(elementSequence,".-.-.") == 0) { decodeChar = 43; } // + if (strcmp(elementSequence,"--..--") == 0) { decodeChar = 44; } // , if (strcmp(elementSequence,"-....-") == 0) { decodeChar = 45; } // - if (strcmp(elementSequence,".-.-.-") == 0) { decodeChar = 46; } // . if (strcmp(elementSequence,"-..-.") == 0) { decodeChar = 47; } // / if (strcmp(elementSequence,"---...") == 0) { decodeChar = 58; } // : if (strcmp(elementSequence,"-.-.-.") == 0) { decodeChar = 59; } // ; //if (strcmp(elementSequence,"-...-") == 0) { decodeChar = 61; } // = (Pause, BT) if (strcmp(elementSequence,"..--..") == 0) { decodeChar = 63; } // ? if (strcmp(elementSequence,".--.-.") == 0) { decodeChar = 64; } // @ if (strcmp(elementSequence,"..--.-") == 0) { decodeChar = 95; } // _ if (strcmp(elementSequence,".-.-") == 0) { decodeChar = 0; } // Ä Special if (strcmp(elementSequence,".--.-") == 0) { decodeChar = 1; } // Á //if (strcmp(elementSequence,".-.-") == 0) { decodeChar = 2; } // Æ if (strcmp(elementSequence,"-.-..") == 0) { decodeChar = 2; } // Ç if (strcmp(elementSequence,"..-..") == 0) { decodeChar = 3; } // É if (strcmp(elementSequence,"---.") == 0) { decodeChar = 4; } // Ö if (strcmp(elementSequence,"..--") == 0) { decodeChar = 5; } // Ü if (strcmp(elementSequence,"--.--") == 0) { decodeChar = 6; } // Ñ //if (strcmp(elementSequence,".-.-") == 0) { decodeProSign = "AA"; } // Prosigns (New Line) if (strcmp(elementSequence,".-.-.") == 0) { decodeProSign = "AR"; } // (End of Message) if (strcmp(elementSequence,".-...") == 0) { decodeProSign = "AS"; } // (Wait) if (strcmp(elementSequence,"-...-.-") == 0) { decodeProSign = "BK"; } // (Break) if (strcmp(elementSequence,"-...-") == 0) { decodeProSign = "BT"; } // (Pause, New Paragraph) if (strcmp(elementSequence,"-.-..-..") == 0){ decodeProSign = "CL"; } // (Clear, Going Off Air) if (strcmp(elementSequence,"-.-.-") == 0) { decodeProSign = "CT"; } // (Start Copying) if (strcmp(elementSequence,"-.--.") == 0) { decodeProSign = "KN"; } // (Invite Specific Station Only) if (strcmp(elementSequence,"...-.-") == 0) { decodeProSign = "SK"; } // (End of Transmission) if (strcmp(elementSequence,"...-.") == 0) { decodeProSign = "SN"; } // (Understood) if (strcmp(elementSequence,"...---...") == 0){ decodeProSign = "SOS"; } // (SOS) if (strcmp(elementSequence,"-.-.--.-") == 0) { decodeProSign = "CQ"; } // (Invitation) if (strcmp(elementSequence,"........") == 0) { decodeProSign = "err"; } // (Error) if (strcmp(elementSequence,"-..-..-") == 0) { decodeProSign = "Speaker"; toneOutput = !toneOutput; } // Control to toggle the speaker } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////