Aus einem Script mit einem Webserver kommunizieren ...

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
der_Mausbiber
User
Beiträge: 72
Registriert: Donnerstag 2. Oktober 2014, 09:51

Schönen guten Tag,

ich beschäftige mich gerade mit einem raspberry Pi.
Dieser soll diverse Aufgaben zuhause übernehmen und steuern will ich das ganze auf dem RasPi mit python.
Grundsätzlich bin ich jetzt soweit das mein Script rund läuft und das macht was es soll.

Jetzt möchte ich aber gerne im nächsten Schritt die Bedienung über eine Website ermöglichen.
Dazu muss gesag werden, das die Website aber nicht(!) auf dem RasPi läuft, sondern auf einem anderen Rechner im lokalen Netzwerk.
Der Webserver ist apache, läuft unter CentOS und die Website ansich besteht aus einem bootstrap-Theme und jquery.

Mein Wunsch wäre es, das ich zum einen aus dem python-script heraus Daten an die Website senden kann (z.Bsp. wenn sich die Temperatur in einem Raum geändert hat).
Zum anderen würde ich gerne Befehle von der Website aus an das python-Script senden (z.Bsp. Button auf der HP drücken schaltet ein Relais über das python-Script).

Dabei sollte das Ganze auch noch in "Echtzeit" ablaufen, d.h. wenn sich ein Wert ändert und das python-Script diesen an die Website gesendet hat, dann soll die Seite diesen auch direkt anzeigen, ohne das ich die Seite manuell aktualisieren muss.

Meine Frage:
Wie gehe ich hier am besten vor? Was ist der beste Weg?

Ich habe mich schon selbst schlau gemacht, aber die Vielzahl an Möglichkeiten und Tutorials hat mich dann doch überfordert.
Ich habe zum Beispiel diverse Beispiele gesehen die Daten von python an ein php-Script übergeben, oder python-Scripts die gleich direkt Webserver sind, etc....
Im Grunde bin ich davon ausgegangen das ich die Daten per json und ajax hin- und herschicken kann - nur ich finde einfach kein Beispiel an dem ich mich abarbeiten könnte.

Kann mir hier jemand weiterhelfen?
Oder noch besser mir ein Beispiel geben oder eine Seite mit einem entsprechend Tutorial nennen?

Vielen Dank schonmal vorweg.
BlackJack

@der_Mausbiber: Von Deinem Programm direkt etwas an den Benutzer zu schicken und das auch noch *über* den Webserver auf einem anderen Rechner ist eher nicht möglich, beziehungsweise (noch) nicht wirklich browserübergreifend. Es wird also auf regelmässiges Abfragen von der Webseite über den Webserver hinauslaufen. Zum Beispiel per AJAX.

Auf dem Raspi müsste man Dein Programm also so auslegen dass man die Funktionalität über irgendeinen RPC-Mechanismus ansprechen kann. Eine REST-API mit JSON würde sich da anbieten. Zum Beispiel JSON-RPC oder ein Webserver der JSON ausliefert. Für letzteren könnte man den Apache als Proxy konfigurieren und damit das Problem lösen, das die Anfragen vom Browser ja irgendwie vom Server auf dem der Apache läuft auch bis zum Raspi durchkommen müssen.
der_Mausbiber
User
Beiträge: 72
Registriert: Donnerstag 2. Oktober 2014, 09:51

Von Deinem Programm direkt etwas an den Benutzer zu schicken und das auch noch *über* den Webserver
Ich glaube da hast du mich falsch verstanden.
Ich möchte keine Daten zu dem Benutzer schicken, sondern nur zum Webserver, so das dieDaten dann dort in "Echtzeit" angezeigt werden.

Den zweiten Teil von dir, mit der REST-API versuche ich gerade noch im Detail zu verstehen.
BlackJack

@der_Mausbiber: Beim Webserver wird nichts angezeigt. Das ist doch ein Dienst ohne irgeneine grafische Oberfläche!?
der_Mausbiber
User
Beiträge: 72
Registriert: Donnerstag 2. Oktober 2014, 09:51

leider auch wieder wahr ;)
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@der_Mausbiber: Wenn Du Befehle schicken willst, muß auf dem RasPi irgendeine Art Server laufen. Statusupdates zu verschicken kannst Du mit einer eigenen Service-Seite am Webserver machen.

Alternativ baust Du Dir ein Messaging-System aka RabbitMQ auf.
der_Mausbiber
User
Beiträge: 72
Registriert: Donnerstag 2. Oktober 2014, 09:51

