/* morse_buddy - by Don Cross http://cosinekitty.com An electronic friend with whom I can converse in Morse Code, this program acts as a prototype for a Morse Code operating system. */ const int pin_MorseIn = 8; // digital pin 8 is physical pin 14 on the Atmega8 const int pin_MorseOut = 13; // a.k.a. the "LED pin", this is physical pin 19 const int pin_LED_ga = 7; // phyiscal pin 13 (ga = green a : tri-color on right) const int pin_LED_ra = 6; // physical pin 12 (ra = red a : tri-color on right) const int pin_LED_gb = 5; // phyiscal pin 11 (gb = green a : tri-color on left) const int pin_LED_rb = 12; // physical pin 12 (rb = red a : tri-color on left) const int pin_LED_ya = 2; // physical pin 4 (ya = yellow a : right yellow LED) const int pin_LED_yb = 3; // physical pin 5 (yb = yellow b : middle yellow LED) const int pin_LED_yc = 4; // physical pin 6 (yc = yellow c : left yellow LED) const int analog_cad = 5; // analog pin 5 is CdS light sensor: physical pin 28 const unsigned char LedPinLookup[] = { pin_LED_ra, pin_LED_ga, pin_LED_rb, pin_LED_gb, pin_LED_ya, pin_LED_yb, pin_LED_yc }; //--------------------------------------------------------------------------------------- const int MAX_LINE_LENGTH = 64; int pace = 60; // milliseconds per morse beat (dit) bool interrupt = false; // set to true when user wants the computer to SHUT UP AND LISTEN!!! //--------------------------------------------------------------------------------------- void TurnOffLeds() { // turn off all LEDs... for (int i=0; i < sizeof(LedPinLookup); ++i) { digitalWrite (LedPinLookup[i], LOW); } } void AttentionSignal() { for (int repeat=0; repeat<3; ++repeat) { for (int pulse=0; pulse<5; ++pulse) { digitalWrite (pin_MorseOut, HIGH); delay (20); digitalWrite (pin_MorseOut, LOW); delay (20); } delay (100); } } void LedDisplayInteger (int x) // displays the lowest 5 bits of x: (0..31 decimal) { digitalWrite (pin_LED_ga, (x & 1) ? HIGH : LOW); digitalWrite (pin_LED_gb, (x & 2) ? HIGH : LOW); digitalWrite (pin_LED_ya, (x & 4) ? HIGH : LOW); digitalWrite (pin_LED_yb, (x & 8) ? HIGH : LOW); digitalWrite (pin_LED_yc, (x & 16) ? HIGH : LOW); // make sure red LEDs are off... digitalWrite (pin_LED_ra, LOW); digitalWrite (pin_LED_rb, LOW); } //--------------------------------------------------------------------------------------- bool CheckInterrupt() // should be called only when we know computer is not talking { if (!interrupt) { int noise = digitalRead (pin_MorseIn); if (noise) { interrupt = true; // now, we need to wait until the line is quiet for 0.1 seconds straight. // this allows us to eat any noise the user is making for the purpose of interrupt, // so that when we start listening again, we don't consider it part of the next message. int quiet_count = 0; // number of consecutive milliseconds we have heard silence while (quiet_count < 100) { delay(1); noise = digitalRead (pin_MorseIn); if (noise) { quiet_count = 0; // start over } else { ++quiet_count; } } // Now send rapid pulses to let user know we heard the interrupt... AttentionSignal(); } } return interrupt; } bool DelayAndListen (int millis) { if (interrupt) { return true; // interrupt has already been detected, so bail out immediately } else { const int safe_wait_for_quiet = 10; // allow this many milliseconds for external relay to open for (int counter=0; counter < millis; ++counter) { delay(1); if (counter >= safe_wait_for_quiet) { if (CheckInterrupt()) { return true; } } } return false; } } //--------------------------------------------------------------------------------------- bool ColorDutyCycle ( int delay_us, int duty_ra, int duty_ga, int duty_rb, int duty_gb, int duty_ya, int duty_yb, int duty_yc) { if (!interrupt) { for (int count=0; count < 100; ++count) { delayMicroseconds (delay_us); digitalWrite (pin_LED_ra, (count < duty_ra) ? HIGH : LOW); digitalWrite (pin_LED_ga, (count < duty_ga) ? HIGH : LOW); digitalWrite (pin_LED_rb, (count < duty_rb) ? HIGH : LOW); digitalWrite (pin_LED_gb, (count < duty_gb) ? HIGH : LOW); digitalWrite (pin_LED_ya, (count < duty_ya) ? HIGH : LOW); digitalWrite (pin_LED_yb, (count < duty_yb) ? HIGH : LOW); digitalWrite (pin_LED_yc, (count < duty_yc) ? HIGH : LOW); } } return CheckInterrupt(); } //--------------------------------------------------------------------------------------- void tone (int beats) { if (!interrupt) { digitalWrite (pin_MorseOut, HIGH); delay (pace * beats); digitalWrite (pin_MorseOut, LOW); DelayAndListen (pace); } } void letter_pause() { DelayAndListen (15 * pace); } void word_pause() { DelayAndListen (20 * pace); } //--------------------------------------------------------------------------------------- const unsigned char MORSE_NULL = 0x0f; const unsigned char AlphaTable[] = { // same encoding as used in c:\don\dev\fun\morse\beepboot\beepboot.asm 0x22, 0x61, 0x65, 0x41, // ABCD 0x00, 0x64, 0x43, 0x60, // EFGH 0x20, 0x6e, 0x45, 0x62, // IJKL 0x23, 0x21, 0x47, 0x66, // MNOP 0x6b, 0x42, 0x40, 0x01, // QRST 0x44, 0x68, 0x46, 0x69, // UVWX 0x6d, 0x63 // YZ }; const unsigned char NumeralTable[] = { 0x9f, 0x9e, 0x9c, 0x98, 0x90, // 01234 0x80, 0x81, 0x83, 0x87, 0x8f // 56789 }; const unsigned char PunctuationTable[] = { // ascii, encoded '.', 0xea, ',', 0xf3, '?', 0xcc, '\'', 0xde, ':', 0xc7, '-', 0xe1, '@', 0xd6, '=', 0x91, 0x00 // marks end of list }; void PlayEncodedByte (unsigned char data) { if (data == MORSE_NULL) { word_pause(); // this is how we handle the space character, and anything we don't understand. } else { unsigned char count = (data >> 5) & 7; // number of dahs/dits (minus 1) is encoded in uppermost 3 bits if (count & 4) { count = (count >> 1) ^ 6; // special case: squeeze an extra data bit using weird count encoding } do { // use do/while loop so that count==0 gives us 1 loop, count==1 gives us 2 loops, etc. tone (((data & 1) << 1) | 1); // based on lowest bit, call either tone(1) or tone(3). data >>= 1; } while (count--); } letter_pause(); // note that in the case of MORSE_NULL, this is in addition to word_pause (on purpose!) } void SendMorseChar (char c) { char encoded = MORSE_NULL; // in case we don't find the character, default to MORSE_NULL if (c >= 'a' && c <= 'z') { encoded = AlphaTable [c - 'a']; } else if (c >= 'A' && c <= 'Z') { encoded = AlphaTable [c - 'A']; } else if (c >= '0' && c <= '9') { encoded = NumeralTable [c - '0']; } else { // search for character in PunctuationTable[]... const unsigned char *punc = PunctuationTable; while (*punc) { if (c == punc[0]) { encoded = punc[1]; // we found it! break; } punc += 2; } } PlayEncodedByte (encoded); } void SendMorseString (const char *string) { for (int i=0; string[i]; ++i) { SendMorseChar (string[i]); } } void SendMorseInteger (int x) { char digits [12]; int numdigits = 0; if (x == 0) { SendMorseChar('0'); } else { if (x < 0) { SendMorseChar('-'); x = -x; // take absolute value } do { digits[numdigits++] = '0' + (x % 10); x /= 10; } while (x != 0); while (numdigits > 0) { SendMorseChar (digits[--numdigits]); } } } //---------------------------------------------------------------------------------------------- char DecodeToneBuffer (const char *tones, int tonecount) { // convert the tone buffer into an encoded morse byte. unsigned char data = 0; for (int i=tonecount-1; i >= 0; --i) { data <<= 1; if (tones[i] == '-') { data |= 1; } } int encoded_count = tonecount - 1; if (encoded_count == 5) { ++encoded_count; // weird encoding rule that allows for extra data bit } data |= (encoded_count << 5); // search all tables for the matching encoded byte. unsigned char decode; for (decode='a'; decode <= 'z'; ++decode) { if (AlphaTable[decode-'a'] == data) { return decode; } } for (decode='0'; decode <= '9'; ++decode) { if (NumeralTable[decode-'0'] == data) { return decode; } } const unsigned char *p = PunctuationTable; while (p[0]) { if (p[1] == data) { return p[0]; } p += 2; } return '\0'; // could not locate this sequence of dits and dahs! } //---------------------------------------------------------------------------------------------- const int MinTime_dit = 5; const int MinTime_dah = 150; const int MinTime_letter = 500; const int MinTime_word = 1600; char ReceiveMorseChar() { const int MAXTONES = 6; char tones [1+MAXTONES] = {0}; // '.' or '-' for each tone received int tonecount = 0; // how many dahs/dits we received char userchar = '\0'; int current_state = digitalRead (pin_MorseIn); int new_state; int noise_clock=0; int silence_clock = 0; int debounce = 0; while (userchar == '\0') { delay(1); new_state = digitalRead (pin_MorseIn); if (new_state == current_state) { debounce = 0; // reset debounce counter if (current_state) { ++noise_clock; } else { ++silence_clock; if (tonecount > 0) { if (silence_clock >= MinTime_letter) { tones[tonecount] = '\0'; // make sure string is null terminated userchar = DecodeToneBuffer (tones, tonecount); } } } } else { // debounce: make sure we see the same state change for a stable period of time. if (++debounce == MinTime_dit) { debounce = 0; current_state = new_state; if (current_state) { // tone is ON: start the noise_clock again noise_clock = 0; } else { // tone is OFF: start the silence_clock again silence_clock = 0; // determine which kind of tone we heard... char tonetype = (noise_clock >= MinTime_dah) ? '-' : '.'; if (tonecount == MAXTONES) { // tone count overflow! ignore this character... break; } tones[tonecount++] = tonetype; //Serial.print (tonetype, BYTE); } } } } return userchar; } //---------------------------------------------------------------------------------------------- typedef bool (* COMMANDFUNC) (const char *args); // returns true if command is "successful" struct tCommandInfo { const char *verb; COMMANDFUNC func; }; bool Command_loop (const char *args) { while (!interrupt) { SendMorseString (args); SendMorseChar ('.'); } return true; } bool Command_alpha (const char *args) { while (!interrupt) { for (char letter='a'; !interrupt && (letter <= 'z'); ++letter) { SendMorseChar (letter); } } return true; } void ColorFade() { int duty; while (!interrupt) { for (duty=0; duty<100; ++duty) { if (ColorDutyCycle (50, duty, 99-duty, 99-duty, duty, 0, 0, 0)) { break; } } for (; duty>=0; --duty) { if (ColorDutyCycle (50, duty, 99-duty, 99-duty, duty, 0, 0, 0)) { break; } } } TurnOffLeds(); } bool Command_led (const char *args) { bool success = true; char count = 0; switch (*args) { case 'c': while (!interrupt) { LedDisplayInteger (count); DelayAndListen (300); count = (1 + count) & 31; } // fall through on purpose, to turn off LEDs... case 'd': TurnOffLeds(); break; case 'f': ColorFade(); break; default: success = false; // unknown argument break; } return success; } int int_sqrt (int x) { int r = 0; if (x > 0) { while (r*r < x) { ++r; } --r; } return r; } bool Command_lum (const char *args) { int prev = 0; while (!interrupt) { DelayAndListen(50); int luminance = analogRead (analog_cad); int adjusted = int_sqrt (luminance); // convert 0..1023 to 0..31 LedDisplayInteger (adjusted); if (*args=='s') { luminance /= 10; // make less sensitive to tiny light fluctuations if (luminance != prev) { tone(1); prev = luminance; } } } TurnOffLeds(); // they seem to make my voltage regulator hot, so they must be using a lot of power! } bool Command_pulse (const char *args) { int silence_ms = 1000; int prev = silence_ms; while (!CheckInterrupt()) { digitalWrite (pin_MorseOut, HIGH); delay(15); digitalWrite (pin_MorseOut, LOW); DelayAndListen (silence_ms); silence_ms = (9 * silence_ms) / 10; if (silence_ms <= 10 || silence_ms == prev) { silence_ms = 1000; } prev = silence_ms; } } bool Command_clock (const char *args) { unsigned long target_time = millis(); int minute_tally = 0; while (!CheckInterrupt()) { unsigned long now = millis(); if (now >= target_time) { SendMorseInteger (minute_tally++); target_time += 60000; } } return true; } const tCommandInfo CommandTable[] = { {"alpha", Command_alpha}, {"clock", Command_clock}, {"led", Command_led}, {"loop", Command_loop}, {"lum", Command_lum}, {"pulse", Command_pulse} }; const int COMMAND_TABLE_SIZE = sizeof(CommandTable) / sizeof(CommandTable[0]); const tCommandInfo *LookupCommand (const char *verb) { for (int i=0; i < COMMAND_TABLE_SIZE; ++i) { if (0 == strcmp(verb, CommandTable[i].verb)) { return &CommandTable[i]; } } return NULL; // could not find the command! } void ExecuteCommand (char *command) { int i; bool success = false; for (i=0; command[i] && command[i]!=' '; ++i) { // do nothing } if (command[i]) { command[i++] = '\0'; // patch and skip - note that repeated spaces are impossible! } const tCommandInfo *info = LookupCommand (command); if (info) { success = (*info->func) (&command[i]); } if (!success) { SendMorseChar ('?'); } } //---------------------------------------------------------------------------------------------- char UserInputString [1+MAX_LINE_LENGTH] = {0}; int UserInputIndex = 0; bool SpaceCharEmitted = false; int SpaceSilenceCount = 0; void ResetUserString() { UserInputString[UserInputIndex=0] = '\0'; // reset for next string SpaceCharEmitted = false; SpaceSilenceCount = 0; interrupt = false; } void AppendUserChar (char userchar) { // try to append this character to the user string //Serial.print (userchar, BYTE); if (UserInputIndex < MAX_LINE_LENGTH) { UserInputString[UserInputIndex++] = userchar; UserInputString[UserInputIndex] = '\0'; // always keep string null terminated } SpaceCharEmitted = (userchar == ' '); } //---------------------------------------------------------------------------------------------- void LedTest() { digitalWrite (pin_LED_ra, HIGH); digitalWrite (pin_LED_rb, HIGH); delay(300); LedDisplayInteger (31); delay(300); TurnOffLeds(); } //---------------------------------------------------------------------------------------------- void setup() { pinMode (pin_MorseIn, INPUT); pinMode (pin_MorseOut, OUTPUT); for (int i=0; i < sizeof(LedPinLookup); ++i) { pinMode (LedPinLookup[i], OUTPUT); } //Serial.begin (9600); LedTest(); AttentionSignal(); } //---------------------------------------------------------------------------------------------- void loop() { // do we hear anything? int noise = digitalRead (pin_MorseIn); if (noise) { SpaceSilenceCount = 0; char userchar = ReceiveMorseChar(); if (userchar) { if (userchar == '.') { //Serial.println("."); // this is like pressing ENTER: it signifies the user is done with this command delay (300); // small delay so user can hear output ExecuteCommand (UserInputString); ResetUserString(); } else { AppendUserChar (userchar); } } } else { // nothing to report yet, sir... delay(1); // maybe this will help us burn up less power? (need to research) // special case: treat periods of silence as entering a space, as needed. if (!SpaceCharEmitted && (UserInputIndex > 0)) { if (++SpaceSilenceCount == MinTime_word) { SpaceSilenceCount = 0; AppendUserChar (' '); } } } } //---------------------------------------------------------------------------------------------- /* $Log: morse_buddy.pde,v $ Revision 1.24 2006/11/29 02:32:30 dcross Was able to squeeze in the clock command after all, and have it play the minutes in morse code! Revision 1.23 2006/11/29 02:29:19 dcross Binary sketch size: 6896 bytes (of a 7168 byte maximum) Achieved smaller size by more replacing int with char, plus realized that loop logic for skipping spaces is not needed; no more than one consecutive space is possible. Revision 1.22 2006/11/29 02:16:49 dcross Removed led_echo functionality... just not worth it... now down to 6960 bytes. Revision 1.21 2006/11/29 01:57:38 dcross Added experimental clock command, and determined that the time function millis() is quite accurate. Commented it out, but I am checking in the commented out code for reference later. Revision 1.20 2006/11/28 22:40:08 dcross Saved another 32 bytes by minor optimization to PlayEncodedByte. It really pays to use char instead of int wherever possible, because far fewer instructions are needed that way. Binary sketch size: 7032 bytes (of a 7168 byte maximum) Revision 1.19 2006/11/28 22:20:24 dcross Saved 54 bytes by removing serial I/O. before removing serial: Binary sketch size: 7118 bytes (of a 7168 byte maximum) after removing serial: Binary sketch size: 7064 bytes (of a 7168 byte maximum) Revision 1.18 2006/11/27 23:18:19 dcross In keeping with the power-saving philosophy, changed default for led_echo to false. Revision 1.17 2006/11/27 23:02:24 dcross Made pulse command leave sound on for 15 ms instead of just 5 ms, so it is more audible. Added 's' option to 'lum' command that sounds a brief tone every time the light level changes. Looks like running LEDs all the time causes the voltage regulator to get pretty hot, so I have decided to turn them off automatically wherever they have been left on. Revision 1.16 2006/11/27 22:07:28 dcross Decreased delay in "led f" command, because faster color fade just looks cooler. Revision 1.15 2006/11/26 01:32:45 dcross Added silly Command_pulse. Revision 1.14 2006/11/26 01:14:45 dcross Shaved off 4 bytes of program memory: Binary sketch size: 6900 bytes (of a 7168 byte maximum) Revision 1.13 2006/11/26 01:11:36 dcross 1. Made bootup LED test simpler, to save scarce code memory. 2. Added ability to do ColorFade on yellow LEDs also. Revision 1.12 2006/11/26 00:49:47 dcross Added subcommand "led a" for toggling the led_echo state. Revision 1.11 2006/11/26 00:46:16 dcross Now take square root of analog value in "lum" command instead of dividing. The idea is that the output will be closer to logarithmic in behavior, and therefore more in keeping with human perception of brightness. Also made code smaller by using a loop, since right now we have very little program memory left: "Binary sketch size: 6978 bytes (of a 7168 byte maximum)" Revision 1.10 2006/11/25 22:21:13 dcross 1. Decreased MinTime_word to 1.6 seconds (was 2.0 seconds... too slow). 2. Got rid of annoying blink in ColorDutyCycle. 3. Added echo of all characters lowest 5 bits with ASCII code. Revision 1.9 2006/11/25 21:31:25 dcross Updated for second rev of hardware: more LEDs! Revision 1.8 2006/11/24 21:20:14 dcross 1. slowed down pace from 55 ms to 60 ms. 2. now play attention signal when ready, instead of slow "ready." in morse code. 3. added "loop" command. 4. Added "led" command, with hardware change of yellow and red LEDs on 2 new output pins. 5. Added cadmium sulfide photoresistor to analog input, and tested with new command "lum". 6. Fixed bug with extraneous spaces every now and then: was forgetting to reset SpaceSilenceCount. Revision 1.7 2006/11/24 19:29:17 dcross Added utility function ScanToken. Added commands mul and alpha. Revision 1.6 2006/11/24 19:12:09 dcross 1. When user interrupt occurs, play rapid pulses to let user know the interrupt was noticed. 2. Added count command. 3. Fixed bug: when user interrupts initial "ready" message, we need to reset the interrupt state, otherwise the first command will get interrupted immediately. Revision 1.5 2006/11/24 18:52:27 dcross Got logic working for entering spaces. Revision 1.4 2006/11/24 17:24:46 dcross Getting command loop working. Revision 1.3 2006/11/24 16:58:18 dcross Starting to get characters recognized! Revision 1.2 2006/11/24 15:44:27 dcross Starting to flesh out how user interrupt will work. Revision 1.1 2006/11/24 15:09:02 dcross Stole code from morse_alpha project, to get ready for this more ambitious project. My intent is to make the microcontroller wait for commands in Morse Code, which it executes and then responds back to the user by sending Morse Code back. */