PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : Roboter in Echtzeit über den Browser steuern



-schumi-
31.03.2014, 14:15
Hallo zusammen,

mich beschäftigt im Moment ein Thema, zu dem es mit Sicherheit schon viele Lösungen gibt, aber es scheint mir fallen nicht die richtigen Suchbegriffe ein.. Und zwar möchte ich meinen Roboter in Echtzeit übers Netzwerk steuern, idealerweise über den Browser. Alternative wäre ein Python-Script auf dem Client. (Auf dem Server/Robo läuft im übrigen ein OpenWRT auf einem TL-MR3020-Router)

Im Moment mache ich das über Buttons auf einer HTML-Seite (aus einem Python-CGI-Script), die per POST an das Python-CGI-Script mitteilen wenn sie gedrückt wurden:


#!/usr/bin/env python

import cgi
from RobotLibrary import i2c as i2c
from RobotLibrary import robot_motorctrl as motor
from RobotLibrary import robot_servoctrl as servo

print """ Content-type: text/html

<html>

<head><title>Robot</title></head>

<body>
<h3> Hello, this is Robot </h3>

<style>
.hide { position:absolute; top:-1px; left:-1px; width:1px; height:1px; }
</style>
<iframe name="hiddenFrame" class="hide"></iframe>

<form action="pytest.py" method="POST" target="hiddenFrame">
Movement:<br/>
<input type="submit" value="↖ " name="Submit_FL" style="height:80px; width:80px" />
<input type="submit" value="↑ " name="Submit_F" style="height:80px; width:80px" />
<input type="submit" value="↗ " name="Submit_FR" style="height:80px; width:80px" />
<br/>
<input type="submit" value="← " name="Submit_L" style="height:80px; width:80px" />
<input type="submit" value="Stop" name="Submit_Stop" style="height:80px; width:80px" />
<input type="submit" value="→ " name="Submit_R" style="height:80px; width:80px" />
<br/>
<input type="submit" value="↙ " name="Submit_BL" style="height:80px; width:80px" />
<input type="submit" value="↓ " name="Submit_B" style="height:80px; width:80px" />
<input type="submit" value="↘ " name="Submit_BR" style="height:80px; width:80px" />

<br/>
<br/>
Mode:</br>
<input type="submit" value="Speed" name="Submit_mode_speed" />
<input type="submit" value="Torque" name="Submit_mode_torque" />
</form>
</body>

</html>
"""

form = cgi.FieldStorage()

i2c.OPEN('/dev/i2c-0')

if "Submit_F" in form:
motor.SET_MOTORS(20,20)
elif "Submit_FL" in form:
motor.SET_MOTORS(10,20)
elif "Submit_FR" in form:
motor.SET_MOTORS(20,10)
elif "Submit_L" in form:
motor.SET_MOTORS(0,15)
elif "Submit_Stop" in form:
motor.STOP()
elif "Submit_R" in form:
motor.SET_MOTORS(15,0)
elif "Submit_B" in form:
motor.SET_MOTORS(-10,-10)
elif "Submit_BL" in form:
motor.SET_MOTORS(-18,-10)
elif "Submit_BR" in form:
motor.SET_MOTORS(-10,-18)

elif "Submit_mode_speed" in form:
motor.MOVEMODE(motor.MoveMode_Speed)
elif "Submit_mode_torque" in form:
motor.MOVEMODE(motor.MoveMode_Torque)

i2c.CLOSE()

Im Prinzip ist das schon recht toll: Sehr einfach zu programmieren und funktioniert im Browser auf dem Laptop als auch Smartphone einwandfrei
27906

Das ganze hat aber ein paar Haken:
- Es ist sehr langsam. Vom Button-Druck bis zur Reaktion des Robos vergehen ca. 2sec
- Ein Verbindungsabbruch lässt den Roboter nicht stoppen. Zwar könnte man mithilfe einer Seite mit automatischem Refresh in einem IFrame ein CGI-Script pollen, aber man kann erst nach 3-4 Sekunden sagen, dass die Verbindung tatsächlich abgebrochen ist. Das ist nur ein grober Hack und dauert viel zu lange

