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