Python Koppler

In diesem Kapitel wird der verwendete Python Koppler beschrieben

Programmcode

#!/usr/bin/env python3

# Imports
import serial
import time
import yaml
import urllib.request
import urllib.parse
import sys
import logging
import os
import signal
import datetime

# Tracebacks unterdrücken
#  sys.tracebacklimit = 0

# Setup Logging
#  if os.path.isfile("dARts.log"):
    #  os.remove("dARts.log")
logging.basicConfig(filename='dARts.log', level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
logging.info("Start der Applikation")

#  systemd stuff
def signal_handler(signal, frame):
    logging.info("Beende Programm!")
    ser.close()
    logging.info("Serielle Kommunikation gestoppt")
    logging.info("ENDE")
    sys.exit(0)

signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)

# Config lesen
try:
    with open ("config.yml", 'r') as ymlconfig:
        cfg = yaml.load(ymlconfig, Loader=yaml.BaseLoader)
except:
    logging.error("Keine Konfigurationsdatei gefunden!")
    sys.exit("ERROR: Keine Konfigurationsdatei gefunden!")

# Scoreboard variablen aus Config
if cfg:
    host = cfg['scoreboard']['host']
    port = cfg['scoreboard']['port']

    # General variablen aus Config
    pfeilzeit = cfg['general']['pfeilzeit']
    serialport = cfg['general']['serial']

    # Serielle Konfiguration
    ser = serial.Serial()
    ser.port = serialport
    ser.baudrate = 9600
    ser.timeout = 1

# Port öffnen
try:
    ser.open()
    logging.info("Serielle Kommunikation gestartet")
except(serial.serialutil.SerialException):
    logging.error("Serielle Kommunikation nicht möglich!")
    sys.exit("ERROR: Serielle Kommunikation nicht möglich!")

# Variablen
matrix_dict = {
    '120': '/20/1',
    '220': '/20/2',
    '320': '/20/3',
    '101': '/1/1',
    '201': '/1/2',
    '301': '/1/3',
    '118': '/18/1',
    '218': '/18/2',
    '318': '/18/3',
    '104': '/4/1',
    '204': '/4/2',
    '304': '/4/3',
    '113': '/13/1',
    '213': '/13/2',
    '313': '/13/3',
    '106': '/6/1',
    '206': '/6/2',
    '306': '/6/3',
    '110': '/10/1',
    '210': '/10/2',
    '310': '/10/3',
    '115': '/15/1',
    '215': '/15/2',
    '315': '/15/3',
    '102': '/2/1',
    '202': '/2/2',
    '302': '/2/3',
    '117': '/17/1',
    '217': '/17/2',
    '317': '/17/3',
    '103': '/3/1',
    '203': '/3/2',
    '303': '/3/3',
    '119': '/19/1',
    '219': '/19/2',
    '319': '/19/3',
    '107': '/7/1',
    '207': '/7/2',
    '307': '/7/3',
    '116': '/16/1',
    '216': '/16/2',
    '316': '/16/3',
    '108': '/8/1',
    '208': '/8/2',
    '308': '/8/3',
    '111': '/11/1',
    '211': '/11/2',
    '311': '/11/3',
    '114': '/14/1',
    '214': '/14/2',
    '314': '/14/3',
    '109': '/9/1',
    '209': '/9/2',
    '309': '/9/3',
    '112': '/12/1',
    '212': '/12/2',
    '312': '/12/3',
    '105': '/5/1',
    '205': '/5/2',
    '305': '/5/3',
    '125': '/25/1',
    '225': '/25/2',
}

valide_wurfzaehler = ["0", "1", "2", "3"]

global pfeile_holen
global knopf_an
global pfeile_abgezogen
global won
global last_throw
global last_hit_time
global stuck_threshold

pfeile_holen = False
knopf_an = False
pfeile_abgezogen = True
won = False
last_throw = "0"
last_hit_time = None
stuck_threshold = datetime.timedelta(0,0,500000)

# Funktionen
def makeRequest(urlpart):
    try:
        url = host + ":" + port + "/game/throw" + urlpart
        response = urllib.request.urlopen(url)
        response_text = response.read().decode('utf-8')
        if "Dart" in response_text:
            set_pfeile_holen(True)
            button_on()
        if "Sieger" in response_text:
            set_won(True)
            button_on()
        if "Winner" in response_text:
            set_won(True)
            button_on()
        logging.info("SCOREBOARDANTWORT: {}".format(response_text))
        return response_text
    except:
        logging.error("Exception in makeRequest")


