#include /* * MorseTransceiver.ino * * Created on: Mar01 2013 * * By: evanmj@gmail.com * * Based morse code/timing from: http://morsecode.scphillips.com/morse2.html * * TODO: Consider beeping while button down? Mayb as an option. * TODO: Not all supported characters handled properly. Need added to array. * TODO: Prosigns not supported, SOS being one of them as 3 chars it no char space. * TODO: full stop (period char) should newline on websocket. * TODO: If any websockets connected, avoid timeout! * TODO: REST output on some signal, maybe full stop, but probably something less used like "!"? * TODO: Handle delete key to clear screen or backspace. "........" * TODO: Different noise if too high or too low * TODO: Shutting lever for a while needs to kill it even when being transmitted to. * */ #include #include #include #include #include #include #include #include #include #define USE_SERIAL Serial // Setup automatic magic wifi selection system. ESP8266WiFiMulti WiFiMulti; // Instantiate server objects ESP8266WebServer server = ESP8266WebServer(80); WebSocketsServer webSocket = WebSocketsServer(81); /* CONSTANTS */ //static unsigned long KEY_DEBOUNCE_TIME = 6000; // Units: microseconds. Per: http://www.qsl.net/sv8gxc/blog/100905-03.jpg, 1.2 sec per pulse = 1 WPM, 140WPM is super fast, so .006 seconds (6ms) seems legit for debounce (max 200WPM detectable). static unsigned long KEY_DEBOUNCE_TIME = 9000; // Units: microseconds. Make a bit softer. A capacitor will probably help but there is not one within reach. static float PULSE_DURATION_THRESHOLD = 0.50; // How close does a duration need to be to a previous duration to match that it is a dot or a dash? (% of time). Higher is easier on the user, but don't go over 50% of you'll lose the ability to distingusih a dot from a dash! static float DASH_TO_DOT_TIME_RATIO = 3.0; // This is standard, the time a dot is is one time unit, one dash is 3 time units. static float TIME_BETWEEN_CHARS = 3.0; // This is standard, three non transmitting time constant between characters, static float TIME_BETWEEN_WORDS = 7.0; // This is standard, the non transmitting time constant between words, typically 7 dot time constants. static float DEFAULT_BASE_TIME_MICROS = 100000; // Default Base time, receive sounds at this rate until user keys in a new rate. Number in microseconds per 'dot' time constant. static unsigned long RECIEVE_SOUND_FREQ = 1000; // Frequency to play incoming data (Hz) 600-1000Hz typical. static unsigned long NO_INPUT_TIMEOUT = 60000; // 30 seconds to shut down with no input. static unsigned long KEY_HELD_TIMEOUT = 5000; // 5 seconds to shut down with key held down. /* LOCAL VARIABLES */ int iState; // State machine state. int iLastState; // Previous State machine state. bool bFirstScanInState; // True for one time through when state is first entered. bool bCharEndTimerElapsed; // True when CharEnd timer expires, user lets off for 3 key durations. bool bWD_Done; // Watchdog timer elapsed. // Time keeping of input key pushes volatile unsigned long iKeyStateChangedTime = 0; // Value in microseconds. volatile unsigned long iKeyInStateForDeltaTime = 0; // How long was the key in state for? bool bKeyPressed; // We will need to keep track of if the button is pressed or not, and the duration. False = not pressed, True = pressed. unsigned long iBaseKeyDuration = DEFAULT_BASE_TIME_MICROS; // Scratch space for storing previous key lengths. Start with a number in case we sound out a message before user inputs one to set base rate. // Declare timer Timer Watchdog_tmr; int Watchdog_tmrEventID; Timer CharEndTimer; int CharEnd_tmrEventID; //In progress character String DotDashString; // dot-dash character array. char DecodedChar; // ascii char decoded from user input //In progress String, pre send to REST, etc. Terminated (eventually) by full stop (period). String CurrentDecodedString; char NewestDecodedCharacter; const char latinAlphabet[30] = { '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', '.', ',' }; // NOTE: Does not handle lower case. Convert all inputs to upper before comparing. const String morseAlphabet[30] = { ".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---", "-.-", ".-..", "--", "-.", "---", ".--.", "--.-", ".-.", "...", "-", "..-", "...-", ".--", "-..-", "-.--", "--..", ".-.-.-", "--..--" }; String DecodedStringToTransmit; // String in morse to send to the speaker. String PayloadAsString; // Uppercase String in morse to send to the speaker. /* END LOCAL VARIABLES */ void morse_to_sound(String inputstring, unsigned freq); void string_to_morse(String &dest, String inputstring); /* WEB FUNCTIONS */ // This function runs when a new websocket event arrives. void webSocketEvent(uint8_t num, WStype_t type, uint8_t * payload, size_t length) { switch(type) { case WStype_DISCONNECTED: USE_SERIAL.printf("[%u] Disconnected!\n", num); break; case WStype_CONNECTED: { IPAddress ip = webSocket.remoteIP(num); USE_SERIAL.printf("[%u] Connected from %d.%d.%d.%d url: %s\n", num, ip[0], ip[1], ip[2], ip[3], payload); // send message to client webSocket.sendTXT(num, "Connected! This message is from the ESP"); } break; case WStype_TEXT: USE_SERIAL.printf("[%u] get Text: %s\n", num, payload); Serial.println(); // Javascript prepends a 't' to let us know to transmit. Maybe S = settings, etc. if(payload[0] == 't') { // we got command data that we need to transmit. PayloadAsString = (const char *) &payload[1]; // Make uppercase so string_to_morse likes it. PayloadAsString.toUpperCase(); // Trim leading character, we are done with it ("t" in this case) //PayloadAsString.substring(1); // Null this so we don't concat like string_to_morse does. DecodedStringToTransmit = ""; // Convert it to dots and dashes. string_to_morse(DecodedStringToTransmit,PayloadAsString); Serial.println("Sounding morse payload: " + DecodedStringToTransmit); morse_to_sound(DecodedStringToTransmit, RECIEVE_SOUND_FREQ); } break; case WStype_BIN: USE_SERIAL.printf("[%u] get binary length: %u\n", num, length); break; } } // Unused, needs implemented instead of other lame 404 /*void handleNotFound(){ digitalWrite(led, 1); String message = "File Not Found\n\n"; message += "URI: "; message += server.uri(); message += "\nMethod: "; message += (server.method() == HTTP_GET)?"GET":"POST"; message += "\nArguments: "; message += server.args(); message += "\n"; for (uint8_t i=0; i KEY_DEBOUNCE_TIME) { iKeyInStateForDeltaTime = micros() - iKeyStateChangedTime; // NOTE: BUG HERE. When this rolls over iKeyStateChangedTime = micros(); // Mark for next time. //Serial.printf("Rising Edge, Key Pressed Going to 0, was on for %d micros", iKeyInStateForDeltaTime); //Serial.println(); attachInterrupt(digitalPinToInterrupt(D7), fallingedge, FALLING); // Now set to interrupt on falling edge. bKeyPressed = 0; } else { //Serial.println("Nulled some rising fuzz"); } } void fallingedge() { // Start with debounce if ((micros() - iKeyStateChangedTime) > KEY_DEBOUNCE_TIME) { iKeyInStateForDeltaTime = micros() - iKeyStateChangedTime; // NOTE: BUG HERE. When this rolls over iKeyStateChangedTime = micros(); // Mark for next time. //Serial.printf("Falling Edge, Key Pressed Going to 1, was on for %d micros", iKeyInStateForDeltaTime); //Serial.println(); attachInterrupt(digitalPinToInterrupt(D7), risingedge, RISING); // Now set to interrupt on rising edge. bKeyPressed = 1; iKeyStateChangedTime = micros(); } else { //Serial.println("Nulled some falling fuzz"); } } void WDTimerElapsed() { Serial.println("WDTimerElapsed() Running."); bWD_Done = 1; } void CharEndTimerElapsed() { bCharEndTimerElapsed = 1; } void setup() { // Set GPIO2 high for reset pin... This will keep things on until we are done. must have 1k or bigger to CH_PD/EN pin. pinMode(D4, OUTPUT); // Initialize pin as an output digitalWrite(D4,HIGH); pinMode(D7, INPUT); // Initialize pin as an input, input from key Serial.begin(115200); Serial.setDebugOutput(true); Serial.println(); Serial.println(); Serial.println(); //WiFiManager //Local intialization. Once its business is done, there is no need to keep it around WiFiManager wifiManager; //reset settings - for testing //wifiManager.resetSettings(); //tries to connect to last known settings //if it does not connect it starts an access point with the specified name //here "AutoConnectAP" with password "password" //and goes into a blocking loop awaiting configuration if (!wifiManager.autoConnect("AutoConnectAP", "password")) { Serial.println("failed to connect, we should reset as see if it connects"); delay(3000); ESP.reset(); delay(5000); } USE_SERIAL.println("Bringing up SPIFFS"); SPIFFS.begin(); // start webSocket server webSocket.begin(); webSocket.onEvent(webSocketEvent); if(MDNS.begin("arenabot")) { USE_SERIAL.println("MDNS responder started"); } // handle notfound by pulling files from eeprom if they exist. //called when the url is not defined here //use it to load content from SPIFFS server.onNotFound([](){ if(!handleFileRead(server.uri())) /// if reding from eeprom fails, send 404 server.send(404, "text/plain", "FileNotFound"); }); server.begin(); // Add service to MDNS MDNS.addService("http", "tcp", 80); MDNS.addService("ws", "tcp", 81); // Init status of pressed bit: if (digitalRead(D7),0) { bKeyPressed = 1; //Serial.println("Key was pressed at start"); // when pin D7 goes high, call the rising function attachInterrupt(digitalPinToInterrupt(D7), risingedge, RISING); } else { bKeyPressed = 0; //Serial.println("Key was not pressed at start"); // when pin D7 goes high, call the rising function attachInterrupt(digitalPinToInterrupt(D7), fallingedge, FALLING); } // Beep so the user knows we are online and connected to wifi. analogWriteFreq(1400); analogWrite(D1, 512); delay(200); analogWrite(D1, 0); //delay(200); analogWriteFreq(1800); analogWrite(D1, 512); delay(100); analogWrite(D1, 0); //delay(200); analogWriteFreq(2200); analogWrite(D1, 512); delay(400); analogWrite(D1, 0); //delay(500); //Debug /*delay(2000); String MySOS = "SOS"; String output; letters_to_morse(output,MySOS); Serial.println(output); morse_to_sound(output, RECIEVE_SOUND_FREQ); delay(2000); morse_to_sound(output, RECIEVE_SOUND_FREQ); delay(2000); morse_to_sound(output, RECIEVE_SOUND_FREQ);*/ //End Debug // Handle printing morse code from URL Request server.on("/write", []() { // TODO: Does not handle spaces properly, repeats a character or somethjing. String val=server.arg("val"); // Force Uppercase val.toUpperCase(); String val_as_morse; // i.e. "..----...--" version of the string the user typed. String val_char_as_morse; // Just one character worth of dots and dashes at a time to morse since some contain other subchars in dot dash land... :-/ string_to_morse(val_as_morse,val); // Convert to morse code string, we won't use 'val_as_morse' except to send it to the user, we need it a character at a time so we can delay properly on send. Serial.println("Got value from URL to sound out: " + val); server.send(200, "text/plain", "Sounding String " + val + ": " + val_as_morse); for (unsigned i = 0; i (iBaseKeyDuration * (1 - PULSE_DURATION_THRESHOLD))) ) // Are we +/- 50% (depending on constant) from valid timing? { Serial.println("110: ."); DotDashString += "."; // Concatenate input we just received. iState = 120; } else if(iKeyInStateForDeltaTime < (iBaseKeyDuration * ((1 + PULSE_DURATION_THRESHOLD) * DASH_TO_DOT_TIME_RATIO)) && (iKeyInStateForDeltaTime > (iBaseKeyDuration * ((1 - PULSE_DURATION_THRESHOLD) * DASH_TO_DOT_TIME_RATIO))) ) // Are we 250%-350%? That would be a dash. { Serial.println("110: -"); DotDashString += "-"; iState = 120; } else{ Serial.printf("Bad Timing: iBaseKeyDuration Was: %d and you entered: %d", iBaseKeyDuration, iKeyInStateForDeltaTime); Serial.println(); iState = 900; // Bad timing... Buzz, then Make user re-key base timing. } } // Watch for timeout if (bWD_Done == 1) { Serial.println("WD in State" + iState); iState = 999; // Shutdown. } break; case 120: // Time the dead space before the next key. This accounts for character endings. // Timing is important, as some characters are long and contain other character's dot dash sequences in them. // Runs one time on state entry. if (bFirstScanInState == 1) { // Start watchdog timer. Watchdog_tmrEventID = Watchdog_tmr.after(NO_INPUT_TIMEOUT, WDTimerElapsed); // Set Timer // Watch for "end of character" signal. If greater than 1 duration, we know we are ending a character (at least, maybe a word). CharEnd_tmrEventID = CharEndTimer.after((iBaseKeyDuration * (1 + PULSE_DURATION_THRESHOLD)) / 1000, CharEndTimerElapsed); // Set timer for 1 durations (plus threshold). } // User pressed key. Go! if (bKeyPressed == 1) { iState = 110; // Back to 110, we have another dot or dash as part of this character (because we didn't time out yet). } // Watch for user to delay long enough to attempt to end the dot-dash string as a character. if (bCharEndTimerElapsed == 1) // It has been 3 durations with no push. Character should be over. { iState = 130; // Head to 130 where we will decide if this is a character end or even a word end. } // Watch for timeout. if (bWD_Done == 1) { Serial.println("WD in State" + iState); iState = 999; // Shutdown. } break; case 130: // Mark Character, wait to see if it is a word. // Runs one time on state entry. if (bFirstScanInState == 1) { // Watch for "end of word" signal. If greater than 3 durations, we know we are ending a character (at least, maybe a word). CharEnd_tmrEventID = CharEndTimer.after((iBaseKeyDuration * TIME_BETWEEN_CHARS * (1 + PULSE_DURATION_THRESHOLD)) / 1000, CharEndTimerElapsed); // Set timer for 3 durations (plus threshold). Serial.println("State 130 Character found was: " + String(morse_to_char(DotDashString))); // Mark this character! CurrentDecodedString += String(morse_to_char(DotDashString)); // Append to current working string, we have not sent it anywhere yet. //TODO: Send char to websocket if enabled. webSocket.sendTXT(0, "m" + String(morse_to_char(DotDashString))); // The m is for the "morse" command so the webpage knows this is morse char to display. Serial.println("State 130 Detected Character End after dot dash string: " + DotDashString); // Here we can also parse the newest string to see if we need to send to REST or if we want to newline, etc. // That value is: String(morse_to_char(DotDashString)) DotDashString = ""; // Null string for next time. } // User pressed key. Go! if (bKeyPressed == 1) { iState = 110; // Back to 110, we have another dot or dash as part of this character (because we didn't time out yet). } // Watch for user to delay long enough to attempt to end the dot-dash string as a character. if (bCharEndTimerElapsed == 1) // It has been 3 durations with no push. Character should be over. { iState = 140; // Head to 140 where we will decide if this is a character end or even a word end. } break; case 140: // Character timing exceeded. If we exceed word timing, print a space. // Runs one time on state entry. if (bFirstScanInState == 1) { // Watch for "end of word" signal. If greater than 3 durations, we know we are ending a character (at least, maybe a word). CharEnd_tmrEventID = CharEndTimer.after((iBaseKeyDuration * TIME_BETWEEN_WORDS * (1 + PULSE_DURATION_THRESHOLD)) / 1000, CharEndTimerElapsed); // Set timer for 3 durations (plus threshold). } // User pressed key. Go! if (bKeyPressed == 1) { iState = 110; // Back to 110, we have another dot or dash as part of this character (because we didn't time out yet). } // Watch for user to delay long enough to attempt to end the word. if (bCharEndTimerElapsed == 1) // It has been 3 durations with no push. Character should be over. { iState = 150; // Head to 150 where we will apend the space to the word. } break; case 150: // Mark Space to end word, then do next char or timeout if user is done for good. // Runs one time on state entry. if (bFirstScanInState == 1) { // Mark this character! CurrentDecodedString += " "; // Append a space to current working string. //TODO: Send char to websocket only if connected. webSocket.sendTXT(0, "m "); // The m is for the "morse" command so the webpage knows this is morse char to display. Serial.println("State 150 Detected End of word."); Serial.println("Word received as: " + CurrentDecodedString); //Serial.println("CurrentDecodedString: " + CurrentDecodedString); DotDashString = ""; // Null string for next time. // Start watchdog timer. Watchdog_tmrEventID = Watchdog_tmr.after(NO_INPUT_TIMEOUT, WDTimerElapsed); // Set Timer } // User pressed key. Go! if (bKeyPressed == 1) { iState = 110; // Back to 110, we have another dot or dash as part of this character (because we didn't time out yet). } // Watch for timeout. if (bWD_Done == 1) { Serial.println("WD in State" + iState); iState = 999; // Shutdown. } break; case 900: // Bad Timing Detected. user missed 1x or 3x (depending on constants) timing, start again. delay(iBaseKeyDuration / 1000); // wait hold tight for a sec so they calm down. // Sound buzzer letting user know they did not match the specified base timing. analogWriteFreq(350); // Nice and low for a shaming effect. analogWrite(D1, 512); delay(iBaseKeyDuration / 1000); // on time. analogWrite(D1, 0); Serial.println("Bad Timing, State 900. Resetting to State 5 to reinit base time, clearing DotDashString."); DotDashString = ""; iState = 5; break; case 999: // Timeout. Time to go night night. // Is anyone connecte to the websockets? if (true == true) { // Debug if (bKeyPressed == 0) { Serial.println("Inactivity. Goodbye World."); } else { Serial.println("User closed line. Goodbye World."); } // Die slowly... sounding buzzer (Hz) for(int x = 300; x > 20; x--) { analogWriteFreq(x); analogWrite(D1, 512); delay(3); } analogWrite(D1, 0); digitalWrite(D4,LOW); // Kill ESP until next user event on the slider. ESP.deepSleep(999999999*999999999U, WAKE_NO_RFCAL); // This is a lame work around for the wiring not working just right. Line above should kill it but does not :-/ Sleep probably uses more power than holding EN down. // May not be needed, as digital write does kill the chip to some degree. Testing needed! TODO. delay(1000); // Wait on death to come. } else { // Someone is connected, let's not die, just reset and wait on new timing inputs. Serial.println("Inactivity. Would reset, but we will keep it open for now. Reinitting base timing."); iState = 5; } break; default: Serial.printf("Invalid State: %d",iState); Serial.println(); break; } // Clever stuff on state transition if (iState != iLastState) // State Change Detected. Null Timer { Watchdog_tmr.update(); Watchdog_tmr.stop(Watchdog_tmrEventID); bWD_Done = 0; // Reset for next time. CharEndTimer.update(); CharEndTimer.stop(Watchdog_tmrEventID); bCharEndTimerElapsed = 0; // Reset for next time. bFirstScanInState = 1; // Trigger first scan in state flag for a run-once on the next state. Serial.printf("State Transition from %d to %d ",iLastState,iState); Serial.println(); iLastState = iState; // Update for next time. } else { bFirstScanInState = 0; // Not the first scan anymore. Watchdog_tmr.update(); CharEndTimer.update(); } }