From 558cf909ff79341e699cf872082231f83f4cdc50 Mon Sep 17 00:00:00 2001 From: Will Roscoe Date: Wed, 8 Feb 2017 21:45:07 -0800 Subject: [PATCH 1/2] multicar support & web pilot switching --- donkey/pilots.py | 196 +++++++++++- donkey/remotes.py | 267 +++++++++++------ donkey/templates/base.html | 4 +- donkey/templates/pilots_list.html | 23 ++ .../{session_view.html => session.html} | 0 donkey/templates/static/main.js | 282 +++++++++--------- .../templates/{monitor.html => vehicle.html} | 27 +- ...{choose_vehicle.html => vehicle_list.html} | 2 +- donkey/vehicles.py | 2 +- 9 files changed, 572 insertions(+), 231 deletions(-) create mode 100644 donkey/templates/pilots_list.html rename donkey/templates/{session_view.html => session.html} (100%) rename donkey/templates/{monitor.html => vehicle.html} (71%) rename donkey/templates/{choose_vehicle.html => vehicle_list.html} (90%) diff --git a/donkey/pilots.py b/donkey/pilots.py index 5657c3db7..9058b137e 100644 --- a/donkey/pilots.py +++ b/donkey/pilots.py @@ -7,9 +7,14 @@ ''' import os -import numpy as np - +import math import random +from operator import itemgetter +from datetime import datetime + +import numpy as np +import cv2 +import keras from donkey import utils @@ -18,7 +23,10 @@ class BasePilot(): Base class to define common functions. When creating a class, only override the funtions you'd like to replace. ''' - + def __init__(self, name=None, last_modified=None): + self.name = name + self.last_modified = last_modified + def decide(self, img_arr): angle = 0 speed = 0 @@ -28,13 +36,19 @@ def decide(self, img_arr): return angle, speed + def load(self): + pass + + + class SwervePilot(BasePilot): ''' Example predictor that should not be used. ''' - def __init__(self): + def __init__(self, **kwargs): self.angle= random.randrange(-45, 46) self.throttle = 20 + super().__init__(**kwargs) def decide(self, img_arr): @@ -45,11 +59,16 @@ def decide(self, img_arr): return self, angle, self.throttle -class KerasAngle(): - def __init__(self, model, throttle): - self.model = model + + +class KerasAngle(BasePilot): + def __init__(self, model_path, throttle=25, **kwargs): + self.model_path = model_path + self.model = None #load() loads the model self.throttle = throttle self.last_angle = 0 + super().__init__(**kwargs) + def decide(self, img_arr): img_arr = img_arr.reshape((1,) + img_arr.shape) @@ -63,3 +82,166 @@ def decide(self, img_arr): return angle, self.throttle + def load(self): + + self.model = keras.models.load_model(self.model_path) + return self + + + + + +class OpenCVLineDetector(BasePilot): + + def __init__(self, M=None, blur_pixels=5, canny_threshold1=100, canny_threshold2=130, + rho=2, theta=.02, min_line_length=80, max_gap=20, hough_threshold=9, **kwargs): + + + self.blur_pixels = blur_pixels + self.canny_threshold1 = canny_threshold1 + self.canny_threshold2 = canny_threshold2 + self.hough_threshold = hough_threshold + self.min_line_length = min_line_length + self.max_gap = max_gap + self.rho = rho + self.theta = theta + if M is not None: + self.M = M + else: + self.M = self.get_M() + + super().__init__(**kwargs) + + + def decide(self, img_arr): + lines = self.get_lines(img_arr, + self.M, + self.blur_pixels, + self.canny_threshold1, + self.canny_threshold2, + self.hough_threshold, + self.min_line_length, + self.max_gap, + self.rho, + self.theta, + ) + #print(lines) + if lines is not None: + line_data = self.compute_lines(lines) + clustered = self.cluster_angles(line_data) + #print('clustered: ', clustered) + angle = self.decide_angle(clustered) + else: + angle = 0 + return angle + + + + def get_M(self): + M = np.array([[ 2.43902439e+00, 6.30081301e+00, -6.15853659e+01], + [ -4.30211422e-15, 1.61246610e+01, -6.61644977e+01], + [ -1.45283091e-17, 4.06504065e-02, 1.00000000e+00]]) + return M + + @staticmethod + def get_lines(img, M, blur_pixels, canny_threshold1, canny_threshold2, + hough_threshold, min_line_length, max_gap, rho, theta ): + + img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) + img_blur = cv2.blur(img_gray,(blur_pixels,blur_pixels)) + img_canny = cv2.Canny(img_blur, canny_threshold1, canny_threshold2) + lines = cv2.HoughLinesP(img_canny, rho, theta, hough_threshold, min_line_length, max_gap) + + if lines is not None: + lines = lines.reshape((lines.shape[0],2,2)) + lines = lines.astype(float) + lines = cv2.perspectiveTransform(lines, M) + return lines + + + @classmethod + def line_length(cls, arr): + l = math.sqrt( (arr[0,0] - arr[1,0])**2 + (arr[0,1] - arr[1,1])**2 ) + return l + + + @classmethod + def line_angle(cls, arr): + dx = arr[1,0] - arr[0,0] + dy = arr[1,1] - arr[0,1] + rads = math.atan2(-dy,dx) + rads %= 2*math.pi + degs = -math.degrees(rads) + if degs <= -180: + degs = degs + 180 + + degs = degs + 90 + return degs + + + @classmethod + def compute_lines(cls, lines): + + line_data = [] + for line in lines: + line_data.append([cls.line_angle(line), cls.line_length(line)]) + + sorted(line_data, key=itemgetter(0)) + return line_data + + @staticmethod + def cluster_angles(line_data): + clusters = [] + last_angle = -180 + for a, l in line_data: + if abs(last_angle - a) > 20: + clusters.append([(a,l)]) + else: + clusters[-1].append((a,l)) + return clusters + + + @classmethod + def decide_angle(cls, clustered_angles): + max_length = 0 + max_cluster_id = -1 + for i, c in enumerate(clustered_angles): + #sum lenght of lines found in clusters, filter out angles > 80 (likely in horizon) + cluster_length = sum([l for a, l in c if abs(a) < 80]) + #print('cluster length', cluster_length) + if cluster_length > max_length: + max_length = cluster_length + max_cluster_id = i + + if max_cluster_id>-1: + angles = [a for a, l in clustered_angles[max_cluster_id]] + #return average angle of cluster + return sum(angles)/len(angles) + #print(angles) + else: + return 0 + + +class PilotHandler(): + """ Convinience class to load default pilots """ + def __init__(self, models_path): + + self.models_path = os.path.expanduser(models_path) + + + def pilots_from_models(self): + """ Load pilots from keras models saved in the models directory. """ + models_list = [f for f in os.scandir(self.models_path)] + pilot_list = [] + for d in models_list: + last_modified = datetime.fromtimestamp(d.stat().st_mtime) + pilot = KerasAngle(d.path, throttle=25, name=d.name, + last_modified=last_modified) + pilot_list.append(pilot) + return pilot_list + + def default_pilots(self): + """ Load pilots from models and add CV pilots """ + pilot_list = self.pilots_from_models() + pilot_list.append(OpenCVLineDetector(name='OpenCV')) + return pilot_list \ No newline at end of file diff --git a/donkey/remotes.py b/donkey/remotes.py index e19961181..e54428f20 100644 --- a/donkey/remotes.py +++ b/donkey/remotes.py @@ -27,7 +27,7 @@ class RemoteClient(): def __init__(self, remote_url, vehicle_id='mycar'): - self.record_url = remote_url + '/vehicle/' + vehicle_id + '/' + self.record_url = remote_url + '/api/vehicles/control/' + vehicle_id + '/' def decide(self, img_arr, angle, throttle, milliseconds): @@ -43,9 +43,20 @@ def decide(self, img_arr, angle, throttle, milliseconds): } - r = requests.post(self.record_url, - files={'img': dk.utils.arr_to_binary(img_arr), - 'json': json.dumps(data)}) #hack to put json in file + r = None + + while r == None: + + try: + r = requests.post(self.record_url, + files={'img': dk.utils.arr_to_binary(img_arr), + 'json': json.dumps(data)}) #hack to put json in file + except (requests.ConnectionError) as err: + print("Vehicle could not connect to server. Make sure you've " + + "started your server and you're referencing the right port.") + time.sleep(3) + + data = json.loads(r.text) angle = int(float(data['angle'])) @@ -70,31 +81,50 @@ def __init__(self, data_path='~/donkey_data/'): self.sessions_path = os.path.join(self.data_path, 'sessions') self.models_path = os.path.join(self.data_path, 'models') + ph = dk.pilots.PilotHandler(self.models_path) + self.pilots = ph.default_pilots() + handlers = [ #temporary redirect until vehicles is not a singleton - (r"/", HomeHandler), + (r"/", HomeView), + + (r"/vehicles/", VehicleListView), - (r"/drive/", ChooseVehicleHandler), + (r"/vehicles/?(?P[A-Za-z0-9-]+)?/", + VehicleView), - (r"/drive/?(?P[A-Za-z0-9-]+)?/", DriveHandler), - (r"/drive/?(?P[A-Za-z0-9-]+)?/mjpeg/?(?P[^/]*)?", - CameraMJPEGHandler + (r"/api/vehicles/?(?P[A-Za-z0-9-]+)?/", + VehicleAPI), + + + (r"/api/vehicles/drive/?(?P[A-Za-z0-9-]+)?/", + DriveAPI), + + (r"/api/vehicles/video/?(?P[A-Za-z0-9-]+)?", + VideoAPI ), - (r"/vehicle/?(?P[A-Za-z0-9-]+)?/", VehicleHandler), + (r"/api/vehicles/control/?(?P[A-Za-z0-9-]+)?/", + ControlAPI), - (r"/sessions/", SessionListHandler), - (r"/sessions/?(?P[^/]+)?/", SessionViewHandler), + (r"/sessions/", SessionListView), + + (r"/sessions/?(?P[^/]+)?/", + SessionView), (r"/session_image/?(?P[^/]+)?/?(?P[^/]+)?", - SessionImageHandler + SessionImageView ), + (r"/pilots/", PilotListView), + + + (r"/static/(.*)", tornado.web.StaticFileHandler, {"path": self.static_file_path}), ] @@ -112,8 +142,10 @@ def start(self, port=8887): def get_vehicle(self, vehicle_id): if vehicle_id not in self.vehicles: + print('new vehicle') sh = dk.sessions.SessionHandler(self.sessions_path) self.vehicles[vehicle_id] = dict({ + 'id': vehicle_id, 'user_angle': 0, 'user_throttle': 0, 'drive_mode':'user', @@ -122,10 +154,18 @@ def get_vehicle(self, vehicle_id): 'pilot': dk.pilots.BasePilot(), 'session': sh.new()}) + #eprint(self.vehicles) return self.vehicles[vehicle_id] -class HomeHandler(tornado.web.RequestHandler): +##################### +# # +# vehicles # +# # +##################### + + +class HomeView(tornado.web.RequestHandler): def get(self): ''' Serves home page. @@ -133,101 +173,62 @@ def get(self): self.render("templates/home.html") -class ChooseVehicleHandler(tornado.web.RequestHandler): +class VehicleListView(tornado.web.RequestHandler): def get(self): ''' Serves home page. ''' data = {'vehicles':self.application.vehicles} - self.render("templates/choose_vehicle.html", **data) - - -class SessionImageHandler(tornado.web.RequestHandler): - def get(self, session_id, img_name): - - print('SessionImageHandler') - - sessions_path = self.application.sessions_path - path = os.path.join(sessions_path, session_id, img_name) - f = Image.open(path) - o = io.BytesIO() - f.save(o, format="JPEG") - s = o.getvalue() - self.set_header('Content-type', 'image/jpg') - self.set_header('Content-length', len(s)) - - print('writing image') - self.write(s) + self.render("templates/vehicle_list.html", **data) - -class SessionListHandler(tornado.web.RequestHandler): - - def get(self): +class VehicleView(tornado.web.RequestHandler): + def get(self, vehicle_id): ''' - Receive post requests from vehicle with camera image. - Return the angle and throttle the car should be goin. - ''' + Serves page for users to control the vehicle. + ''' - session_dirs = [f for f in os.scandir(self.application.sessions_path) if f.is_dir() ] - data = {'session_dirs': session_dirs} - self.render("templates/session_list.html", **data) + V = self.application.get_vehicle(vehicle_id) + pilots = self.application.pilots + data = {'vehicle': V, 'pilots': pilots} + print(data) + self.render("templates/vehicle.html", **data) -class SessionViewHandler(tornado.web.RequestHandler): +class VehicleAPI(tornado.web.RequestHandler): - def get(self, session_id): + def post(self, vehicle_id): + ''' + Currently this only changes the pilot. ''' - Receive post requests from vehicle with camera image. - Return the angle and throttle the car should be goin. - ''' - from operator import itemgetter - - - sessions_path = self.application.sessions_path - path = os.path.join(sessions_path, session_id) - imgs = [dk.utils.merge_two_dicts({'name':f.name}, dk.sessions.parse_img_filepath(f.path)) for f in os.scandir(path) if f.is_file() ] - sorted_imgs = sorted(imgs, key=itemgetter('name')) - session = {'name':session_id, 'imgs': sorted_imgs[:100]} - data = {'session': session} - self.render("templates/session_view.html", **data) + V = self.application.get_vehicle(vehicle_id) - def post(self, session_id): data = tornado.escape.json_decode(self.request.body) + print(data) + pilot = next(filter(lambda p: p.name == data['pilot'], self.application.pilots)) + V['pilot'] = pilot.load() - if data['action'] == 'delete_images': - sessions_path = self.application.sessions_path - path = os.path.join(sessions_path, session_id) - for i in data['imgs']: - os.remove(os.path.join(path, i)) - print('%s removed' %i) - - - -class DriveHandler(tornado.web.RequestHandler): - def get(self, vehicle_id): - ''' - Serves page for users to control the vehicle. - ''' - V = self.application.get_vehicle(vehicle_id) - self.render("templates/monitor.html") +class DriveAPI(tornado.web.RequestHandler): def post(self, vehicle_id): ''' Receive post requests as user changes the angle and throttle of the vehicle on a the index webpage ''' + + V = self.application.get_vehicle(vehicle_id) + data = tornado.escape.json_decode(self.request.body) angle = data['angle'] throttle = data['throttle'] - V = self.application.get_vehicle(vehicle_id) + #Set recording mode if data['recording'] == 'true': @@ -252,14 +253,16 @@ def post(self, vehicle_id): print(V) - -class VehicleHandler(tornado.web.RequestHandler): +class ControlAPI(tornado.web.RequestHandler): def post(self, vehicle_id): ''' Receive post requests from vehicle with camera image. Return the angle and throttle the car should be goin. ''' + + V = self.application.get_vehicle(vehicle_id) + img = self.request.files['img'][0]['body'] img = Image.open(io.BytesIO(img)) img_arr = dk.utils.img_to_arr(img) @@ -267,10 +270,9 @@ def post(self, vehicle_id): #Hack to take json from a file #data = json.loads(self.request.files['json'][0]['body'].decode("utf-8") ) - V = self.application.vehicles[vehicle_id] #Get angle/throttle from pilot loaded by the server. - pilot_angle, pilot_throttle = V['pilot'].decide(img) + pilot_angle, pilot_throttle = V['pilot'].decide(img_arr) V['img'] = img V['pilot_angle'] = pilot_angle @@ -293,17 +295,18 @@ def post(self, vehicle_id): angle, throttle = V['pilot_angle'], V['pilot_throttle'] print('%s: A: %s T:%s' %(V['drive_mode'], angle, throttle)) + #retun angel/throttle values to vehicle with json response self.write(json.dumps({'angle': str(angle), 'throttle': str(throttle)})) -class CameraMJPEGHandler(tornado.web.RequestHandler): +class VideoAPI(tornado.web.RequestHandler): @tornado.web.asynchronous @tornado.gen.coroutine - def get(self, vehicle_id, file): + def get(self, vehicle_id): ''' Show a video of the pictures captured by the vehicle. ''' @@ -330,3 +333,101 @@ def get(self, vehicle_id, file): else: yield tornado.gen.Task(ioloop.add_timeout, ioloop.time() + interval) + +##################### +# # +# pilots # +# # +##################### + + +class PilotListView(tornado.web.RequestHandler): + def get(self): + ''' + Render a list of pilots. + ''' + ph = dk.pilots.PilotHandler(self.application.models_path) + pilots = ph.default_pilots() + data = {'pilots': pilots} + self.render("templates/pilots_list.html", **data) + + + + +##################### +# # +# sessions # +# # +##################### + + + +class SessionImageView(tornado.web.RequestHandler): + def get(self, session_id, img_name): + + print('SessionImageHandler') + + sessions_path = self.application.sessions_path + path = os.path.join(sessions_path, session_id, img_name) + f = Image.open(path) + o = io.BytesIO() + f.save(o, format="JPEG") + s = o.getvalue() + self.set_header('Content-type', 'image/jpg') + self.set_header('Content-length', len(s)) + + print('writing image') + self.write(s) + + + +class SessionListView(tornado.web.RequestHandler): + + def get(self): + ''' + Receive post requests from vehicle with camera image. + Return the angle and throttle the car should be goin. + ''' + + session_dirs = [f for f in os.scandir(self.application.sessions_path) if f.is_dir() ] + data = {'session_dirs': session_dirs} + self.render("templates/session_list.html", **data) + + + +class SessionView(tornado.web.RequestHandler): + + def get(self, session_id): + ''' + Receive post requests from vehicle with camera image. + Return the angle and throttle the car should be goin. + ''' + from operator import itemgetter + + + sessions_path = self.application.sessions_path + path = os.path.join(sessions_path, session_id) + imgs = [dk.utils.merge_two_dicts({'name':f.name}, dk.sessions.parse_img_filepath(f.path)) for f in os.scandir(path) if f.is_file() ] + + sorted_imgs = sorted(imgs, key=itemgetter('name')) + session = {'name':session_id, 'imgs': sorted_imgs[:100]} + data = {'session': session} + self.render("templates/session_view.html", **data) + + def post(self, session_id): + data = tornado.escape.json_decode(self.request.body) + + if data['action'] == 'delete_images': + sessions_path = self.application.sessions_path + path = os.path.join(sessions_path, session_id) + + for i in data['imgs']: + os.remove(os.path.join(path, i)) + print('%s removed' %i) + + + + + + + diff --git a/donkey/templates/base.html b/donkey/templates/base.html index 04936bdc0..69f91675e 100644 --- a/donkey/templates/base.html +++ b/donkey/templates/base.html @@ -45,8 +45,8 @@ diff --git a/donkey/templates/pilots_list.html b/donkey/templates/pilots_list.html new file mode 100644 index 000000000..cf24d24d4 --- /dev/null +++ b/donkey/templates/pilots_list.html @@ -0,0 +1,23 @@ + +{% extends "base.html" %} +{% block content %} +
+