def requestRematch():
    try:
        url = host + ":" + port + "/game/rematch"
        response = urllib.request.urlopen(url)
        response_text = response.read().decode('utf-8')
        logging.info("SCOREBOARDANTWORT: {}".format(response_text))
        button_off()
        set_won(False)
        return response_text
    except:
        logging.error("Exception in requestRematch()")


def requestStuck():
    try:
        url = host + ":" + port + "/game/stuck"
        response = urllib.request.urlopen(url)
        response_text = response.read().decode('utf-8')
        logging.info("SCOREBOARDANTWORT: {}".format(response_text))
        button_on()
        time.sleep(.25)
        button_off()
        time.sleep(.25)
        return response_text
    except:
        logging.error("Exception ind requestStuck()")


def nextPlayer():
    try:
        url = host + ":" + port + "/game/nextPlayer"
        response = urllib.request.urlopen(url)
        response_text = response.read().decode('utf-8')
        if "Dart" in response_text:
            set_pfeile_holen(True)
            button_on()
        outputString = "NEXT\n"
        ser.write(outputString.encode('utf-8'))
        logging.info("SCOREBOARDANTWORT: {}".format(response_text))
        return response_text
    except:
        logging.error("Exception in nextPlayer()")


def get_wurfzaehler():
    try:
        url = host + ":" + port + "/game/getThrowcount"
        response = urllib.request.urlopen(url)
        response_text = response.read().decode('utf-8')
        global pfeile_holen
        if not pfeile_holen:
            check_button_on(response_text)
        return response_text
    except:
        logging.error("Exception in get_Wurfzaehler()")


def check_button_on(wurfzaehler):
    try:
        global knopf_an
        if wurfzaehler == "2":
            if not knopf_an:
                button_on()
        else:
            if knopf_an:
                button_off()
    except:
        logging.error("Exception in check_button_on()")


def button_on():
    global knopf_an
    outputString = "BAN\n"
    ser.write(outputString.encode('utf-8'))
    knopf_an = True


def button_off():
    global knopf_an
    outputString = "BAUS\n"
    ser.write(outputString.encode('utf-8'))
    knopf_an = False


def read_serial():
    string = ser.readline()
    if string:
        string = string[:-2]
        string = string.decode()
        return string
    else:
        return ""


def get_pfeile_holen():
    global pfeile_holen
    return pfeile_holen


def set_pfeile_holen(status):
    global pfeile_holen
    pfeile_holen = status
    return get_pfeile_holen()


def get_pfeile_abgezogen():
    global pfeile_abgezogen
    return pfeile_abgezogen


def set_pfeile_abgezogen(status):
    global pfeile_abgezogen
    pfeile_abgezogen = status
    return get_pfeile_abgezogen()


def get_won():
    global won
    return won


def set_won(status):
    global won
    won = status
    return get_won()


def get_last_throw():
    global last_throw
    return last_throw


def set_last_throw(string):
    global last_throw
    last_throw = string
    return get_last_throw()


def get_last_hit_time():
    global last_hit_time
    return last_hit_time


def set_last_hit_time(time):
    global last_hit_time
    last_hit_time = time
    return get_last_hit_time()


def main():
    if get_wurfzaehler() == "3":
        set_pfeile_holen(True)
        set_pfeile_abgezogen(False)
        button_on()

    while True:
        string = read_serial()
        if string:
            current_time = datetime.datetime.now()
            logging.info("string ist: {}".format(string))
            if string in matrix_dict:
                wurfzaehler = get_wurfzaehler()
                if not get_pfeile_holen():
                    if not wurfzaehler == "3":
                        if get_pfeile_abgezogen():
                            if string == get_last_throw():
                                # Zeit prüfen
                                global stuck_threshold
                                if (current_time - get_last_hit_time()) < stuck_threshold:
                                    logging.error("Dart steckt fest")
                                    antwort = requestStuck()
                                else:
                                    antwort = makeRequest(matrix_dict[string])

                                set_last_hit_time(datetime.datetime.now())

                            else:
                                antwort = makeRequest(matrix_dict[string])
                                set_last_hit_time(datetime.datetime.now())

                            set_last_throw(string)

            elif string == "FEHLWURF":
                wurfzaehler = get_wurfzaehler()
                if not get_pfeile_holen():
                    if not wurfzaehler == "3":
                        if get_pfeile_abgezogen():
                            antwort = makeRequest('/0/1')

            elif string == "KNOPF":
                if get_won():
                    antwort = requestRematch()
                else:
                    wurfzaehler = get_wurfzaehler()
                    if wurfzaehler in valide_wurfzaehler:
                        if not get_pfeile_holen():
                            while not wurfzaehler == "3":
                                antwort = makeRequest('/0/1')
                                wurfzaehler = get_wurfzaehler()
                                button_on()
                            set_pfeile_abgezogen(False)

                        else:
                            antwort = nextPlayer()
                            set_pfeile_holen(False)
                            set_pfeile_abgezogen(True)
                            button_off()

            elif string == "PFEILE":
                #wurfzaehler = get_wurfzaehler()
                #if get_pfeile_holen() and wurfzaehler == "3":
                if get_pfeile_holen():
                    outputString = "PERK\n"
                    ser.write(outputString.encode('utf-8'))
                    time.sleep(int(pfeilzeit))
                    antwort = nextPlayer()
                    set_pfeile_holen(False)
                    set_pfeile_abgezogen(True)
                    button_off()

