CW or Morse Code is a way ham radio operators send messages with a simple tone of two durations. It was a must before the days of wireless voice communications and still remains popular because it is a fun challenge.
There are many online tools that will convert text into Morse Code. However, a Google AIY Voice Bonnet came into our possession and we thought we could take that another step by creating a "Speech to Morse" translator that would actually send code over the air on the ham radio bands from spoken word.
To use the rig, you don't need a ham radio license unless you connect to an actually ham radio transmitter; which is kinda the whole point. My quick ham radio advertisement: Get a FCC Amateur Radio license; it's not hard and is an extremely interesting hobby. BTW, learning Morse Code is no longer a requirement.
-----
When we were given the AIY Voice Bonnet we really didn't even know what it did. We knew it needed a Raspberry PI and it could act somewhat like a Google Voice Assistant. We had a Raspberry PI Zero handy and after some Google search engineering we found a disk image file to get us started.
Like a lot of internet projects APIs can change and documentation can be conflicting, we finally got the RasPI and Voice Bonnet to talk to the web after some effort. A full blown Google Voice Assistant this thing is NOT; if you want one of those then BUY, don't BUILD. After leaning into the AIY GitHub for knowledge we were finally rewarded with success.
-----
The connections are easy and just follow the AIY GitHub to wire the ACTIVATE BUTTON. We used IO Pin 16 on the RasPI for the relay control because it wasn't used by the AIY Voice Bonnet. The AIY Voice Bonnet has four GPIO pins on board, but we could never get them to work. Here's the summary:
And here is the rig in action. The Python code to make it run is below.
Demo at 20 WPM:
Thanks and 73! Here is our Python source code:
#!/usr/bin/env python3
#
# July 2020
# Voice to Morse Code Translator
# WhiskeyTangoHotel.Com
#
# RaspberryPI Zero and AIY Voice Bonnet.
#
# Use Google Voice AIY to convert speech to text.
# Read that text file created and convert the text.
# Turn that text into to Morse Code "." and "-".
# Close a relay to transmit it over the air with a ham radio
#
#
# BTW, CW is fun to learn.
# * --... / ...-- // --... / ...--
# Some program set up and Define some variables
import os # needed to delete the text file after reading
import time # needed for delays
from datetime import datetime # need to calc run time
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM) # Use BCM I/O board pin numbers
SendingLine = 0 # this is the text file line we will send
StartCW = 0 # Char position to start sending from the parsed line
EndCW = 0 # Char position to stop sending from the parsed line
keyer = 16 # Pin where the keyer relay is connected
GPIO.setup(keyer, GPIO.OUT)
GPIO.output(keyer, GPIO.LOW) # make certain we are not keyed down
# Load the CW database 'dits' and 'dahs' for each Alphanumeric. Boring....
A = ".-"
B = "-..."
C = "-.-."
D = "-.."
E = "."
F = "..-."
G = "--."
H = "...."
I = ".."
J = ".---"
K = "-.-"
L = ".-.."
M = "--"
N = "-."
O = "---"
P = ".--."
Q = "--.-"
R = ".-."
S = "..."
T = "-"
U = "..-"
V = "...-"
W = ".--"
X = "-..-"
Y = "-.--"
Z = "--.."
period = ".-.-.-"
zero = "-----"
one = ".----"
two = "..---"
three = "...--"
four = "....-"
five = "....."
six = "-...."
seven = "--..."
eight = "---.."
nine = "----."
# WPM spacing calculated using the "Paris Standard"
# Per Morse code convention: 1 Dah = 3 Dits, LETTERS are spaced by one Dit, WORDS are spaced by one Dah.
WPM = 20.0 # Don't leave off the ".0" or WPM will be an integer (we want it to float)
Dit = 1.200 / WPM
Dah = Dit * 3.0
between_ditdah_spacing = Dit
between_character_spacing = Dit * 2
between_word_spacing = Dit * 3
"""Calls to Google Assistant GRPC recognizer."""
# We use AIY in the cloud to do the speech to text processing. The RaspberryPI just can't do it as good.
# Copyright 2017 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import locale
import logging
import signal
import sys
from aiy.assistant.grpc import AssistantServiceClientWithLed
from aiy.board import Board
def volume(string):
value = int(string)
if value < 0 or value > 100:
raise argparse.ArgumentTypeError('Volume must be in [0...100] range.')
return value
def locale_language():
language, _ = locale.getdefaultlocale()
return language
def main():
logging.basicConfig(level=logging.DEBUG)
signal.signal(signal.SIGTERM, lambda signum, frame: sys.exit(0))
parser = argparse.ArgumentParser(description='Assistant service example.')
parser.add_argument('--language', default=locale_language())
parser.add_argument('--volume', type=volume, default=100)
args = parser.parse_args()
with Board() as board:
assistant = AssistantServiceClientWithLed(board=board,
volume_percentage=args.volume,
language_code=args.language)
while True:
logging.info('Press button to start conversation...')
board.button.wait_for_press()
logging.info('Conversation started!')
assistant.conversation()
# Voice to CW routine starts here are the dim RED button is pressed
# Button will turn bright RED while the rig is listening
# Button will dim blink RED when ready for next speech input
# Read the text file created by the speech to text and get it ready to send as CW
# Look for "Trigger Lines" in the text file. Parse and send the line above it
Trig1 = "Recording stopped."
Trig2 = "End of audio request detected"
Trig3 = "Updating conversation state"
i = -1
Get_file = "keywhat.txt" # this is the text file created from AIY that we are going to process
mylines = []
with open (Get_file, 'rt') as myfile: #'rt' means 'read text data'
for line in myfile:
i = i + 1
mylines.append(line)
# If we find one to the Trigger lines, we have passed the voice text by one line in the file
if mylines[i].find(Trig1) and mylines[i].find(Trig2) and mylines[i].find(Trig3)!= -1 :
SendingLine = i-1
print (SendingLine, mylines[SendingLine])
Send_text = mylines[SendingLine]
# Strip out only the text we want to send over the air
StartCW = int((mylines[SendingLine].find('"')) + 1)
EndCW = int(len(mylines[SendingLine]) - 3)
# print StartCW, EndCW # used for debug
SendasCW = Send_text[StartCW:EndCW] # this is the text sent as CW
#SendasCW = "test" # used for debug
SendasCW = (SendasCW.upper()) # and we always make certain it is all caps
# We have the text. Now parse it and send each letter as CW
print ("Going to send: " + SendasCW + " at " + str(int(WPM)) + " WPM.")
print ("-------------------------------------------")
L = len(SendasCW)
t0 = datetime.now()
for x in range(0,L): # Start character converstion to dit and dahs
Text_substring = SendasCW[x:x+1]
print ("Sending: " + Text_substring) # for debug
if Text_substring == "0" : Text_substring = zero
if Text_substring == "1" : Text_substring = one
if Text_substring == "2" : Text_substring = two
if Text_substring == "3" : Text_substring = three
if Text_substring == "4" : Text_substring = four
if Text_substring == "5" : Text_substring = five
if Text_substring == "6" : Text_substring = six
if Text_substring == "7" : Text_substring = seven
if Text_substring == "8" : Text_substring = eight
if Text_substring == "9" : Text_substring = nine
if Text_substring == "." : Text_substring = period
if Text_substring == "A" : Text_substring = A
if Text_substring == "B" : Text_substring = B
if Text_substring == "C" : Text_substring = C
if Text_substring == "D" : Text_substring = D
if Text_substring == "E" : Text_substring = E
if Text_substring == "F" : Text_substring = F
if Text_substring == "G" : Text_substring = G
if Text_substring == "H" : Text_substring = H
if Text_substring == "I" : Text_substring = I
if Text_substring == "J" : Text_substring = J
if Text_substring == "K" : Text_substring = K
if Text_substring == "L" : Text_substring = L
if Text_substring == "M" : Text_substring = M
if Text_substring == "N" : Text_substring = N
if Text_substring == "O" : Text_substring = O
if Text_substring == "P" : Text_substring = P
if Text_substring == "Q" : Text_substring = Q
if Text_substring == "R" : Text_substring = R
if Text_substring == "S" : Text_substring = S
if Text_substring == "T" : Text_substring = T
if Text_substring == "U" : Text_substring = U
if Text_substring == "V" : Text_substring = V
if Text_substring == "W" : Text_substring = W
if Text_substring == "X" : Text_substring = X
if Text_substring == "Y" : Text_substring = Y
if Text_substring == "Z" : Text_substring = Z
# Now send the dit/dahs for the Self Test Callsign Character
CW_length = int(len(Text_substring))
time.sleep(between_character_spacing) # a delay between characters
for j in range(0, CW_length): # Send tones and blink LED in CW
Dit_or_Dah = Text_substring[j:j+1]
# HIGH closes a relay connect to the ham radio. LOW opens the relay
# This relay closure is the same as pushing down on a straight key
if (Dit_or_Dah) == "." : # process a Dit
print (Dit_or_Dah)
GPIO.output(keyer, GPIO.HIGH)
time.sleep(Dit) # a delay of "1 Dit" between dit/dahs in a character
GPIO.output(keyer, GPIO.LOW) #make certain we are not keyed down
time.sleep(between_ditdah_spacing) # a delay bewteen characters
# end if process a Dit
if (Dit_or_Dah) == "-" : # process a Dah
print (Dit_or_Dah)
GPIO.output(keyer, GPIO.HIGH)
time.sleep(Dah) # a delay of "1 Dit" between dit/dahs in a character
GPIO.output(keyer, GPIO.LOW) #make certain we are not keyed down
time.sleep(between_ditdah_spacing) # a delay bewteen characters
# end if process a Dah
if (Dit_or_Dah) == " " : # process a SPACE (delay between words)
print ("<SPACE>")
GPIO.output(keyer, GPIO.LOW) #make double certain we are not keyed down
time.sleep(between_word_spacing) # a delay of "3 Dit" between words
# end if process a SPACE
GPIO.output(keyer, GPIO.LOW) #make absolutely certain we are not keyed down
t1 = datetime.now() - t0 # how long did it take to send?
print ("-------------------------------------------")
print ("Completed in " + str(t1) + " seconds at " + str(int(WPM)) + " WPM to send:")
print (SendasCW)
# close and delete voice to text cloud file to get ready for the next voice decode
time.sleep(1) # file handling delay, probably not needed
myfile.close()
os.remove(Get_file)
time.sleep(1) # file handling delay, probably not needed
myfile = open(Get_file, "w")
if __name__ == '__main__':
main()
-----