Pilots

+ + + +
+{% end %} diff --git a/donkey/templates/session_view.html b/donkey/templates/session.html similarity index 100% rename from donkey/templates/session_view.html rename to donkey/templates/session.html diff --git a/donkey/templates/static/main.js b/donkey/templates/static/main.js index ec9d86364..1e489fcf5 100644 --- a/donkey/templates/static/main.js +++ b/donkey/templates/static/main.js @@ -1,180 +1,192 @@ -$( document ).ready(function() { - console.log( "document ready!" ); - velocity.bind() - - var joystick_options = { - zone: document.getElementById('joystick_container'), // active zone - color: '#668AED', - size: 350, - }; - - var manager = nipplejs.create(joystick_options); - - bindNipple(manager); - -}); - - -//Defaults -var postLoopRunning=false; -var sendURL = "" - - -function sendControl(angle, throttle, drive_mode, recording) { - //Send post request to server. - data = JSON.stringify({ 'angle': angle, - 'throttle':throttle, - 'drive_mode':drive_mode, - 'recording':recording}) - $.post(sendURL, data) -} - - -// Send control updates to the server every .1 seconds. -function postLoop () { - setTimeout(function () { - sendControl($('#angleInput').val(), - $('#throttleInput').val(), - 'user', - 'true') - - if (postLoopRunning) { - postLoop(); - } else { - // Send zero angle, throttle and stop recording - sendControl(0, 0, 'user', 'false') - } - }, 100) -} - - -function bindNipple(manager) { - manager.on('start end', function(evt, data) { - $('#angleInput').val(0); - $('#throttleInput').val(0); - - if (!postLoopRunning) { - postLoopRunning=true; - postLoop(); - } else { - postLoopRunning=false; - } - }).on('move', function(evt, data) { - angle = data['angle']['radian'] - distance = data['distance'] +var driveHandler = (function() { + //functions used to drive the vehicle. - $('#angleInput').val(Math.round(distance * Math.cos(angle)/2)); - $('#throttleInput').val(Math.round(distance * Math.sin(angle)/2)); - }); -} + var angle = 0 + var throttle = 0 + var driveMode = 'user' + var recording = false + var pilot = 'None' + var angleEl = "#angleInput" + var throttleEl = "#throttleInput" -$(document).keydown(function(e) { - if(e.which == 73) { - // 'i' throttle up - velocity.throttleUp() - } + var joystickLoopRunning=false; + var vehicle_id = "" + var driveURL = "" + var vehicleURL = "" - if(e.which == 75) { - // 'k' slow down - velocity.throttleDown() - } + var load = function() { - if(e.which == 74) { - // 'j' turn left - velocity.angleLeft() - } + vehicle_id = $('#vehicle_id').data('id') + driveURL = '/api/vehicles/drive/' + vehicle_id + "/" + vehicleURL = '/api/vehicles/' + vehicle_id + "/" - if(e.which == 76) { - // 'l' turn right - velocity.angleRight() - } + bindKeys() + bindPilotSelect() - if(e.which == 65) { - // 'a' turn on auto mode - velocity.updateDriveMode('auto') - } - if(e.which == 68) { - // 'a' turn on auto mode - velocity.updateDriveMode('user') - } - if(e.which == 83) { - // 'a' turn on auto mode - velocity.updateDriveMode('auto_angle') + var joystick_options = { + zone: document.getElementById('joystick_container'), // active zone + color: '#668AED', + size: 350, + }; + + var manager = nipplejs.create(joystick_options); + + bindNipple(manager) + }; + + + + var bindKeys = function() { + //Bind a function to capture the coordinates of the click. + $(angleEl).change(function(e) { + postDrive() + }); + $(throttleEl).change(function(e) { + postDrive() + }); + + $(document).keydown(function(e) { + if(e.which == 73) { + // 'i' throttle up + throttleUp() + } + + if(e.which == 75) { + // 'k' slow down + throttleDown() + } + + if(e.which == 74) { + // 'j' turn left + angleLeft() + } + + if(e.which == 76) { + // 'l' turn right + angleRight() + } + + if(e.which == 65) { + // 'a' turn on auto mode + updateDriveMode('auto') + } + if(e.which == 68) { + // 'a' turn on auto mode + updateDriveMode('user') + } + if(e.which == 83) { + // 'a' turn on auto mode + updateDriveMode('auto_angle') + } + }); + }; + + + function bindNipple(manager) { + manager.on('start end', function(evt, data) { + $('#angleInput').val(0); + $('#throttleInput').val(0); + + if (!joystickLoopRunning) { + joystickLoopRunning=true; + joystickLoop(); + } else { + joystickLoopRunning=false; + } + + }).on('move', function(evt, data) { + radian = data['angle']['radian'] + distance = data['distance'] + + angle = Math.round(distance * Math.cos(radian)/2) + throttle = Math.round(distance * Math.sin(radian)/2) + recording = true + + }); } -}); -var velocity = (function() { - //functions to change velocity of vehicle + var bindPilotSelect = function(){ + $('#pilot_select').on('change', function () { + pilot = $(this).val(); // get selected value + postPilot() + }); + }; - var angle = 0 - var throttle = 0 - var driveMode = 'user' - var angleEl = "#angleInput" - var throttleEl = "#throttleInput" - var sendURL = "" - var bind = function(data){ - //Bind a function to capture the coordinates of the click. - $(angleEl).change(function(e) { - sendVelocity() - }); - $(throttleEl).change(function(e) { - sendVelocity() - }); + var postPilot = function(){ + data = JSON.stringify({ 'pilot': pilot }) + console.log(data) + $.post(vehicleURL, data) + } + + + var updateDisplay = function() { + $(throttleEl).val(throttle); + $(angleEl).val(angle); + $('#driveMode').val(driveMode); }; - var sendVelocity = function() { + var postDrive = function() { //Send angle and throttle values - data = JSON.stringify({ 'angle': angle, 'throttle':throttle, 'drive_mode':driveMode, 'recording':'false'}) - $.post(sendURL, data) + updateDisplay() + data = JSON.stringify({ 'angle': angle, 'throttle':throttle, + 'drive_mode':driveMode, 'recording': recording}) + console.log(data) + $.post(driveURL, data) }; + + // Send control updates to the server every .1 seconds. + function joystickLoop () { + setTimeout(function () { + postDrive() + + if (joystickLoopRunning) { + joystickLoop(); + } else { + // Send zero angle, throttle and stop recording + angle = 0 + throttle = 0 + recording = 0 + postDrive() + } + }, 100) + } + var throttleUp = function(){ //Bind a function to capture the coordinates of the click. throttle = Math.min(throttle + 5, 400); - $(throttleEl).val(throttle) - sendVelocity() + postDrive() }; var throttleDown = function(){ //Bind a function to capture the coordinates of the click. throttle = Math.max(throttle - 10, -200); - $(throttleEl).val(throttle); - sendVelocity() + postDrive() }; var angleLeft = function(){ //Bind a function to capture the coordinates of the click. angle = Math.max(angle - 10, -90) - $(angleEl).val(angle); - sendVelocity() + postDrive() }; var angleRight = function(){ //Bind a function to capture the coordinates of the click. angle = Math.min(angle + 10, 90) - $(angleEl).val(angle); - sendVelocity() + postDrive() }; var updateDriveMode = function(mode){ //Bind a function to capture the coordinates of the click. driveMode = mode; - $('#driveMode').val(mode); - sendVelocity() + postDrive() }; - return { - bind: bind, - throttleUp: throttleUp, - throttleDown: throttleDown, - angleLeft: angleLeft, - angleRight: angleRight, - sendVelocity: sendVelocity, - updateDriveMode: updateDriveMode - }; + + return { load: load }; })(); diff --git a/donkey/templates/monitor.html b/donkey/templates/vehicle.html similarity index 71% rename from donkey/templates/monitor.html rename to donkey/templates/vehicle.html index 1366dfb30..e06597e62 100644 --- a/donkey/templates/monitor.html +++ b/donkey/templates/vehicle.html @@ -2,7 +2,7 @@ {% block content %}
-

Donkey Control

+

Vehicle: {{vehicle['id']}}

Use this website to drive your vehicle. Data will only be recorded when you are actively steering.
@@ -10,11 +10,19 @@

Donkey Control

Image Stream (MJPEG)

- +

+ + Pilot: +
@@ -53,5 +61,20 @@

Donkey Control

Click/touch to use joystic.
+
+ +
+
+ + + {% end %} \ No newline at end of file diff --git a/donkey/templates/choose_vehicle.html b/donkey/templates/vehicle_list.html similarity index 90% rename from donkey/templates/choose_vehicle.html rename to donkey/templates/vehicle_list.html index 52b563a04..0addcf4e8 100644 --- a/donkey/templates/choose_vehicle.html +++ b/donkey/templates/vehicle_list.html @@ -7,7 +7,7 @@

Vehicles