Jetzt die Frage:
Kennt jemand einen ähnlich einfachen Ansatz um einen Roboter in Echtzeit steuern zu können? Am allerbesten wäre etwas fertiges das auch eine Steuerung über die Tastatur oder Joystick auf dem Bildschirm (Für Maus oder Touch) ermöglicht. Hintergrund ist, dass ich mich in der Webprogrammierung nicht gerade gut auskenne und bis auf HTML/CSS/ein wenig PHP nicht viel gemacht habe..

Viele Grüße
schumi

PICture
31.03.2014, 14:49
Hallo!


Das ganze hat aber ein paar Haken:
- Es ist sehr langsam. Vom Button-Druck bis zur Reaktion des Robos vergehen ca. 2sec
- Ein Verbindungsabbruch lässt den Roboter nicht stoppen. Zwar könnte man mithilfe einer Seite mit automatischem Refresh in einem IFrame ein CGI-Script pollen, aber man kann erst nach 3-4 Sekunden sagen, dass die Verbindung tatsächlich abgebrochen ist. Das ist nur ein grober Hack und dauert viel zu lange

Es lässt sich sicher nicht ändern. Ich wollte mal mit Hilfe vom "Skype" mit Kollegen (Gitaristen und Keyborder aus Europa) live Musik machen. Weil die o.g. ca. 2 Sekunden haben sich nicht eliminieren lassen, mussten wir leider aufgeben._.

Clark79
31.03.2014, 16:01
Hallo! Ein Bekannter von mir hat das auch mal versucht, aber die Zeitverzögerung ist trotzdem geblieben. Gibt es da nicht aber eine spezielle Software oder so dafür?

-schumi-
31.03.2014, 17:20
Ich glaube ich habe den größten Flaschenhals gefunden: beim CGI muss erst Python gestartet werden, und auf einem 400MHz MIPS core dauert das anscheinend ein bisschen.. :)

Aber ich habe einen Lösungspfad gefunden -> Einen Webserver in Python, der auf einem anderen Port läuft und die POST-Anfragen entgegen nimmt und gleich die anderen µC über I2C ansteuert.
Das HTML-File mit den Buttons (fast gleich wie oben, sieht auch genauso aus):


<html>

<head><title>Robot</title></head>

<body>
<h4> Hello, this is Robot </h4>

<style>
.hide { overflow:hidden; position:absolute; top:-1px; left:-1px; width:1px; height:1px; }
</style>
<iframe name="hiddenFrame" class="hide">⇄ </iframe>

<form action="http://192.168.0.251:8001/" method="POST" target="hiddenFrame">
Movement:<br/>
<input type="submit" value="↖ " name="Submit_FL" style="height:80px; width:80px" />
<input type="submit" value="↑ " name="Submit_F" style="height:80px; width:80px" />
<input type="submit" value="↗ " name="Submit_FR" style="height:80px; width:80px" />
<br/>
<input type="submit" value="← " name="Submit_L" style="height:80px; width:80px" />
<input type="submit" value="Stop" name="Submit_Stop" style="height:80px; width:80px" />
<input type="submit" value="→ " name="Submit_R" style="height:80px; width:80px" />
<br/>
<input type="submit" value="↙ " name="Submit_BL" style="height:80px; width:80px" />
<input type="submit" value="↓ " name="Submit_B" style="height:80px; width:80px" />
<input type="submit" value="↘ " name="Submit_BR" style="height:80px; width:80px" />

<br/>
<br/>
Mode:</br>
<input type="submit" value="Speed" name="Submit_mode_speed" />
<input type="submit" value="Torque" name="Submit_mode_torque" />
</form>
</body>

</html>


Und das Python-Script, das auf Port 8001 horcht:


#!/usr/bin/env python

import SimpleHTTPServer
import SocketServer
import logging
import cgi

import sys

from RobotLibrary import i2c as i2c
from RobotLibrary import robot_motorctrl as motor
from RobotLibrary import robot_servoctrl as servo

PORT = 8001
I = ""

class ServerHandler(SimpleHTTPServer.SimpleHTTPRequestHa ndler):

