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.
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
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.
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.
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