Wenn Du Befehle schicken willst, muß auf dem RasPi irgendeine Art Server laufen.
Da würde mir spontan einfallen dem python-Script einen Socket-Server zu verpassen, das bekomme ich noch hin.
Nur wie kommuniziere ich dann mit jquery/Bootstrap?

Weiß niemand eingutes Tutorial für einen python-Anfänger wie mich?
BlackJack

@der_Mausbiber: Mit JavaScript im Browser kommuniziert man am einfachsten per AJAX über HTTP. Da ist alles schon da und jQuery hat Funktionen dafür. Sich selber einen Socket-Server mit einem selbstgestrickten Protokoll zu schreiben ist das letzte was mir einfallen würde. Das ist eine Ebene auf der man nicht mehr entwickeln sollte wenn man dafür nicht *sehr* gute Gründe hat. Ich würde wahrscheinlich einen Single-Threaded WSGI-Server mit dem Mikrorahmenwerk Bottle auf die Programmlogik aufsetzen. Da kann man dann mit AJAX Anfragen stellen und JSON als Antwort ausliefern lassen.
der_Mausbiber
User
Beiträge: 72
Registriert: Donnerstag 2. Oktober 2014, 09:51

So, nachdem ich die letzten Wochen kaum Zeit gefunden habe, bin ich am Wochenende nochmal mein Problem angegangen.

Damit wir nicht aneinander vorbei reden, alles was ich brauche ist ein in python geschriebener websockets-server.
Dieser soll ersten unter python3 laufen, zweitens sowohl unter Linux als auch unter Windows funktionieren und drittens ich möchte dafür keine extra-Libs oder Erweiterungen installieren.

Mit diesem websocket-Server kann ich dann von meiner Homepage aus über javascript kommunizieren.
Zum testen nutze ich die Android-App netIO.

Am Wochenende habe ich also das Internet nach Beispiel-Code durchsucht und musste leider feststellen das die meisten Beispiele entweder nicht unter python 3 oder nicht unter Windows laufen.
Nach langem Suchen bin ich dann auf insgesamt 3 x funktionierende Beispiele gestossen, wobei jede Lösung anders ist.

Zuerst einmal meine 3 x Beispiele:

#1

Code: Alles auswählen

import socket

HOST = '192.168.2.20'
PORT = 5500

if __name__ == '__main__':

    myserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    myserver.bind((HOST, PORT))
    myserver.listen(1)
    connection, addr = myserver.accept()
    print('Connected by', addr)
    while True:
        data = connection.recv(1024)
        if not data:
            break

        print(str(data, "ascii").strip())

        if str(data, "ascii").strip() == "ausgabe":
            t = "543534"
            connection.send(t.encode('utf-8'))
        else:
            connection.sendall(data)
    print('Connection lost.')
    connection.close()
Die Probleme hier sind:
- nachdem das Script das erste mal irgendwas empfangen hat bleibt es stehen, und zwar ohne Fehlermeldung und die Verbindung zur App steht auch noch
- obwohl ich den Inhalt von "t" umwandel und dann erst sende, kommt er nicht in der App an


#2

Code: Alles auswählen

import socket
import sys
import _thread as thread



HOST = '192.168.2.20'
PORT = 5500

myserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print('Socket created')

try:
    myserver.bind((HOST, PORT))
except socket.error as msg:
    print('Bind failed. Error Code : ' + str(msg[0]) + ' Message ' + msg[1])
    sys.exit()

print('Socket bind complete')


myserver.listen(10)
print('Socket now listening')


def clientthread(conn):
    connection.send('Welcome to the server. Type something and hit enter\n')

    while True:

        data = connection.recv(1024)
        reply = 'OK...' + data
        if not data:
            break

        connection.sendall(reply)

    connection.close()

while 1:
    connection, addr = myserver.accept()
    print('Connected with ' + addr[0] + ':' + str(addr[1]))

    start_new_thread(clientthread, (connection,))

myserver.close()
Die Probleme:
- das Script läuft nicht unter Windows, es startet zwar, aber direkt beim empfangen der ersten Daten stürzt es ab "NameError: name 'start_new_thread' is not defined"

#3 (das bisher besste Beispiel)

Code: Alles auswählen

import asyncio


class SimpleEchoProtocol(asyncio.Protocol):
    def connection_made(self, transport):
        print("Connection received!")
        self.transport = transport

    def data_received(self, data):
        print(data)
        self.transport.write(b'echo:')
        self.transport.write(data)

    def connection_lost(self, exc):
        print("Connection lost! Closing server...")
        server.close()


