Wednesday, July 21, 2010

Arduino project - Morse code (part 2)

In response to Greg who wrote a comment about my pet project and to keep my blog alive I have completed the morse code project. It's actually been done for a while but I've been busy with work and home stuff as of late.

The code stream uses 7478 byes so it's fairly compact. I have successfully 'input' a sample code by using the onboard supply and tapping the input pin. It worked great :) It does have limits though, say a user was transmitting and varied speed too much, this would cause some of the dashes to be dots if they decided to send slower. There are a few solutions to this, one is to attempt to detect spaces and flush the system to reset the timers (timers are used to identify dash and dots).

There's a few options built into the code:
rate -- rate at which the program transmits
recdelay -sample frequency for input
perr - Error recovery, increase this if a user is off on timing but can blur differences between the sequences.
rectimeout - expected time window to when a user transmits.
help - prints out commands one can set :)

There's a few parts to the code stream that are critical: the first one is priority. Basically I want to make sure that any incoming signals have priority over any outgoing. That is if I receive a signal it will stay on that until the time lapses and then send the signal I want to transmit. This is simply done in the loop() statement


void loop() {
//if there's data on the serial port go and read that
if(digitalRead(input)) {
int bits[5000];
buffer_signal(bits);
decode(bits);
}
if(Serial.available()) {
//will get one bit at a time to encode and transmit
char mybit = Serial.read();
if(!scancontrol(mybit)) {
flash(encode(mybit));
}
}
//if there's data on the input port read that
//potentailly make this an interrupt process?

}

Let's first skip to the user input part to create a baseline. The first argument is scanning for a control statement


//scan for control bits from console
boolean scancontrol(char mybit) {
if(mybit == '#') {
delay(1); //delay to allow serial to recover
//this is a control bit flag
char ctrl[20];
int i=0;
//buffer the command
while(Serial.available()) {
ctrl[i++] = Serial.read();
delay(1); //delay 1ms to allow serial to recover
}
ctrl[i] = '\0';

if(strcmp(ctrl, "rate", 0)) {
char nr[20]; //make the same size as ctrl
substr(nr, ctrl, 4, -1);
Serial.print("Set Tx rate=");
Serial.println(nr);
rate=atoi(nr);
}
else if(strcmp(ctrl, "recdelay", 0)) {
char nr[20]; //make the same size as ctrl
substr(nr, ctrl, 4, -1);
Serial.print("Set rec_delay=");
Serial.println(nr);
recdelay=atoi(nr);
}
else if(strcmp(ctrl, "perr", 0)) {
char nr[20]; //make the same size as ctrl
substr(nr, ctrl, 4, -1);
Serial.print("Set perr=");
Serial.println(nr);
recdelay=atoi(nr);
}
else if(strcmp(ctrl, "rectimeout", 0)) {
char nr[20]; //make the same size as ctrl
substr(nr, ctrl, 4, -1);
Serial.print("Set rec_timeout=");
Serial.println(nr);
rectimeout=atoi(nr);
}
else if(strcmp(ctrl, "help", 0)) {
Serial.println("#rate n - set dot rate (ms)");
Serial.println("#recdelay n - set rec_delay (ms)");
Serial.println("#perr n - set error for decoding");
Serial.println("#rectimeout n - set rec_timeout (ms)");
}
else {
Serial.print("Unknown command: ");
Serial.println(ctrl);
}
//Serial.flush();
return true;
}
return false; //false means it didnt find a control bit,
}


If there is no control statement found it will return false and allow flash() to execute. Flash, however, needs a string of encoded signals to transmit and that's where encode() is used.

Encode() is very straight forward. It takes in a single char and dumps out the equivalent string of dash and dot combination. This is nothing new and can be easily found on a wiki site.


//encode each character
char * encode(char chr) {
switch(chr) {
case 'a': return ".-";
case 'b': return "-...";
case 'c': return "-.-.";
case 'd': return "-..";
case 'e': return ".";
case 'f': return "..-.";
case 'g': return "--.";
case 'h': return "....";
case 'i': return "..";
case 'j': return ".---";
case 'k': return "-.-";
case 'l': return ".-..";
case 'm': return "--";
case 'n': return "-.";
case 'o': return "---";
case 'p': return ".--.";
case 'q': return "--.-";
case 'r': return ".-.";
case 's': return "...";
case 't': return "-";
case 'u': return "..-";
case 'v': return "...-";
case 'w': return ".--";
case 'x': return "-..-";
case 'y': return "-.--";
case 'z': return "--..";
case ' ': return " ";
case '0': return "-----";
case '1': return ".----";
case '2': return "..---";
case '3': return "...--";
case '4': return "....-";
case '5': return ".....";
case '6': return "-....";
case '7': return "--...";
case '8': return "---..";
case '9': return "----.";
case '.': return ".-.-.-";
case ',': return "--..--";
case '?': return "..--..";
case '\'': return ".----.";
case '!': return "-.-.--";
case '/': return "-..-.";
case '(': return "-.--.";
case ')': return "-.--.-";
case '&': return ".-...";
case ':': return "---...";
case ';': return "-.-.-.";
case '=': return "-...-";
case '+': return ".-.-.";
case '-': return "-....-";
case '_': return "..--.-";
case '"': return ".-..-.";
case '$': return "...-..-";
case '@': return ".--.-.";
default: return "";
}
}


