Morsecode entschlüsseln mit Micropython

Bei mir in der Nähe gibt es einen Mystery Cache GC6CYF1 mit dem Namen Komische Töne 1. Es dauerte nicht lang und die Vermutung lag nahe dass es sich hierbei um Morsecodes handeln könnte. Nur wie entschlüssle ich diesen am besten? Vorallem, wie würde ich das machen wenn ich bei einem Cache unterwegs Morsecodes entschlüsseln muss? Einfach eine App installieren? Viel zu einfach! Als Bastler kann man da doch sicher was basteln…

Nachdem ich Micropython verwenden wollte und nicht viel Geld ausgeben war ein ESP32 NodeMCU Board die richtige Wahl. Kriegt man für ein paar Euro und im Gegensatz zum ESP8266 NodeMCU hat man mehr RAM und vorallem einen ADC der über die kompletten 0V – 3.3V geht. Der vom ESP8266 geht nur von 0V – 1V. Das erfordert dann also einen Spannungsteiler und den wollte ich mir sparen. Das der ESP32 WiFi kann ist zwar toll. Hier aber nicht nötig!

Zum hören hatte ich in irgendeinem Arduino-Sortiment noch ein “1 piece High Sensitivity Audio Microphone Module Audio Amplifier 20 dB Low Noise Absorb DC 3.3/5 V” um es mal im schönsten Denglish zu schreiben. Prinzipiell nichts anderes als ein Elekt-Mikrofon und ein LM393 Op-Amp mit ein bisschen Hühnerfutter. Das Modul läuft mit 3.3V und bietet sowohl einen digitalen Ausgang (Ton ist da, oder nicht) und einen analogen Ausgang der die verstärkte und angepasste Spannung des Mikrofons ausgibt.

Der digitale Ausgang kann mit einem Poti auf eine Lautstärke eingestellt werden ab der er einen Ton erkennt. Sobald er einen Ton erkennt geht D0 auf High und die LED leuchtet. Das ganze war mir aber zu unsicher und ich wollte nicht unterwegs irgendwo einen Lautstärkeregler mit einem Schraubenzier erst an meine Morsecodes anpassen müssen. Deshalb also der analoge Ausgang A0 den wir mit dem ESP32 (GPIO36 / A0 / ADC0 / SVP) verbinden. Dann noch 3.3V und GND verbinden und schon wären wir soweit.

Davor hatte ich mit dem Oszi gemessen, das Modul gibt bei keinem Ton ca. 1.6V aus (also in etwa die Mitte von 3.3V). Bei einem leisen Ton schwankt es 200mV Peak-To-Peak und bei einem lauten Ton bis zu 2V PP.

Den Programmcode habe ich in ein Modul gepackt, das Modul sieht so aus:

import time
import machine

