Jester
Jester

Reputation: 11

How do I use a button to input Morse code?

So I’m working on a project for a class where I need to take button presses, as Morse code and then translate them to English. I am using a dictionary for this. However I am a little stuck on where to go after the dictionary and variables.

My variables are:

I have already imported gpiozero and time. And I know that I need to make some sort of timer to differentiate between dots and dashes, and a timer to differentiate between character space, letter space and a regular space. But I’m not quite sure how to set all of it up and then translate it to English.

Edit:

def user_message():
  message = ""
  current_word = ""
  start_time = 0
  while True:
    if button.is_pressed = False:
      start_time = time.time()
  else:
     if start_time != 0:
       end_time = time.time()
       pressed_time = end_time - start_time
       start_time = 0
       if pressed_time > dot_time:
         current_word += "-"
       else:      
         current_word += "."  
       time.sleep(character_space)  

This is the current code I have right now, sorry for my not being clear. The button is on a breadboard connected to a raspberry pi. And what I am trying to do is take inputs from the button in the form of Morse code and then have the Morse code translated to English. Thank you for being patient with me since I’m new to the platform.

I have tried to do functions and referenced multiple GitHub sources that do similar things but they haven’t worked.

Upvotes: 0

Views: 184

Answers (1)

simon
simon

Reputation: 5133

Update: I should perhaps make it clear that in this setup, I assume that there are no events available through the button that we could attach to – which, I guess, is the case for a button connected via a breadboard, as described in the question. Event handling would imply a different approach.

I see as the main issues of your approach of tracking the signal length, that (a) the times of spaces (character, letter, word) are currently not recorded, and (b) the sleep times between repeatedly querying the button are way too long:

As to (a): To distinguish where the sequence of characters (.,-) of your current letter ends and where the sequence of letters of your current word ends, character spaces, letter spaces, and word spaces need to be distinguished. You should therefore not only handle and distinguish the periods of the button being pressed, but also the periods of the button being released (not pressed).

As to (b): At present, after each character evaluation, the sleep time is set to character_space. In my opinion, this is way too long: consider that the times of the button presses may not be perfect and that your own code also has its own processing time; this will soon get your processing out of sync with the actual signal sequence (if it has ever been in sync in the first place).

Proposed adjustments

I think, both issues can be fixed with the following approach:

  1. Sample the button state with a higher frequency (say, ¹/₁₀ of the shortest relevant length, thus of the length of dot and character space).
  2. Record the time intervals of both alternating button states: pressed and released.

This can be achieved with the following code:

times = []  # Record time of all symbols (even idx: released, odd idx: pressed)
was_previously_pressed = False  # Monitor previous button state
start_time = time.time()

while True:
    is_pressed = button.is_pressed
    # Button state toggled → toggle time state and record time delta
    if was_previously_pressed != is_pressed:
        toggle_time = time.time()
        times.append(toggle_time - start_time)
        was_previously_pressed = is_pressed
        start_time = toggle_time
    # Stop listening after long silence (i.e. button released)
    if not is_pressed and time.time() - start_time > 5 * max(time_by_symbol.values()):
        break
    # Wait a tiny bit to spare CPU cycles
    time.sleep(.1 * min(time_by_symbol.values()))

Here, time_by_symbol is a dictionary that captures the expected intervals, following your definitions (at present we only use its values, but we will later also use its keys):

# ".": dot, "-": dash, "": char sep., " ": letter sep., " / ": space (word sep.)
time_by_symbol = {".": .15, "-": .45, "": .15, " ": .45, " / ": 1.05}

As a result, we get the list times, which captures the time intervals for the alternating buttons states, starting with the released / not pressed state. In other words: At even indexes of times, we now have the time intervals of spaces, at odd indexes of times, we now have the time intervals of characters.

What is now still missing is the decoding of the message.

Decoding

Decoding, in my opinion, should be broken down into the following steps: (1) decode time intervals to Morse characters and spaces, (2) decode Morse character sequences to Morse letters, (3) decode Morse letters to Latin letters (a, b, c, …).

As to (1): I would simply map each time interval to its closest match in expected length, after skipping the initial silence (and thus evaluating characters as candidates for the even indexes and spaces as candidates for the odd indexes in times[1:]):

decoded_morse = ""
for i, t in enumerate(times[1:]):
    # Skip initial silence (now even idx: pressed, odd idx: released), then
    # find and append symbol that is closest in length to the measured length
    candidates = ["", " ", " / "] if (i % 2) else [".", "-"]
    closest_symbol = lambda cand: abs(t - time_by_symbol[cand])
    decoded_morse += min(candidates, key=closest_symbol)

As a result, we should now have the string decoded_morse, which consists of Morse characters (.,-), spaces as Morse letter separators, and slashes (/) as word separators.

As to (2) and (3): I would define a list of all known Morse letters and a sequence of all corresponding Latin letters, where the index of the Morse letter matches the index of the Latin letter (alternatively, a dictionary would work). I would then try to find, for each sequence of Morse characters, the index of its corresponding Morse letter in the list of Morse letters, then use this index to get the corresponding Latin letter. Here we have to take care of some special cases:

  • If no Morse letter can be found for a given Morse character sequence (because the sequence is corrupted for some reason), a separate Latin character that signifies corruption could be returned instead (we will use "-" below).
  • The Morse word separator, which we decoded as / in decoded_morse, can be treated as its own Morse letter, which is simply being decoded as a space in the Latin letter sequence.

