Funktionen laufen, Listing der Sketches und Skripte
Arduino-Sketch
Der Arduino-Sketch ist eine Abwandlung eines Sketches von der Fundinio-Webseite.
#define TRG 8 #define SIG 9 long dauer; float entfernung; void setup() { Serial.begin(9600); pinMode(TRG, OUTPUT); pinMode(SIG, INPUT); } void loop() { digitalWrite(TRG, LOW); delay(2); digitalWrite(TRG, HIGH); delay(10); digitalWrite(TRG, LOW); dauer = pulseIn(SIG, HIGH); entfernung = (dauer/2)*0.03432; if (entfernung >= 500 || entfernung <= 0){ Serial.print("DSTE "); Serial.println(entfernung); } else { Serial.print("DST "); Serial.println(entfernung); } }
Der Sketch ist nicht weiter komplex. Einzig der Befehl pulseIn() in Zeile 19 ist interessant. Er gibt die Zeit bis zur Input-Pulsmessung in µs aus. Der Rest ist einfach zu verstehen.
Python-Skripte
kalibrierung.py
''' Skript zur Kalibrierung des Experiments "Funktionen laufen" zur Langen Nacht der Wissenschaften 2019 Es erstellt die Datei calib.cal mit folgenden Daten: - port: USB-Port des Arduino - steps: Wertebereich der Funktion W=[0, steps] - min: Abstand zum Wert 0 - max: Abstand zum Wert steps Auswahl des Ports werden (sofern Port-Wahl erfolgreich, d.h. Verbindung ist möglich und der Arduino sendet das richtige Messprotokoll (DST bzw. DSTE) folgende Schritte ausgeführt: - Auswahl des Maximalwerts des Wertebereichs - Festlegung von min und max - Sind min und max festgelegt, schaltet die Entfernungsanzeige auf Funktionswerte um, sodass man leichter die Striche anbringen kann. Das Skript wurde von Martin Döpel erstellt (post@martin-doepel.de). Weimar/Jena, Oktober/November 2019 ''' from PyQt5 import QtCore, QtGui, QtWidgets import serial.tools.list_ports as lipo import serial import time import sys class Helper(): def __init__(self): self.port = "" self.steps = 5 self.min = -1.0 self.max = -1.0 self.ser = "" self.connected = False def getPorts(self): rv = [] ports = list(lipo.comports()) for p in ports: rv.append(str(p).split(" - ")) return rv def correctProtocol(self): try: self.ser = serial.Serial(port=self.port, baudrate=9600) except serial.serialutil.SerialException: return 1 stime = time.time() misses = 0 self.ser.flushInput() while (time.time()-stime < 4) and (misses < 7): proto = str(self.ser.readline()).split(" ")[0] if (proto == "b\'DST") or (proto == "b\'DSTE"): self.ser.flushInput() self.connected = True return 0 misses += 1 return 2 def saveData(self): file = open("calib.cal", "w") s = "port "+self.port+":\n" s += "steps "+str(self.steps)+"\n" s += "min " + str(self.min) + "\n" s += "max " + str(self.max) file.write(s) file.close() self.ser.close() def getValues(self): while True and self.connected: try: strg = self.ser.readline() d = int(float(str(self.ser.readline()).split("\\")[0].split(" ")[1])) return d except: return -1 return -1 class OFlaeche(QtWidgets.QWidget): def __init__(self): super().__init__() self.setupUi() def setupUi(self): self.h = Helper() self.cdst = 0.0 self.font1 = QtGui.QFont() self.font1.setPointSize(12) self.font2 = QtGui.QFont() self.font2.setPointSize(40) self.resize(350, 350) self.setWindowTitle("Kalibrierung Funktionen laufen") self.HeadArduino = QtWidgets.QLabel(self) self.HeadArduino.setText("Arduino-Port") self.HeadArduino.setGeometry(QtCore.QRect(30, 10, 131, 16)) self.HeadArduino.setFont(self.font1) self.Head2 = QtWidgets.QLabel(self) self.Head2.setText("Maximaler Funktionswert") self.Head2.setGeometry(QtCore.QRect(30, 70, 191, 16)) self.Head2.setFont(self.font1) self.Head2.setVisible(False) self.Head3 = QtWidgets.QLabel(self) self.Head3.setText("Position Extremwerte") self.Head3.setGeometry(QtCore.QRect(30, 140, 191, 16)) self.Head3.setFont(self.font1) self.Head3.setVisible(False) self.arduinooptions = QtWidgets.QComboBox(self) self.arduinooptions.setGeometry(QtCore.QRect(30, 30, 181, 22)) for p in self.h.getPorts(): self.arduinooptions.addItem(p[1]+" - "+p[0]) self.arduinochoose = QtWidgets.QPushButton(self) self.arduinochoose.setGeometry(QtCore.QRect(230, 30, 75, 23)) self.arduinochoose.setObjectName("arduinochoose") self.arduinochoose.setText("wählen") self.arduinochoose.clicked.connect(self.AChoose) self.mindst = QtWidgets.QLabel(self) self.mindst.setGeometry(QtCore.QRect(130, 170, 151, 20)) self.mindst.setObjectName("mindst") self.mindst.setText("noch nicht gewählt") self.mindst.setVisible(False) self.minchoose = QtWidgets.QPushButton(self) self.minchoose.setGeometry(QtCore.QRect(30, 170, 75, 23)) self.minchoose.setObjectName("minchoose") self.minchoose.setText("Minimum") self.minchoose.clicked.connect(self.MiCAction) self.minchoose.setVisible(False) self.maxchoose = QtWidgets.QPushButton(self) self.maxchoose.setGeometry(QtCore.QRect(30, 210, 75, 23)) self.maxchoose.setObjectName("maxchoose") self.maxchoose.setText("Maximum") self.maxchoose.clicked.connect(self.MaCAction) self.maxchoose.setVisible(False) self.maxdst = QtWidgets.QLabel(self) self.maxdst.setGeometry(QtCore.QRect(130, 210, 151, 20)) self.maxdst.setObjectName("maxdst") self.maxdst.setText("noch nicht gewählt") self.maxdst.setVisible(False) self.schritte = QtWidgets.QComboBox(self) self.schritte.setGeometry(QtCore.QRect(30, 100, 69, 22)) self.schritte.setObjectName("schritte") self.schritte.addItem("2") self.schritte.addItem("3") self.schritte.addItem("4") self.schritte.addItem("5") self.schritte.addItem("6") self.schritte.addItem("7") self.schritte.setCurrentIndex(3) self.schritte.currentIndexChanged.connect(self.StepChoose) self.schritte.setVisible(False) self.currdst = QtWidgets.QLabel(self) self.currdst.setGeometry(QtCore.QRect(130, 250, 201, 41)) self.currdst.setFont(self.font2) self.currdst.setObjectName("currdst") self.currdst.setText("nn") self.currdst.setVisible(False) self.calibexit = QtWidgets.QPushButton(self) self.calibexit.setGeometry(QtCore.QRect(30, 310, 181, 23)) self.calibexit.setObjectName("calibexit") self.calibexit.setText("Kalibrierung beenden") self.calibexit.clicked.connect(self.FinishAll) self.calibexit.setVisible(False) self.timer = QtCore.QTimer(self) self.timer.timeout.connect(self.updateDSTValue) self.timer.start(20) self.show() def updateDSTValue(self): dst = self.h.getValues() if dst == -1: self.currdst.setText("Error") elif (self.h.min < 0.0 or self.h.max < 0.0): self.cdst = dst self.currdst.setText(str(dst)) else: if (self.h.min < self.h.max): schrittweite = (self.h.max - self.h.min) / self.h.steps wert = (dst - self.h.min)/schrittweite else: schrittweite = (self.h.min - self.h.max) / self.h.steps wert = (self.h.min - dst) / schrittweite self.currdst.setText(str(wert)) self.calibexit.setVisible(True) def MiCAction(self): self.h.min = self.cdst self.mindst.setText(str(self.h.min)) def MaCAction(self): self.h.max = self.cdst self.maxdst.setText(str(self.h.max)) def StepChoose(self): self.h.steps = int(self.schritte.currentText()) def AChoose(self): self.h.port = str(self.arduinooptions.currentText()).split(" - ")[1] if self.h.correctProtocol() == 0: self.Head2.setVisible(True) self.Head3.setVisible(True) self.currdst.setVisible(True) self.schritte.setVisible(True) self.maxdst.setVisible(True) self.maxchoose.setVisible(True) self.minchoose.setVisible(True) self.mindst.setVisible(True) self.schritte.setVisible(True) elif self.h.correctProtocol() == 2: self.h.ser.close() msg = QtWidgets.QMessageBox() msg.setIcon(QtWidgets.QMessageBox.Critical) msg.setText("Falsches Protokoll") msg.setInformativeText("Der angeschlossene Arduino sendet nicht das korrekte Protokoll. Bitte versuchen Sie es noch einmal erneut und prüfen Sie bei nochmaligem Fehler das Programm des Arduino.") msg.setWindowTitle("Protokollfehler") msg.exec_() else: msg = QtWidgets.QMessageBox() msg.setIcon(QtWidgets.QMessageBox.Critical) msg.setText("Hardware-Problem") msg.setInformativeText("Verbindung zum Arduino kann nicht hergestellt werden.") msg.setWindowTitle("Verbindungsfehler") msg.exec_() def FinishAll(self): self.h self.h.saveData() self.close() if __name__== "__main__": app = QtWidgets.QApplication([]) ex = OFlaeche() sys.exit(app.exec_())
Im Wesentlichen handelt es sich hierbei um eine PyQt5-Oberfläche und einige Funktionen. Dennoch gibt es drei Funktionen, die ich gern kurz vorstellen würde. Sie sind im Listing hervorgehoben.
getPorts, Zeilen 38-43
Diese Funktion liefert eine Liste aller Ports mit angeschlossenen seriellen Endgeräten, also auch Arduinos. Mit dieser Funktion lassen sich alle zur Verfügung stehenden Ports auflisten.
correctProtocol, Zeilen 45.60
Diese Funktion überprüft, ob der angeschlossene Arduino Daten mit dem richtigen Protokoll sendet. Da in meinem Projekt vorgesehen ist, Arduinos immer wieder mit neuen Programmen zu benutzen, um bestimmte Messaufgaben zu erfüllen, ist dies eine wichtige Funktionalität, da so sichergestellt werden kann, dass immer die richtigen Daten geloggt werden.
getValues, Zeilen 72-80, insbes. Zeile 76
Hier werden die vom Arduino übertragenen Daten als String ausgewertet. Wichtig ist, dass beim Casten des seriellen Bytestreams als String erhält man eine Zeichenfolge wie z.B. b’DST 10.23\r\n‘. Offensichtlich kann durch ein anderes Decoden das b‘ vermieden werden (mehr dazu hier). In künftigen Fassungen wird dieser Fehler behoben (ich habe ihn erst beim Schreiben dieser Seite bemerkt). Problematisch ist jedoch \r\n, deswegen wird der String zunächst beim ersten Backslash geteilt, davon der erste Teil wird dann erneut am Leerzeichen geteilt. Der zweite Teil dieser Trennung ist 10.23. Diese Zahl wird zunächst in eine Gleitkommazahl und diese dann in einen Ganzzahl umgewandelt. So erhält man aus dem Protokoll den Abstand in cm.
updateDSTValue, Zeilen 187-202
Diese Funktion erneuert laufend die Anzeige der Abstandsangabe. Sind der Minimal- und der Maximalwert noch nicht gesetzt, wird nur die Distanz in cm angezeigt (191-193). Ansonsten werden die Werte umgerechnet. Diese Methode wird in Zeile 183 von einem QtTimer aufgerufen.
arduinohelper.py
import serial class ArduinoHelper(): def __init__(self, ardport, noe, scmin, scmax): self.arduinoport = ardport self.number_of_steps = noe self.scale_minimum = scmin self.scale_maximum = scmax self.distance_per_step = abs(scmax-scmin)/self.number_of_steps self.arduino = serial.Serial(port=self.arduinoport, baudrate=9600) self.min_smaller_max = scmin < scmax def getDistance(self): self.arduino.flushInput() misses = 0 errorous = True while misses < 10 and errorous: try: dis = int(float(str(self.arduino.readline()).split("\\")[0].split(" ")[1])) errorous = False except: misses += 1 if errorous: return -1 if self.min_smaller_max: rv = (dis - self.scale_minimum)/self.distance_per_step else: rv = (self.scale_minimum-dis)/self.distance_per_step if rv < 0: return 0 if rv > self.number_of_steps: return self.number_of_steps return rv
Dieses Skript übernimmt für das Spiel die Auswertung der vom Arduino gesendeten Messwerte.
starten.py
import tkinter as tk from time import time, sleep import arduinohelper sublevel = -1 root = tk.Tk() #Dimensionen bestimmen cw = root.winfo_screenwidth() #Breite der Diagrammfläche ch = root.winfo_screenheight()-50 # Höhe der Diagrammfläche #Kalibrierungsdatei lesen und Werte festlegen noe = -1 #Wie viele Einheiten auf y-Achse scalemin = -1 #Ort des 0-Punkts der Skala vor dem Sensor scalemax = -1 #Ort des Maximal-Punkts auf der Skala vor dem Sensor arduinoport = "" #Portname des Arduinoports file = open("calib.cal", "r") for line in file.readlines(): l = line.split(" ") if l[0] == "port": arduinoport = l[1].split(":")[0] elif l[0] == "steps": noe = int(l[1]) elif l[0] == "min": scalemin = int(l[1]) elif l[0] == "max": scalemax = int(l[1]) t = 10 #Zeit in s ey = round(ch-100)/noe-5 #Länge einer Einheit auf der y-Achse ex = round(cw-100)/t-2 #Länge einer Einheit auf der x-Achse arduino = arduinohelper.ArduinoHelper(arduinoport, noe, scalemin, scalemax) def disable_event(): #Wird aufgerufen, wenn der Schließen-Button gedrückt wird, verhindert den Abbruch des Experiments pass def close_program(event): #Bricht Experiment ab root.destroy() def getFunktion(name): #holt die Funktion aus der Datei <name>.txt, die kritische Punkte enthält file = open(name+".txt", "r") rv = [] for line in file.readlines(): ll = line.split(",") rv.append((float(ll[0]), float(ll[1]))) return rv def getFktValue(locp, x): #gibt den Wert der Zielfunktion für einen bestimmten x-Wert aus. locp: List of critical points getmn = False for i in range(len(locp)-1): if (locp[i][0] <= x) and (locp[i+1][0] > 0): m = (locp[i+1][1]-locp[i][1])/(locp[i+1][0]-locp[i][0]) n = locp[i+1][1]-m*locp[i+1][0] getmn = True if not getmn: return -1 return m*x+n def getError(locp, point, maxy): #Berechnet die derzeitige Abweichung und die maximal mögliche Abweichung von der Zielfunktion fy = getFktValue(locp, point[0]) if fy > maxy/2: maxe = fy else: maxe = maxy - fy return(round(abs(fy-point[1]),8), round(maxe, 8)) def calculateScore(maximal_possible_score): #Berechnet die Score global positions accumulated_error = 0 maximal_error = 0 for p in positions: fehler = getError(targetfunction, p, noe) accumulated_error += fehler[0] maximal_error += fehler[1] return (1-accumulated_error/maximal_error)*maximal_possible_score def drawTargetFunction(name): #Holt und zeichnet die Zielfunktion global tarfunc_graph rv=getFunktion(name) for i in range(0, len(tarfunc_graph)): canvas.delete(tarfunc_graph[i]) for i in range(len(rv)-1): p1 = getKoord(rv[i], False) p2 = getKoord(rv[i+1], False) tarfunc_graph.append(canvas.create_line(p1[0], p1[1], p2[0], p2[1], fill="#BBBBBB", width=4)) return rv def getKoord(k, protocol): #Errechnet die Koordinaten zur Darstellung, bei protocol=True werden die Ergebnisse protokolliert if protocol: positions.append(k) return (50+round(k[0]*ex), ch-50-round(k[1]*ey)) def updatePosition(op, xval=0,protocol=False): #Holt die derzeitige Position vom Arduino, bewegt den Punkt und zeichnet (bei Protokollierung) auch den Strich global pos, canvas, graph nk=getKoord((xval, arduino.getDistance()), protocol) canvas.move(pos, nk[0]-op[0], nk[1]-op[1]) if protocol: graph.append(canvas.create_line(op[0], op[1], nk[0], nk[1], fill="blue", width=3)) canvas.pack() canvas.update() return nk def countdown(l): #Zeigt Countdown der Länge l (in sek) an global canvas, pos secs = canvas.create_text(cw/2, ch/2, text=str(l), font=("Arial", 80, "bold"), fill="red") op = (0,0) pos = canvas.create_oval(op[0] - 8, op[1] - 8, op[0] + 8, op[1] + 8, fill="blue") canvas.update() for i in range(0, l): canvas.itemconfigure(secs, text=str(l-i)) canvas.update() stime = time() ctime = time() while ctime-stime < 1: op = updatePosition(op) ctime = time() canvas.delete(secs) sleep(0.2) return op def lauflos(): #Hier wird die Funktion gelaufen global pos koord = countdown(5) starttime = time() currenttime = time() while currenttime - starttime < t: currenttime = time() koord = updatePosition(koord, xval=currenttime-starttime, protocol=True) sleep(0.01) canvas.delete(pos) def chooseLvlClick(event): #Event-Handler LevelChooser global lvlc, cl, sublevel p1=(cw/2-200, ch/2-75) p2=(cw/2+50, ch/2-75) if cl and (event.x >= p1[0]) and (event.x <= p1[0]+150) and (event.y >=p1[1]) and (event.y <= p1[1]+150): sublevel += 1 lvlc = "f1"+str(sublevel%3) if cl and (event.x >= p2[0]) and (event.x <= p2[0]+150) and (event.y >=p2[1]) and (event.y <= p2[1]+150): sublevel += 1 lvlc = "f2"+str(sublevel%3) def chooseLevel(): #Level-Chooser global lvlc, cl cl = True headline = canvas.create_text(cw/2, ch/2-200, text="Level wählen", font=("Arial", 40, "bold"), fill="black") #easybutton = tk.Button(text="1", command=chooseLvlClick1, anchor="w") #easybutton.configure(width=150, activebackground="#106010") #easybutton.window = canvas.create_window(cw/2-200, ch/2-20, anchor="nw", window=easybutton) easybutton = canvas.create_rectangle(cw/2-200, ch/2-75, cw/2-50, ch/2+75, fill="#106010", tags=('easy')) easytext = canvas.create_text(cw/2-125, ch/2, text="1", font=("Arial", 30, "bold"), fill="white") diffbutton = canvas.create_rectangle(cw/2+50, ch/2-75, cw/2+200, ch / 2 + 75, fill="#6e0e10", tags=('diff')) difftext = canvas.create_text(cw / 2 + 125, ch / 2, text="2", font=("Arial", 30, "bold"), fill="white") canvas.bind("<Button-1>", chooseLvlClick) canvas.pack() while lvlc not in ["f10", "f11", "f12", "f20", "f21", "f22"]: canvas.update() rv = lvlc lvlc = 0 cl = False canvas.delete(headline) canvas.delete(easybutton) canvas.delete(easytext) canvas.delete(diffbutton) canvas.delete(difftext) canvas.update() return rv def startbutton(event): #Event-Handler Startbutton global showscore showscore = False def loescheVerlauf(): #Löscht den Verlauf des Trials global graph, positions, tarfunc_graph, targetfunction positions = [] for i in range(len(graph)): canvas.delete(graph[i]) if i in range(len(tarfunc_graph)): canvas.delete(tarfunc_graph[i]) canvas.update() targetfunction = [] def showscoreText(): #Zeigt den Punktwert an global showscore t = canvas.create_text(cw/2, ch/2, text="Berechne die Punktzahl", font=("Arial", 60, "bold")) canvas.update() sleep(1) canvas.delete(t) canvas.update() t = canvas.create_text(cw / 2, ch / 2-80, text="Punkte: "+str(int(calculateScore(10000))), font=("Arial", 60, "bold")) t2 = canvas.create_text(cw / 2, ch / 2+80, text="Zum Fortfahren Leertaste drücken!", font=("Arial", 40, "bold"), fill = "red") showscore = True while showscore: canvas.update() canvas.delete(t) canvas.delete(t2) canvas.update() root.title("Funktionen laufen") canvas = tk.Canvas(root, bg="white", width=cw, height=ch) canvas.pack() # Diagramm initialisieren sysItems = [] sysItems.append(canvas.create_line(50, 50, 50, ch-50, fill="black", width=2)) sysItems.append(canvas.create_line(50, ch-50, cw-50, ch-50, fill="black", width=2)) #Grid zeichnen for i in range(1,noe+1): sysItems.append(canvas.create_line(cw-50, ch-50-i*ey, 45, ch-50-i*ey, fill="#aaaaaa", width=1, dash=(2,2))) sysItems.append(canvas.create_text(30, ch-50-i*ey, text=str(i), font=("Arial", 20))) for i in range(1,t+1): sysItems.append(canvas.create_line(50+i*ex, 50, 50+i*ex, ch-50, fill="#aaaaaa", width=1, dash=(2, 2))) sysItems.append(canvas.create_text(50+i*ex, ch-30, text=str(i), font=("Arial", 20))) #Einstellen, dass das Programm nur mit der Tastenkombination <F10><q><x> geschlossen werden kann root.protocol("WM_DELETE_WINDOW", disable_event) root.bind("<F10><q><x>", close_program) #Event-Bindings für Level-Chooser und Startbutton root.bind("<space>", startbutton) canvas.bind("<Button-1>", chooseLvlClick) #Loslaufen positions = [] #gemessene Funktionswerte graph = [] #gelaufene Funktion, Canvas-Objekte tarfunc_graph = [] #Zielfunktion, Canvas-Objekte pos = "" #später wird darauf der Punkt initialisiert lvlc = "" #Variable für Level-Choose-Handler cl = True #gibt an, ob gerade ein Level ausgewählt werden kann (für Level-Choose-Event-Handler) showscore = False #gibt an, ob gerade ein Punktwert angezeigt wird (für Punktanzeige-Event-Handler) while True: targetfunction = drawTargetFunction(chooseLevel()) lauflos() showscoreText() loescheVerlauf() root.mainloop()
Das Spiel selbst wurde als tkinter-Oberfläche realisiert. Das Skript ist hoffentlich selbsterklärend.