Building Morsecipher

10 minute read

The other day I was feeling stagnation in my bones. After approximately 10 years of object-oriented madness, I realized that it isn’t fun anymore. I got bored. The Ruby programming language felt so comfortable that I barely noticed how fast time passed. Today, after seeing so many different abstractions, overcomplications, and whatnot it felt natural to step up outside of the monotonous routine, which became a tiny box I lived in for a while. It was time for a change and I was looking forward to it.

The new beginning

I felt like I wanted to try something simple - implement an algorithm, a small program. Approximately 10 years ago, I imagined making a tool to convert Tweets to morse code. The interaction of the software with the hardware sounded both profoundly intriguing and challenging. Nevertheless, I am sorry for spoiling it for you, but I have decided to drop the Twitter integration part because of my countless experiments writing Ruby bots and analyzing tweets in the past; I find that challenge not to be as fascinating anymore. In fact, I wanted something new and fresh to explore. But if you are interested in that, you can definitely check out my old articles about Twitter integration. The principle should stay the same.

Morseficator

If you are curious about the origin of the name, the inspiration was Huificator™, developed by my good friend @kenjione

I opened the Wikipedia page explaining what a Morse code is. There I found the specifications I needed for my basic program. I made a sketch and shared it with my friends, who helped me to improve the implementation, which eventually was opensourced. That’s how I started, in a nutshell. What I did at first was define a struct for mapping the letters to the respective dots and dashes.

defmodule Morseficator.Code do
  defstruct alphabet: %{
    "a" => ".-",
    "b" => "-...",
    "c" => "-.-.",
    "d" => "-..",
    "e" => "."
  }
end

To convert the text into code, I did the following logic. Relatively simple, eh? It seemed like all I need to care is just to convert the alphabet to actual code.

@encode_alphabet %Morseficator.Code{}.alphabet

def encode(text) do
  text
  |> String.codepoints
  |> to_morse
  |> Enum.join(" ")
end

defp to_morse_char(character) do
  @encode_alphabet[String.downcase(character)] || "/"
end

defp to_morse(characters) do
  Enum.map(characters, &to_morse_char/1)
end
iex(1)> Morseficator.encode("ab c")
".- -... / -.-."

Here’s a link to morseficator. You are welcome to contribute bug fixes and improvements!

After the basic logic of converting text to morse code was done, I considered the scenario of interpreting the result. At first, I was toying with the idea of printing text on screen, but then I realized I could use the concept of adapters, which would allow me to make this library comparatively easy to extend. So, instead of only converting text to morse code, you could do other things as well (i.e. visualize it with the lightbulb, play MIDI, print it or send IR signals, you name it!).

While I was on a call with a good friend of mine @pgaspar, who also happens to be a code freak like myself, we decided to give it a shot and try implementing a MIDI adapter with a library called fhunleth / midi_synth.

Together, as if working in our experiments in our virtual garage, we managed to set up the high-tech laboratory where we were comparing our program’s audio output with “SOS” performances found on YouTube. It was captivating, good times! We were doing this via Zoom, sharing screen, passing the remote control; it was sick and so much fun! We tried to stream it using Twitch, but, yeah, our gig didn’t succeed with our first attempt, but who cares?

Below you can find the brushed up adapter to interpret the morse code using MIDISynth library:

defmodule Morseficator.Adapter.Midi do
  @behaviour Morseficator.Adapter

  @unit 55
  @instrument_code 83
  @pitch 78
  @initial_pause 1000

  @spec setup :: {pid}
  def setup do
    {:ok, synth} = MIDISynth.start_link([])
    MIDISynth.Keyboard.change_program(synth, @instrument_code)

    :timer.sleep(@initial_pause)

    {synth}
  end

  @spec beep({pid}, non_neg_integer()) :: :ok
  def beep({synth}, times) do
    MIDISynth.Keyboard.play(synth, @pitch, @unit*times)
  end

  @spec sleep(non_neg_integer) :: :ok
  def sleep(n) do
    :timer.sleep(@unit*n)
  end
end

By the way, if you want to try the implementation, you can clone a repository from here:

git clone [email protected]:morsecipher/morseficator.git
cd morseficator
iex -S mix

And you can play the morse code interpretation with any adapter. For example, Morseficator.Adapter.Printer is included with the standard package.

iex(1)> Morseficator.Adapter.interpret("sos", Morseficator.Adapter.Printer)
beep 
*sleep (1)*
beep 
*sleep (1)*
beep 
*sleep (3)*
*sleep (3)*
beep beep beep 
*sleep (1)*
beep beep beep 
*sleep (1)*
beep beep beep 
*sleep (3)*
*sleep (3)*
beep 
*sleep (1)*
beep 
*sleep (1)*
beep 
*sleep (3)*
:ok

Morsecipher

When the main program to convert text to morse code, and even play it, was ready. I thought it’s time to evolve. I decided to try to implement a REST API with Phoenix. I wanted to convert text to (AS YOU CAN PROBABLY GUESS BY NOW) morse code! And this is how we made Morsecipher. It has that name because I had plans to expand the concept and add a cipher algorithm that I could decipher to learn that part of the problem better.

