-
Notifications
You must be signed in to change notification settings - Fork 8
/
neatopylot_client.py
394 lines (286 loc) · 11.3 KB
/
neatopylot_client.py
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
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
#!/usr/bin/env python
'''
neatopylot_client.py - client code for Neato XV-11 Autopylot
Copyright (C) 2013 Suraj Bajracharya and Simon D. Levy
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
Revision history:
24-JAN-2013 Suraj Bajracharya Initial version as SLAMgui.py
28-JAN-2013 Simon D. Levy Timer-based update
29-JAN-2013 SDL Fixed degrees->radians conversion
04-FEB-2013 SDL True LIDAR display
12-FEB-2013 SDL Steers XV-11 via joystick
22-FEB-2013 SDL Uses arrow keys when joystick not available
09-SEP-2014 SDL Migrated to github
'''
# Params =======================================================================
# Update period in milliseconds (less than 200 may cause problems!)
UPDATE_MSEC = 200
# Index of base axis of controller
FIRST_AXIS = 2
# Index of autopilot button
AUTOPILOT_BUTTON = 9
# Maximum LIDAR distance in mm
MAX_LIDAR_DIST_MM = 2500
# Motor parameters
DIST = 10000 # Arbitrarily large distance to simulate infinity
SPEED = 200
# Frame title
BASE_TITLE = "Neatopylot"
DISPLAY_SIZE = 500 # square
BACKGROUND_COLOR = "black"
FOREGROUND_COLOR = "green"
# ==============================================================================
import socket
import sys
import struct
import time
from math import *
import breezypythongui
# Import agent code
from neatopylot_agent import *
# Import constants common to client and server
from neatopylot_header import *
# Import pygame: Comment-out if you have problems!
import pygame
#pygame = None
# XXX popup
def error(dlg, msg):
dlg.messageBox("Error", msg)
sys.exit(1)
def message(dlg, msg):
dlg.messageBox("Alert", msg)
class Neato_Client:
# Constructor opens socket to server
def __init__(self, host, port):
# Connect to host over port
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((host, port))
# Track drive commands for update
self.lastx, self.lasty = 0,0
# Get LIDAR scan.
# Returns scan as a list of (angle, distance, intensity) tuples
def getScan(self):
# Request a LIDAR scan from the server
self._send_message('scan')
# Grab the current scan buffer from the server
scandata = ''
while True:
buf = self.sock.recv(MAX_SCANDATA_BYTES)
scandata += buf.decode('utf-8')
if len(buf) < MAX_SCANDATA_BYTES:
break
# Parse the scan into a list of tuples
scanvals = []
for line in scandata.split('\n'):
try:
vals = line.split(',')
# Only use legitimate scan tuples with zero error
if len(vals) == 4 and not int(vals[3]):
angle = int(vals[0])
distance = int(vals[1])
intensity = int(vals[2])
scanvals.append((angle, distance, intensity))
except:
None
return scanvals
# Drives in specified direction
def drive(self, x, y):
# Status changed
if (x,y) != (self.lastx,self.lasty):
# Special handling for halt
if x == 0 and y == 0:
self._setMotors(1, 1, 1)
# Convert (x,y) joystick to motor scaling factors
# XXX Maybe we can simplify this
if y == 0:
if x > 0:
lft,rgt = 1,0
elif x < 0:
lft,rgt = 0,1
else:
lft,rgt = 0,0
else:
if x > 0:
lft,rgt = 1.0,0.5
elif x < 0:
lft,rgt = 0.5,1.0
else:
lft,rgt = 1.0,1.0
lft *= y
rgt *= y
self._setMotors(int(lft*DIST), int(rgt*DIST), SPEED)
# Track previous joystick status for update
self.lastx, self.lasty = x, y
# Done
def close(self):
self.sock.close()
# Send robot server a mesage to set motor distances and speed
def _setMotors(self, leftDist, rightDist, speed):
message = 'm ' + str(leftDist) + ' ' + str(rightDist) + ' ' + str(speed)
self._send_message(message)
# Send robot server a message padded to MESSAGE_SIZE_BYTES
def _send_message(self, msg):
while len(msg) < MESSAGE_SIZE_BYTES:
msg += ' '
self.sock.send(bytes(msg.encode('utf-8')))
# GUI ==========================================================================
class LIDAR_GUI(breezypythongui.EasyFrame):
def __init__(self, host, port, client, controller, agent):
# Set up the window
breezypythongui.EasyFrame.__init__(self, title = BASE_TITLE)
# Report missing client
if not client:
error(self, 'Unable to connect to server on host ' +
host + ' over port ' + str(port) +
'. Make sure server is running and try again.')
# Canvas
self.canvas = self.addCanvas(row = 0, column = 0,
columnspan = 1,
width = DISPLAY_SIZE,
height = DISPLAY_SIZE,
background = BACKGROUND_COLOR)
self.setResizable(False)
# Holds the vectors after they're drawn
self.items = list()
# Track drive commands for update
self.lastx, self.lasty = 0,0
# Store stuff for timer task
self.client = client
self.controller = controller
self.lastpress_sec = 0
self.keydown = False
self.autopilot = False
self.agent = agent
# Set up window-close handler
self.bind('<Destroy>', self.quit)
def quit(self, event):
self.client.close()
# Deadband filter for noisy controller
def deadbandFilter(value):
return 0 if abs(value) < .01 else value
# Timer task for gui update
def task(gui):
# Assume no axis input
axis_x, axis_y = 0,0
# Use controller if available
if gui.controller:
# Force joystick polling
pygame.event.pump()
# Pressing autopilot button turns on autopilot; other buttons turn it off
for k in range(gui.controller.get_numbuttons()):
if gui.controller.get_button(k):
if k == AUTOPILOT_BUTTON:
gui.autopilot = True
#else:
# gui.autopilot = False
# Grab joystick axis values (forward comes in negative)
axis_x = deadbandFilter(gui.controller.get_axis(FIRST_AXIS))
axis_y = deadbandFilter(-gui.controller.get_axis(FIRST_AXIS+1))
# Axes disable autopilot
if axis_x or axis_y:
gui.autopilot = False
# If no controller, use timing to check key-release
else:
lag = 1000 * (time.time() - gui.lastpress_sec)
if lag < UPDATE_MSEC:
if gui.keydown:
axis_x, axis_y = gui.axis_x, gui.axis_y
gui.keydown = True
else:
gui.keydown = False
# Request a LIDAR scan from the server
scan = gui.client.getScan()
# Clear the canvas
for item in gui.items:
gui.canvas.deleteItem(item)
gui.items = list()
# Locate the center of the screen (also the scaling factor)
ctr = DISPLAY_SIZE / 2
# Read the distance values and draw them as line segments
for scanline in scan:
angle = scanline[0]
dist = scanline[1]
angle = angle * pi / 180 # degrees to radians
dist = dist / float(MAX_LIDAR_DIST_MM) # millimeters to (0,1)
scale = ctr
x = ctr - (scale * dist * sin(angle))
y = ctr - (scale * dist * cos(angle))
gui.items.append(gui.canvas.drawLine(ctr, ctr, x, y,
fill=FOREGROUND_COLOR))
# We will update the title based on whether the autpilot is on
title = BASE_TITLE
if gui.autopilot:
title += ' AUTOPILOT ON'
# Get action from agent
axis_x, axis_y = gui.agent.getAxes(scan, gui.canvas)
# Update the title to report autopilot as needed
gui.setTitle(title)
# Use axis values to drive robot
gui.client.drive(axis_x, axis_y)
# Reschedule event
gui.after(UPDATE_MSEC, task, gui)
# Key-press handler for GUI frame
def keypress(event):
# Strip literal quotes from key symbol
keysym = repr(event.keysym)[1:-1]
# Assume no arrow keys pressed
event.widget.axis_x, event.widget.axis_y = 0,0
# Check all arrow keys
if keysym == 'Right':
event.widget.axis_x = +1
elif keysym == 'Left':
event.widget.axis_x = -1
if keysym == 'Up':
event.widget.axis_y = +1
elif keysym == 'Down':
event.widget.axis_y = -1
# Spacebar toggles autopilot
elif keysym == 'space':
event.widget.autopilot = not event.widget.autopilot
# If any arrow key was pressed, axes have been set
if event.widget.axis_x or event.widget.axis_y:
# Record arrow-key press time for fake release
event.widget.lastpress_sec = time.time()
# Axis control disables autopilot
event.widget.autopilot = False
# main =========================================================================
# Create client on host, port specified on command line
try:
client = Neato_Client(HOST, PORT)
except:
client = None
# Assume no controller
controller = None
# Set up PyGame and the controller if available
if pygame:
pygame.joystick.init()
pygame.display.init()
try:
controller = pygame.joystick.Joystick(0)
controller.init()
try:
controller.get_axis(FIRST_AXIS)
except:
controller = None
except:
pass
# Initialize the autopilot agent
agent = Neatopylot_Agent()
# Create gui with host, port, controller
gui = LIDAR_GUI(HOST, PORT, client, controller, agent)
# Start timer-task
gui.after(UPDATE_MSEC, task, gui)
# Use arrow keys if no controller
if not controller:
message(gui, 'No joystick available: Use arrow keys and spacebar')
gui.bind('<Key>', keypress)
gui.focus_set()
# Handle GUI events till done
gui.mainloop()