Altogether, this could look as follows:

abc_latin = "abcdefghijklmnopqrstuvwxyz "
# only "ab…yz" for simplicity, plus "/" for space
abc_morse = [
    ".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---",
    "-.-", ".-..", "--", "-.", "---", ".--.", "--.-", ".-.", "...", "-", "..-",
    "...-", ".--", "-..-", "-.--", "--..", "/"
]
decoded_latin = "".join(abc_latin[abc_morse.index(l)] if l in abc_morse
                        else "-" for l in decoded_morse.split(" "))

As a result, we should now have the actual message, decoded as a string of Latin letters, in decoded_latin.

Putting it all together

Putting it all together, and including a button simulator that runs in its own thread, this could look as follows:

import threading
import time

# ".": dot, "-": dash, "": char sep., " ": letter sep., " / ": space (word sep.)
time_by_symbol = {".": .15, "-": .45, "": .15, " ": .45, " / ": 1.05}

abc_latin = "abcdefghijklmnopqrstuvwxyz "
# only "ab…yz" for simplicity, plus "/" for space
abc_morse = [
    ".-", "-...", "-.-.", "-..", ".", "..-.", "--.", "....", "..", ".---",
    "-.-", ".-..", "--", "-.", "---", ".--.", "--.-", ".-.", "...", "-", "..-",
    "...-", ".--", "-..-", "-.--", "--..", "/"
]

# TODO: With ``given_latin``, provide a sequence of letters and spaces to be
#   encoded to morse and decoded back to latin letters ("abc…")
given_latin = "hello world"
# TODO: With ``verbose``, provide whether the capturing of button states should
#   be printed (True) or not (False)
verbose = True
assert set(given_latin).issubset(set(abc_latin))  # Check for valid letters
given_morse = " ".join([abc_morse[abc_latin.index(l)] for l in given_latin.strip().lower()])

print(f"Given sequence (latin): '{given_latin}'")
print(f"Given sequence (Morse): '{given_morse}'")

# Provide code that simulates the button
class Button(threading.Thread):
    def __init__(self, sequence):
        super().__init__(daemon=True)
        assert set(sequence).issubset(".- /")  # Check for valid characters
        # Split the sequence into lists of words of lists of letters
        self._sequence = [sub.split(" ") for sub in sequence.split(" / ")]
        self.is_pressed = False
        self.start()  # Auto-start the value generation

    def run(self):
        time.sleep(time_by_symbol[" / "])  # Head-start with a bit of waiting
        for word in self._sequence:
            for letter in word:
                for char in letter:
                    self.is_pressed=True
                    time.sleep(time_by_symbol[char])  # Dot/dash length
                    self.is_pressed=False
                    time.sleep(time_by_symbol[""])  # Char sep. length
                time.sleep(time_by_symbol[" "] - time_by_symbol[""])  # Letter sep.
            time.sleep(time_by_symbol[" / "] - time_by_symbol[" "])  # Word sep.
        while True:  # End of sequence → sleep forever
            time.sleep(time_by_symbol[" / "])

print("Produce and capture sequence …")
# Create the button simulator, passing it the sequence
button = Button(given_morse)

times = []  # Record time of all symbols (even idx: released, odd idx: pressed)
was_previously_pressed = False  # Monitor previous button state
start_time = time.time()

while True:
    is_pressed = button.is_pressed
    # Button state toggled → toggle time state and record time delta
    if was_previously_pressed != is_pressed:
        toggle_time = time.time()
        times.append(toggle_time - start_time)
        if verbose:
            # Show "not pressed" for ``is_pressed=True`` and vice versa,
            # because the time is for the previous state and ``is_pressed``
            # is the new state
            print(f"({times[-1]:.3f}) {'not ' if is_pressed else ''}pressed ")
        was_previously_pressed = is_pressed
        start_time = toggle_time
    # Stop listening after long silence (i.e. button released)
    if not is_pressed and time.time() - start_time > 5 * max(time_by_symbol.values()):
        break
    # Wait a tiny bit to spare CPU cycles
    time.sleep(.1 * min(time_by_symbol.values()))
    
print("Decode sequence …")
decoded_morse = ""
for i, t in enumerate(times[1:]):
    # Skip initial silence (now even idx: pressed, odd idx: released), then
    # find and append symbol that is closest in length to the measured length
    candidates = ["", " ", " / "] if (i % 2) else [".", "-"]
    closest_symbol = lambda cand: abs(t - time_by_symbol[cand])
    decoded_morse += min(candidates, key=closest_symbol)

decoded_latin = "".join(abc_latin[abc_morse.index(l)] if l in abc_morse
                        else "-" for l in decoded_morse.split(" "))
print(f"Decoded sequence (Morse): '{decoded_morse}'")
print(f"Decoded sequence (latin): '{decoded_latin}'")

Note that I included an external link to an earlier version of this code, in a comment to the question. I have done so since the question was closed at that time, and there are some minor differences as compared to the code above.

Upvotes: 1

Related Questions