# Main Loop
if __name__ == "__main__":
    main()

Imports

In diesem Abschnitt werden notewendige Imports gemacht. Darunter zum Beispiel die Programmbibliothek serial, die den Arduino via serieller Konsole ausliest.

Systemd stuff

Dieser Abschnitt ist notwendig, um den Programmcode sauber beenden zu können, wenn er als systemd Dienst verwendet wird. Andernfalls würde beim Stoppen des Dienstes die serielle Kommunikation nicht sauber beendet.

Config

In diesem Abschnitt wird die config.yml ausgelesen und interpretiert.

Scoreboard variablen

Die Variablen über den Host und den Port, wo der Dart-O-Mat 3000 zu finden sind werden hier gespeichert.

General variablen

Die Variablen über die Auszeit beim Pfeile Abziehen und den seriellen Port werden hier gespeichert.

Serielle Konfiguration

Es wird eine Instanz ser der Klasse serial.Serial() erzeugt und konfiguriert.

Ports öffnen

In diesem Abschnitt wird versucht die serielle Kommunikation mit dem Arduino zu starte. Schlägt das fehl wird der Code beendet mit einer Fehlermeldung.

Variablen

In diesem Abschnitt werden die notwendigen Variablen definiert, die zum Abprüfen von Zuständen verwendet werden. Außerdem werden die Daten aus dem Matrix Array in das API Format vom Dart-O-Mat 3000 übersetzt, sodass zum Beispiel der Wert 320 (Triple 20) einen URL Anteil von 20/3 (Triple 20) ergibt. Dieses Dicitionary wird später von der Funktion makeRequest(urlpart) verwendet.

Funktionen

makeRequest(urlpart)

Diese Funktion konstruiert aus der host und port Variablen, sowie dem übergebenen urlpart einen Request an das Scoreboard. Dann wird die Antwort ausgewertet. Enthält sie das Wort “Dart” wird die Variable pfeile_holen auf True gesetzt. Beim Wort “Sieger” oder “Winner” wird die Variable won auf True gesetzt. Außerdem wird in beiden Fällen das Licht am Knopf aktiviert, indem mithilfe der Funktion button_on() das Wort BAN auf die Konsole geschrieben wird.

Die aufgerufenen URL an der Scoreboard API lautet: http://IP:PORT/game/throw/Wurfwert(z.B. /20/3 für triple 20)

requestRematch()

Diese Funktion wird ausgeführt, wenn die Variable won auf True steht und der Knopf gedrückt wird. So kann schnell ein neues Match mit den gleichen Einstellungen gestartet werden.

Die aufgerufenen URL an der Scoreboard API lautet: http://IP:PORT/game/rematch

requestStuck()

Diese Funktion wird aufgerufen, wenn in kürzester Zeit mehrmals die gleichen Werte auf der Konsole empfangen werden. Hierbei darf man davon ausgehen, dass ein Dartpfeil stecken geblieben ist. Das Scoreboard wird audiovisuell reagieren, sodass der Spieler auf den steckengebliebenen Pfeil aufmerksam gemacht wird.

Die aufgerufenen URL an der Scoreboard API lautet: http://IP:PORT/game/stuck

nextPlayer()

Diese Funktion schaltet zum nächsten Spieler um.

Die aufgerufenen URL an der Scoreboard API lautet: http://IP:PORT/game/nextPlayer

