Morse over IP - PI

Officially this was part of a school outreach project, however, truthfully it was a chance to play with the new Pi-zero-W. I must admit i was expecting these to be a little sluggish, but actually they are very usable, ideal for small embedded projects. What i particularly like is that once you get the wifi interface configured you no longer need USB or HDMI connections, simply SSH into the Pi, mount the home directory using SFTP and you good to go, no cables, no clutter, a nice clean development environment. The aim of this project was to show how information can be encoded and communicated electronically. Could of taken this in different directions, but i like the sound of "Morse over IP", a blending of the old and the new.

Wifi

As the title suggests communication between the Morse transmitters-receiver units is wireless, using the Pi-zero-W's Wifi interface. The Wifi network is implemented using an old BT home router, shown in figure 1. This is not connected to a phone line, so the Pi does not have internet access, it is simple acting as a Wifi network hub / DHCP server.


Figure 1 : Old Wifi router

To configure the Wifi network on the Pi the /etc/network/interfaces file used is:

auto lo
iface lo inet loopback

auto wlan0
iface wlan0 inet dhcp
  wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf

The wpa_supplicant.conf file contain the Wifi SSID and password. The easiest way to configure this is to used the wpa_passphrase commandline tool, which generates the WPA PSK passphrase for a specified SSID. Type commands below replacing SSID and PASWORD to match:

sudo su -s /bin/bash
wpa_passphrase SSID PASSWORD > /etc/wpa_supplicant/wpa_supplicant.conf
exit

This will write the text:

network={
	ssid="SSID"
	#psk="PASSWORD"
	psk=HASH
}

Note, remember check that the wpa_supplicant.conf file is only rw for root, if your security minded also delete the password from this file and delete the .bash_history to remove the plain text password. This should auto connect on boot. Final remember to change the default password for the pi, especially if you have SSH enabled. Password changes and enabling SSH can both be achieved by running sudo raspi-config at the commandline. To make configuration easier later need to ensure that the same IP address is assigned to a Morse transmitters-receiver unit each time the lease expires, this is done in the router, as shown in figure 2.


Figure 2 : DHCP table

To ensure that the pi is connected to the router e.g. if the router was not turned on when the pi was booting the following shell script testWifi.sh and cron job runs every 5 minutes.

#!/bin/sh

#testWifi.sh - ping router if no response restart wifi

ping -c1 -w2 192.168.1.254 1> /dev/null 2> /dev/null

if test $? -ne 0
then
	echo wifi down
	/sbin/ifdown --force wlan0
	/sbin/ifup wlan0
else
	echo wifi up
fi 

To run this test every 5 minutes type at the commandline sudo crontab -e then add the following:

*/5 * * * * /home/pi/Morse/testWifi.sh 1>> /home/pi/Morse/log.txt 2>> /home/pi/Morse/error.txt