def do_HEAD(self):
self.send_response(200)
self.send_header('Content-type', 'text/html')
self.end_headers()

def do_GET(self):
self.do_HEAD()
self.wfile.write("""Thank you for your request..""") # Text der zurueckgegeben wird
def do_POST(self):
logging.warning("======= POST STARTED =======")
logging.warning(self.headers)
form = cgi.FieldStorage(
fp=self.rfile,
headers=self.headers,
environ={'REQUEST_METHOD':'POST',
'CONTENT_TYPE':self.headers['Content-Type'],
})
logging.warning("======= POST VALUES =======")
print(form)
i2c.OPEN('/dev/i2c-0')

if "Submit_F" in form:
motor.SET_MOTORS(20,20)
elif "Submit_FL" in form:
motor.SET_MOTORS(10,20)
elif "Submit_FR" in form:
motor.SET_MOTORS(20,10)
elif "Submit_L" in form:
motor.SET_MOTORS(0,15)
elif "Submit_Stop" in form:
motor.STOP()
elif "Submit_R" in form:
motor.SET_MOTORS(15,0)
elif "Submit_B" in form:
motor.SET_MOTORS(-10,-10)
elif "Submit_BL" in form:
motor.SET_MOTORS(-18,-10)
elif "Submit_BR" in form:
motor.SET_MOTORS(-10,-18)

elif "Submit_mode_speed" in form:
motor.MOVEMODE(motor.MoveMode_Speed)
elif "Submit_mode_torque" in form:
motor.MOVEMODE(motor.MoveMode_Torque)

i2c.CLOSE()

logging.warning("\n")
#SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET( self)
self.do_GET()

httpd = SocketServer.TCPServer(("", 8001), ServerHandler)

print("RoboServer, Port = 8001")
httpd.serve_forever()

Wohl noch einige Fehler drin, denn das ganze ist mehr oder weniger zusammen gecopy- & pasted ( von hier https://snipt.net/raw/f8ef141069c3e7ac7e0134c6b58c25bf/?nice und hier https://www.assembla.com/spaces/oniontv-plugin/wiki/A_simple_HTTP_server_in_python/history )

Aber: Jetzt ist die Reaktionsgeschwindigkeit VIEL VIEL höher, so um die 200ms grob geschätzt :!: Supergeile Sache, jetz muss ich das nur noch ordentlich Programmieren. Und jetzt kann man evtl. auch mit z.B. Javascript alle 500ms oder so pollen, so dass der Robo nach spätestens 1sec ohne Poller stehenbleibt

oberallgeier
31.03.2014, 18:33
... Roboter in Echtzeit übers Netzwerk steuern, idealerweise über den Browser ...Im WLAN habe ich mit ner Steuerung eines Controllers keine Probleme (vermutlich auch nicht über die heimatliche Routergrenze hinweg).

Ansatz: Raspberry Pi ohne Bildschirm und ohne Tastatur, mit WLAN-Stick, USB-UART-Adapter und CuteCom (graphisches Terminal, ähnlich dem br@y-Terminal). Die UART-Verbindung geht an den Controller.

Der RasPi ist so eingestellt, dass er bis zur GUI "alleine" startet - es sind keine zusätzlichen Eingaben nötig. Auf meinem (Windows)PC starte ich den RemoteDesktop - da bekomme ich die Linuxoberfläche auf den Schirm und starte CuteCom. Nun kann ich alle möglichen Befehlstelegramme per Terminal/USB-UART an den Controller senden . . . Das Ganze ist sicher eine ziemliche KrückenLösung; sie läuft mit kaum erkennbarer Zeitverzögerung. Pings habe ich noch nicht gemessen.

marot90
10.04.2014, 08:26
So wie ich das verstehe, will -schumi- das nicht einfach über WLan, sondern übers Internet steuern. Und da kommt dann, wie ja auch schon beim Skype Beispiel erwähnt wurde, durchaus einiges an Latenz zusammen. Selebst wenn alles optimiert wird wird das auch bei einer guten Verbidnung kaum unter eine Sekunde sinken. Nur über Wlan als "Sender" sozusagen ist das wohl weniger ein Problem.

Thomas$
11.04.2014, 09:05
zu den latenzen im Netzwerk sind das eine,
ich bin ja für die in html5 vorhande websocket funktion und javascript (stichwort onkeydown)

python websocket server + serielle

javascript ständig befehle senden lassen und am µC watchdog nutzen und immer zurücksetzen wenn befehl mit prüfsumme angekommen ist

-schumi-
12.04.2014, 15:00
Danke für eure Antworten :) (Und ein extra Danke für Thomas für die guten Schlagwörter)

