TCP-Socket mehrere Befehle emfpangen (Buffer-Problem?)

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
TrashCo92
User
Beiträge: 9
Registriert: Montag 15. Mai 2023, 09:40

Hallo Zusammen,

ich spiele mich gerade mit Python etwas rum, komme aus der C-Welt und versuche mich damit zurecht zu finden scheitere jedoch an einem Problem:

Im folgenden habe ich einen TCP-Socket Server auf Python aufgesetzt an welchem Sachen wie ein Servo und Schrittmotor angeschlossen sind. Funktionieren tut alles soweit, ich öffne vom Client eine Verbindung zum Socket-Server, sende meinen Befehl und schließe die Verbindung mit socket_close() am Client wieder. Wenn ich jedoch vom Client aus Verbinde und mehrere Befehle senden möchte funktioniert es nicht, es wird nur der erste Befehl angenommen, wenn ich einen weiteren sende bekomme ich aber vom Server über die Konsole die Info dass er die Informationen vom ersten Befehl erhalten hat, für mich sieht das so aus als wäre der Buffer noch mit dem alten Befehl befüllt und muss vorher geleert werden bevor ein weiterer eingelesen werden kann. Wie setze ich das denn um?!

Code: Alles auswählen

from pyfirmata import Arduino, util, SERVO
import socket
from time import sleep

HOST = '192.168.0.100'
PORT = 3000
response ="ok"
answer_ready_request = "ready"

arduino = Arduino('COM2')

print("Script Running...")

def rotateServo(servopin, angle):
    arduino.digital[servopin].mode = SERVO
    arduino.digital[servopin].write(angle)

def rotateStepper(dirpin, steppin, direction, steps, speed):
    if direction == "cw":
        direction_int = 1
    elif direction == "ccw":
        direction_int = 0
    arduino.digital[dirpin].write(direction_int)
    stepdelay = (60000 / (speed * 400)) * 0.000001
    for i in range (steps):
        arduino.digital(steppin).write(1)
        sleep(stepdelay)
        arduino.digital(steppin).write(0)
        sleep(stepdelay)



with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen(1)

    while True:
        
        c, addr = s.accept()
        print(f"Connected by {addr}")
        
        try:    
            msg = c.recv(1024)

            if not msg:
                break

            elif msg.decode() == "ready?":
                print("Answer Ready Request")
                print("Sending Response")
                c.send(answer_ready_request.encode())
                print("Answer Ready Request Done")

            elif msg.decode().find("servo") !=-1:
                print("Receive Move Servo Request")
                request, value1, value2 = msg.decode().split(';')
                print("Received Servo Values:", "Servopin:", value1, "Angle:", value2)
                servopin = int(value1)
                angle = int(value2)
                print("Moving Servo")
                rotateServo(servopin, angle)
                print("Sending Response")
                c.send(response.encode())
                print("Servo Move Request Done")

            elif msg.decode().find("stepper") !=-1:
                print("Receive Move Stepper Request")
                request, value1, value2, value3, value4, value5 = msg.decode().split(';')
                print("Received Stepper Values:", "Direction:", value3, "Steps:", value4, "Speed:", value5)
                dirpin = int(value1)
                steppin = int(value2)
                direction = value3
                steps = int(value4)
                speed = int(value5)
                print("Moving Stepper")
                rotateStepper(dirpin, steppin, direction, steps, speed)
                print("Sending Response")
                c.send(response.encode())
                print("Stepper Move Request Done")

        except socket.error as socketerror:
            print ("ERROR")

Danke mal vorab für Eure Hilfe ;)
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Neben dem üblichen Socket-Problem - so zu tun, als ob das diskrete Nachrichten sind, statt einem Byte-Strom, dessen Grenzen durch ein Protokoll definiert werden müssen - ist das Problem doch relativ klar: du wartest immer auf eine neue Verbindung (accept), und machst dann einmal was mit dem resultierenden socket (recv). Das wars dann. Wenn du mit dem dauerhaft arbeiten willst, benutz ihn weiter.

Aus den erstgenannten Gründen, und auch diesen Details des Verbindungsmanagements, würde ich zu einer Middleware wie ZeroMQ oder nanomsg raten.
Benutzeravatar
__blackjack__
User
Beiträge: 13572
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@TrashCo92: TCP funktioniert nicht so, weder in Python noch in C. Das ist ein Datenstrom, keine einzelnen Nachrichten. Weder `send()` noch `recv()` geben irgendwelche Garantien was alles gesendet oder empfangen wird über „es ist mindestens 1 Byte“ hinaus. Beim Senden ist das noch relativ einfach lösbar: `send_all()` statt dem eher nutzlosen `send()` verwenden.