class Morse:
    def __init__(self, adcpin, minloudness):
        # Globals
        self.minloudness = minloudness
        # Local Attributes
        self.hearingactive = 0
        self.tones = []
        self.pauses = []    
        self.morsestring = ''
        self.decodedstring = ''
        # Init the ADC
        self.adc_pin = machine.Pin(adcpin)
        self.adc = machine.ADC(self.adc_pin)
        self.adc.atten(self.adc.ATTN_11DB)
        # Morse alphabet used:
        # https://de.wikipedia.org/wiki/Morsezeichen
        self.morsecode = []
        self.morsecode.append({'A': '.-'})
        self.morsecode.append({'B': '-...'})
        self.morsecode.append({'C': '-.-.'})
        self.morsecode.append({'D': '-..'})
        self.morsecode.append({'E': '.'})
        self.morsecode.append({'F': '..-.'})
        self.morsecode.append({'G': '--.'})
        self.morsecode.append({'H': '....'})
        self.morsecode.append({'I': '..'})
        self.morsecode.append({'J': '.---'})
        self.morsecode.append({'K': '-.-'})
        self.morsecode.append({'L': '.-..'})
        self.morsecode.append({'M': '--'})
        self.morsecode.append({'N': '-.'})
        self.morsecode.append({'O': '---'})
        self.morsecode.append({'P': '.--.'})
        self.morsecode.append({'Q': '--.-'})
        self.morsecode.append({'R': '.-.'})
        self.morsecode.append({'S': '...'})
        self.morsecode.append({'T': '-'})
        self.morsecode.append({'U': '..-'})
        self.morsecode.append({'V': '...-'})
        self.morsecode.append({'W': '.--'})
        self.morsecode.append({'X': '-..-'})
        self.morsecode.append({'Y': '-.--'})
        self.morsecode.append({'Z': '--..'})
        self.morsecode.append({'1': '.----'})
        self.morsecode.append({'2': '..---'})
        self.morsecode.append({'3': '...--'})
        self.morsecode.append({'4': '....-'})
        self.morsecode.append({'5': '.....'})
        self.morsecode.append({'6': '-....'})
        self.morsecode.append({'7': '--...'})
        self.morsecode.append({'8': '---..'})
        self.morsecode.append({'9': '----.'})
        self.morsecode.append({'0': '-----'})
        self.morsecode.append({'À': '.--.-'})
        self.morsecode.append({'Ä': '.-.-'})
        self.morsecode.append({'È': '.-..-'})
        self.morsecode.append({'É': '..-..'})
        self.morsecode.append({'Ö': '---.'})
        self.morsecode.append({'Ü': '..--'})
        self.morsecode.append({'ß': '...--..'})
        self.morsecode.append({'-CH-': '----'})
        self.morsecode.append({'Ñ': '--.--'})
        self.morsecode.append({'.': '.-.-.-'})
        self.morsecode.append({',': '--..--'})
        self.morsecode.append({':': '---...'})
        self.morsecode.append({';': '-.-.-.'})
        self.morsecode.append({'?': '..--..'})
        self.morsecode.append({'-': '-....-'})
        self.morsecode.append({'_': '..--.-'})
        self.morsecode.append({'(': '-.--.'})
        self.morsecode.append({')': '-.--.-'})
        self.morsecode.append({"'": '.----.'})
        self.morsecode.append({'=': '-...-'})
        self.morsecode.append({'+': '.-.-.'})
        self.morsecode.append({'/': '-..-.'})
        self.morsecode.append({'@': '.--.-.'})
        self.morsecode.append({'-KA-': '-.-.-'})
        self.morsecode.append({'-BT-': '-...-'})
        self.morsecode.append({'-AR-': '.-.-.'})
        self.morsecode.append({'-VE-': '...-.'})
        self.morsecode.append({'-SK-': '...-.-'})
        self.morsecode.append({'-SOS-': '...---...'})
        self.morsecode.append({'-HH-': '........'})
        
    # Sample at around 8 kHz (120 µS between samples)
    # Sample 50 times, which needs 0.006 seconds     
    def sample(self):
        values = []
        start = time.ticks_ms()
        for i in range(50):
            val = self.adc.read()
            values.append(val)
        return (time.ticks_ms() - start, max(values) - min(values))     

    def getloudness(self):
        # Sample for around 0.1 seconds and return loudness
        maxloudness = 0
        for i in range(16):
            timetaken, loudness = self.sample()  
            if loudness > maxloudness:
                maxloudness = loudness
        return maxloudness  

    def hearformorse(self):
        toneduration = 0
        pauseduration = 0
        intone = 0
        inpause = 0
        self.tones = []
        self.pauses = []
        # Check how loud it is
        minloud = self.getloudness()
        minloud = minloud + self.minloudness
        # Hear while our attribute is != 0 - an Interrupt can deactivate it
        self.hearingactive = 1
        while self.hearingactive != 0:
            timetaken, loudness = self.sample()
            
            if loudness > minloud:
                intone = 1
                if inpause == 1:
                    # We come from a pause, this is a new tone, save the old one
                    if toneduration > 0:
                        self.tones.append(toneduration)
                    toneduration = 0
                    inpause = 0
                toneduration = toneduration + timetaken
            else:
                inpause = 1
                if intone == 1:
                    # We come from a tone, this is a new pause, save the old one
                    if pauseduration > 0:
                        self.pauses.append(pauseduration)
                    pauseduration = 0
                    intone = 0
                pauseduration = pauseduration + timetaken
        # Done? Add the last pause and tone!
        self.tones.append(toneduration)
        self.pauses.append(pauseduration)
    
    def decodeintomorse(self):
        # Merge the two lists
        tones_and_pauses = []
        for pair in zip(self.pauses, self.tones):
            tones_and_pauses.extend(pair)   
        # Calculate the thresholds
        # For tones
        threshes = []
        for i in range(len(self.tones)-1):
            threshes.append({'jump': abs((self.tones[i+1] - self.tones[i]) / 2), 'val': abs((self.tones[i] + self.tones[i+1]) / 2)})
        threshes = sorted(threshes, reverse = True, key=lambda k: k['jump'])    
        tonethresh = threshes[0]['val']
        threshes.clear()
        # For pauses - remove the first and the last one, because it is a (very long) pause
        templist = self.pauses[1:-1]
        templist.sort() 
        threshes = []
        for i in range(len(templist)-1):
            threshes.append({'jump': abs((templist[i+1] - templist[i]) / 2), 'val': abs((templist[i] + templist[i+1]) / 2)})
        threshes = sorted(threshes, reverse = True, key=lambda k: k['jump']) 
        longpausethres = threshes[0]['val']
        mediumpausethres = threshes[1]['val']
        templist.clear()
        threshes.clear()        
        # Build morse string
        self.morsestring = ''
        i = -1
        for actsign in tones_and_pauses:
            i = i + 1
            if i % 2 != 0:
                # We're a tone
                if actsign > tonethresh:
                    self.morsestring = self.morsestring + '-'
                else:
                    self.morsestring = self.morsestring + '.'
            else:
                # We're a pause
                if actsign > longpausethres:
                    # New word
                    self.morsestring = self.morsestring + 'XZ'
                if actsign > mediumpausethres:
                    # New sign
                    self.morsestring = self.morsestring + 'X'       
        return self.morsestring
    
    def decodemorseintotext(self):
        self.decodedstring = ''
        for actval in self.morsestring.split('X'):
            if actval == 'Z':
                self.decodedstring = self.decodedstring + ' '
            else:
                for acttest in self.morsecode:
                    acttestval = list(acttest.values())[0]
                    acttestlen = len(acttestval)
                    acttestdecoded = list(acttest.keys())[0]    
                    if actval == acttestval:
                        self.decodedstring = self.decodedstring + acttestdecoded
        # Because of the first long pause we have a space at the beginning, remove it
        self.decodedstring = self.decodedstring[1:]
        return self.decodedstring