The last bit that I have is the tougher one, decoding. I wanted to allow this to be somewhat dynamic in that a user doesn't need to know exactly what the rate should be.


void decode(int *bits) {
int i=0; //assume first bit is high
int max_on=0;
int max_off=0;
int min_off=-1;
int min_on=-1;
//first get the max cycle (dash)
while(bits[i]) {
if(max_on < bits[i]) max_on = bits[i];
if(max_off < bits[i+1]) max_off = bits[i+1];
if(min_on < 0 || min_on > bits[i]) min_on = bits[i];
if(min_off < 0 || min_off > bits[i]) min_off = bits[i];
for(int z=i; z <= i+1; z++) {
Serial.print(z);
Serial.print(">");
Serial.println(bits[z]);
}
i+=2; //i+1 = space/seperator
}
i=0;
char myset[100];
int z=0;
while(bits[i]) {
if(bits[i] <= (max_on*(1+perr/100.0)) && bits[i] >= (max_on*(1-perr/100.0))) myset[z] = '-';
else myset[z] = '.';

Serial.print(myset[z]);

z++;
//check the space, see if there is a space or new char
if(min_off <= (max_off*(1-perr/100.0)) &&
bits[i+1] <= (max_off*(1+perr/100.0)) && bits[i+1] >= (max_off*(1-perr/100.0))) {
myset[z++] = ' ';
Serial.println("");
}
i+=2;
}
myset[z]='\0';
Serial.println("");
}

The basic idea behind the decode is this: buffer the digital input and log the times the bit is high and low. By doing this I can simply search though the buffer to figure out the lengths of the longest 'on' period which will be signified as a dash. The dot is found by looking for the minimum time the bit is high. With a few error corrections I can find out the differences between the dashes and dots. At the same time this code also dumps the dash/dots to the terminal

With some of my tests I did the code seems to work just fine. I can tap a digital input, the classic S O S and it decodes it. Transmission seems to work as expected as well, too bad I don't have another, I could get them both talking to do a much better test!

I never got past the point of hooking it up to the radio and decoding something but I'm sure that getting some circuitry it can be done. All there really needs to be is something to smooth out the audio tone (bridge+caps?) and use an opamp. The opamp can be set up so it hits max gain when a tone comes in, at which point it would simulate a digital signal. Sending one would require an oscillator and set to a good frequency (probably somewhere around 400+ Hz). I suppose a lower tone would work just fine but too low and the oscillations wouldn't keep up with the transmissions.


int led = 13; //local LED pin
int sig = 9; //pin to write to to transmit
int input = 8; //pin to read from
int rate = 120; //basic rate (set to human readable)
int recdelay = 20;
int rectimeout = recdelay*20;
int perr = 10; //in percentage

void setup()
{
Serial.begin(9600);
establishContact();
pinMode(led, OUTPUT); //local LED indicator
pinMode(sig, OUTPUT); //output signal to transmit
pinMode(input, INPUT);
}

void loop() {
//if there's data on the serial port go and read that
if(digitalRead(input)) {
int bits[5000];
buffer_signal(bits);
decode(bits);
}
if(Serial.available()) {
//will get one bit at a time to encode and transmit
char mybit = Serial.read();
if(!scancontrol(mybit)) {
flash(encode(mybit));
}
}
//if there's data on the input port read that
//potentailly make this an interrupt process?

}

void decode(int *bits) {
int i=0; //assume first bit is high
int max_on=0;
int max_off=0;
int min_off=-1;
int min_on=-1;
//first get the max cycle (dash)
while(bits[i]) {
if(max_on < bits[i]) max_on = bits[i];
if(max_off < bits[i+1]) max_off = bits[i+1];
if(min_on < 0 || min_on > bits[i]) min_on = bits[i];
if(min_off < 0 || min_off > bits[i]) min_off = bits[i];
for(int z=i; z <= i+1; z++) {
Serial.print(z);
Serial.print(">");
Serial.println(bits[z]);
}
i+=2; //i+1 = space/seperator
}
i=0;
char myset[100];
int z=0;
while(bits[i]) {
if(bits[i] <= (max_on*(1+perr/100.0)) && bits[i] >= (max_on*(1-perr/100.0))) myset[z] = '-';
else myset[z] = '.';

Serial.print(myset[z]);

z++;
//check the space, see if there is a space or new char
if(min_off <= (max_off*(1-perr/100.0)) &&
bits[i+1] <= (max_off*(1+perr/100.0)) && bits[i+1] >= (max_off*(1-perr/100.0))) {
myset[z++] = ' ';
Serial.println("");
}
i+=2;
}
myset[z]='\0';
Serial.println("");
}

