//----------------------------------------------------------- // Morse trainer // 2024.07.07 RSP // Target: Pro Mini 5V/16MHz // + 8 digit alphanumeric display // + 3 lighted buttons // + 15 LED neopixel strip // + piezo speaker or amplified speaker // + mode switch and speed knob // // Operation: // Key mode // Inputs morse, displays equivalent character if recognized // Key button keys as long as the button is pressed // Dot/Dash buttons key fixed length dot/dashes // Receive mode // Test morse character recognition skill // A Morse equivalent of a character is played // The buttons select one of 3 possible answers // Learn mode // Displays a character and plays it's Morse equivalent // Key button toggles automatic mode // Dot/Dash buttons select a character manually // // Diagnostics: hold Key button at power on to clear EEPROM //----------------------------------------------------------- #include "FEARLESS_8digit_14seg_HT16K33.h" #include #define VOLUME 10 #define DOT_TIME 150 #define DOT_FREQ 600 #define DASH_TIME 250 #define DASH_FREQ 450 #define SPACE_TIME 50 //efine coBegin { static int _state_ = 0; switch(_state_) { case 0:; #define coBegin { switch(_state_) { case 0:; #define coEnd _state_ = 0; }} #define coContinue { _state_ = 0; return; } #define coWaitWhile(expr) { _state_ = __LINE__; return; case __LINE__: if (expr) return; } #define coDelay(msec) { static uint32_t tm; _state_ = __LINE__; tm=millis(); return; case __LINE__: if (millis()-tm < msec) return; } #define coDelayWhile(msec,expr) { static uint32_t tm; _state_ = __LINE__; tm=millis(); return; case __LINE__: if ((millis()-tm < msec) && (expr)) return; } #define coDebounce(msec,expr) { static uint32_t tm; _state_ = __LINE__; tm=millis(); return; case __LINE__: if (!(expr)) tm=millis(); if (millis()-tm < msec) return; } // dual use buttons - morse key + multiple choice answer #define KEY_DEBOUNCE 50 struct button // illuminated buttons { byte btn_pin; byte led_pin; boolean state; uint32_t tm; uint32_t debounce; }; button dot_btn = { 2, A3, false, 0, 0 }; button dash_btn = { 3, A2, false, 0, 0 }; button key_btn = { 4, 13, false, 0, 0 }; void buttonLeds( boolean onoff ) { digitalWrite( dot_btn.led_pin, onoff ); digitalWrite( dash_btn.led_pin, onoff ); digitalWrite( key_btn.led_pin, onoff ); } #define SPEED_POT A0 // 0.5 to 2.0 speed #define MODE_RX_PIN 11 // mode switch (ON-OFF-ON) #define MODE_TX_PIN 12 #define PIX_PIN 8 // morse dot-dash display bar #define NUM_PIXELS 15 #include Adafruit_NeoPixel pixelz = {NUM_PIXELS, PIX_PIN, NEO_GRB + NEO_KHZ800}; void clearPixelz() { for (byte i=0; i < NUM_PIXELS; i++) pixelz.setPixelColor( i, pixelz.Color(0, 0, 1) ); pixelz.show(); } const struct { char ascii; byte length; byte data; // bitmapped (MSB first) dot=0, dash=1 } morse_code_table[] = { {'A',2,0b01000000}, {'B',4,0b10000000}, {'C',4,0b10100000}, {'D',3,0b10000000}, {'E',1,0b00000000}, {'F',4,0b00100000}, {'G',3,0b11000000}, {'H',4,0b00000000}, {'I',2,0b00000000}, {'J',4,0b01110000}, {'K',3,0b10100000}, {'L',4,0b01000000}, {'M',2,0b11000000}, {'N',2,0b10000000}, {'O',3,0b11100000}, {'P',4,0b01100000}, {'Q',4,0b11010000}, {'R',3,0b01000000}, {'S',3,0b00000000}, {'T',1,0b10000000}, {'U',3,0b00100000}, {'V',4,0b00010000}, {'W',3,0b01100000}, {'X',4,0b10010000}, {'Y',4,0b10110000}, {'Z',4,0b11000000}, {'0',5,0b11111000}, {'1',5,0b01111000}, {'2',5,0b00111000}, {'3',5,0b00011000}, {'4',5,0b00001000}, {'5',5,0b00000000}, {'6',5,0b10000000}, {'7',5,0b11000000}, {'8',5,0b11100000}, {'9',5,0b11110000}, {'?',6,0b00110000}, {'!',6,0b10101100}, {'.',6,0b01010100}, {',',6,0b11001100}, //{';',6,0b10101000}, // no corresponding display char //{':',6,0b11100000}, // no corresponding display char {'+',6,0b01010000}, {'-',6,0b10000100}, {'/',5,0b10010000}, {'=',5,0b10001000} }; #define MORSE_TABLE_SIZE 44 String out = ""; // key output (paddle) buffer byte in_length = 0; // key input buffer byte in_data; uint32_t in_tm; display_8digit_14seg display; String disp = ""; // display buffer //----------------------------------------------------------- #include // save random seed to EEPROM void saveRandom() { static boolean saved = false; // only save once per power cycle if (saved) return; saved = true; // locate cursor (zero element) uint32_t seed; for (int i=0; i < 256; i++) { EEPROM.get( i*4, seed ); if (seed == 0) { // drop seed at cursor seed = random(); while (seed == 0) seed = random(); // zero not allowed EEPROM.put( i*4, seed ); // advance cursor i = (i+1) % 256; seed = 0; EEPROM.put( i*4, seed ); return; } } // seed table is fucked, reset it initializeRandom(); } // initialize EEPROM seed table // (fill it with random seeds) void initializeRandom() { uint32_t seed = 0; EEPROM.put( 0, seed ); // cursor element for (int i=1; i < 256; i++) { seed = random(); while (seed == 0) seed = random(); // zero not allowed EEPROM.put( i*4, seed ); } } //----------------------------------------------------------- // helper to display a morse code on the neopixel strip #define LEDBAR_BRIGHTNESS 16 void morseBitzToPixelz( byte data, byte length ) { byte i,j,d; for (i=d=0; i < length; i++) if (data & (0x80 >> i)) d++; // count number of dashes if (d < 4) d = 3; else d = 2; // dash pixel size for (i=0; i < NUM_PIXELS; i++) pixelz.setPixelColor( i, pixelz.Color(0, 0, 1) ); for (i=j=0; i < length && i < 6; i++) if (data & (0x80 >> i)) { // dash for (byte n=0; n < d; n++) if (j < NUM_PIXELS) pixelz.setPixelColor( j++, pixelz.Color(0, LEDBAR_BRIGHTNESS, 0) ); j++; // spacer pixel } else // dot { if (j < NUM_PIXELS) pixelz.setPixelColor( j++, pixelz.Color(LEDBAR_BRIGHTNESS, 0, 0) ); j++; // spacer pixel } pixelz.show(); } //----------------------------------------------------------- // helper to calculate time adjusted per speed knob uint32_t timeWarp( uint32_t msec ) { static uint16_t factor = 100; static uint32_t tm = 0; if (millis() - tm > 100) // read pot 100 times/sec max { tm = millis(); // 0.5 to 2.0 with 1.0 at center uint16_t adc = analogRead( SPEED_POT ); if (adc < 512) factor = map( adc, 0,511, 200,100 ); else factor = map( adc, 512,1023, 100,50 ); } return msec * factor / 100; } //----------------------------------------------------------- //-- Receive mode (Morse recognition training) -------------- //----------------------------------------------------------- byte random_level = 1; // level 1 is alpha chars, 2 adds numbers, 3 adds punctuation byte random_size = 26; // number of chars to test byte random_list[MORSE_TABLE_SIZE]; // list of random char numbers // generate random receive char list for current level void generateRandomLevel() { // build new random list for (byte i=0; i < random_size; i++) random_list[i] = i+1; // shuffle for (byte j=0; j < 10; j++) for (byte i=0; i < random_size; i++) { byte k = random( 0, random_size ); if (k == i) break; byte x = random_list[i]; random_list[i] = random_list[k]; random_list[k] = x; } } // show the receive level void displayRandomLevel() { display.paint_string( "LEVEL" ); display.write_digit( 6, random_level ); display.paint_display(); } // sound out a morse character int play_index = -1; // char (number in morse_code_table) to play as morse void playerTask( boolean kill_task = false ) { static byte dit; static uint32_t ms; static int _state_ = 0; if (kill_task) { // task clean up toneAC(); _state_ = 0; play_index = -1; return; } coBegin coWaitWhile( play_index < 0 ) for (dit = 0; dit < morse_code_table[play_index].length; dit++) { ms = morse_code_table[play_index].data & (0x80 >> dit) ? DASH_TIME:DOT_TIME; ms = timeWarp(ms); toneAC( morse_code_table[play_index].data & (0x80 >> dit) ? DASH_FREQ:DOT_FREQ, VOLUME, ms, true ); coDelay(ms) toneAC(); coDelay(timeWarp(SPACE_TIME)) } coEnd play_index = -1; // clear play request } // detect answer buttons int answer = -1; void answerTask() { if (answer != -1) return; // lock out if (!digitalRead( key_btn.btn_pin)) answer = 0; if (!digitalRead(dash_btn.btn_pin)) answer = 2; if (!digitalRead( dot_btn.btn_pin)) answer = 1; } // receive recognition test void recognition( boolean kill_task = false ) { static byte ch[3]; // random characters, ch[0] is the correct one static byte random_order; // alternate ordering that's being used for display const byte alt[6][3] = { {0,1,2}, {0,2,1}, {1,0,2}, {1,2,0}, {2,0,1}, {2,1,0} }; byte i,lo,hi; static int _state_ = 0; if (kill_task) { // task clean up _state_ = 0; buttonLeds(LOW); clearPixelz(); pixelz.show(); return; } coBegin // choose a character // 0..25 are alpha // 26..35 are numbers // 36..43 are punctuatikon if (random_list[0] == 0) // random list exhausted ? { // bump level if (random_level < 3) random_level++; random_size = (random_level == 1 ? 26 : (random_level == 2 ? 36 : 44)); // generate level generateRandomLevel(); displayRandomLevel(); coDelay(2000) // grab value from full random list ch[0] = random_list[random_size-1] - 1; random_list[random_size-1] = 0; } else // take next random from list { ch[0] = random_list[0]-1; for (i=0; i < random_size-1; i++) random_list[i] = random_list[i+1]; random_list[random_size-1] = 0; } // choose two random wrong characters // from the same type (alpha, num, punct) if (ch[0] < 26) { lo=0; hi=26; } else if (ch[0] < 36) { lo=26; hi=36; } else { lo=36; hi=44; } ch[1] = random( lo,hi ); while (ch[1] == ch[0]) ch[1] = random( lo,hi ); ch[2] = random( lo,hi ); while (ch[2] == ch[0] || ch[2] == ch[1]) ch[2] = random( lo,hi ); // pick random order to present the chars random_order = random(0,6); // 123 132 213 231 312 321 // 12345678 // X X X display.paint_fill(' '); display.write_ascii(0, morse_code_table[ ch[alt[random_order][0]] ].ascii); display.write_ascii(3, morse_code_table[ ch[alt[random_order][1]] ].ascii); display.write_ascii(6, morse_code_table[ ch[alt[random_order][2]] ].ascii); display.paint_display(); // light up all buttons buttonLeds(HIGH); // show on neopixel strip morseBitzToPixelz( morse_code_table[ ch[0] ].data, morse_code_table[ ch[0] ].length ); // prompt for answer coDebounce( 50, digitalRead(key_btn.btn_pin) && digitalRead(dot_btn.btn_pin) && digitalRead(dash_btn.btn_pin) ) answer = -1; while (answer < 0) { // sound out morse code play_index = ch[0]; // wait for answer, replay every 5 seconds coDelayWhile( 5000, answer < 0 ) }; saveRandom(); // show result digitalWrite( key_btn.led_pin, answer == 0 ); digitalWrite( dot_btn.led_pin, answer == 1 ); digitalWrite( dash_btn.led_pin, answer == 2 ); if (random_order > 5) random_order = 5; if (answer > 2) answer = 2; display.paint_string( alt[random_order][answer] == 0 ? "CORRECT" : " WRONG" ); display.paint_display(); // wait for key release coDebounce( 100, digitalRead(key_btn.btn_pin) && digitalRead(dot_btn.btn_pin) && digitalRead(dash_btn.btn_pin) ) // show result for a while coDelayWhile( 5000, digitalRead(key_btn.btn_pin) && digitalRead(dot_btn.btn_pin) && digitalRead(dash_btn.btn_pin) ) // put back on list if wrong if (alt[random_order][answer] != 0) for (i=0; i < random_size; i++) if (random_list[i] == 0) { random_list[i] = ch[0]+1; break; } coEnd } //----------------------------------------------------------- //-- SEND mode (morse keyer with code identification) ------- //----------------------------------------------------------- // detect end of morse character & attempt to match it void inputTask() { if (in_length) if (!digitalRead(key_btn.led_pin)) if (millis() - in_tm > timeWarp(DASH_TIME)) { // end of character code Serial.print("DECODE "); Serial.print(in_length); Serial.print(" = "); Serial.println(in_data,HEX); for (byte i=0; i < MORSE_TABLE_SIZE; i++) if (morse_code_table[i].data == in_data && morse_code_table[i].length == in_length) { Serial.print("MATCHED "); Serial.write(morse_code_table[i].ascii); Serial.println(); disp += morse_code_table[i].ascii; if (disp.length() > 8) disp.remove(0,8); display.paint_string(disp); display.paint_display(); break; } in_length = in_data = 0; } } // debounce dot/dash/key buttons // generate signal output (out) // add dot/dash to in_data/in/length accumulator void keyTask() { // dot button - automatic timing if (!digitalRead( dot_btn.btn_pin )) { if (!dot_btn.state) { dot_btn.state = true; out += "."; } dot_btn.tm = millis(); } else if (dot_btn.state) if (millis() - dot_btn.tm > KEY_DEBOUNCE) dot_btn.state = false; // dash button - automatic timing if (!digitalRead( dash_btn.btn_pin )) { if (!dash_btn.state) { dash_btn.state = true; out += "-"; } dash_btn.tm = millis(); } else if (dash_btn.state) if (millis() - dash_btn.tm > KEY_DEBOUNCE) dash_btn.state = false; // key button (dot or dash) - user timing if (!digitalRead( key_btn.btn_pin )) { if (!key_btn.state) { key_btn.state = true; digitalWrite( key_btn.led_pin, HIGH ); toneAC( 500, VOLUME, 0, true ); key_btn.tm = millis(); } key_btn.debounce = millis(); } else if (key_btn.state) if (millis() - key_btn.debounce > KEY_DEBOUNCE) { key_btn.state = false; toneAC(); digitalWrite( key_btn.led_pin, LOW ); uint32_t dt = (millis() - key_btn.tm) - KEY_DEBOUNCE; if (dt > DOT_TIME+(DASH_TIME-DOT_TIME)/2) in_data |= (0x80 >> in_length); in_length++; in_tm = millis(); morseBitzToPixelz( in_data, in_length ); // show on neopixel strip } } // transfer out to hardware with pacing void outputTask( boolean kill_task = false ) { static uint32_t ms; static int _state_ = 0; if (kill_task) { // task clean up _state_ = 0; toneAC(); buttonLeds(LOW); return; } coBegin if (out.length()) { saveRandom(); ms = (out[0] == '.') ? DOT_TIME:DASH_TIME; ms = timeWarp(ms); // turn on LEDs & play sound for dot/dash duration digitalWrite( out[0] == '.' ? dot_btn.led_pin:dash_btn.led_pin, HIGH ); digitalWrite( key_btn.led_pin, HIGH ); toneAC( out[0] == '.' ? DOT_FREQ:DASH_FREQ, VOLUME, ms, true ); coDelay(ms) // add to input recognizer if (out[0] == '-') in_data |= (0x80 >> in_length); in_length++; in_tm = millis(); // show on neopixel strip morseBitzToPixelz( in_data, in_length ); // turn off LEDs digitalWrite( out[0] == '.' ? dot_btn.led_pin:dash_btn.led_pin, LOW ); digitalWrite( key_btn.led_pin, LOW ); coDelay(timeWarp(SPACE_TIME)) out.remove(0,1); } coEnd } //----------------------------------------------------------- //-- LEARN mode --------------------------------------------- //----------------------------------------------------------- // play one learn char int learn_index = -1; void learnCharTask( boolean kill_task = false ) { static byte i; static byte dit; static uint32_t ms; static int _state_ = 0; if (kill_task) { // task clean up _state_ = 0; learn_index = -1; toneAC(); return; } coBegin coWaitWhile( learn_index == -1 ) // wait for command // show the character display.write_ascii( 3, morse_code_table[learn_index].ascii ); display.paint_display(); morseBitzToPixelz( morse_code_table[ learn_index ].data, morse_code_table[ learn_index ].length ); // play the character for (i=0; i < 2; i++) // replay twice { for (dit = 0; dit < morse_code_table[learn_index].length; dit++) { ms = morse_code_table[learn_index].data & (0x80 >> dit) ? DASH_TIME:DOT_TIME; ms = timeWarp(ms); toneAC( morse_code_table[learn_index].data & (0x80 >> dit) ? DASH_FREQ:DOT_FREQ, VOLUME, ms, true ); coDelay(ms) toneAC(); coDelay(timeWarp(SPACE_TIME)) } coDelay( 1000 ) } learn_index = -1; coEnd } // run the learn interface void learnTask( boolean kill_task = false ) { static boolean auto_mode = false; static byte learn_i = 0; // auto mode char index static boolean first = true; static int _state_ = 0; if (kill_task) { // task clean up _state_ = 0; toneAC(); clearPixelz(); return; } coBegin coDebounce( 50, digitalRead(key_btn.btn_pin) && digitalRead(dot_btn.btn_pin) && digitalRead(dash_btn.btn_pin) ) answer = -1; if (!auto_mode) { coWaitWhile(answer == -1) } // yield until button else { coDelayWhile(5000,answer == -1) // yield until button or timeout if (answer == -1) // timeout { if (learn_i >= MORSE_TABLE_SIZE-1) learn_i = 0; else learn_i++; } } if (answer == 0) { auto_mode = !auto_mode; if (!auto_mode) { learnCharTask( true ); coContinue } } if (answer == 1) { if (!first) { if (learn_i == 0) learn_i = MORSE_TABLE_SIZE-1; else learn_i--; } } if (answer == 2) { if (!first) { if (learn_i >= MORSE_TABLE_SIZE-1) learn_i = 0; else learn_i++; } } first = false; // play the char learnCharTask( true ); // kill currently playing char display.paint_fill(' '); // blank the display coDelay( 250 ) // start playing the character learn_index = learn_i; coEnd } //----------------------------------------------------------- //----------------------------------------------------------- //----------------------------------------------------------- void setup() { //Serial.begin(115200); //Serial.println("STARTED!"); pinMode( key_btn.btn_pin, INPUT_PULLUP ); pinMode( key_btn.led_pin, OUTPUT ); pinMode( dot_btn.btn_pin, INPUT_PULLUP ); pinMode( dot_btn.led_pin, OUTPUT ); pinMode( dash_btn.btn_pin, INPUT_PULLUP ); pinMode( dash_btn.led_pin, OUTPUT ); pinMode( MODE_RX_PIN, INPUT_PULLUP ); pinMode( MODE_TX_PIN, INPUT_PULLUP ); pixelz.begin(); // INITIALIZE NeoPixel strip object (REQUIRED) pixelz.clear(); pixelz.show(); clearPixelz(); Wire.begin(); display.begin(); // startup display display.paint_string(" MORSE"); display.paint_display(); // hold down Key button at power on to initialize EEPROM if (!digitalRead(key_btn.btn_pin)) { while (!digitalRead(key_btn.btn_pin)) random(); initializeRandom(); } // get random seed loadSeed(); // generate 1'st receive level generateRandomLevel(); // splash dwell delay(2000); } //- - - - - - - - - - - - - // locate current seed // (first nonzero seed) void loadSeed() { // get random seed from EEPROM uint32_t seed; // locate cursor (zero element) for (int i=0; i < 256; i++) { EEPROM.get( i*4, seed ); if (seed == 0) { i = (i+1) % 256; // current seed is next element EEPROM.get( i*4, seed ); randomSeed(seed); return; } } // hmmm. initializeRandom(); } //----------------------------------------------------------- void loop() { // morse recognition tester (receive) if (!digitalRead(MODE_RX_PIN)) { display.paint_string( "RECEIVE" ); display.paint_display(); delay(2000); displayRandomLevel(); delay(2000); while (!digitalRead(MODE_RX_PIN)) { recognition(); playerTask(); answerTask(); random(); } // clean up tasks playerTask(true); recognition(true); saveRandom(); return; } // morse key (sender) if (!digitalRead(MODE_TX_PIN)) { display.paint_string( " KEY" ); display.paint_display(); while (!digitalRead(MODE_TX_PIN)) { keyTask(); outputTask(); inputTask(); random(); } // clean up tasks outputTask(true); saveRandom(); return; } // learn morse code display.paint_string( " LEARN" ); display.paint_display(); buttonLeds(HIGH); while (digitalRead(MODE_TX_PIN) && digitalRead(MODE_RX_PIN)) { learnTask(); learnCharTask(); answerTask(); // get button input random(); } // clean up tasks learnTask(true); learnCharTask(true); buttonLeds(LOW); saveRandom(); }