Chandru Subramanian

A few experiments, some notes, and miscellaneous writing.

An Overengineered Morse Code Key

Jan 19, 2024

Blinking LEDs on esp8286 is the hello-world of embedded programming. This got boring really quick.

I inherited my grandpa’s telegraphy key recently, and it got me thinking: why not build a WiFi-enabled telegraph key that would receive words via HTTP and then flash them.

My objective is to learn C++ so the goal is pristine C++ code.

What is Morse

Morse code is an early method to encode text characters as sequences of two signal durations: the short signal, dit (usually represented by a dot), and the longer signal dah (usually represented by a dash).

For example, the letter S is represented as the sequence: dit dit dit (or, . . .). The letter O (oh) is represented by the sequence: dah dah dah (or, - - -). The international sequence for SOS is: . . . - - - . . ..

System

I built this on a NodeMCU development board for no special reason other than I had one in my inventory. The esp8266 runs a full WiFi stack and an HTTP server. We use the D7 GPIO pin to attach an LED. Issuing a GET on /morse/_text_> will cause the LED to flash the message in Morse.

The Arduino library layer is excellent and takes care of a lot of annoying bits in embedded programming. For most of my services, I include the following. It is bonkers how much we can get for free with a few includes.

#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>

/* MDNS - support for local discovery */
#include <ESP8266mDNS.h>

/* time is hard on embedded devices */
#include <NTPClient.h>
#include <WiFiUdp.h>

Basic setup

Setting up the esp to run a webserver is surprisingly easy. Configure WiFi and launch the webserver configured with some callbacks.

Configuring WiFi

/* turn the radio and listen for connections */
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(100);

/* enable multi-cast DNS (mdns) */
if (MDNS.begin(name)) {
    Serial.print(name);
    Serial.print(".local");
} else {
    Serial.print(WiFi.localIP());
}

Handling HTTP servers

Surprisingly setting up the webserver is easy because of the extremely well maintained ESP8266WebServer library.

/* Yup global variable! */
ESP8266WebServer server(80);

/* register callback on "/" */
server.on(
    F("/"), []()
    { server.send(200, F("text/plain"), F("what hath god wrought")); });

/* The F macro puts this in flash memory so we conserve RAM */

Programming notes

Reconnecting with C++ programming is refreshing. Operator overloading is elegant and powerful. It is TypeSafe. Python is nice but seems a little high-level for getting close to the processor. I am not masochistic enough (yet) to write assembler.

Storing encoding of the sequences

Since the esp is tight on memory, we use two arrays (static const char*) to store the encoding for 26 letters of the alphabet, and 10 numerals. We can retrieve the encoding for a character by indexing into the array:

static const char *alpha[] = {
  // A = 0
  ".-", 
...
}

if (isalpha(c))
  return alpha[toupper(c) - 'A'];

(ab)using operator overloading

Flashing an LED on ESP means toggling a GPIO pin high or low.

digitalWrite(pin, HIGH);
delay(for_ms);
digitalWrite(pin, LOW);

Given C++’s Unix legacy, redirecting messages and objects from input or to output feels “typical”. In the over-engineered Morse code project, we send messages to the Telegraph key. The standard C++ way to take input (cin is stdin) or print output (cout is stdout) is as follows:

int i;
cin >> i;
cout << i;

It strikes me that Telegraph keys are conceptually character streams. They should behave similarly.

We overload operator<< to allow c++ style redirection. So you “send” a message to the telegraph key - which spits out Morse. If the telegraph key “knows” how to flash an LED, it flashes Morse code, if it knows how to talk to a serial port, it sends ascii characters to the console.

Telegraph key = ...;
key << "PARIS"

Feels like most of what we do in these devices is to get input, minor processing, and sending out messages. So maybe there is a fair bit of operator overloading. Or maybe I really like operator overloading.

Sending a single character to our telegraph key

Each character (alpha or letter) expands into a sequence of signals (. or -). Fairly straightforward to iterate through the encoded sequence and either issue . or -.

// pull the GPIO up and down for a short duration
void FlashTelegraphKey::dit() const;

// pull the GPIO up and down for a long duration
void FlashTelegraphKey::dah() const;

TelegraphKey & TelegraphKey::operator<<(const char c)
{
    const char *seq = getMorseSeq(c);

    if (seq)
    {
        for (size_t i = 0; i < strlen(seq); i++)
        {
            switch (seq[i])
            {
            case '.':
                dit();
                break;
            case '-':
                dah();
                break;
            case ' ':
                break_();
                break;
            }
        }
        endchar();
    }
    return *this;
}

// now we can send single literal characters to the key
key << 'M';
// we can chain multiple calls
key << 'S' << 'O' << 'S';

Note the endchar() - this is merely a longer delay that signals that the character is complete. Returning the object from the method allows us to chain calls.

Sending an entire word

Once we have the ability to send a character, sending entire words is pretty clean. I feel this is where operator overloading makes code really elegant.

TelegraphKey & TelegraphKey::operator<<(const char *s)
{
    for (size_t i = 0; i < strlen(s); i++) *this << s[i];
    return *this;
}

// sending entire words
key << "PARIS"

Future enhancements

  1. Given the static nature of this encoding table, we should probably be storing this in Flash memory.
  2. C++ uses a traits class called char_traits which abstracts characters. We could define our own Morse character to help us get rid of the switch statement (a switch statement could be hiding a class hierarchy). Use of conversion cast operators could convert Ascii characters into our custom Morse characters. Morse characters could be sent to Telegraph key. I think this will further simplify the code.
  3. The WiFi and webserver helpers abstract a lot of complexity but they mix C style strings. It would be interesting to rewrite the lower level libraries in pure C++. It should be possible approach the memory footprint of C in C++ - most of the heavy lifting is done by the compiler.

← Back to all articles