Und die main.py die das Modul nutzt sieht so aus:

#esptool.py --port /dev/ttyUSB0 erase_flash
#esptool.py --port /dev/ttyUSB0 --chip esp32 write_flash -z 0x1000 /home/tc/Downloads/esp32-20181118-v1.9.4-684-g51482ba92.bin
#shell --buffer-size=30 -p /dev/ttyUSB0
#cp /home/ingres/Desktop/morse.py /pyboard
import morse
import machine

# Globals
# Where is the Microphone connected?
adcpin = 36
# Where is the button connected
inputbuttonpin = 0
# How much above normal loudness level is a beep?
minloudness = 150

def stophearing(pin):
    # Setting this to zero will stop the haring of the class
    mymorse.hearingactive = 0

btn1 = machine.Pin(inputbuttonpin, machine.Pin.IN, machine.Pin.PULL_UP)
btn1.irq(trigger=machine.Pin.IRQ_RISING, handler = stophearing)        

mymorse = morse.Morse(adcpin, minloudness)
mymorse.hearformorse()
mymorse.decodeintomorse()
mymorse.decodemorseintotext()

Das ganze gibt es auf Github:

https://github.com/ThomasChr/ESP32-Micropython-MorseDecode

Von der Logik her nehme ich einfach mit dem ADC ganz viele kleine Schnippselchen und messe wie groß MIN und MAX in einem Schnippselchen war. Ist es über einem bestimmten Wert so gehe ich davon aus dass es wohl ein Ton war. War es ein Ton so wird es zu einem Array der Töne hinzugefügt, war es kein Ton so wird es zu einem Array der Pausen hinzugefügt.

Die Ermittlung der Tonlänge für einen kurzen und langen Ton, sowie die Ermmittlung der Pausen-Längen (Pause zwischen den Tönen, Pause zwischen Buchstaben, Pause zwischen Wörtern) erfolgt automatisch.
Der ADC des ESP32 scheint dabei mit Micropython ca. 8 kHz Sampling zu erreichen, was wohl so ziemlich für den schnellsten Morser reichen sollte.

Bei dem Cache hat es meistens (wenn niemand im Hintergrund laut geredet hat) den Morsecode problemlos entschlüsseln können. Hier kann man sicherlich noch etwas Zeit reinstecken um das ganze stabiler zu gestalten.

Wenn man es unterwegs mitnehmen will so wäre es durchaus denkbar dass sobald man eine Powerbank anschließt dass Modul gleich loslegt und sobald man den Button drückt es fertig ist und entschlüsselt. Stromverbrauch ist für ein paar Minuten Anwendung kein Thema deshalb muss auch kein stromsparen implementiert werden. Es fehlt also nur noch ein Gehäuse mit einem Button und ein LCD um den entschlüsselten Text anzuzeigen. Der Button beendet dabei sowohl das hören auf Morsecodes und kann danach zum scrollen des Textes verwendet werden.

Gehäuse, LCD und Button ist noch nicht fertig. Mal sehen ob ich dazu mal lust habe…

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.