Beim Empfangen bedeutet ein `recv(n)`-Aufruf, das mindestens 1 Byte und maximal n aus dem Datenstrom geliefert werden. Der Code muss aber grundsätzlich damit klar kommen können, das bei jedem Aufruf nur 1 Byte geliefert wird. Und umgekehrt aber auch, dass mehr als eine Nachricht geliefert wird, und das die aber auch nicht vollständig sein müssen, also die erste und die letzte Nachricht die in einem `recv()`-Aufruf müssen nicht vollständig sein, wenn der erste Teil der ersten Nachricht vorher schon gelesen wurde. Und die letzte muss halt nicht vollständig sein. Da Code drum herum zu schreiben der Nachrichten erkennt und trennt, und unvollständige Nachrichten puffert bis sie vollständig gelesen wurden ist Aufgabe des Programmierers. Und das ist nervige Fummelarbeit, da immer alle möglichen Fälle fehlerfrei zu berücksichtigen. Darum als allererstes die Frage: Musst Du das unbedingt selbst basteln? Warum nimmst Du nicht eines der vielen fertigen Protokolle für die es schon Bibliotheken gibt?

`find()` würde ich grundsätzlich nicht benutzen wegen der komischen API. Wenn einen der konkrete Index nicht interessiert ist es auch die falsche Methode. Zum testen von Enthalten-sein gibt es den ``in``-Operator. Aber auch das ist hier falsch, denn es interessiert ja sicher nicht ob beispielsweise "servo" *irgendwo* vorkommt, sondern ob dass das Kommando am Anfang ist. Dafür gibt es `startswith()`.

`msg.decode()` steht da viel zu oft im Quelltext. Das muss doch eigentlich nur genau *einmal* passieren, nachdem man eine Nachricht vollständig beisammen hat. Vielleicht wäre es auch sinnvoll das aufteilen an ";" ebenfalls einmal, vor den Abfragen zu machen falls `request` das Kommando ist welches ausgewertet werden soll. Und eventuell sollte man statt selbst was zu basteln beispielsweise das `csv`-Modul zum aufteilen verwenden. Dann können auch Trennzeichen selbst in den Daten vorkommen. Oder man serialisiert die Befehle als JSON, dann muss man beim deserialisieren nicht selbst in ganze Zahlen umwandeln.

Namen sollten nicht kryptisch abgekürzt werden, schon gar nicht mit einzelnen Buchstaben. Also `message` statt `msg`, `server_socket` statt `s`, und `client` oder `client_socket` statt `c`.
„Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.“ — Brian W. Kernighan
TrashCo92
User
Beiträge: 9
Registriert: Montag 15. Mai 2023, 09:40

Danke schonmal für die ganzen Tipps soweit, die startswith() funktion klingt schonmal gut :)
Dass ich msg.decode jetzt mal überall eingebaut habe war für den ersten Versuch um Fehler auszuschließen, wollte erstmal das ganze zum laufen bringen und habs so quasi für "dummies" mal ganz grob gecoded, die Abkürzungen liegen an meiner sagen wir mal faulheit schnell was zum laufen zu bringen, wenns mal klappt wirds natürlich sauber umprogrammiert ;)

Leider muss ich so mit TCP-Socket arbeiten, eine Middleware ist glaub ich nicht umsetzbar, die gegenstelle (der Client) ist ein Roboter der Marke Universal Robots, da gibts nur für TCP-Socket die Befehle socket_open, socket_send_string() socket_read_string() & socket_close, mehr kann der Kollege nicht, damit muss ich arbeiten, zwar gäbe es noch Modbus und ProfiNet etc. aber dass ist uns gerade zu aufwendig für die Aufgabenstellung die doch sehr einfach und pragmatisch ist. Das Empfangen und sicherstellen von übertragenen Kommandos federn wir ab indem wir den Roboter in Warteschleifen setzen und er solange wartet bis der response gesendet wurde, das klappt soweit ganz gut.

Ich hatte letzte Woche Versuchsweise nach einem accept das ganze in eine weitere Schleife gesetzt, dann hat er schon mehrere Befehle angenommen, nach dem close() jedoch konnte ich nicht neu verbinden da er in der schleife hing. Mit socket habe ich leider zuvor nicht gearbeitet, auch nicht in C.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich weiß nicht, was du vorher gemacht hast. Aber prinzipiell musst du so lange mit dem socket arbeiten, bis der von der gegenstelle geschlossen wird. Und dann kommt, wenn ich mich recht erinnere, ein Leerstring. Darauf musst du also prüfen.
Sirius3
User
Beiträge: 18054
Registriert: Sonntag 21. Oktober 2012, 17:20

Auch wenn Du eingeschränkt bist, mußt Du Dir ein Protokoll definieren. Das einfachste wäre ein Zeilenbasiertes Protokoll zu benutzen, mit makefile dir ein Fileähnliches Objekt geben zu lassen und damit dann zu arbeiten:

Code: Alles auswählen

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server:
    server.bind((HOST, PORT))
    server.listen(1)

    while True:
        connection, addr = server.accept()
        print(f"Connected by {addr}")
        try:
            file = connection.makefile('rw', encoding='ASCII')
            for line in file:
                line = line.strip()
                if line == "ready?":
                    print("Answer Ready Request")
                    print("Sending Response")
                    file.write(answer_ready_request + "\n")
                    print("Answer Ready Request Done")
                elif line.startswith("servo"):
                    print("Receive Move Servo Request")
                    request, value1, value2 = line.split(';')
                    print("Received Servo Values:", "Servopin:", value1, "Angle:", value2)
                    servopin = int(value1)
                    angle = int(value2)
                    print("Moving Servo")
                    rotateServo(servopin, angle)
                    print("Sending Response")
                    file.write(response + "\n")
                    print("Servo Move Request Done")
                elif line.startswith("stepper"):
                    print("Receive Move Stepper Request")
                    request, value1, value2, value3, value4, value5 = line.split(';')
                    print("Received Stepper Values:", "Direction:", value3, "Steps:", value4, "Speed:", value5)
                    dirpin = int(value1)
                    steppin = int(value2)
                    direction = value3
                    steps = int(value4)
                    speed = int(value5)
                    print("Moving Stepper")
                    rotateStepper(dirpin, steppin, direction, steps, speed)
                    print("Sending Response")
                    file.write(response + "\n")
                    print("Stepper Move Request Done")
        except socket.error as socketerror:
            print("ERROR")
TrashCo92
User
Beiträge: 9
Registriert: Montag 15. Mai 2023, 09:40

Super, danke für die Infos!
Ich werd das entsprechend mal implementieren & diese Woche noch Testen, hab auf Stackoverflow dazu auch parallel einen Thread noch gefunden wie die Verarbeitung des Leerstrings nach einem close abläuft.
Benutzeravatar
grubenfox
User
Beiträge: 542
Registriert: Freitag 2. Dezember 2022, 15:49

Was ist das eigentlich Real-Time Data Exchange (RTDE) Guide?

Ganz unten gibt es noch einen Link nach RTDE client library - Python
Library implements API for Universal Robots RTDE realtime interface.
mit
  • rtde.py: RTDE connection management object
und
  • example_control_loop.py - example for controlling robot motion. Program moves robot between 2 setpoints. Copy rtde_control_loop.urp to the robot. Start python script before starting program.
TrashCo92
User
Beiträge: 9
Registriert: Montag 15. Mai 2023, 09:40

grubenfox hat geschrieben: Dienstag 16. Mai 2023, 09:25 Was ist das eigentlich Real-Time Data Exchange (RTDE) Guide?

Ganz unten gibt es noch einen Link nach RTDE client library - Python
Library implements API for Universal Robots RTDE realtime interface.
mit
  • rtde.py: RTDE connection management object
und
  • example_control_loop.py - example for controlling robot motion. Program moves robot between 2 setpoints. Copy rtde_control_loop.urp to the robot. Start python script before starting program.
Das ist eine Schnittstelle um den Roboter fernzusteuern per Python von einem externen Rechner bzw. dessen Zustände wie Position, I/O-States und diverse Informationen über Last, Motorstrom etc. auszulesen. Für unsere Anwendung nicht geeignet da der Roboter der Master ist und externe Peripherie ansteuern soll, deshalb fällt die Wahl nur auf ProfiNet, Modbus oder TCP-Socket.
Es gäbe auch noch XMLRPC und ROS, beides aber sehr aufwendig in der Umsetzung und macht nur Sinn bei größeren Automatisierungsprojekten.

Bei mir in der Firma verwenden wir mehrere dieser Roboter, sie werden ausschließlich mit ProfiNet (in der CNC-Abteilung zur Kommunikation und Bestückung der Maschinen), Modbus (super Schnittstelle zu IT-Systemen wie SAP oder Jenkins) oder ROS (in der Montage eine sehr starke Schnittstelle für Kameras, Lichtschranken, Sensoren etc.) gesteuert, XMLRPC & RTDE haben sich als sehr buggy und unzuverlässig erwiesen.
Benutzeravatar
grubenfox
User
Beiträge: 542
Registriert: Freitag 2. Dezember 2022, 15:49

Nun bin ich etwas verwirrt...
TrashCo92 hat geschrieben: Dienstag 16. Mai 2023, 09:47 Das ist eine Schnittstelle um den Roboter fernzusteuern per Python von einem externen Rechner bzw. dessen Zustände wie Position, I/O-States und diverse Informationen über Last, Motorstrom etc. auszulesen. Für unsere Anwendung nicht geeignet da der Roboter der Master ist und externe Peripherie ansteuern soll, deshalb fällt die Wahl nur auf ProfiNet, Modbus oder TCP-Socket.
TrashCo92 hat geschrieben: Montag 15. Mai 2023, 10:36 Leider muss ich so mit TCP-Socket arbeiten, eine Middleware ist glaub ich nicht umsetzbar, die gegenstelle (der Client) ist ein Roboter der Marke Universal Robots, da gibts nur für TCP-Socket die Befehle socket_open, socket_send_string() socket_read_string() & socket_close, mehr kann der Kollege nicht, damit muss ich arbeiten,
Aber egal, mit
  • rtde.py: RTDE connection management object
haben wir ein Python-Beispiel für die eine Seite der Connection (mit TCP-Sockets). Kann man sich vielleicht etwas abgucken...
Antworten