1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283 | #!/usr/bin/env python3
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)
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(r'^(.*)\?', 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()
|