void buffer_signal(int *bits) {
boolean complete=false;
int cnt=0;
int i=0;
int valprev=1; //start out high
while(!complete) {
int val = digitalRead(input);
cnt++;
if(!val) digitalWrite(led, LOW);
else digitalWrite(led, HIGH);

if(val != valprev) {
bits[i++] = cnt;
cnt=0;
}
valprev = val;
delay(recdelay); //make this dymaic
if(!val && cnt >= rectimeout) complete=true; //make this dynamic
}
bits[i]=0;
bits[i+1]=0;
}

//scan for control bits from console
boolean scancontrol(char mybit) {
if(mybit == '#') {
delay(1); //delay to allow serial to recover
//this is a control bit flag
char ctrl[20];
int i=0;
//buffer the command
while(Serial.available()) {
ctrl[i++] = Serial.read();
delay(1); //delay 1ms to allow serial to recover
}
ctrl[i] = '\0';

if(strcmp(ctrl, "rate", 0)) {
char nr[20]; //make the same size as ctrl
substr(nr, ctrl, 4, -1);
Serial.print("Set Tx rate=");
Serial.println(nr);
rate=atoi(nr);
}
else if(strcmp(ctrl, "recdelay", 0)) {
char nr[20]; //make the same size as ctrl
substr(nr, ctrl, 4, -1);
Serial.print("Set rec_delay=");
Serial.println(nr);
recdelay=atoi(nr);
}
else if(strcmp(ctrl, "perr", 0)) {
char nr[20]; //make the same size as ctrl
substr(nr, ctrl, 4, -1);
Serial.print("Set perr=");
Serial.println(nr);
recdelay=atoi(nr);
}
else if(strcmp(ctrl, "rectimeout", 0)) {
char nr[20]; //make the same size as ctrl
substr(nr, ctrl, 4, -1);
Serial.print("Set rec_timeout=");
Serial.println(nr);
rectimeout=atoi(nr);
}
else if(strcmp(ctrl, "help", 0)) {
Serial.println("#rate n - set dot rate (ms)");
Serial.println("#recdelay n - set rec_delay (ms)");
Serial.println("#perr n - set error for decoding");
Serial.println("#rectimeout n - set rec_timeout (ms)");
}
else {
Serial.print("Unknown command: ");
Serial.println(ctrl);
}
//Serial.flush();
return true;
}
return false; //false means it didnt find a control bit,
}

//pull out a section of the string
void substr(char *ret, char *msg, int offset, int len) {
if(len<=0) len = strlen(msg);
for(int i=offset-1; i < len && i < strlen(msg); i++) {
ret[i-offset] = msg[i];
}
ret[len-offset]='\0';
}

//compare strings
boolean strcmp(char *msg, char*srch, int offset) {
for(int i=offset; i < strlen(srch) && i < strlen(msg); i++) {
if(srch[i] != msg[i]) return false;
}
return true;
}

//flash the morse code to output
void flash(char *msg) {
int i=0;
while(msg[i]) {
if(msg[i] == ' ') {
delay(rate); //space is a standard dot delay
}
else {
digitalWrite(led, HIGH);
digitalWrite(sig, HIGH);
if(msg[i] == '.') delay(rate); //default delay for a dot
else if(msg[i] == '-') delay(rate*2); //double delay for a dash
digitalWrite(led, LOW);
digitalWrite(sig, LOW);
}
Serial.print(msg[i]);
delay(rate); //delay slightly before next 'bit'
i++;
}
delay(rate*2);
Serial.println("");
}

//encode each character
char * encode(char chr) {
switch(chr) {
case 'a': return ".-";
case 'b': return "-...";
case 'c': return "-.-.";
case 'd': return "-..";
case 'e': return ".";
case 'f': return "..-.";
case 'g': return "--.";
case 'h': return "....";
case 'i': return "..";
case 'j': return ".---";
case 'k': return "-.-";
case 'l': return ".-..";
case 'm': return "--";
case 'n': return "-.";
case 'o': return "---";
case 'p': return ".--.";
case 'q': return "--.-";
case 'r': return ".-.";
case 's': return "...";
case 't': return "-";
case 'u': return "..-";
case 'v': return "...-";
case 'w': return ".--";
case 'x': return "-..-";
case 'y': return "-.--";
case 'z': return "--..";
case ' ': return " ";
case '0': return "-----";
case '1': return ".----";
case '2': return "..---";
case '3': return "...--";
case '4': return "....-";
case '5': return ".....";
case '6': return "-....";
case '7': return "--...";
case '8': return "---..";
case '9': return "----.";
case '.': return ".-.-.-";
case ',': return "--..--";
case '?': return "..--..";
case '\'': return ".----.";
case '!': return "-.-.--";
case '/': return "-..-.";
case '(': return "-.--.";
case ')': return "-.--.-";
case '&': return ".-...";
case ':': return "---...";
case ';': return "-.-.-.";
case '=': return "-...-";
case '+': return ".-.-.";
case '-': return "-....-";
case '_': return "..--.-";
case '"': return ".-..-.";
case '$': return "...-..-";
case '@': return ".--.-.";
default: return "";
}
}

void establishContact() {
while(Serial.available() <= 0) {
Serial.println("CN");
delay(1000);
}
Serial.flush();
}