Unfortunately after some testing it was discovered that the router does not remember to assign the same IP address to each Pi i.e. it resets when power cycled :(, therefore needed to discover the IP addresses manually based on a known MAC address. Perhaps there is an easier way, but the shell script findPi.sh below does the job, writing the discovered IP address to the file ip-addr.txt (if found), otherwise writes a blank file.

#!/bin/bash
# findPi.sh - scan IP address from 60 - 80 for matching MAC address

MAX=80

#clean arp table
count=60
while test $count -lt $MAX
do
        ipaddr=192.168.1.$count
        arp -d $ipaddr 1> /dev/null 2> /dev/null
        count=$[$count+1]
done

#find this machines MAC address
mac=`ifconfig wlan0 | grep "HWaddr" | tr -s ' ' | cut -d ' ' -f5`
#echo $mac

#match to known pairs
match=false
if test $mac = "b8:27:eb:d5:8a:d6"
then
        #echo PI-1
        match=true
        PI="b8:27:eb:fa:e7:97"
fi

if test $mac = "b8:27:eb:fa:e7:97"
then
        #echo PI-2
        match=true
        PI="b8:27:eb:d5:8a:d6"
fi

if test $match = true
then
        #echo $PI
	#ping IP address, then test to see if MAC in arp table matches
        count=60
        while true
        do
                ipaddr=192.168.1.$count
                #echo $ipaddr
                ping -c1 $ipaddr 1> /dev/null 2> /dev/null
                count=$[$count+1]

                if test $count -gt $MAX
                then
                        echo > ip-addr.txt
                        exit 0
                fi

                matched_mac=`arp -n | grep $PI | tr -s ' ' | cut -d' ' -f3`
                matched_ip=`arp -n | grep $PI | tr -s ' ' | cut -d' ' -f1`

                if test $matched_mac = $PI
                then
                        echo $matched_ip
                        echo $matched_ip > /home/pi/Morse/ip-addr.txt
                        exit 0
		fi

        done
else
	echo > ip-addr.txt
fi

Morse Code

Originally Morse code was developed for land based electric telegraph systems, around the 1830s. Letter are encoded using a sequence of short and long current pulses, transmitted down a cable, commonly referred to as dots (.) and dashes (-). The sequence of pulses used to encode each letter is based on the frequency of the letter within the English language, as shown in figure 3 i.e. to reduce transmission times commonly used letters were assigned shorter sequences of dots and dashes.


Figure 3 : Morse code

By the 1890s, with the development of underwater cables these techniques enabled inter-continental communications, as shown in figure 4. This illustrates another factor i like about Morse code, it demonstrates the difference between a digital (discrete) and analogue (continuous) signal representation. When transmitting signals down these very long cables the resistance of the cable will reduce the received voltage, therefore, limiting the output (received) current. This could be a problem if an analogue representation is used i.e. the voltage level encodes the information, this reduction in received voltage therefore changes its value. However, as Morse code only uses two states: on/off, even if the received voltage is reduced we can use a simple relay as a discrete "amplifier" to regain the original signal e.g. the received voltage is large enough to energise the coil of a sensitive relay, controlling a pair of contacts that can switch a larger voltage, either making an audible noise or to drive this 'amplified' signal down a further cable i.e. a repeater.


Figure 4 : Telegraph system

Like most people i had played around with Morse code e.g. .../---/..., however, i was not aware that when transmitting these letters there are few more subtleties regarding the length (time) of the dots, dashes and spaces:

When transmitting this information there is no standard unit of time for a dot etc, this is up to the user. When first considering how the Pi based system should be implemented, did consider having code that would identify if the user had keyed a dot or a dash, then transmit a 'standard' dot or dash. However, decided this was needlessly complex. Therefore, implementing Morse over IP is quite simple, all you need to do is transmit the time the Morse key is held down i.e. the length of the pulse entered by the user. This does result in a delay i.e. the receiver only gets the transmitted dot/dash when the key is released, but as these unit will be spread across a room you can't see the transmitting unit, so you don't see the delay :)

UPDATE 16/08/17: this implementation doesn't quite work as specified i.e. if you hold down the key you will get a long space, followed by a long dash which breaks the above timing rules, however, in the context of a teaching aid its still ok, as the receiver can output a short (dot) and a long (dash) tone/light pulse, its just that the inter dot/dash timing isn't quite right.

Hardware

To start with i used bought in Morse keys, not a cheap items, an example is shown in figure 5. If i was to do this again would look into 3D printing these to save money, very expensive bit of kit. The Morse transmitters-receiver units are based on the Pi zero W, shown in figure 6, communication is wireless using the Wifi network interface. Each Pi is assigned a fixed IP address based on its MAC address, allowing one or more units to be paired together i.e. available hardware units can be configured into separate groups depending on the class/group sizes.


Figure 5 : Morse Key


Figure 6 : Pi zero W

To help reduce contact bounce the simple RC circuit shown in figure 7 was used. This also includes the resistors and LEDs used to indicate power, transmitted signal and received signal. The received and transmitted pulses are also used to trigger the pezio electric buzzers i.e. transmitter and receiving software are implemented as two concurrent processes each having a visual (LED) and audible (buzzer) indicator. Could of used a single buzzer, but decided to use two, so that i could keep the transmitter and receiving system completely separated (the two buzzer disks are just mounted in the same cone). Construction of the Morse transmitters-receiver units is shown in figure 8. Connection to the Morse key is via a 3.5mm audio jack, could of used different connectors, just what i had spare. The 3D printed units were based on these units on thingyverse: LINK LINK.




Figure 7 : Interface circuit












Figure 8 : Morse transmitters-receiver unit

To run the receive (rx.py) and transmit (tx.py) code the following shell script go.sh is used (below). This first calls findPi.sh (previously discussed) which will identify the tx IP address of the other Pi, saving this address to the file ip-addr.txt, whcih is then accessed by the transmit (tx.py) code.

#!/bin/sh
# go.sh - runs rx and tx processes
sleep 10
sudo /home/pi/Morse/findPi.sh 
sudo python /home/pi/Morse/rx.py &
sudo python /home/pi/Morse/tx.py &

To run this code at boot the following crontab entry is used (root):

@reboot /home/pi/Morse/go.sh 1>> /home/pi/Morse/log.txt 2>> /home/pi/Morse/error.txt

The python code for the receive (rx.py) and transmit (tx.py) is shown in figures 9 and 10 below. As always a mixer of method, Golden-Rule on coding style and software structure: it was easy for me to program and it works :), i'm sure there are better solutions.

