Tuesday, July 14, 2020

Send Morse Code (CW) with your Voice

-----
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:
Demo at 12 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()
-----