/*
sketch_morse_3.1_analog
MORSE EN-DE-CODER 1.06
A Morse encoder / decoder for the Arduino.
May, 11, 2015
*/
// BB variables
unsigned int pitch = 800;
boolean refreshScreen = true; //false;
const byte pinA = 2; // encoder pin A to Arduino pin 2 which is also interrupt pin 0 which we will use
const byte pinB = 3; // encoder pin B to Arduino pin 3 which is also interrupt pin 1 but we won't use it
byte state = 0; // will store two bits for pins A & B on the encoder which we will get from the pins above
int bump[] = {0, 0, -1, 1};
int level = 0; // rotary encoder
boolean switchState = 1; // encoder pin state
// switch stuff unused!
int encoderSwitchPin = 4; //encoder pin
//boolean switchState = 1; // encoder pushswitch
int increment = 4; // rotary encoder increment - 1
// BB -----------------------------------
// Simple analog input signal threshold (512 = middle-ish)
// Set high enough to avoid noise, low enough to get signal
// ( 512 + noise < AudioThreshold < 512 + signal )
int AudioThreshold = 400; // (BB) works for me using my preamp
unsigned int wpm = 20; // Word-per-minute speed
const char MorseCommand = '<'; // Used as a command character to adjust some settings via serial.
unsigned long debounceDelay = 20; // the debounce time. Keep well below dotTime!! 20 this is ok to at least 200wpm
// Other Morse variables
unsigned long dotTime = 1200 / wpm; // morse dot time length in ms
unsigned long dashTime = 3 * 1200 / wpm;
unsigned long wordSpace = 7 * 1200 / wpm;
const int analogPin = 0; // Analog input pin for audio morse code
const int morseInPin = 7; // The Morse keyer button
const int morseOutPin = 13; // For Morse code output
unsigned long markTime = 0; // timers for mark and space in morse signal
unsigned long spaceTime = 0; // E=MC^2 ;p
boolean morseSpace = false; // Flag to prevent multiple received spaces
boolean gotLastSig = true; // Flag that the last received morse signal is decoded as dot or dash
const int morseTreetop = 63; // This is for ITU with puncutation, but without non-english extensions
const int morseTableLength = (morseTreetop * 2) + 1;
const int morseTreeLevels = log(morseTreetop + 1) / log(2);
int morseTableJumper = (morseTreetop + 1) / 2;
int morseTablePointer = morseTreetop;
// This is the table for ITU with punctuation (but without non-english characters - for now)
char morseTable[] = "*5*H*4*S***V*3*I***F***U?!_**2*E***L\"**R*+.****A***P@**W***J'1* *6-B*=*D*/"
"*X***N***C;*!K*()Y***T*7*Z**,G***Q***M:8*****O*9***0*"; //BB 52 characters, I think
int morseSignals; // nr of morse signals to send in one morse character
char morseSignal[] = "......"; // temporary string to hold one morse character's signals to send
int morseSignalPos = 0;
int sendingMorseSignalNr = 0;
unsigned long sendMorseTimer = 0;
boolean morseEcho = false; // Echoes character to encode back to serial and Morse signal input to output pin
boolean listeningAudio = false;
boolean sendingMorse = false;
boolean morseSignalState = false;
boolean lastKeyerState = false;
unsigned long lastDebounceTime = 0; // the last time the input pin was toggled
void setup()
{
// BB rotary encoder stuff ------------------------------------
pinMode(pinA, INPUT); // reads Pin A of the encoder
pinMode(pinB, INPUT); // reads Pin B of the encoder
digitalWrite(pinA, HIGH);
digitalWrite(pinB, HIGH);
level = wpm; // starting point for encoder
tone(8, pitch); // (BB) duration is always on (port,freq,millseconds)
pinMode(encoderSwitchPin, INPUT); //switch
digitalWrite(encoderSwitchPin, HIGH); //turn pullup resistor on
// Set up to call our knob function any time pinA rises
attachInterrupt(0, knobTurned, RISING); // calls 'knobTurned()' function when pinA goes from LOW to HIGH
pinMode(morseInPin, INPUT);
digitalWrite(morseInPin, HIGH); // internal pullup resistor on
pinMode(morseOutPin, OUTPUT);
// ------------------------------------------------------------
Serial.begin(115200); // 9600 115200 300 2 <------- if garbage on screen, check terminal baud rate ***
Serial.println(F("Morse en-/de-coder by raron. Customed by Flat5"));
help();
displayCurrentSettings(); // default settings at this point (BB added)
markTime = millis();
spaceTime = millis();
}
void loop()
{
switchState == digitalRead(encoderSwitchPin);
if (switchState != HIGH) {
knobTurned();
}
boolean morseKeyer = !digitalRead(morseInPin); // inverted for active-low input
// If the switch changed, due to noise or pressing:
if (morseKeyer != lastKeyerState)
{
lastDebounceTime = millis(); // reset timer
listeningAudio = false; // disable listen to audio-mode
}
// debounce the morse keyer, unless listening to audio morse signal
if (!listeningAudio && (millis() - lastDebounceTime) > debounceDelay)
{
// whatever the reading is at, it's been there for longer
// than the debounce delay, so take it as the actual current state:
morseSignalState = morseKeyer;
// differentiante mark and space times
if (morseSignalState) markTime = lastDebounceTime; else spaceTime = lastDebounceTime;
}
// If no manual morse keying the last second, enter audio listen mode
if (!morseKeyer && millis() - lastDebounceTime > 1000) listeningAudio = true;
// Filter audio morse signal
if (listeningAudio)
{
int audioMorse = analogRead(analogPin);
unsigned long currentTime = millis();
// Check for an audio signal...
if (audioMorse > AudioThreshold)
{
// If this is a new morse signal, reset morse signal timer
if (currentTime - lastDebounceTime > dotTime / 2)
{
markTime = currentTime;
morseSignalState = true; // there is currently a signal
}
lastDebounceTime = currentTime;
} else {
// if this is a new pause, reset space time
if (currentTime - lastDebounceTime > dotTime / 2 && morseSignalState == true)
{
spaceTime = lastDebounceTime; // not too far off from last received audio
morseSignalState = false; // No more signal
}
}
}
// Morse output, or a feedback when keying.
if (!sendingMorse && morseEcho) digitalWrite(morseOutPin, morseSignalState);
// Encode Morse code or execute commands
if (Serial.available() > 0 && !sendingMorse)
{
char encodeMorseChar = Serial.read();
// if a command instead, adjust some settings
if (encodeMorseChar == MorseCommand)
{
// parser BB modified ----------------------------------------------------------------------------------
int digits;
int value = 0;
int var = wpm; // If wpm is very fast keyboard input requires very fast typing. After command input re-set wpm
wpm = 5; // for now
do
{
digits = Serial.available();
}
while (digits < 1);
// Read what setting
char morseSet = Serial.read();
do
{
digits = Serial.available();
}
while (digits < 0);
value = Serial.parseInt();
Serial.flush(); // just in case
// Adjust and print the new setting
// BB added
if (refreshScreen == true) refresh_Screen();
else Serial.println();
switch (morseSet)
{
case '>':
refresh_Screen();
break;
case 'a': case 'A': // Audio input threshold value
AudioThreshold = value;
if (AudioThreshold < 0) AudioThreshold = 0; // not recommended
Serial.print(F(" > Audio threshold:"));
Serial.print (value, DEC);
Serial.println(" <");
break;
case 'd': case 'D': // Debounce value
debounceDelay = (unsigned long) value;
if (debounceDelay < 0) debounceDelay = 0;
Serial.print(F(" > Debounce (ms):"));
Serial.print (value, DEC);
Serial.println(F(" <"));
break;
case 'e': case 'E': // Turn on / off Morse echo back to serial and Morse output pin
if (value > 0)
{
morseEcho = true;
Serial.println(F(" > Echo: on <"));
} else {
morseEcho = false;
Serial.println(F(" > Echo: off <"));
}
break;
// BB added ------------------------------------
case 'p': case 'P': // tone frequency
pitch = value;
if (pitch == 0)
{
pitch = 0;
noTone(8);
Serial.println(F(" > Tone echo is now off. <"));
break;
}
if (pitch < 31)
{
Serial.println(F(" > 31 is minimum <"));
break;
}
if (pitch > 24000)
{
Serial.print(F(" > 24000 is max <"));
break;
}
Serial.print(F(" > Tone freq. is "));
Serial.print(pitch, DEC);
Serial.println(F(" <"));
tone(8, pitch);
break;
case 'r': case 'R': // clear screen. works only with real terminals otherwise puts garbage on screen.
if (value > 0) // r1 to enable | r to disable
{
refreshScreen = true;
Serial.println(F(" > Screen refresh enabled <"));
}
else
{
refreshScreen = false;
Serial.println(F(" >Screen refresh disabled <"));
}
break;
case 'h': case 'H': case '?': case 'm': case 'M': // display options and current settings
help();
displayCurrentSettings();
break;
case 't': case 'T': // encoder increment
if (value > 0 && value < 201)
{
increment = value - 1;
Serial.print(F("> Increment: "));
Serial.println(value);
Serial.print(F(" <"));
}
else
{
Serial.println(F("> out of range: 1-200 <"));
}
break;
//--------------------------------------------- BB
case 'w': case 'W': // Morse speed setting in wpm
wpm = value;
var = value; // re-set wpm again at end of command parsing
if (wpm <= 4)
{
wpm = 5;
var = 5;
Serial.println(F(" > 5 min <"));
}
if (wpm > 200)
{
wpm = 200;
var = 200;
Serial.println(F(" > 200 max <"));
}
setDotDash();
Serial.print(F(" > wpm: "));
Serial.print (wpm, DEC);
Serial.println(F(" <"));
break;
case 'i': case 'I': // Display info (current settings).
wpm = var;
displayCurrentSettings();
break;
default:
Serial.print(F(" > Unrecognized command <"));
}
// Mark that we have executed a command (don't send morse)
encodeMorseChar = MorseCommand;
wpm = var; // done typing, re-set wpm
}
// end of command parsing ------------------------------------------------------------
if (encodeMorseChar != MorseCommand)
{
// change to capital letter if not
if (encodeMorseChar > 'Z') encodeMorseChar -= 'z' - 'Z';
// Scan for the character to send in the Morse table
int i;
for (i = 0; i < morseTableLength; i++) if (morseTable[i] == encodeMorseChar) break;
int morseTablePos = i + 1; // 1-based position
// Reverse dichotomic / binary tree path tracing
// Find out what level in the binary tree the character is
int test;
for (i = 0; i < morseTreeLevels; i++)
{
test = (morseTablePos + (0x0001 << i)) % (0x0002 << i);
if (test == 0) break;
}
int startLevel = i;
morseSignals = morseTreeLevels - i; // = the number of dots and/or dashes
morseSignalPos = 0;
// Travel the reverse path to the top of the morse table
if (morseSignals > 0)
{
// build the morse signal (backwards from last signal to first)
for (i = startLevel; i < morseTreeLevels; i++)
{
int add = (0x0001 << i);
test = (morseTablePos + add) / (0x0002 << i);
if (test & 0x0001 == 1)
{
morseTablePos += add;
// Add a dot to the temporary morse signal string
morseSignal[morseSignals - 1 - morseSignalPos++] = '.';
} else {
morseTablePos -= add;
// Add a dash to the temporary morse signal string
morseSignal[morseSignals - 1 - morseSignalPos++] = '-';
}
}
} else { // unless it was on the top to begin with (A space character)
morseSignal[0] = ' ';
morseSignalPos = 1;
morseSignals = 1; // cheating a little; a wordspace for a "morse signal"
}
morseSignal[morseSignalPos] = '\0';
// Echo back the letter with morse signal as ASCII to serial output
if (morseEcho)
{
Serial.print(encodeMorseChar);
Serial.print(morseSignal);
Serial.print(" ");
}
if (morseTablePos - 1 != morseTreetop)
{
Serial.println();
// Serial.print(F("..Hm..error? MorseTablePos = "));
// Serial.println(morseTablePos);
}
// start sending the character
sendingMorse = true;
sendingMorseSignalNr = 0;
sendMorseTimer = millis();
if (morseSignal[0] != ' ') digitalWrite(morseOutPin, HIGH);
}
}
// Send Morse signals to output
if (sendingMorse)
{
switch (morseSignal[sendingMorseSignalNr])
{
case '.': // Send a dot (actually, stop sending a signal after a "dot time")
if (millis() - sendMorseTimer >= dotTime)
{
digitalWrite(morseOutPin, LOW);
sendMorseTimer = millis();
morseSignal[sendingMorseSignalNr] = 'x'; // Mark the signal as sent
}
break;
case '-': // Send a dash (same here, stop sending after a dash worth of time)
if (millis() - sendMorseTimer >= dashTime)
{
digitalWrite(morseOutPin, LOW);
sendMorseTimer = millis();
morseSignal[sendingMorseSignalNr] = 'x'; // Mark the signal as sent
}
break;
case 'x': // To make sure there is a pause between signals and letters
if (sendingMorseSignalNr < morseSignals - 1)
{
// Pause between signals in the same letter
if (millis() - sendMorseTimer >= dotTime)
{
sendingMorseSignalNr++;
digitalWrite(morseOutPin, HIGH); // Start sending the next signal
sendMorseTimer = millis(); // reset the timer
}
} else {
// Pause between letters
if (millis() - sendMorseTimer >= dashTime)
{
sendingMorseSignalNr++;
sendMorseTimer = millis(); // reset the timer
}
}
break;
case ' ': // Pause between words (minus pause between letters - already sent)
default: // Just in case its something else
if (millis() - sendMorseTimer > wordSpace - dashTime) sendingMorse = false;
}
if (sendingMorseSignalNr >= morseSignals) sendingMorse = false; // Ready to encode more letters
}
// Decode morse code
if (!morseSignalState)
{
if (!gotLastSig)
{
if (morseTableJumper > 0)
{
// if pause for more than half a dot, get what kind of signal pulse (dot/dash) received last
if (millis() - spaceTime > dotTime / 2)
{
// if signal for more than 1/4 dotTime, take it as a valid morse pulse
if (spaceTime - markTime > dotTime / 4)
{
// if signal for less than half a dash, take it as a dot, else if not, take it as a dash
// (dashes can be really really long...)
if (spaceTime - markTime < dashTime / 2) morseTablePointer -= morseTableJumper;
else morseTablePointer += morseTableJumper;
morseTableJumper /= 2;
gotLastSig = true;
}
}
} else { // error if too many pulses in one morse character
Serial.println(F("<ERROR: unrecognized signal!>"));
gotLastSig = true;
morseTableJumper = (morseTreetop + 1) / 2;
morseTablePointer = morseTreetop;
}
}
// Write out the character if pause is longer than 2/3 dash time (2 dots) and a character received
// if ((millis() - spaceTime >= (dotTime * 2)) && (morseTableJumper < ((morseTreetop+1)/2)))
if ((millis() - spaceTime >= (dotTime * 2)) && (morseTableJumper < ((morseTreetop + 1) / 2)))
{
char morseChar = morseTable[morseTablePointer];
Serial.print(morseChar);
morseTableJumper = (morseTreetop + 1) / 2;
morseTablePointer = morseTreetop;
}
// Write a space if pause is longer than 2/3rd wordspace
if (millis() - spaceTime > (wordSpace * 2 / 3) && morseSpace == false)
{
Serial.print(" ");
morseSpace = true ; // space written-flag
}
} else {
// while there is a signal, reset some flags
gotLastSig = false;
morseSpace = false;
}
// save last state of the morse signal for debouncing
lastKeyerState = morseKeyer;
}
// end loop ----------------------------------------------------
void displayCurrentSettings() {
Serial.println();
Serial.print(F("Morse speed: ")); Serial.print (wpm, DEC); Serial.print(F(" wpm (dot=")); Serial.print (dotTime, DEC);
Serial.print(F("ms, dash=")); Serial.print (dashTime, DEC); Serial.println(F("ms)"));
Serial.print(F("Rotary Encoder increment: ")); Serial.println(increment + 1);
Serial.print(F("Audio threshold: ")); Serial.println(AudioThreshold, DEC);
Serial.print(F("Tone freq: ")); Serial.print(pitch); Serial.println(F("Hz"));
Serial.print(F("Debounce: ")); Serial.print(debounceDelay, DEC); Serial.println(F(" ms."));
Serial.print(F("Screen Refresh ")); if (refreshScreen == 0) Serial.println(F("off")); else Serial.print(F("on"));
Serial.println();
}
void knobTurned() { // rotary encoder
level = wpm;
state = 0; // reset this value each time
state = state + digitalRead(pinA); // add the state of Pin A
state <<= 1; // shift the bit over one spot
state = state + digitalRead(pinB); // add the state of Pin B
delay(2); // (BB) using .01uf caps & 4.7k resistors for debounce. It is causing a double speak.
level = level + bump[state];
if (level > wpm) wpm = level + increment; if (wpm > 200) wpm = 200; // increment wpm by 5 or user defined. stop at 200
else wpm = level - increment; if (wpm < 5) wpm = 5; // decrement wpm by 5 or user defined. stop at 5
setDotDash();
refresh_Screen;
Serial.println(); Serial.print(wpm);
delay(2); // debounce
}
void setDotDash()
{
dotTime = 1200 / wpm;
dashTime = 3 * 1200 / wpm;
wordSpace = 7 * 1200 / wpm;
}
void refresh_Screen()
{
if (refreshScreen == true)
{
Serial.write(27); // ESC
Serial.write("[2J"); // clear screen
Serial.write(27); // ESC
Serial.write("[H"); // cursor to home
}
}
void help() // Help Screen
{
Serial.println();
Serial.println(F("To change paramaters"));
Serial.println(F("Options begin with: <\n"));
Serial.println(F("This help screen: <h"));
Serial.println(F("Morse speed: <w5-200"));
Serial.println(F("Rotary Encoder increment: <t1-200"));
Serial.println(F("Hz. Tone pitch: <p31-24000 or <0 for off"));
Serial.println(F("Display current settings: <i"));
Serial.println(F("ms. Debounce: <d0-50"));
Serial.println(F("ms. Audio threshold: <a300-600"));
Serial.println(F("Screen Refresh for real terminals: <r (off) <r1 (on)"));
Serial.println(F("To refresh screen: <>\n"));
Serial.println(F("Reset monitor or Arduino to restore default settings"));
}
/*
Some of these comments are outdated (BB)
3 input methods for decoding and encoding:
- Audio Morse signals on an analog input (for decoding).
- Morse keying on digital input (for decoding).
- ASCII text via serial communication (for encoding. Also for adjusting settings).
2 outputs for encoded and decoded Morse:
- Morse-code toggled output pin (for encoded Morse).
- ASCII text via serial communication (for decoded Morse) .
It will enter audio listen mode if nothing is keyed in within a second.
Serial input:
Only International Morse Code (letters A-Z, numbers), and space, are
encoded and sent as Morse code automatically unless whatever the "command"
character is (default '<') is received.
No punctuation supported (but the morse table can be extended easily for that)
Morse en-/de-coder < commands via serial:
The ASCII "less than" character '<' denotes a command instead of encoding and
sending Morse. The format is:
<Lxxx
where L is a letter and xxx is a three-digit decimal number 000-999.
One exception is the <i command for info (displays current settings).
The letter can be upper or lower case W, A, D, E or I. Examples:
<w008 - sets speed to 8 wpm
<a900 - sets audio threshold to be 900
<D020 - sets debounce delay to be 20 milliseconds.
<e000 - turn off Morse echo back to serial as ASCII and output pin as Morse
<e001 - turn on Morse echo (any value above 0 will do)
<i - info - displays some settings
Full duplex:
It can both transmit and receive simultaneously, but serial output will
be a mixed mess unless echo is turned off.
NOTE: Values for tolerance (like dot time or dash time divided by 2 etc) are
more or less arbitrarily/experimentally chosen.
Sorry the source code is a big mess for now...
Based on Debounce example from the Arduino site for the morse keyer button
(http://www.arduino.cc/en/Tutorial/Debounce).
Loosely based on Morse Decoder 3.5 from 1992 for the Amiga by the same guy.
Contact: raronzen@gmail.com (not checked too often..)
Copyright (C) 2010 raron
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
History:
1992.01.06 - Morse decoder 3.5 - 68000 Assembler version for the Amiga 600
using a binary tree (dichotomic table) for Morse decoding
2010.11.11 - Rewrite for the Arduino
2010.11.27 - Added Morse encoding via reverse-dichotomic path tracing.
Thus using the same Morse table / binary tree for encoding and decoding.
2010.11.28 - Added a simple audio Morse signal filter. Added a simple command parser
2010.11.29 - Added echo on/off command
2015.3 - some modification by Barry Block to use the 'tone' ability.
input & output circuits modified
plan to add a rotary encoder to adjust speed.
TODO: Make the timings signed long again, to avoid rollover issue
Avoid calling millis() all the time?
Make functions or classes out of it sometime...
Generally tidy it up
Not enough variable memory to include the LCD backpack routines. :-(
*/