loop = asyncio.get_event_loop()
server = loop.run_until_complete(loop.create_server(SimpleEchoProtocol, '192.168.2.20', 5500))
loop.run_until_complete(server.wait_closed())
Probleme hier:
- bisher keine gefunden, nur das hier das Beispiel komplett anders ist als alle anderen die ich bisher gefunden habe. Irgendwie ist das hier im Beispiel alles anders aufgebaut.


Meine Fragen nun an euch:
1. Welches der drei Beispiele ist der "richtige" bzw. "beste" Weg?
2. Woran können die Fehler in Beispiel 1 und 2 liegen und wie bekomme ich die weg?
3. Das letzte Beispiel, das mit asyncio, scheint ja genau das zu machen was ich will, es kommt mir nur irgendwie "nicht richtig" vor (kann das schlecht erklären). Täusche ich mich da?

Und ganz vielen Danke schonmal im Vorraus für eure Mühe.
Ich weiß das das Anfängerfragen sind und ich mich damit als Noob oute - trotzdem wäre ich für Hilfe ehrlich dankbar.

der Mausbiber
BlackJack

@der_Mausbiber: #1 Hat ein Problem beim `recv()` denn es ist nicht garantiert wie lang der Ausschnitt aus dem Datenstrom ist, den das liest. Das 'ausgabe' muss nicht bei einem Aufruf ankommen, sondern kann auch auf zwei verteilt sein, und dann erkennt der Server das nicht. Beim Senden kann es passieren das die Ziffernfolge zumindest eine Weile im Ausgabepuffer liegen bleibt. Da sollte man auch `sendall()` verwenden.

#2 läuft nicht nur unter Windows nicht. Wo sollte der Name `start_new_thread` denn auch herkommen!? Das `thread`-Modul sollte man ausserdem unter Python 2 schon nicht mehr verwenden, weshalb es unter Python 3 den Unterstrich am Anfang bekommen hat, was kennzeichnet dass das nicht Teil der öffentlichen API ist. Ersetzt wurde das schon seit langem durch das `threading`-Modul.
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@der_Mausbiber: die ersten beiden Beispiele haben ihre Fehler und bauen direkt auf unterster Socket-Ebene auf. Normalerweise möchte man ja auf einer höheren Ebene Programmieren, dazu ist auch asyncio da.
Möchtest Du wirklich das websockets-Protokoll nachprogrammieren?
BlackJack

Ups, das habe ich irgendwie überlesen. Da würde ich wohl auch erst einmal schauen was es an fertigen Implementierungen gibt: https://pypi.python.org/pypi?:action=se ... mit=search
der_Mausbiber
User
Beiträge: 72
Registriert: Donnerstag 2. Oktober 2014, 09:51

Danke für eure Antworten.

Bei den Beispielen 1 + 2 sieht man was heraus kommt wenn man nur dumm abschreibt.

Wenn ich euch richtig verstanden habe, dann ist Beispiel 3 das was ich suche.

Nein, ich will das websocket-Protokoll nicht nachprogrammieren, da habe ich mich falsch ausgedrückt.
Im Prinzip reicht ein einfache "Frage->Antwort Server" - ich denke ich werde dann mit Beispiel 3 weiter arbeiten.

Trotzdem vielen herzlichen Dank.

Ich werde jetzt erstmal versuchen das Beispiel auch wirklich zu verstehen bevor ich weitermache.
BlackJack

@der_Mausbiber: Bist Du Dir da sicher? Wenn Du mit JavaScript damit kommunizieren möchtest, dann müsste es schon ein Websocket-Server sein, oder Du benutzt AJAX, dann brauchst Du einen HTTP-Server. Also in beiden Fällen etwas ”oberhalb” von einfachen Sockets und etwas was man nicht selber schreibt wenn man dafür keinen guten Grund hat.
der_Mausbiber
User
Beiträge: 72
Registriert: Donnerstag 2. Oktober 2014, 09:51

@BlackJack
Da hast du wohl leider Recht, ich dachte ein einfacher SocketServer würde reichen.
Ich musste aber feststellen das wie von dir geschrieben das Ganze nicht mit javascript funktioniert.
Erst als ich dich python-Erweiterung "websockets" installiert und genutzt habe konnte ich von javascript darauf zugreifen.
Seltsamerweise funktioniert die netIO-App nur mit einem "einfachen" socketServer, nicht jedoch mit einem websocketsServer.

Naja, für mich egal, ich muss also die Variante mit "websockets" nehmen.
Ansich nicht schlimm, da websockets auf asyncio aufsetzt und somit sowohl unter Linux, als auch unter Windows ohne Probleme läuft.