Also ich möchte das ganze nur im heimischen (W)Lan machen, d.h. Stecke sind maximal zwei Router.

Die Lösung oben mit dem Python-Webserver war schon man nicht schlecht. Dann habe ich mein Python3-Buch herausgekram und mit Sockets angefangen. Dann kam Thomas$ mit Websockets und da bin ich jetzt.

Und zwar habe ich jetzt einen Tornado-Python-Websocket-Server:


#!/usr/bin/env python3

# import the libraries
import tornado.web
import tornado.websocket
import tornado.ioloop

import threading

from RobotLibrary import i2c as i2c
from RobotLibrary import robot_motorctrl as motor
from RobotLibrary import robot_servoctrl as servo

class WebSocketHandler(tornado.websocket.WebSocketHandle r):
# the client connected
def open(self):
print("New client connected")
self.write_message("You are connected")

# the client sent the message
def on_message(self, message):
print("Got a message: "+message+", parsing it..")
requestlist = eval(message)
self.write_message(RobotRequestHandler(requestlist ))

# client disconnected
def on_close(self):
print("Client disconnected")

motorr = 0
motorl = 0
def RobotRequestHandler(requestlist):
global motorr
global motorl
global watchdogctr
i2c.OPEN('/dev/i2c-0')
returnlist = {}
for request in requestlist:
if request == "command":
commandlist = requestlist[request]
returncommandlist = {}
for command in commandlist:
if command == "set":
setlist = commandlist[command]
returnsetlist = {}
for thisset in setlist:
if thisset == "motorr":
motorr = int(setlist[thisset])
returnsetlist[thisset]="Done"
elif thisset == "motorl":
motorl = int(setlist[thisset])
returnsetlist[thisset]="Done"
else:
returnsetlist[thisset]="Not Found"
try:
motor.SET_MOTORS(motorl,motorr)
except:
pass
returncommandlist[command]=returnsetlist
elif command == "get":
getlist = commandlist[command]
returngetlist = {}
for thisget in getlist:
if thisget == "motorr":
returngetlist[thisget]=str(motorr)
elif thisget == "motorl":
returngetlist[thisget]=str(motorl)
else:
returngetlist[thisget]="Not Found"
returncommandlist[command]=returngetlist
else:
returncommandlist[command] = "Not Found"
returnlist[request]=returncommandlist
elif request == "ping":
watchdogctr = 500
returnlist[request]="Done"
print(returnlist)
i2c.CLOSE()
return returnlist

watchdogctr = 500
def watchdog():
global watchdogctr
watchdogctr -= 100
if watchdogctr <= 0:
RobotRequestHandler({"command":{"set":{"motorl":"0", "motorr":"0"}}})
watchdogctr = 500
threading.Timer(0.1, watchdog).start()
watchdog()

# start a new WebSocket Application
# use "/" as the root, and the
# WebSocketHandler as our handler
application = tornado.web.Application([
(r"/", WebSocketHandler),
])

# start the tornado server on port 8888
if __name__ == "__main__":
application.listen(8888)
tornado.ioloop.IOLoop.instance().start()


Alles sehr klein und einfach, nur die RobotRequestHandler() ist etwas größer, weil meine Befehle im Moment z.B. so aussehen: {"command":{"set":{"motorl":"20", "motorr":"20"}}}
Das parst diese Funktion und gibt zurück was sie alles gemacht hat: {"command": {"set": {"motorr": "Done", "motorl": "Done"}}}