Initially I adopted the design of adapters in morsecipher, because I wished to make it easy to create multiple adapters. So that one could connect to interact with, for example, a Raspberry Pi. I strongly believed doing something with Raspberry Pi could help increase the number of views on Twitch.

Hey, look at that. We built a service through which you guys can interact with us! Isn’t that fun?!

For some reason, I thought it could increase the chances of building the community of developers I was looking for. In my mind, online streams kind of replaced meetups, which opened up new opportunities… It actually answers WHY I did it. I thought this would attract more people!

Broadcasting

First, we’ve made the tunnel using ngrok to expose the web service. @pgaspar quickly implemented a React interface which is shared here. The UI was made to send requests to the server running on my machine via a tunnel so that people could interact with us in a more friendly way.

Then, I was thinking, instead of ngrok, I could run the tunnel directly through my own VPS. This way, I wouldn’t need to disclose my IP Address and avoid any additional third-party services. Later, @pgaspar noted it would be nice to have it running all the time, even when we were offline. First, I thought to run Elixir on Raspberry Pi Zero, but I didn’t know that Zero is quite limited in its resources. Therefore, after 2-3 hours of compiling Erlang & Elixir and seeing how slow these queries are, compared to when I ran the server on my laptop, I ditched the idea. Moreover, it wasn’t optimal anyway. Who wants to serve production requests on Raspberry Pi? Knowing that anyone could DDoS it, besides why host on your home machine if you can deploy it somewhere and make it more reliable!

We have decided to move the service to the cloud. But then, “How do we light up an LED on my Raspberry Pi? How about the interaction with my #not-yet-followers-on-twitch?” I thought. I had many ideas floating around my head: HTTP API, RabbitMQ, Redis, but over a casual chat with another good friend, @dmrz, he sparked an excellent idea of trying a lightweight MQTT, which was made especially for IoT devices. I could subscribe from the Raspberry PI to the channel and, once I’d received the command, execute it right away.

MQTT is an OASIS standard messaging protocol for the Internet of Things (IoT). It is designed as an extremely lightweight publish/subscribe messaging transport that is ideal for connecting remote devices with a small code footprint and minimal network bandwidth.

Production ready

So essentially, it was established to connect external devices to receive the commands from the main - morsecipher server that would send the signal to light on or off the LED. The downside of this approach would be that it happens in real-time. Meaning, to ensure that the raspberry pi has obtained the message, we need to increase QoS (Quality of Service). And yet, if one would suffer from the slow internet connection, or there would be disruptiveness, it would be a good idea to play the message ONLY once it has fully received it.

The explanation of the diagram:

  1. A user executes a request via browser/curl to send the message that has to be interpreted.
  2. Morsecipher adds the message to play in the queue so that the messages wouldn’t overlap.
  3. Morseficator converts the text message to morse code and then sends it further down through the custom adapter.
  4. The adapter splits the message and prepares it to transmit over MQTT with the topic.
  5. PiLED application running on Raspberry PI subscribes to the topic and listens to the following orders from Morsecipher.

Before deploying

Since I have to run phoenix and mosquitto services, it would be beneficial to set it up and test the identical configuration before deploying it to production. By now, you probably understand where I am going with this. Let’s use Docker, duh! This way, I could test the production-ready configuration locally. And one of the sweetest spots would be to share it with my friends, of course.

I copied the implementation for introducing MOSQUITTO_USERNAME and MOSQUITTO_PASSWORD to change the credentials easier with .env configuration file from here https://github.com/thelebster/example-mosquitto-simple-auth-docker. If you are interested in adding this to your mosquitto container, ABSOLUTELY-hundred-percent check this out!

version: "2"
volumes:
  node_modules:
  build:
services:
  mosquitto:
    container_name: mosquitto
    build: ./config/mosquitto
    image: eclipse-mosquitto
    restart: always
    ports:
      - 1883:1883
    environment:
      - MOSQUITTO_USERNAME=${MOSQUITTO_USERNAME}
      - MOSQUITTO_PASSWORD=${MOSQUITTO_PASSWORD}
    volumes:
      - ./config/mosquitto/config:/mosquitto/config
      - ./config/mosquitto/data:/mosquitto/data
      - ./config/mosquitto/log:/mosquitto/log
  web:
    build: .
    command: mix phx.server
    container_name: morsecipher
    environment: 
      - SECRET_KEY_BASE=${SECRET_KEY_BASE}
      - MOSQUITTO_SERVER=mosquitto
      - MOSQUITTO_USERNAME=${MOSQUITTO_USERNAME}
      - MOSQUITTO_PASSWORD=${MOSQUITTO_PASSWORD}
    volumes:
      - .:/app
      - node_modules:/app/assets/node_modules
      - build:/app/_build
    ports:
      - 4000:4000

And finally, here’s the adapter for interaction with Raspberry Pi. Super easy, right?