Leider finde ich kaum Beispielcode der diese Erweiterung nutzt, somit bin ich auf das wenige angwiesen was in der Dokumentaion steht (http://aaugustin.github.io/websockets/).

Zum testen habe ich folgendes Script benutzt:

Code: Alles auswählen

#!/usr/bin/env python

import asyncio
import websockets

@asyncio.coroutine
def hello(websocket, path):
    name = yield from websocket.recv()
    print("< {}".format(name))
    greeting = "Hello {}!".format(name)
    yield from websocket.send(greeting)
    print("> {}".format(greeting))

start_server = websockets.serve(hello, 'localhost', 8765)

asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
Daraufhin konnte ich über mein javascript eine Nachricht an das python-Script senden und auch die entsprechende Antwort empfangen.
Schritt 1 wäre also geschafft :)

Das erste Problem ist, das direkt nach dem senden der Antwort die Verbindung geschlossen wird.
Dazu habe ich dann diesen Abschnitt gefunden

Code: Alles auswählen

@asyncio.coroutine
def handler(websocket, path):
    while True:
        message = yield from websocket.recv()
        if message is None:
            break
        yield from consumer(message)
Wenn ich alles richtig verstanden habe, dann geht das Script damit in eine Endlos-Schleife und bleibt dauerhaft mit dem javascript verbunden, richtig?
Ich müsste jetzt nur noch "consumer(message)" durch eine entsprechende Funktion ersetzen, in welcher je nach empfangener Nachricht eine entsprechende Antwort geschickt wird (ich denke das nennt man die Logik des Programms), richtig?

Soweit, so gut, ich bin also schon einmal in der Lage mein python-script auf eingehende Nachrichten reagieren zu lassen.
Der zweite Schritt besteht nun darin, das mein python-Script eine Nachricht an den Client sendet ohne vorher eine empfangen zu haben.

Etwas genauer erklärt. Ich habe einen Raspberry Pi an welchem ein Temperaturfühler (Tinkerforge-Hardware) angeschlossen ist.
Im python-script wird die Temeperatur über eine lib von Tinkerforge ausgelesen. Diese haut bei jeder Temperaturänderung ein Callback raus und führt somit eine von mir definierte Funktion aus.

Ziel ist es, das dieser Callback dafür sorgt das der neue Temperaturwert als Nachricht (z.Bsp. "temperatur:37,5") an den angeschlossenen Client gesendet.
Nur, das bekomme ich nicht hin.

Hat hier jemand eine Idee?
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@der_Mausbiber: Du mußt am Anfang Deines Handlers einen Event-Handler bei Deinem Temperaturleser registrieren, der bei entsprechender Temperaturänderung websocket.send aufruft.
der_Mausbiber
User
Beiträge: 72
Registriert: Donnerstag 2. Oktober 2014, 09:51