Auf der Client-Seite siehts im Moment so aus:


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Robot-Control</title>
<script type="text/javascript">
var ws;
function WebSocket_Close()
{
ws.close();
}
function WebSocket_Open()
{
ws = new WebSocket("ws://192.168.0.251:8888");
ws.onerror = function(error)
{
console.log('Error detected: ' + error);
}
ws.onopen = function()
{
console.log('Connection opened!');
}
ws.onclose = function()
{
console.log('Connection closed!');
}
ws.onmessage = function(e)
{
var message = e.data;
console.log('Received message: '+message);
}
}
function WebSocket_Send(data)
{
ws.send(data);
console.log('Sent data: '+data)
}

function Robot_move(direction)
{
if(direction == "forwards")
WebSocket_Send('{"command":{"set":{"motorl":"20", "motorr":"20"}}}')
if(direction == "backwards")
WebSocket_Send('{"command":{"set":{"motorl":"-20", "motorr":"-20"}}}')
if(direction == "left")
WebSocket_Send('{"command":{"set":{"motorl":"-20", "motorr":"20"}}}')
if(direction == "right")
WebSocket_Send('{"command":{"set":{"motorl":"20", "motorr":"-20"}}}')
if(direction == "stop")
WebSocket_Send('{"command":{"set":{"motorl":"0", "motorr":"0"}}}')
}


function verifyKey(e)
{
var keycode;
if (window.event)
keycode = window.event.keyCode;
else if (e)
keycode = e.which;

if (keycode == 38)
Robot_move('forwards')
else if (keycode == 40)
Robot_move('backwards')
else if (keycode == 37)
Robot_move('left')
else if (keycode == 39)
Robot_move('right')
else if (keycode == 32)
Robot_move('stop');
else
console.log(keycode)

return false;
}

setInterval(Robot_ping, 300);
function Robot_ping()
{
WebSocket_Send('{"ping"}')
}


// Gamepad support:
var mygamepad;
var gamepadconnected = false;
var x = 0;
var y = 0;
function gamepadConnected(e) {
gamepadconnected = true;
mygamepad = e.gamepad

function pollAxis() {
//console.log(" Axis " + mygamepad.axes);
x = parseInt(mygamepad.axes[3]*20)
y = parseInt(-mygamepad.axes[2]*20)
//console.log(x+" : "+y)
}
setInterval(pollAxis, 30);


function GamepadRobotControl()
{
if ( gamepadconnected == true)
{
WebSocket_Send('{"command":{"set":{"motorl":"'+(y+x)+'", "motorr":"'+(y-x)+'"}}}')
}

}
setInterval(GamepadRobotControl, 400);

}

function gamepadDisconnected() {
gamepadconnected = false;
}

window.addEventListener("gamepadconnected", gamepadConnected, false);
window.addEventListener("gamepaddisconnected", gamepadDisconnected, false);

</script>
</head>

<body>
<input type="button" value="Connect" onclick="WebSocket_Open();" />
<input type="button" value="Disconnect" onclick="WebSocket_Close();" />
<br/><br/>
<input type="button" style="height:80px; width:80px" value="⇧ " onclick="Robot_move('forwards');" />
<input type="button" style="height:80px; width:80px" value="⇩ " onclick="Robot_move('backwards');" />
<input type="button" style="height:80px; width:80px" value="⇦ " onclick="Robot_move('left');" />
<input type="button" style="height:80px; width:80px" value="⇨ " onclick="Robot_move('right');" />
<input type="button" style="height:80px; width:80px" value="Stop" onclick="Robot_move('stop');" />
<br/><br/>
Keyboard-control:
<input type="text" style="height:15px; width:15px" onkeydown="return verifyKey(event)">
</body>
</html>

27966

Es funktioniert alles einwandfrei (auch wenns bisher eher ein proof-of-concept ist):

Watchdog funktioniert
Latenz sehr sehr klein
Steuerung über Buttons geht
Steuerung über Pfeiltasten der Tastatur geht
Steuerung über Gamepad geht


Aber als erstes werde ich noch meinen Fahrcontroller/-regler etwas umprogrammieren, damit er schneller beschleunigt. Bei dem dauert es jetzt mit Abstand am längsten bis was passiert :D

PS: Schade, dass es hier kein Syntaxhighlighting für Python gibt..

i_make_it
17.04.2014, 08:21
1234567890