import RPi.GPIO as GPIO
import socket
import time
import sys
import os

RX_BUZZER = 27 
RX_LED = 3 

# setup GPIO
GPIO.setwarnings( False )
GPIO.setmode( GPIO.BCM )
GPIO.setup( RX_BUZZER, GPIO.OUT )
GPIO.setup( RX_LED, GPIO.OUT )

# power on LED signal
GPIO.output( RX_LED, False )
GPIO.output( RX_BUZZER, False )
time.sleep(5)
  
# get local machine name
if( len(sys.argv) == 1 ):
  os.system('ifconfig wlan0 | grep "inet\ addr" | cut -d: -f2 | cut -d" " -f1 > /home/pi/Morse/rx-ip.txt')
  f = open('/home/pi/Morse/rx-ip.txt', 'r')
  host = f.read()
  f.close()
else:
  host = str( sys.argv[1] )

port = 9999   
print "RX: ", host                                       

# create a socket object
serversocket = socket.socket( socket.AF_INET, socket.SOCK_STREAM ) 
 
# bind to the port
serversocket.bind((host, port))                                  
  
# queue up to 5 requests
serversocket.listen(5)                                           
  
while True:
  # establish a connection
  clientsocket,addr = serversocket.accept()      
  
  # Receive no more than 1024 bytes
  time_data = clientsocket.recv(1024)
  clientsocket.close()
  
  #print("Got a connection from %s" % str(addr))
  #print time_data
  
  loop_count = (int(float(time_data) / 0.002) * 10) 
  #print loop_count
 
  # oscillate RX buzzer 
  for i in range( loop_count ):  
    GPIO.output( RX_LED, True )
    GPIO.output( RX_BUZZER, True )
    time.sleep(0.001)
    GPIO.output( RX_BUZZER, False )
    time.sleep(0.001)    

  GPIO.output( RX_LED, False )  

Figure 9 : Receiver code

import RPi.GPIO as GPIO
import socket
import time
import sys
import os

TX_SWITCH = 4
TX_BUZZER = 17 
TX_LED = 2 

# setup GPIO
GPIO.setwarnings( False )
GPIO.setmode( GPIO.BCM )
GPIO.setup( TX_BUZZER, GPIO.OUT )
GPIO.setup( TX_LED, GPIO.OUT )
GPIO.setup( TX_SWITCH, GPIO.IN, pull_up_down=GPIO.PUD_UP )

GPIO.output( TX_LED, False )
GPIO.output( TX_BUZZER, False )
time.sleep(5)

# get machine name
if( len(sys.argv) == 1 ):
  f = open('/home/pi/Morse/ip-addr.txt', 'r')
  host = f.read()
  f.close()                         
else:
  host = str( sys.argv[1] )

port = 9999
print "TX: ", host

# wait 100 sec for rx unit to boot
host_ready = False
GPIO.output( TX_LED, True ) 
for i in range(10):
  response = os.system("ping -c1 -w2 " + host + " > /dev/null 2>&1")
  if response == 0:
    host_ready = True
    print host, 'is up!'
    break
  else:
    print host, 'is down!'
     
  time.sleep(10)

GPIO.output( TX_LED, False )
start = 0
while True:
  # has button been pressed?
  if( GPIO.input( TX_SWITCH ) == 0):
    # record start time - not really needed 
    if( start == 1 ):
      start_time = time.clock() 
      start = 2

    # oscillate tx buzzer
    GPIO.output( TX_LED, True )
    GPIO.output( TX_BUZZER, True )
    time.sleep(0.001)
    GPIO.output( TX_BUZZER, False )
    time.sleep(0.001)

  # button not pressed
  else: 
    GPIO.output( TX_LED, False )
    
    # if previously pressed - open socket - tx on time
    if( start == 2 ):
      end_time = time.clock()
      elapse_time = end_time - start_time
      #print "Elapse Time: ", elapse_time
      start = 1

      # connection to RX 
      if host_ready:
        clientsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
        try:
          clientsocket.connect((host, port)) 
          clientsocket.send(str(elapse_time))
          clientsocket.close()      
        except socket.error as msg:
          print "Socket Error: %s" % msg
        except TypeError as msg:
          print "Type Error: %s" % msg
        
    if( start == 0 ):
      start = 1 

Figure 10 : Transmitter code

Need to make it a little more robust e.g. what is a MAC address can not be found, what if one of the units is turn off etc. Also need to add support for multiple receivers and transmitters e.g. one transmit and multiple receive etc.

Creative Commons Licence

This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

Contact details: email - mike.freeman@york.ac.uk, telephone - 01904 32(5473)

Back