legoEv3/ev3dev2/control/webserver.py
import logging
import os
import re
from ev3dev2.motor import MoveJoystick, list_motors, LargeMotor
from http.server import BaseHTTPRequestHandler, HTTPServer
log = logging.getLogger(__name__)
# ==================
# Web Server classes
# ==================
class RobotWebHandler(BaseHTTPRequestHandler):
"""
Base WebHandler class for various types of robots.
RobotWebHandler's do_GET() will serve files, it is up to the child
class to handle REST APIish GETs via their do_GET()
self.robot is populated in RobotWebServer.__init__()
"""
# File extension to mimetype
mimetype = {
'css' : 'text/css',
'gif' : 'image/gif',
'html' : 'text/html',
'ico' : 'image/x-icon',
'jpg' : 'image/jpg',
'js' : 'application/javascript',
'png' : 'image/png'
}
def do_GET(self):
"""
If the request is for a known file type serve the file (or send a 404) and return True
"""
if self.path == "/":
self.path = "/index.html"
# Serve a file (image, css, html, etc)
if '.' in self.path:
extension = self.path.split('.')[-1]
mt = self.mimetype.get(extension)
if mt:
filename = os.curdir + os.sep + self.path
# Open the static file requested and send it
if os.path.exists(filename):
self.send_response(200)
self.send_header('Content-type', mt)
self.end_headers()
if extension in ('gif', 'ico', 'jpg', 'png'):
# Open in binary mode, do not encode
with open(filename, mode='rb') as fh:
self.wfile.write(fh.read())
else:
# Open as plain text and encode
with open(filename, mode='r') as fh:
self.wfile.write(fh.read().encode())
else:
log.error("404: %s not found" % self.path)
self.send_error(404, 'File Not Found: %s' % self.path)
return True
return False
def log_message(self, format, *args):
"""
log using our own handler instead of BaseHTTPServer's
"""
# log.debug(format % args)
pass
max_move_xy_seq = 0
motor_max_speed = None
medium_motor_max_speed = None
joystick_engaged = False
class TankWebHandler(RobotWebHandler):
def __str__(self):
return "%s-TankWebHandler" % self.robot
def do_GET(self):
"""
Returns True if the requested URL is supported
"""
if RobotWebHandler.do_GET(self):
return True
global motor_max_speed
global medium_motor_max_speed
global max_move_xy_seq
global joystick_engaged
if medium_motor_max_speed is None:
motor_max_speed = self.robot.left_motor.max_speed
if hasattr(self.robot, 'medium_motor'):
medium_motor_max_speed = self.robot.medium_motor.max_speed
else:
medium_motor_max_speed = 0
'''
Sometimes we get AJAX requests out of order like this:
2016-09-06 02:29:35,846 DEBUG: seq 65: (x, y): 0, 44 -> speed 462 462
2016-09-06 02:29:35,910 DEBUG: seq 66: (x, y): 0, 45 -> speed 473 473
2016-09-06 02:29:35,979 DEBUG: seq 67: (x, y): 0, 46 -> speed 483 483
2016-09-06 02:29:36,033 DEBUG: seq 69: (x, y): -1, 48 -> speed 491 504
2016-09-06 02:29:36,086 DEBUG: seq 68: (x, y): -1, 47 -> speed 480 494
2016-09-06 02:29:36,137 DEBUG: seq 70: (x, y): -1, 49 -> speed 501 515
2016-09-06 02:29:36,192 DEBUG: seq 73: (x, y): -2, 51 -> speed 509 536
2016-09-06 02:29:36,564 DEBUG: seq 74: (x, y): -3, 51 -> speed 496 536
2016-09-06 02:29:36,649 INFO: seq 75: CLIENT LOG: touchend
2016-09-06 02:29:36,701 DEBUG: seq 71: (x, y): -1, 50 -> speed 512 525
2016-09-06 02:29:36,760 DEBUG: seq 76: move stop
2016-09-06 02:29:36,814 DEBUG: seq 72: (x, y): -1, 51 -> speed 522 536
This can be bad because the last command sequentially was #76 which was "move stop"
but we RXed seq #72 after that so we started moving again and never stopped
A quick fix is to have the client send us an AJAX request to let us know
when the joystick has been engaged so that we can ignore any move-xy events
that we get out of order and show up after "move stop" but before the
next "joystick-engaged"
We can also ignore any move-xy requests that show up late by tracking the
max seq for any move-xy we service.
'''
path = self.path.split('/')
seq = int(path[1])
action = path[2]
# desktop interface
if action == 'move-start':
direction = path[3]
speed_percentage = path[4]
log.debug("seq %d: move %s" % (seq, direction))
left_speed = int(int(speed_percentage) * motor_max_speed)/100.0
right_speed = int(int(speed_percentage) * motor_max_speed)/100.0
if direction == 'forward':
self.robot.left_motor.run_forever(speed_sp=left_speed)
self.robot.right_motor.run_forever(speed_sp=right_speed)
elif direction == 'backward':
self.robot.left_motor.run_forever(speed_sp=left_speed * -1)
self.robot.right_motor.run_forever(speed_sp=right_speed * -1)
elif direction == 'left':
self.robot.left_motor.run_forever(speed_sp=left_speed * -1)
self.robot.right_motor.run_forever(speed_sp=right_speed)
elif direction == 'right':
self.robot.left_motor.run_forever(speed_sp=left_speed)
self.robot.right_motor.run_forever(speed_sp=right_speed * -1)
# desktop & mobile interface
elif action == 'move-stop':
log.debug("seq %d: move stop" % seq)
self.robot.left_motor.stop()
self.robot.right_motor.stop()
joystick_engaged = False
# medium motor
elif action == 'motor-stop':
motor = path[3]
log.debug("seq %d: motor-stop %s" % (seq, motor))
if motor == 'medium':
if hasattr(self.robot, 'medium_motor'):
self.robot.medium_motor.stop()
else:
raise Exception("motor %s not supported yet" % motor)
elif action == 'motor-start':
motor = path[3]
direction = path[4]
speed_percentage = path[5]
log.debug("seq %d: start motor %s, direction %s, speed_percentage %s" % (seq, motor, direction, speed_percentage))
if motor == 'medium':
if hasattr(self.robot, 'medium_motor'):
if direction == 'clockwise':
medium_speed = int(int(speed_percentage) * medium_motor_max_speed)/100.0
self.robot.medium_motor.run_forever(speed_sp=medium_speed)
elif direction == 'counter-clockwise':
medium_speed = int(int(speed_percentage) * medium_motor_max_speed)/100.0
self.robot.medium_motor.run_forever(speed_sp=medium_speed * -1)
else:
log.info("we do not have a medium_motor")
else:
raise Exception("motor %s not supported yet" % motor)
# mobile interface
elif action == 'move-xy':
x = int(path[3])
y = int(path[4])
if joystick_engaged:
if seq > max_move_xy_seq:
self.robot.on(x, y, motor_max_speed)
max_move_xy_seq = seq
log.debug("seq %d: (x, y) (%4d, %4d)" % (seq, x, y))
else:
log.debug("seq %d: (x, y) %4d, %4d (ignore, max seq %d)" %
(seq, x, y, max_move_xy_seq))
else:
log.debug("seq %d: (x, y) %4d, %4d (ignore, joystick idle)" %
(seq, x, y))
elif action == 'joystick-engaged':
joystick_engaged = True
elif action == 'log':
msg = ''.join(path[3:])
re_msg = re.search('^(.*)\?', msg)
if re_msg:
msg = re_msg.group(1)
log.debug("seq %d: CLIENT LOG: %s" % (seq, msg))
else:
log.warning("Unsupported URL %s" % self.path)
# It is good practice to send this but if we are getting move-xy we
# tend to get a lot of them and we need to be as fast as possible so
# be bad and don't send a reply. This takes ~20ms.
if action != 'move-xy':
self.send_response(204)
return True
class RobotWebServer(object):
"""
A Web server so that 'robot' can be controlled via 'handler_class'
"""
def __init__(self, robot, handler_class, port_number=8000):
self.content_server = None
self.handler_class = handler_class
self.handler_class.robot = robot
self.port_number = port_number
def run(self):
try:
log.info("Started HTTP server (content) on port %d" % self.port_number)
self.content_server = HTTPServer(('', self.port_number), self.handler_class)
self.content_server.serve_forever()
# Exit cleanly, stop both web servers and all motors
except (KeyboardInterrupt, Exception) as e:
log.exception(e)
if self.content_server:
self.content_server.socket.close()
self.content_server = None
for motor in list_motors():
motor.stop()
class WebControlledTank(MoveJoystick):
"""
A tank that is controlled via a web browser
"""
def __init__(self, left_motor, right_motor, port_number=8000, desc=None, motor_class=LargeMotor):
MoveJoystick.__init__(self, left_motor, right_motor, desc, motor_class)
self.www = RobotWebServer(self, TankWebHandler, port_number)
def main(self):
# start the web server
self.www.run()