Schrittmotor am Raspberry Pi über Webinterface steuern
Der in diesem Beitrag veröffentlichte Programm-Code ermöglicht die Steuerung eines Schrittmotors mit ULN 2003 Treiber Board über ein auf dem Raspberry Pi vorhandenes Webinterface. Die beiden Skripte sind Teil eines Projektes, in dem ich den Sichtbereich der Kamera mittels eines Schrittmotors über ein Webinterface einstelle. Da ich in diesem Projekt mittels Bewegungsmelder auf die Kamera zugreife, stelle ich der Weboberfläche das Kamerabild über eine Bilderreihe, anstatt einem Videostream zur Verfügung.
Funktionsweise des Programmcodes
Vorbereitung
Damit die zur Verfügung gestellten Programmskripte funktionieren, sind neben dem Anschluss des Schrittmotors, folgende Vorbereitungen notwendig.
Für die Verbindung zwischen Python und HTML/JavaScript muss neben einem eingerichteten Webserver die Tornado Websocket Bibliothek vorhanden sein. Informationen zur Verwendung des Tornado Websocket findest du in meinem Beitrag “Web zu UART Terminalprogramm…”.
Wenn du auch das Kamerabild über die Bildreihe dem Webinterface zur Verfügung stellen möchtest, dann schaue dir mal die Beiträge “Kameraaufnahme mit HC-SR501 Bewegungsmelder am Raspberry Pi” und zur Zwischenspeicherung der Bilder meinen Beitrag zur Anlage einer Ram-Disk an.
Anschluss und Ansteuerung des Schrittmotors
Bei dem von mir verwendeten Schrittmotor handelt es sich um einen 28YBJ-48DC 5V 4 Phasen Motor mit ULN2003 Treiber Board. Da Christoph Scherbeck mit seinem Beitrag “Schrittmotorsteuerung mit dem Raspberry Pi” eine super Anleitung zum Anschluss und Ansteuerung veröffentlicht hat, welche auch die Grundlage für meine Umsetzung ist, möchte ich auf diesen Verweisen.
Das Python-Skript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 |
#!/usr/bin/python # coding=utf-8 # Python Version 2.7 # motor.py # Schrittmotorsteuerung mit Kamerabildanzeige im Browser über Tornado Websocket #------------------------------------------------------------------------------ # Bibliotheken #-------------------- # Einbindung der notwendigen Grundbibliotheken import os, sys, time, threading # Einbindung GPIO's import RPi.GPIO as gp # Einbindung der notwendigen Bibliotheken für den Tornado Websocket import tornado.httpserver, tornado.websocket, tornado.ioloop, tornado.web, datetime, json # Einbindung der Datenbank - in diesem Fall SQLite3 import sqlite3 # Einbindung der Kamera import picamera # Deklarationen #-------------------- # Global für Programmstatus programmStatus = 1 # Datenbank mit der gespeicherten Stellung der Schrittmotorstellung sqlitemotorStellungDBName = "/var/www/html/sqlite/motorstellung.db" # Global für Schrittmotor schrittMotorStellung = 0 # Stellung des Schrittmotors # Global für Videostream dateiNameStream = '' # zur Übergabe des aktuellen Dateinamen an die Weboberfläche Kamera_Stream_aktiv = False # als Erweiterung der folgenden Streaming-Option per Socket kameraStreamStatus = False # notwendig für die Kamerasteuerung bilderStreamPfad = "/var/www/html/streamtmp/" # Ram-Disk # Websocket #---------- class TornadoThread (threading.Thread): def __init__(self): threading.Thread.__init__(self) application = tornado.web.Application([(r'/ws', WSHandler),]) http_server = tornado.httpserver.HTTPServer(application) http_server.listen(8888) def run(self): print "Tornado Websocketdienst gestartet.\nWarte auf Verbindung.\n...\n" tornado.ioloop.IOLoop.instance().start() class WSHandler(tornado.websocket.WebSocketHandler): def check_origin(self, origin): return True def open(self): global alterDatenUebergabeWert alterDatenUebergabeWert = '' self.connected = True print "Verbindung mit WEB-Oberfläche aufgebaut.\n" self.timeout_loop() def on_message(self, message): global Kamera_Stream_aktiv print "Übergabewert von WEB-Interface: ", message if message == "page_2": print "User befindet sich auf motor.html - setze Kamera_Stream_aktiv auf True" Kamera_Stream_aktiv = True message = "" elif message == "cam-left": print "Schrittmotor in linke Richtung drehen lassen" schrittmotor("positiv") elif message == "cam-right": print "Schrittmotor in rechte Richtung drehen lassen" schrittmotor("negativ") else: print "unbekannter Befehl" def on_close(self): self.connected = False print "Verbindung zum WEB-Interface geschlossen." self.timeout_loop() def timeout_loop(self): global Kamera_Stream_aktiv, dateiNameStream if self.connected: if Kamera_Stream_aktiv == True: self.write_message(dateiNameStream) tornado.ioloop.IOLoop.instance().add_timeout(datetime.timedelta(seconds=.1), self.timeout_loop) # Funktionen #----------------------------- def gpioInitalisieren(): # GPIO zur Verwendung vorbereiten try: gp.setmode(gp.BCM) # BCM Nummern verwenden gp.setwarnings(False) # Warnmeldungen abschalten # GPIO Ports für das ULN2003A Treiberboard für den 28BYJ-48 Schrittmotor initalisieren gp.setup(20, gp.OUT) # Pin38 - BCM Nummer 20 - GPIO20 wird Ausgang = IN1 gp.output(20, gp.LOW) gp.setup(16, gp.OUT) # Pin36 - BCM Nummer 16 - GPIO16 wird Ausgang = IN2 gp.output(16, gp.LOW) gp.setup(19, gp.OUT) # Pin35 - BCM Nummer 19 - GPIO19 wird Ausgang = IN3 gp.output(19, gp.LOW) gp.setup(13, gp.OUT) # Pin33 - BCM Nummer 13 - GPIO13 wird Ausgang = IN4 gp.output(13, gp.LOW) except: # Fehler bei der GPIO-Initialisierung programmStatus = 102 def schrittmotor(richtung): global schrittMotorStellung # damit die aktuelle Position nach einem Neustart bekannt ist, wird die Position abgespeichert # damit es evtl. keine Probleme mit den Kabelverbindungen gibt, wird die Bewegung eingschränkt bewegungsStop = 130 print "aktuelle schrittMotorStellung = ", schrittMotorStellung if richtung == "negativ": print "Negativ" i = 1 zukuenftigeSchrittMotorStellung = schrittMotorStellung - 16 if zukuenftigeSchrittMotorStellung > -bewegungsStop: while i < 16 : schrittmotorStepNegativ() i = i + 1 schrittMotorStellung = schrittMotorStellung - i else: print "keine Bewegung da außerhalb gesetzter Position" elif richtung == "positiv": print "Positiv" i = 1 zukuenftigeSchrittMotorStellung = schrittMotorStellung + 16 if zukuenftigeSchrittMotorStellung < bewegungsStop: while i < 16 : schrittmotorStepPositiv() i = i + 1 schrittMotorStellung = schrittMotorStellung + i else: print "keine Bewegung da außerhalb gesetzter Position" sql = 'UPDATE schrittmotorstellung SET position = "' + str(schrittMotorStellung) + '"' motorStellung_db_update(sql) def schrittmotorStepPositiv(): zeit = 0.01 #Step 1 gp.output(13, gp.HIGH) time.sleep(zeit) gp.output(13, gp.LOW) #Step 2 gp.output(13, gp.HIGH) gp.output(19, gp.HIGH) time.sleep(zeit) gp.output(13, gp.LOW) gp.output(19, gp.LOW) #Step 3 gp.output(19, gp.HIGH) time.sleep(zeit) gp.output(19, gp.LOW) #Step 4 gp.output(16, gp.HIGH) gp.output(19, gp.HIGH) time.sleep(zeit) gp.output(16, gp.LOW) gp.output(19, gp.LOW) #Step 5 gp.output(16, gp.HIGH) time.sleep(zeit) gp.output(16, gp.LOW) #Step 6 gp.output(20, gp.HIGH) gp.output(16, gp.HIGH) time.sleep(zeit) gp.output(20, gp.LOW) gp.output(16, gp.LOW) #Step 7 gp.output(20, gp.HIGH) time.sleep(zeit) gp.output(20, gp.LOW) #Step 8 gp.output(13, gp.HIGH) gp.output(20, gp.HIGH) time.sleep(zeit) gp.output(13, gp.LOW) gp.output(20, gp.LOW) def schrittmotorStepNegativ(): zeit = 0.01 #Step 8 gp.output(13, gp.HIGH) gp.output(20, gp.HIGH) time.sleep(zeit) gp.output(13, gp.LOW) gp.output(20, gp.LOW) #Step 7 gp.output(20, gp.HIGH) time.sleep(zeit) gp.output(20, gp.LOW) #Step 6 gp.output(20, gp.HIGH) gp.output(16, gp.HIGH) time.sleep(zeit) gp.output(20, gp.LOW) gp.output(16, gp.LOW) #Step 5 gp.output(16, gp.HIGH) time.sleep(zeit) gp.output(16, gp.LOW) #Step 4 gp.output(16, gp.HIGH) gp.output(19, gp.HIGH) time.sleep(zeit) gp.output(16, gp.LOW) gp.output(19, gp.LOW) #Step 3 gp.output(19, gp.HIGH) time.sleep(zeit) gp.output(19, gp.LOW) #Step 2 gp.output(13, gp.HIGH) gp.output(19, gp.HIGH) time.sleep(zeit) gp.output(13, gp.LOW) gp.output(19, gp.LOW) #Step 1 gp.output(13, gp.HIGH) time.sleep(zeit) gp.output(13, gp.LOW) def motorStellung_db_anlegen(): global sqlitemotorStellungDBName, schrittMotorStellung connection = sqlite3.connect(sqlitemotorStellungDBName) cursor = connection.cursor() sql = "CREATE TABLE schrittmotorstellung(" \ "position INTEGER)" cursor.execute(sql) sql = "INSERT INTO schrittmotorstellung VALUES('"+ str(schrittMotorStellung) + "')" cursor.execute(sql) connection.commit() connection.close() def motorStellung_db_auslesen(): global sqlitemotorStellungDBName, schrittMotorStellung dbSensorBezeichnung = [] dbSensorName = [] connection = sqlite3.connect(sqlitemotorStellungDBName) cursor = connection.cursor() sql = "SELECT * FROM schrittmotorstellung" cursor.execute(sql) for dsatz in cursor: print "ausgelesene Daten für Motorstellung ", dsatz[0] schrittMotorStellung = dsatz[0] connection.close() def motorStellung_db_update(sql): global sqlitemotorStellungDBName connection = sqlite3.connect(sqlitemotorStellungDBName) cursor = connection.cursor() cursor.execute(sql) print "Datenbank für Motorstellung aktualisiert mit ", sql connection.commit() connection.close() #GPIO initialisieren gpioInitalisieren() # Überprüfung Existenz der SQLite3-Datei für Schrittmotorstellung if (not os.path.isfile(sqlitemotorStellungDBName)): motorStellung_db_anlegen() else: motorStellung_db_auslesen() # Festlegung Tornado Websocket TornadoThread = TornadoThread() TornadoThread.start() print "Programminformation: es sind Insgesamt ", threading.active_count(), " Threads aktiv." # Hauptroutine i = 0 while programmStatus == 1: if Kamera_Stream_aktiv == True: if kameraStreamStatus == False: # Kamera initialisieren cam = picamera.PiCamera() cam.resolution = (640, 480) # Auflösung für Bildspeicherung print "Bildaufnahme für Stream beginn" kameraStreamStatus = True cam.start_preview() dateiName = bilderStreamPfad + str(i) + ".jpg" time.sleep(0.05) cam.capture(dateiName) dateiNameStream = "stream|streamtmp/" + str(i) + ".jpg" cam.stop_preview() i = i + 1 if i == 100: i = 0 else: if kameraStreamStatus == True: cam.close() # damit die Kamera nicht ständig in Betrieb ist kameraStreamStatus = False i = 0 # Programmende durch Veränderung des programmStatus # GPIO Einstellungen löschen gp.cleanup() # Ausgabe der Nachricht für das Programmende print "Programm wurde wegen eines Fehlers beendet. Fehlercode: ", programmStatus if programmStatus == 102: print "Die Initialisierung des GPIO ist fehlgeschlagen." else: print "Der Fehlercode wurde nicht hinterlegt." |
Das HTML-Skript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
<!doctype html> <html> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <!-- Android --> <meta name="mobile-web-app-capable" content="yes" /> <!-- iOS --> <meta name="apple-mobile-web-app-capable" content="yes" /> <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags --> <meta name="description" content="Websteuerung für Schrittmotor am Raspberry Pi"> <meta name="author" content="Wolfgang Raab - www.webnist.de"> <title>Motorsteuerung</title> <script src="js/jquery.min.js"></script> <script src="dist/Chart.bundle.js"></script> <!-- Bootstrap core CSS --> <link href="css/bootstrap.min.css" rel="stylesheet"> <!-- Custom styles for this template --> <link href="css/style.css" rel="stylesheet"> <!-- Just for debugging purposes. Don't actually copy these 2 lines! --> <!--[if lt IE 9]><script src="js/ie8-responsive-file-warning.js"></script><![endif]--> <script src="js/ie-emulation-modes-warning.js"></script> <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries --> <!--[if lt IE 9]> <script src="js/html5shiv.min.js"></script> <script src="js/respond.min.js"></script> <![endif]--> <script> $(document).ready(function () { var host = "ws://"+document.location.host+":8888/ws"; var socket = new WebSocket(host); var position = 0; var startStatus = 0; socket.onopen = function(evt) { var conn_status = document.getElementById('conn_text'); socket.send('page_2'); conn_status.innerHTML = "aufgebaut"; }; socket.onmessage = function(evt) { var datenVonServer = evt.data; // Zeichenkette von Python var datenVonServerSplit = datenVonServer.split("|"); if (datenVonServerSplit[0] == 'stream'){ document.getElementById("streamBild").src = datenVonServerSplit[1]; } }; socket.onclose = function(evt) { var conn_status = document.getElementById('conn_text'); conn_status.innerHTML = "geschlossen"; }; $('.button-auswahl button').click(function(evt) { var button = $(this); var message = button.attr('uart-wert'); socket.send(message); }); function handleOrientation(event) { var meldungBewegung = document.getElementById('meldungBewegung'); var sensorVorhandenText = document.getElementById('SensorVorhandenText'); var bewegungsRichtunng = document.getElementById('bewegungsRichtung'); var gradzahl = 10; sensorVorhandenText.innerHTML = "Bewegungssensor: "; x = parseInt(event.gamma); meldungBewegung.innerHTML = x; bewegungsRichtung.innerHTML = ''; if (startStatus == 0){ position = x; startStatus = 1; } else { if (x > position+gradzahl){ socket.send("cam-left"); bewegungsRichtung.innerHTML = 'Links'; position = x; } if (x < position-gradzahl){ socket.send("cam-right"); bewegungsRichtung.innerHTML = 'Rechts'; position = x; } } } window.addEventListener('deviceorientation', handleOrientation); }); </script> </head> <body> <header data-role="header"> <nav class="navbar navbar-fixed-top navbar-inverse"> <div class="navbar-header"> <p class="navbar-brand" id="page-title"> <a class="side" href="javascript:window.location.replace('index.html');"><span class="glyphicon glyphicon-home" aria-hidden="true"></span> Startseite</a> </p> </div> </nav> </header> <div class="container content"> <div class="row"> <div class="col-xs-12 camcontainer"> <img id="streamBild" src="load.jpg" class="campic" /> </div> <div id="kamera-button" class="button-auswahl text-center"> <button uart-wert="cam-left" id="cam-left-button" class="btn btn-lg button-uart"><span class="glyphicon glyphicon-triangle-left" aria-hidden="true"></span></button> <button uart-wert="cam-right" id="cam-right-button" class="btn btn-lg button-uart"><span class="glyphicon glyphicon-triangle-right" aria-hidden="true"></span></button> </div> </div> </div> <footer data-role="footer"> Verbindungsstatus: <label id="conn_text"></label> | <label id="SensorVorhandenText"></label> <label id="meldungBewegung"></label> <label id="bewegungsRichtung"></label> </footer> <!-- Bootstrap core JavaScript ================================================== --> <script src="js/bootstrap.min.js"></script> <!-- IE10 viewport hack for Surface/desktop Windows 8 bug --> <script src="js/ie10-viewport-bug-workaround.js"></script> </body> </html> |
Die Übergabe der gewünschten Drehrichtung an das Python-Skript erfolgt mit der entsprechenden Funktion socket.send(). Die Überwachung und Abfrage der Button erfolgt in $(‘.button-auswahl button’).click(function(evt).
Auslesen der Bewegungssensoren mit JavaScript
Wenn der Aufruf der Webseite mit einem Smartphone bzw. Tablet erfolgt, wird die Drehrichtung in der Funktion handleOrientation(event) überwacht und verarbeitet. In meinem Fall möchte ich nicht jede Gradänderung weitergeben und führe diese erst nach Erfüllung der if-Option durch. Damit die erste Positionsabfrage keine Einwirkung auf den Schrittmotor hat, verwende ich im Skript var startStatus. Weitere Informationen zur Erfassung der Sensorwerte des Gerätes findest du in der API Beschreibung “Detecting device orientation”.
Sollte die Abfrage auf deinem Gerät oder verwendeten Browser nicht unterstützt werden, erfolgt auch keine entsprechende Text-Ausgabe im Footer in den entsprechenden <label> Tags.
Download aller Dateien
Die beiden Skripte mit den notwendigen Bootstrap Bibliotheken kannst du dir hier als ZIP-Archiv herunterladen.
Hallo
vielen dank für deine Programme, Ich bin jedoch blutiger anfänger was den Raspy betrifft. (Ich Programmiere normalerweise Codesys (Beckhoff) mit Strukturiertem Text.
Ich komme momentan jedoch mit dem Programm hier nicht klar. Ich hab einen Raspberry 3B+ mit aktueller Software und demnach auch Python 3 am laufen. Jedoch bekomme ich hier immer fehler. (Ich hab die Print befehle jetzt zumindest schon mal an die Synthax von python3 angepasst.)
Ich bekomme wenn ich es im thonny laufen lassen den Fehler
File “/usr/lib/python3/dist-packages/tornado/ioloop.py”, line 237, in current
loop = asyncio.get_event_loop()
File “/usr/lib/python3.9/asysncio/events.py”. line 642, in get event_loop
raise RuntimeError(‘There is no current event loop in thread %r.’
RuntimeError: There is no current event loop in thread #Thread-1′.
Vielleicht können Sie mir weiter helfen.
Vielen Dank schon mal und schöne Woche noch.
Ich kann ihnen auch das komplette Programm schicken wenn es ihnen was bringt.
Hallo,
vielen Dank für die Nachricht.
Den Fehler kann ich leider nicht ohne Weiteres reproduzieren. Eventuell muss der Code nicht nur bei den Print-Befehlen angepasst werden. Ein Blick in Google mit der Fehlermeldung hat leider keine empfehlenswerte Fehlerbehebung gebracht.
Eine Anpassung des Codes an Python 3 und dem aktuellen Raspberry Pi OS ist geplant, allerdings komme ich zeitnah bedauerlicherweise nicht dazu dies zu realisieren. Aber vielleicht kann ein anderer Leser dieses Beitrages eine Hilfestellung liefern.
Viele Grüße
Wolfgang
Servus Wolfgang,
wirklich super Seite! Bin bei der Umsetzung meines Projektes (pan/tilt Steuerung einer Kamera) auf deine Seite gestoßen.
Hab deinen Code auch soweit übernommen und das script gestartet. Dieses läuft auch (Warte auf Verbindung, 2 Threads aktiv.), allerdings kommt es zu keiner Verbindung laut Browser. Es steht nur “Verbindungsstatus: I”
Ich habe zwar keine Kamera angeschlossen, die Ansteuerung des Motors sollte aber doch trotzdem funktionieren.
Hast du eine Idee woran es liegt?
Beste Grüße
Mark
Hallo Mark,
vielen Dank für Dein Lob. Hast Du für die Verbindung zwischen Browser und Python-Skript auch die Tornado Websocket Bibliothek installiert und getestet?
Viele Grüße
Wolfgang
Servus Wolfgang,
erstmal: tolle Seite, mit jede Menge interressantem Wissen.
geau sowas habe ich gesucht.
Meine Aussenkamera läuft mit einer normalen Schrittmotor-Steuerung.
Programm habe ich in Gambas geschrieben,
Kamera-Bild zeige ich mit Firefox an.
Umstandlich ? Ich weiss, aber besser konnt ich´s nicht.
Jetzt meine Frage: wo muss ich die Dateien aus der *.zip
unterbringen, dass die sich alle wiederfinden. 🙂
Danke im Vorraus.
Wenn alles läuft, bin ich auch gern zu einer Spende bereit.
Hallo Jürgen,
vielen Dank für deine Nachricht.
Bei mir sind die beiden Skripts im www Ordner des Webserver gespeichert.
Viele Grüße
Wolfgang
Kleiner Hinweis, alle Kommentare werden moderiert.
Dies bedeutet, der Kommentar wird vor der Veröffentlichung durchgelesen und von mir geprüft. Auch behalte ich mir vor, jeden Kommentar zu löschen, der nicht direkt auf das Thema abzielt oder nur den Zweck hat, Leser oder Autoren herabzuwürdigen.
Neuste Beiträge
Kleinen Spickzettel für Raspbian OS Befehle
Entdecke die Welt von Webnist.de
Erfahre mehr über die Hintergründe meines Blogs und wie ich dich bei deinen digitalen Projekten unterstützen kann.Kategorien
KATEGORIEN
Aktuelles Video auf YouTube
Beschreibung der Verwendung eines TTP223B Touch Sensors am GPIO Port des Raspberry Pi mit Python.
Die aktuell 6 derzeit meistbesuchten Beiträge
Beiträge mit den meisten Kommentaren
Funktional Immer aktiv
Vorlieben
Statistiken
Marketing