get_wurfzaehler()

Diese Funktion ermittelt die Anzahl der Würfe, die ein Spieler in der aktuellen Wurfrunde (0-3) bereits gemacht hat. Die Wurfanzahl wird an mehreren Stellen ausgewertet und ist entscheiden dafür, wie sich der Python Koppler verhält. Außerdem wird über diese Funktion gesteuert, ob der Knopf angeschaltet werden muss, zum Beispiel wenn schon 3 Würfe gemacht wurden. Dies ist das Signal für den Spieler, dass er seine Pfeile holen muss.

Die aufgerufenen URL an der Scoreboard API lautet: http://IP:PORT/game/getThrowcount

check_button_on()

In dieser Funktion wird anhand der Anzahl der bereits geworfenen Darts entschieden, ob der Knopf aus oder an sein muss.

button_on()

Mithilfe dieser Funktion wird das Wort “BAN” auf die serielle Konsole geschrieben und der Arduino schaltet so die LED des Knopfs an.

button_off()

Mithilfe dieser Funktion wird das Wort “BAUS” auf die serielle Konsole geschrieben und der Arduino schaltet so die LED des Knopfs aus.

button_blinken()

Mithilfe dieser Funktion wird der Knopf zum Blinken gebracht

read_serial()

Diese Funktion liest einen String von der seriellen Konsole aus und formatiert ihn (Entfernt Zeilenumbrüche). Dann gibt sie den formatierten Wert zurück.

GET- und SET-Methoden

Die unterschiedlichen GET- und SET-Methoden sind dazu da die globalen Variablen für die Spielsteuerung zu schreiben oder auszulesen.

main() Als Hauptprogrammloop

Diese Funktion ist eine endlosschleife, wie im Arduino Sketch und steuert zyklisch die Kommunikation zwischen Arduino und dem Scoreboard Dart-O-Mat 3000.

Im Schritt 1 wird ein String von der Konsole empfangen mithilfe der Funktion read_serial(). Wenn ein String gelesen werden konnte wird die aktuelle Zeit als Zeitstempel ermittelt (Dient der Stuck Dart Erkennung).

Im Schritt 2 wird ausgewertet, ob der erkannte String im Dictionary der Dart Matrix steht. Wenn ja und weder pfeile_holen True ist oder der Wurfzähler 3 beträgt so wird anschließend auf Stuck Dart geprüft. Der Stuck Dart wird ermittelt, indem verglichen wird welches der letzte Wurfwert war. Ist es derselbe, wie der aktuelle Wurfwert wird das Zeitdelta verglichen zwischen letztem Empfang des Wertes und dem aktuellen. Ist dieser kleiner als die Zeitschwelle (halbe Sekunde) wird von einem Stuck Dart ausgegangen und die Funktion requestStuck() aufgerufen. Andernfalls wird der Wert an das Scoreboard gesendet via makeRequest() und der Wurf wird verbucht.

Ist der String kein Wurfwert wird in Schritt 3 kontrolliert, ob es das Wort “FEHLWURF” ist. Wenn ja und werder pfeile_holen True ist noch der Wurfzähler 3 beträgt und die Variable pfeile_abgezogen True ist, so wird via makeRequest() der Wert 0, also Fehlwurf an das Scoreboard geschickt.

Ist der String kein Fehlwurf wird in Schritt 4 kontrolliert, ob es das Wort “KNOPF” ist. Wenn ja, wird geprüft, ob won auf True steht. Ist das der Fall wird via requestRematch() ein neues Match angefordert. Andernfalls wird der Wurfzähler geprüft. Abhängig davon welchen Wert er hat werden entweder die Würfe mit Fehlwürfen (makeRequest(‘/0/1’)) aufgefüllt oder auf den nächsten Spieler gewechselt (nextPlayer()).

Ist der String nicht Knopf wird in Schritt 5 kontrolliert, ob es das Wort “PFEILE” ist. In diesem Fall und dem Fall, dass sowohl pfeile_holen True ist und der Wurfzähler 3 beträgt wird die Pfeileholzeit lang gewartet und dann auf den nächsten Spieler geschaltet.

Dann startet der cycle von vorne.

Logging

Alle Events werden zentral in der Datei dARts.log festgehalten. Man kann sich die Datei zu Debug zwecken also auf der Konsole des Pi’s anschauen, indem man folgenden Aufruf im Ordner des Python Kopplers startet:

tail -f dARts.log