defmodule Morsecipher.Verbalizer.Adapter do
  @behaviour Morseficator.Adapter
  @unit 55

  @spec setup :: {}
  def setup do
    {}
  end

  @spec beep(pid, non_neg_integer()) :: :ok
  def beep(_, times) do
    {:ok, _ref} = Tortoise.publish(Morsecipher, "morse/msg", "1", qos: 2)
    :timer.sleep(@unit * times)
    {:ok, _ref} = Tortoise.publish(Morsecipher, "morse/msg", "0", qos: 2)
    :ok
  end

  @spec sleep(any) :: :ok
  def sleep(n) do
    :timer.sleep(@unit * n)
  end
end

I used a Mosquitto server to send the messages using the MQTT protocol. Luckily enough, the tortoise library is available, making communication with the MQTT broker so much easy!

And now, meet the receiver, The Pi-LED! 👏

Go ahead and download it from here. https://github.com/morsecipher/piled

At first, I “implemented” (copy-pasted) an approach using Python. But then I thought, where is the fun in this? I decided to try Go instead! I tested the prototype with the adjusted script, and then I was ready to make a compiled binary that wouldn’t require installing all the necessary libraries. The best part was that I didn’t know any Golang, which added a spicy flavor this whole thing.

package main

import (
	"flag"
	"fmt"
	"time"

	mqtt "github.com/eclipse/paho.mqtt.golang"
	rpio "github.com/stianeikeland/go-rpio"
)

var (
	morseDirections string
	clientId        string
	username        string
	password        string
	host            string
	port            int
	gpin            int
)

var messagePubHandler mqtt.MessageHandler = func(client mqtt.Client, msg mqtt.Message) {
	err := rpio.Open()
	if err != nil {
		panic(fmt.Sprint("unable to open gpio", err.Error()))
	}

	defer rpio.Close()

	pin := rpio.Pin(gpin)
	pin.Output()

	if string(msg.Payload()) == "1" {
		pin.High()
		fmt.Printf("1\n")
	} else {
		pin.Low()
		fmt.Printf("0\n")
	}
}

var connectHandler mqtt.OnConnectHandler = func(client mqtt.Client) {
	opts := client.OptionsReader()
	fmt.Printf("Connected to %s\n", opts.Servers()[0].String())
	sub(client, morseDirections)
}

var connectLostHandler mqtt.ConnectionLostHandler = func(client mqtt.Client, err error) {
	fmt.Printf("Connect lost: %v\n", err)
} 

func main() {
	opts := mqtt.NewClientOptions()

	flag.StringVar(&host, "h", "morsecipher.xyz", "Specify a broker's host. Default is morsecipher.xyz")
	flag.IntVar(&port, "p", 1883, "Specify a broker's port. Default is 1883")
	flag.StringVar(&clientId, "c", "piled", "Specify a client_id for broker connection. Default is piled")
	flag.StringVar(&username, "u", "morsecipher", "Specify a username for broker connection. Default is morsecipher")
	flag.StringVar(&password, "P", "password", "Specify a password for broker connection. Default is password")
	flag.StringVar(&morseDirections, "t", "morse/msg", "Specify a custom topic to subscribe. Default is morse/msg")
	flag.IntVar(&gpin, "gpin", 4, "Specify a custom PIN to receive the signals on. Default is 4")

	flag.Parse()

	opts.AddBroker(fmt.Sprintf("tcp://%s:%d", host, port))
	opts.SetClientID(clientId)
	opts.SetUsername(username)
	opts.SetPassword(password)
	opts.SetDefaultPublishHandler(messagePubHandler)
	opts.SetAutoReconnect(true)
	opts.SetOrderMatters(true)

	opts.OnConnect = connectHandler
	opts.OnConnectionLost = connectLostHandler
	client := mqtt.NewClient(opts)
	if token := client.Connect(); token.Wait() && token.Error() != nil {
		panic(token.Error())
	}

	for {
		time.Sleep(time.Second)
	}
}

func sub(client mqtt.Client, topic string) {
	token := client.Subscribe(topic, 2, nil)
	token.Wait()
	fmt.Printf("Subscribed to topic: %s\n", topic)
}

To compile program for your raspberry pi

env GOOS=linux GOARCH=arm GOARM=5 go build piled.go

Transfer the binary to the raspberry pi with rsync

rsync -v piled [email protected]:~

And once it settled there, you can connect to mosquitto service like this:

[email protected] ~> ./piled -h your_ip_address -P password
Connected to tcp://your_ip_address:1883
Subscribed to topic: morse/msg

What’s next?

In the future, I want to go deeper, like a real ninja diver, to be rolling-in-the-deep! Aside from implementing cipher to encrypt morse code messages, I’m also considering making it possible to register and connect your Raspberry Pis through a personal account on the website, so you could interact with your own registered devices using Morsecipher. - God knows why you want it. I think there are a lot of interesting challenges we can work on. BUT! Not today, Satan! Let’s hold our horses for now and enjoy the way it works now - check it out on the YouTube video below!

Cheers! See you in the next episode, whatever that means!

Showcase

  1. https://github.com/morsecipher/morseficator
  2. https://github.com/morsecipher/morsecipher
  3. https://github.com/morsecipher/morsecipher_ui
  4. https://github.com/morsecipher/piled
  5. https://morsecipher.netlify.app