From 55744f3983c76e0b118e0cbb6e87254545c6b3fd Mon Sep 17 00:00:00 2001 From: Christian Schiller Date: Mon, 19 Dec 2022 22:33:23 +0100 Subject: [PATCH] =?UTF-8?q?GUI-Funktion=20hinzugef=C3=BCgt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Nutzeranleitung-Einzelspielmodus.md | 9 + .../Nutzeranleitung-Gruppenspielmodus.md | 8 + Calliope-Rennspiel/Python/ki-datenlogger.py | 11 +- Calliope-Rennspiel/Python/ki-gui-win.py | 304 ++++++++++++++++++ .../Python/ki-trainieren-sklearn.py | 18 +- 5 files changed, 344 insertions(+), 6 deletions(-) create mode 100755 Calliope-Rennspiel/Python/ki-gui-win.py diff --git a/Calliope-Rennspiel/Dokumentation/Nutzeranleitung-Einzelspielmodus.md b/Calliope-Rennspiel/Dokumentation/Nutzeranleitung-Einzelspielmodus.md index d15bbe8..e374f20 100644 --- a/Calliope-Rennspiel/Dokumentation/Nutzeranleitung-Einzelspielmodus.md +++ b/Calliope-Rennspiel/Dokumentation/Nutzeranleitung-Einzelspielmodus.md @@ -20,6 +20,15 @@ Pro Schüler wird 1 "Rennspiel-Calliope" benötigt, der direkt per USB an den Re ![Einzelspielmodus Basisversion](./einzelspiel-basis.png) +#### GUI (Grafische Nutzeroberfläche) + +Seit Release "Speedy" (19.12.2022) können die Schritte 1 bis 5A statt manuell bzw. via Kommandozeile +auch via einer komfortablen grafischen Nutzeroberfläche (GUI) durchgeführt werden (Stand 19.12.2022 nur für Windows verfügbar) + +Dazu im Unterverzeichnis `/ki-in-schulen-master/Calliope-Rennspiel/Python/` folgenden Befehl ausführen: `python ki-gui-win.py` + +Die unten beschriebenen Schritte sind dann entsprechend über die Menüpunkte des erscheinenden Fenster nutzbar. + #### Schritt 1 - Rennspiel installieren Diesen Schritt muss jeder Schüler durchführen. diff --git a/Calliope-Rennspiel/Dokumentation/Nutzeranleitung-Gruppenspielmodus.md b/Calliope-Rennspiel/Dokumentation/Nutzeranleitung-Gruppenspielmodus.md index fe70843..6f4fc24 100644 --- a/Calliope-Rennspiel/Dokumentation/Nutzeranleitung-Gruppenspielmodus.md +++ b/Calliope-Rennspiel/Dokumentation/Nutzeranleitung-Gruppenspielmodus.md @@ -13,6 +13,14 @@ Pro Schülergruppe wird 1 "Datensammler-Calliope" benötigt, zu dem mehrere "Ren ![Gruppenspielmodus](./gruppenspiel.png) +#### GUI (Grafische Nutzeroberfläche) + +Seit Release "Speedy" (19.12.2022) können die Schritte 1 bis 6A statt manuell bzw. via Kommandozeile auch via einer komfortablen grafischen Nutzeroberfläche (GUI) durchgeführt werden (Stand 19.12.2022 nur für Windows verfügbar). + +Dazu im Unterverzeichnis `/ki-in-schulen-master/Calliope-Rennspiel/Python/` folgenden Befehl ausführen: `python ki-gui-win.py` + +Die unten beschriebenen Schritte sind dann entsprechend über die Menüpunkte des erscheinenden Fenster nutzbar. + #### Schritt 1 - Rennspiel auf den Calliope Minis der Schüler installieren Diesen Schritt mit angepasster Funkgruppe (__funkgruppe1__, __funkgruppe2__, ...) wiederholen, bis alle "Rennspiel-Calliopes" für alle Schüler aller Schülergruppen installiert sind. diff --git a/Calliope-Rennspiel/Python/ki-datenlogger.py b/Calliope-Rennspiel/Python/ki-datenlogger.py index c2d9347..dd5cb2e 100644 --- a/Calliope-Rennspiel/Python/ki-datenlogger.py +++ b/Calliope-Rennspiel/Python/ki-datenlogger.py @@ -1,7 +1,8 @@ # # ki-datenlogger.py$ # -# (C) 2020, Christian A. Schiller, Deutsche Telekom AG +# (C) 2020-2022, Christian A. Schiller, Deutsche Telekom AG +# Diese Version: V2 vom 19.12.2022 auf Basis Workshoperfahrungen 2022 # # Deutsche Telekom AG and all other contributors / # copyright owners license this file to you under the @@ -121,6 +122,12 @@ def on_press(key): # Speichern der gesammelten Daten in CSV-Datei stamp = strftime("%Y%m%d%H%M%S", gmtime()) -file = './csv-rohdaten/ki-rennspiel-log-'+stamp+'.csv' + +# Festlegen der Ausgabedatei (ohne Endung) +try: + file = sys.argv[2] +except: + file = './csv-rohdaten/ki-rennspiel-log-'+stamp+'.csv' + print("Trainingsdaten gespeichert in Datei: "+file) collect.to_csv(file,index=False) diff --git a/Calliope-Rennspiel/Python/ki-gui-win.py b/Calliope-Rennspiel/Python/ki-gui-win.py new file mode 100755 index 0000000..f75303e --- /dev/null +++ b/Calliope-Rennspiel/Python/ki-gui-win.py @@ -0,0 +1,304 @@ +# +# ki-gui.py$ +# +# (C) 2022, Arndt Baars, Christian A. Schiller, Deutsche Telekom AG +# +# Deutsche Telekom AG and all other contributors / +# copyright owners license this file to you under the +# MIT License (the "License"); you may not use this +# file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://opensource.org/licenses/MIT +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +# +# +# GUI fuer den KI-Workshop mit den Calliope +# +# Diese GUI ermoeglicht einen einfachen und schnellen Umgang mit den KI-Ressourcen. +# +# Folgende Features sind bereits enthalten: +# - Kopieren der Datensammler-Software auf den Calliope. Hier stehen zwei Optionen zur Auswahl. +# Zum einen auf Basis von OpenRoberta (Wahl des Kanals/Gruppe auf dem Calliope). +# Zum anderen auf Basis von Makecode (Direkte Auswahl von vier Punktgruppen). +# - Kopieren der Rennspieldateien auf den Calliope. Hier stehen zwei Optionen zur Auswahl. +# Zum einen auf Basis von OpenRoberta (Wahl des Kanals/Gruppe auf dem Calliope). +# Zum anderen auf Basis von Makecode (Direkte Auswahl von vier Punktgruppen). +# - Einfacher Start des Datensammlers auf dem lokalen Rechner. +# - Einfacher Start des KI-Trainings inkl. Layer-Konfig-Abfrage. +# - Einfacher Start des lokalen Rennspiels mit dem aktuell trainierten Modell. +# - Automatische USB-Port-Ermittlung +# - Automatische Calliope-Erkennung +# - Moeglichkeit, eigene Dateinamen vorzugeben +# - Sicherung der Einstellungen/Konfigurationen +# +# Geplante Features: +# - Einfaches Kopieren von trainierten Modellen auf den Calliope. +# - Einfuegen einer Hilfe mit Verweisen auf die vorhandenen Präsentationen und Materialien. +# - Portierung für Linux und MacOS +# +# Version 0.21 (Stand: 19.12.2022) +# +########## + +from tkinter import * +from tkinter import simpledialog +import sys +import subprocess +import serial.tools.list_ports +import os +from ctypes import windll, create_unicode_buffer, c_wchar_p, sizeof +from string import ascii_uppercase +import pickle +import locale + +os_umgebung = sys.platform +os_locale = locale.getdefaultlocale()[0] +print ("OS-Umgebung:") +print (os_umgebung) +print (os_locale) +print ("") + +# donothing +def donothing(): + win = Toplevel(fenster) + button = Button(win, text="Do nothing button") + button.pack() + +def configEinlesen(): + try: + f = open("ki-gui-win_conf.cfg","rb") + dict = pickle.load(f) + f.close() + return dict + except: + return {} + +def configSpeichern(dict): + f = open("ki-gui-win_conf.cfg","wb") + pickle.dump(dict,f) + f.close() + +global dictConfig +dictConfig = configEinlesen() +if (not dictConfig): + # Default-Konfig + dictConfig = {"PythonEXE":".", "CalliKIDir":".", "COMPort":"COM7", "Dateiname":"ki-rennspiel-mein-spiel"} +print("Einstellungen:") +print (dictConfig["Dateiname"]) +print (dictConfig["PythonEXE"]) +print (dictConfig["CalliKIDir"]) +print (dictConfig["COMPort"]) +print ("") + +# Kopiert die notwendigen Daten fuer einen Datensammler auf den Calliope. +# Wenn keine Gruppe angegeben wird, dann wird der Datensammler von OpenRoberta genutzt. +# Sonst wird der entsprechende Datensammler auf Basis von Makecode kopiert. +def copyDatensammler(gruppe=0): + src = dictConfig["CalliKIDir"] + '\\OpenRoberta\\datensammler-openroberta.hex' + if (gruppe != 0): + if (gruppe == 1): + src = dictConfig["CalliKIDir"] + '\Makecode\\datensammler-funkgruppe1-makecode.hex' + elif (gruppe == 2): + src = dictConfig["CalliKIDir"] + '\Makecode\\datensammler-funkgruppe2-makecode.hex' + elif (gruppe == 3): + src = dictConfig["CalliKIDir"] + '\Makecode\\datensammler-funkgruppe3-makecode.hex' + else: + src = dictConfig["CalliKIDir"] + '\Makecode\\datensammler-funkgruppe4-makecode.hex' + dst = get_calliope_drive() + if dst: + dst = dst + '\\' + cmd = 'copy "%s" "%s"' % (src, dst) + os.system(cmd) + else: + print ("Leider konnte kein Ziellaufwerk ermittelt werden!") + +# Kopiert die notwendigen Daten fuer das Rennspiel auf den Calliope. +# Wenn keine Gruppe angegeben wird, dann wird das Rennspiel von OpenRoberta genutzt. +# Sonst wird das entsprechende Rennspiel auf Basis von Makecode kopiert. +def copyRennspiel(gruppe=0): + src = dictConfig["CalliKIDir"] + '\\OpenRoberta\\rennspiel-openroberta.hex' + if (gruppe != 0): + if (gruppe == 1): + src = dictConfig["CalliKIDir"] + '\Makecode\\rennspiel-funkgruppe1-makecode.hex' + elif (gruppe == 2): + src = dictConfig["CalliKIDir"] + '\Makecode\\rennspiel-funkgruppe2-makecode.hex' + elif (gruppe == 3): + src = dictConfig["CalliKIDir"] + '\Makecode\\rennspiel-funkgruppe3-makecode.hex' + else: + src = dictConfig["CalliKIDir"] + '\Makecode\\rennspiel-funkgruppe4-makecode.hex' + dst = get_calliope_drive() + if dst: + dst = dst + '\\' + cmd = 'copy "%s" "%s"' % (src, dst) + os.system(cmd) + else: + print ("Leider konnte kein Ziellaufwerk ermittelt werden!") + +def portErmitteln(): + for port, desc, hwid in serial.tools.list_ports.grep("USB Serial Device"): + return port + else: + for port, desc, hwid in serial.tools.list_ports.grep("Serielles USB"): + return port + else: + return False + + """ + ports = serial.tools.list_ports.comports() + for port, desc, hwid in sorted(ports): + print(desc) + if (desc.startswith('USB Serial Device')): + # desc.startswith('Serielles USB') für Deutsch + #'print("{}: {} [{}]".format(port, desc, hwid)) + print(f"Erkannter Port: {port}") + return port + """ + +# Unabhängig von der Konfigurationsdatei (Einstellungsseite) den Port für diesen Rechner automatisch ermitteln. +# Falls das nicht funktioniert, wird der Port aus der Konfiguration verwendet. +usbPort = portErmitteln() +if usbPort: + dictConfig["COMPort"] = usbPort + print(f"Erkannter Port: {usbPort}") + +def get_calliope_drive(): + volumeNameBuffer = create_unicode_buffer(1024) + fileSystemNameBuffer = create_unicode_buffer(1024) + serial_number = None + max_component_length = None + file_system_flags = None + erg = "" + drive_names = [] + # Get the drive letters, then use the letters to get the drive names + bitmask = (bin(windll.kernel32.GetLogicalDrives())[2:])[::-1] # strip off leading 0b and reverse + drive_letters = [ascii_uppercase[i] + ':/' for i, v in enumerate(bitmask) if v == '1'] + for d in drive_letters: + rc = windll.kernel32.GetVolumeInformationW(c_wchar_p(d), volumeNameBuffer, sizeof(volumeNameBuffer), + serial_number, max_component_length, file_system_flags, + fileSystemNameBuffer, sizeof(fileSystemNameBuffer)) + if rc: + #drive_names.append(f'{volumeNameBuffer.value}({d[:2]})') # disk_name(C:) + if (volumeNameBuffer.value == "MINI"): + erg = d.rstrip(d[-1]) + return erg + +# ki-datenlogger.py +def ki_datenlogger(): + # Hier wird als erster Prozessschritt ein Dateiname abgefragt und auch für die weiteren Schritte genutzt. + # Falls hier kein Dateiname angegeben wird, wird der Dateiname aus der Konfig weiter verwendet. + dateiname = simpledialog.askstring("Input", "Bitte einen Dateinamen angeben (ohne Leerzeichen!)", initialvalue=dictConfig["Dateiname"], parent=fenster) + if dateiname is not None: + dictConfig["Dateiname"] = dateiname + cmd = dictConfig["CalliKIDir"] + '\\Python\\ki-datenlogger.py' + outputfile = dictConfig["CalliKIDir"] + '\\Python\\csv-rohdaten\\' + dictConfig["Dateiname"] + '.csv' + subprocess.run([sys.executable, cmd, dictConfig["COMPort"], outputfile], check=True) + +# ki-trainieren-sklearn.py +def ki_trainieren(): + cmd = dictConfig["CalliKIDir"] + '\\Python\\ki-trainieren-sklearn.py' + layer = simpledialog.askstring("Input", "Bitte Hidden Layer definieren - A,B oder A,B,C", initialvalue='7,7', parent=fenster) + param = dictConfig["CalliKIDir"] + '\\Python\\csv-rohdaten\\' + dictConfig["Dateiname"] + '.csv' + outputbase = dictConfig["CalliKIDir"] + '\\Python\\modelle\\' + dictConfig["Dateiname"] + subprocess.run([sys.executable, cmd, param, layer, outputbase], check=True) + +# ki-rennspiel.py +def ki_rennspiel(): + cmd = dictConfig["CalliKIDir"] + '\\Python\\ki-rennspiel.py' + param = dictConfig["CalliKIDir"] + '\\Python\\modelle\\' + dictConfig["Dateiname"] + '.pkcls' + subprocess.run([sys.executable, cmd, 'sklearn', param], check=True) + +# Einstellungen-Win definieren +def einstellungen(): + einstellungenWin = Toplevel(fenster) + einstellungenWin.geometry('700x250') + + def exit_btn(): + dictConfig["Dateiname"] = dateiname_feld.get() + dictConfig["PythonEXE"] = pythonVerz_feld.get() + dictConfig["CalliKIDir"] = kiVerz_feld.get() + configSpeichern(dictConfig) + einstellungenWin.destroy() + einstellungenWin.update() + + dateiname_label = Label(einstellungenWin, text="Dateiname") + dateiname_feld = Entry(einstellungenWin, justify=LEFT, bd=5, width=40) + dateiname_feld.insert(END, dictConfig["Dateiname"]) + pythonVerz_label = Label(einstellungenWin, text="Python-Verzeichnis") + pythonVerz_feld = Entry(einstellungenWin, justify=LEFT, bd=5, width=80) + pythonVerz_feld.insert(END, dictConfig["PythonEXE"]) + kiVerz_label = Label(einstellungenWin, text="KI-Verzeichnis") + kiVerz_feld = Entry(einstellungenWin, justify=LEFT, bd=5, width=80) + kiVerz_feld.insert(END, dictConfig["CalliKIDir"]) + usbPort_label = Label(einstellungenWin, text="USB-Port") + usbPort_feld = Entry(einstellungenWin, justify=LEFT, bd=5, width=20) + usbPort_feld.insert(END, dictConfig["COMPort"]) + #uebernehmen_button = Button(einstellungenWin, text="Einstellungen übernehmen", command=einstellungenWin.destroy) + uebernehmen_button = Button(einstellungenWin, text="Einstellungen übernehmen", command=exit_btn) + + dateiname_label.grid(row = 0, column = 0, pady = 15, padx = 20) + dateiname_feld.grid(row = 0, column = 1, sticky=W) + pythonVerz_label.grid(row = 1, column = 0, pady = 15, padx = 20) + pythonVerz_feld.grid(row = 1, column = 1, sticky=W) + kiVerz_label.grid(row = 2, column = 0, pady = 15, padx = 20) + kiVerz_feld.grid(row = 2, column = 1, sticky=W) + usbPort_label.grid(row = 3, column = 0, pady = 15, padx = 20) + usbPort_feld.grid(row = 3, column = 1, sticky=W) + uebernehmen_button.grid(row = 4, column = 0, columnspan = 2, pady = 15) + +# Ein Fenster erstellen +fenster = Tk() +# Den Fenstertitle erstellen +fenster.title("KI mit dem Calliope") +# Fenstergroesse vorgeben +fenster.geometry('400x200') + +# Ein Menu anlegen +menubar = Menu(fenster) +# Datei-Menu +dateimenu = Menu(menubar, tearoff=0) +dateimenu.add_command(label="Einstellungen", command=einstellungen) +dateimenu.add_separator() +dateimenu.add_command(label="Beenden", command=fenster.quit) +menubar.add_cascade(label="Datei", menu=dateimenu) + +# Calli-Menu +callimenu = Menu(menubar, tearoff=0) +callimenu.add_command(label="Datensammler kopieren", command=copyDatensammler) +callimenu.add_command(label="Rennspiel kopieren", command=copyRennspiel) +callimenu.add_separator() +callimenu.add_command(label="Makecode Datensammler kopieren Gruppe 1", command=lambda: copyDatensammler(1)) +callimenu.add_command(label="Makecode Datensammler kopieren Gruppe 2", command=lambda: copyDatensammler(2)) +callimenu.add_command(label="Makecode Datensammler kopieren Gruppe 3", command=lambda: copyDatensammler(3)) +callimenu.add_command(label="Makecode Datensammler kopieren Gruppe 4", command=lambda: copyDatensammler(4)) +callimenu.add_command(label="Makecode Rennspiel kopieren Gruppe 1", command=lambda: copyRennspiel(1)) +callimenu.add_command(label="Makecode Rennspiel kopieren Gruppe 2", command=lambda: copyRennspiel(2)) +callimenu.add_command(label="Makecode Rennspiel kopieren Gruppe 3", command=lambda: copyRennspiel(3)) +callimenu.add_command(label="Makecode Rennspiel kopieren Gruppe 4", command=lambda: copyRennspiel(4)) +menubar.add_cascade(label="Calli", menu=callimenu) + +# KI-Menu +kimenu = Menu(menubar, tearoff=0) +kimenu.add_command(label="Datensammler starten", command=ki_datenlogger) +kimenu.add_separator() +kimenu.add_command(label="KI anlernen", command=ki_trainieren) +kimenu.add_command(label="KI testen", command=ki_rennspiel) +#kimenu.add_separator() +#kimenu.add_command(label="KI auf Calli kopieren", command=donothing) +menubar.add_cascade(label="KI", menu=kimenu) + +# Hilfe-Menu +#hilfemenu = Menu(menubar, tearoff=0) +#hilfemenu.add_command(label="Hilfe", command=donothing) +#menubar.add_cascade(label="Hilfe", menu=hilfemenu) + +fenster.config(menu=menubar) +# In der Ereignisschleife auf Eingabe des Benutzers warten. +fenster.mainloop() diff --git a/Calliope-Rennspiel/Python/ki-trainieren-sklearn.py b/Calliope-Rennspiel/Python/ki-trainieren-sklearn.py index ca91a97..3ccf910 100644 --- a/Calliope-Rennspiel/Python/ki-trainieren-sklearn.py +++ b/Calliope-Rennspiel/Python/ki-trainieren-sklearn.py @@ -1,7 +1,8 @@ # # ki-trainieren-sklearn.py$ # -# (C) 2020, Christian A. Schiller, Deutsche Telekom AG +# (C) 2020-2022, Christian A. Schiller, Deutsche Telekom AG +# Diese Version: V2 vom 19.12.2022 auf Basis Workshoperfahrungen 2022 # # Deutsche Telekom AG and all other contributors / # copyright owners license this file to you under the @@ -68,7 +69,7 @@ # Laden der Rohdaten (Terminal-Output als CSV-Datei) filename_raw = sys.argv[1] -# Festelegen der Hidden Layer Size +# Festlegen der Hidden Layer Size try: hidden_layers_raw = sys.argv[2] try: @@ -83,6 +84,15 @@ hidden_layers = list(map(int, hidden_layers)) print("Hidden Layers:",hidden_layers) +# Timestamp erzeugen +stamp = strftime("%Y%m%d%H%M%S", gmtime()) + +# Festlegen der Ausgabedatei (ohne Endung) +try: + outputfilebase = sys.argv[3] +except: + outputfilebase = './modelle/sklearn-py-modell-'+stamp + df_raw = pd.read_csv(filename_raw) Xr = df_raw[['PlayerPos','Car1Pos','Car2Pos','Car3Pos','Car4Pos','Car5Pos']].values yr = df_raw['Action'].values @@ -119,7 +129,7 @@ stamp = strftime("%Y%m%d%H%M%S", gmtime()) # Speichern des Modells (Pickle-Format zur Nutzung im Python-Rennspiel) -filename = './modelle/sklearn-py-modell-'+stamp+'.pkcls' +filename = outputfilebase + '.pkcls' mlp_file = open(filename, 'wb') pickle.dump(mlp, mlp_file) print("Pickle-Datei des trainierten ML-Modells gespeichert.") @@ -143,7 +153,7 @@ for item in mlp.intercepts_: data['intercepts'].append(item.tolist()) -filename = './modelle/sklearn-py-modell-'+stamp+'.json' +filename = outputfilebase + '.json' def round_floats(o): if isinstance(o, float): return round(o, 6)