Danke, ich frage mich nur gerade wie?
Irgendwie übersteigt das derzeit meinen Horizont .... :(
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@der_Mausbiber: Asynchrone ereignisgesteuerte Programmierung gehört auch nicht gerade zu den einfachsten Dingen. Um Dir konkret zu helfen, mußt Du auch konkreter werden.
der_Mausbiber
User
Beiträge: 72
Registriert: Donnerstag 2. Oktober 2014, 09:51

hmm, okay, am besten zeige ich mal wie weit ich bin.

Das ist das python script welches auf dem Raspberry Pi läuft:

Code: Alles auswählen

#!/usr/bin/env python3

HOST = "localhost"
PORT = 4223
TEMP_UID = "nkv"

import asyncio

import websockets
from tinkerforge.ip_connection import IPConnection
from tinkerforge.bricklet_temperature import Temperature


def change_temperature(temp):
    # changed temperature in temp
    new_temperature = temp / 100.0
    print(new_temperature)
    # von hier aus müsste ich jetzt irgendwie senden können, bzw. eine entsprechende Funktion anstossen
    # etwas wie -> websocket.send("Temperature:",new_temperature)


@asyncio.coroutine
def socket_handler(websocket, path):
    while True:
        # get message from client
        message_rec = yield from websocket.recv()
        # leave if client is disconnect
        if message_rec is None:
            break
        # switch to different tasks
        if message_rec == "set_light":
            print('Licht schalten')
        elif message_rec == "set_light":
            print('TV aus')
        else:
            print(message_rec)


if __name__ == "__main__":
    # get connection to Tinkerforge-MasterBrick - Step 1
    ipcon = IPConnection()
    # set up Temperature-Bricklet
    bricklet_temperatur = Temperature(TEMP_UID, ipcon)
    # connect with MasterBrick
    ipcon.connect(HOST, PORT)
    # set up Temperature-Bricklet callback-Time
    bricklet_temperatur.set_temperature_callback_period(1000)
    # set up Temperature-Bricklet callback
    bricklet_temperatur.register_callback(bricklet_temperatur.CALLBACK_TEMPERATURE, change_temperature)

    # set up Websocket-Server
    start_server = websockets.serve(socket_handler, '192.168.127.30', 5500)

    # run Websocket-Server
    asyncio.get_event_loop().run_until_complete(start_server)
    asyncio.get_event_loop().run_forever()

    # Verbindung zum Master-Brick trennen
    ipcon.disconnect()
und hier die entsprechenden html & js -Dateien:

Code: Alles auswählen

<!DOCTYPE html>
<html lang="de">
<head>
	<meta charset="utf-8">
	<title>WebSockets</title>

	<link rel="stylesheet" href="style.css">
</head>
<body>
	<div id="page-wrapper">
		<h1>WebSockets Demo</h1>

		<div id="status">Connecting...</div>

		<a href="#" onClick="set_light();">Lichtschalter</a><br>

		<a href="#" onClick="set_TV();">TV on/off</a><br>

    Temperatur:<div id="temperatur">... °C</div>

	</div>

	<script src="app.js"></script>
</body>
</html>

Code: Alles auswählen

window.onload = function() {

  // Get references to elements on the page.
  var socketStatus = document.getElementById('status');
  var temperature = document.getElementById('temperatur');


  // Create a new WebSocket.
  var socket = new WebSocket('ws://192.168.127.30:5500');


  // Handle any errors that occur.
  socket.onerror = function(error) {
    console.log('WebSocket Error: ' + error);
  };


  // Show a connected message when the WebSocket is opened.
  socket.onopen = function(event) {
    socketStatus.innerHTML = 'Connected to: ' + event.currentTarget.url;
    socketStatus.className = 'open';
  };


  // Handle messages sent by the server.
  socket.onmessage = function(event) {
    var temp_data = event.data;
		console.log('Empfangen: ' + temp_data);
		var pos = temp_data.indexOf(":");
		if (pos>0) {
			var befehl = temp_data.slice(0,pos-1);
			switch (befehl) {
				case "temperature":
    			temperature.innerHTML = temp_data + ' °C';
					break;
			}
		}
  };


  // Show a disconnected message when the WebSocket is closed.
  socket.onclose = function(event) {
    socketStatus.innerHTML = 'Disconnected from WebSocket.';
    socketStatus.className = 'closed';
  };


	set_light = function() {
		socket.send("set_light");
	};

	set_tv = function() {
		socket.send("set_light");
	};

};


So, was mir jetzt fehlt ist eine Möglichkeit bei einer Änderung der Temperatur, diese an den verbunden Client zu senden.
Der entsprechende Callback wird auch korrekt ausgeführt, nur weiß ich nicht wie ich von dort das senden anstossen kann.

Erschwerend kommt hinzu das dies nicht der einzige Callback bleiben wird, später kommen noch welche für Luftdruck, Feuchtigkeit, etc... dazu.

Das senden von der html-Seite aus klappt auch schon ganz gut.
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

Da es mehrere Websocketverbindungen geben kann, die die Temperatur wissen wollen, mußt Du ein Producer-Consumer-Schema implementieren, z.B. über Queues:

Code: Alles auswählen

import asyncio
from asyncio.queues import Queue
import websockets
 
temperature_consumers = []

def change_temperature(temp):
    # changed temperature in temp
    new_temperature = temp / 100.0
    print(new_temperature)
    for consumer in temperature_consumers:
        consumer.put_nowait(new_temperature)


@asyncio.coroutine
def temperature_loop(websocket):
    try:
        temperature_changed = Queue()
        temperature_consumers.append(temperature_changed)
        while True:
            temp = yield from temperature_changed.get()
            yield from websocket.send('Temperatur: %f' % temp)
    finally:
        temperature_consumers.remove(temperature_changed)
 
 
@asyncio.coroutine
def socket_handler(websocket, path):
    task = asyncio.async(temperature_loop(websocket))
    while True:
        # get message from client
        message_rec = yield from websocket.recv()
        # leave if client is disconnect
        if message_rec is None:
            break
        # switch to different tasks
        if message_rec == "set_light":
            print('Licht schalten')
        elif message_rec == "set_light":
            print('TV aus')
        else:
            print(message_rec)
     task.cancel()

 
Antworten