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 from __future__ import with_statement
29
30 """iRobot Roomba Serial control Interface (SCI) and Turtlebot Open Interface (OI).
31
32 turtlebot.py is a fork of PyRobot.
33
34 PyRobot was originally based on openinterface.py, developed by iRobot
35 Corporation. Many of the docstrings from openinterface.py, particularly those
36 which describe the specification, are also used here. Also, docstrings may
37 contain specification text copied directly from the Roomba SCI Spec Manual and
38 the Turtlebot Open Interface specification.
39
40 Since SCI is a subset of OI, PyRobot first defines the Roomba's functionality
41 in the Roomba class and then extends that with the Turtlebot's additional
42 functionality in the Turtlebot class. In addition, since OI is built on SCI the
43 SerialCommandInterface class is also used for OI.
44
45 """
46 __author__ = "damonkohler@gmail.com (Damon Kohler)"
47
48 import logging
49 import math
50 import serial
51 import struct
52 import time
53 import threading
54 import traceback
55 import rospy
56
57 ROOMBA_OPCODES = dict(
58 start = 128,
59 baud = 129,
60 control = 130,
61 safe = 131,
62 full = 132,
63 power = 133,
64 spot = 134,
65 clean = 135,
66 max = 136,
67 drive = 137,
68 motors = 138,
69 leds = 139,
70 song = 140,
71 play = 141,
72 sensors = 142,
73 force_seeking_dock = 143,
74 )
75
76 CREATE_OPCODES = dict(
77 soft_reset = 7,
78 low_side_drivers = 138,
79 song = 140,
80 play_song = 141,
81 pwm_low_side_drivers = 144,
82 direct_drive = 145,
83 digital_outputs = 147,
84 stream = 148,
85 query_list = 149,
86 pause_resume_stream = 150,
87 send_ir = 151,
88 script = 152,
89 play_script = 153,
90 show_script = 154,
91 wait_time = 155,
92 wait_distance = 156,
93 wait_angle = 157,
94 wait_event = 158,
95 )
96
97 REMOTE_OPCODES = {
98
99 129: 'left',
100 130: 'forward',
101 131: 'right',
102 132: 'spot',
103 133: 'max',
104 134: 'small',
105 135: 'medium',
106 136: 'large',
107 136: 'clean',
108 137: 'pause',
109 138: 'power',
110 139: 'arc-left',
111 140: 'arc-right',
112 141: 'drive-stop',
113
114 142: 'send-all',
115 143: 'seek-dock',
116
117 240: 'reserved',
118 242: 'force-field',
119 244: 'green-buoy',
120 246: 'green-buoy-and-force-field',
121 248: 'red-buoy',
122 250: 'red-buoy-and-force-field',
123 252: 'red-buoy-and-green-buoy',
124 254: 'red-buoy-and-green-buoy-and-force-field',
125 255: 'none',
126 }
127
128 BAUD_RATES = (
129 300,
130 600,
131 1200,
132 2400,
133 4800,
134 9600,
135 14400,
136 19200,
137 28800,
138 38400,
139 57600,
140 115200)
141
142 CHARGING_STATES = (
143 'not-charging',
144 'charging-recovery',
145 'charging',
146 'trickle-charging',
147 'waiting',
148 'charging-error')
149
150 OI_MODES = (
151 'off',
152 'passive',
153 'safe',
154 'full')
155
156
157 WHEEL_DROP_CASTER = 0x10
158 WHEEL_DROP_LEFT = 0x08
159 WHEEL_DROP_RIGHT = 0x04
160 BUMP_LEFT = 0x02
161 BUMP_RIGHT = 0x01
162
163 OVERCURRENTS_DRIVE_LEFT = 0x10
164 OVERCURRENTS_DRIVE_RIGHT = 0x08
165 OVERCURRENTS_MAIN_BRUSH = 0x04
166 OVERCURRENTS_VACUUM = 0x02
167 OVERCURRENTS_SIDE_BRUSH = 0x01
168
169 BUTTON_POWER = 0x08
170 BUTTON_SPOT = 0x04
171 BUTTON_CLEAN = 0x02
172 BUTTON_MAX = 0x01
173
174 SENSOR_GROUP_PACKET_LENGTHS = {
175 0: 26,
176 1: 10,
177 2: 6,
178 3: 10,
179 4: 14,
180 5: 12,
181 6: 52,
182 100: 80 }
183
184
185 RADIUS_TURN_IN_PLACE_CW = -1
186 RADIUS_TURN_IN_PLACE_CCW = 1
187 RADIUS_STRAIGHT = 32768
188 RADIUS_MAX = 2000
189
190 VELOCITY_MAX = 500
191 VELOCITY_SLOW = int(VELOCITY_MAX * 0.33)
192 VELOCITY_FAST = int(VELOCITY_MAX * 0.66)
193
194 MAX_WHEEL_SPEED = 500
195 WHEEL_SEPARATION = 260
196
197 SERIAL_TIMEOUT = 2
198 START_DELAY = 5
199
200 assert struct.calcsize('H') == 2, 'Expecting 2-byte shorts.'
201
204
206
207 """A higher-level wrapper around PySerial specifically designed for use with
208 iRobot's SCI.
209
210 """
212 self.ser = serial.Serial(tty, baudrate=baudrate, timeout=SERIAL_TIMEOUT)
213 self.ser.open()
214 self.wake()
215 self.opcodes = {}
216
217
218
219 self.lock = threading.RLock()
220
222 """wake up robot."""
223 self.ser.setRTS(0)
224 time.sleep(0.1)
225 self.ser.setRTS(1)
226 time.sleep(0.75)
227 for i in range(3):
228 self.ser.setRTS(0)
229 time.sleep(0.25)
230 self.ser.setRTS(1)
231 time.sleep(0.25)
232
234 """Add available opcodes to the SCI."""
235 self.opcodes.update(opcodes)
236
237 - def send(self, bytes):
238 """send a string of bytes to the robot."""
239 with self.lock:
240 self.ser.write(struct.pack('B' * len(bytes), *bytes))
241
242
243 - def read(self, num_bytes):
244 """Read a string of 'num_bytes' bytes from the robot."""
245 logging.debug('Attempting to read %d bytes from SCI port.' % num_bytes)
246 with self.lock:
247 data = self.ser.read(num_bytes)
248 logging.debug('Read %d bytes from SCI port.' % len(data))
249 if not data:
250 raise DriverError('Error reading from SCI port. No data.')
251 if len(data) != num_bytes:
252 raise DriverError('Error reading from SCI port. Wrong data length.')
253 return data
254
259
261 """Turtlebots methods for opcodes on the fly.
262
263 Each opcode method sends the opcode optionally followed by a string of
264 bytes.
265
266 """
267
268 if name in self.opcodes:
269 def send_opcode(*bytes):
270 logging.debug('sending opcode %s.' % name)
271 self.send([self.opcodes[name]] + list(bytes))
272 return send_opcode
273 raise AttributeError
274
275
277
278 """Represents a Roomba robot."""
279
281 self.tty = None
282 self.sci = None
283 self.safe = True
284
285 - def start(self, tty='/dev/ttyUSB0', baudrate=57600):
289
291 """Sets the baud rate in bits per second (bps) at which SCI commands and
292 data are sent according to the baud code sent in the data byte.
293
294 The default baud rate at power up is 57600 bps. (See Serial Port Settings,
295 above.) Once the baud rate is changed, it will persist until Roomba is
296 power cycled by removing the battery (or until the battery voltage falls
297 below the minimum required for processor operation). You must wait 100ms
298 after sending this command before sending additional commands at the new
299 baud rate. The SCI must be in passive, safe, or full mode to accept this
300 command. This command puts the SCI in passive mode.
301
302 """
303 if baud_rate not in BAUD_RATES:
304 raise DriverError('Invalid baud rate specified.')
305 self.sci.baud(baud_rate)
306 self.sci = SerialCommandInterface(self.tty, baud_rate)
307
309 """Put the robot in passive mode."""
310 self.sci.start()
311 time.sleep(0.5)
312
314 """Start the robot's SCI interface and place it in safe mode."""
315 self.passive()
316 if not self.safe:
317 self.sci.full()
318 else:
319 self.sci.safe()
320
321 time.sleep(0.5)
322
324
325 vl = int(velocity_left) & 0xffff
326 vr = int(velocity_right) & 0xffff
327 self.sci.direct_drive(*struct.unpack('4B', struct.pack('>2H', vr, vl)))
328
329 - def drive(self, velocity, radius):
330 """controls Roomba's drive wheels.
331
332 NOTE(damonkohler): The following specification applies to both the Roomba
333 and the Turtlebot.
334
335 The Roomba takes four data bytes, interpreted as two 16-bit signed values
336 using two's complement. The first two bytes specify the average velocity
337 of the drive wheels in millimeters per second (mm/s), with the high byte
338 being sent first. The next two bytes specify the radius in millimeters at
339 which Roomba will turn. The longer radii make Roomba drive straighter,
340 while the shorter radii make Roomba turn more. The radius is measured from
341 the center of the turning circle to the center of Roomba.
342
343 A drive command with a positive velocity and a positive radius makes
344 Roomba drive forward while turning toward the left. A negative radius
345 makes Roomba turn toward the right. Special cases for the radius make
346 Roomba turn in place or drive straight, as specified below. A negative
347 velocity makes Roomba drive backward.
348
349 Also see drive_straight and turn_in_place convenience methods.
350
351 """
352
353 velocity = int(velocity) & 0xffff
354 radius = int(radius) & 0xffff
355
356
357
358
359 bytes = struct.unpack('4B', struct.pack('>2H', velocity, radius))
360 self.sci.drive(*bytes)
361
363 """Set velocity and radius to 0 to stop movement."""
364 self.drive(0, 0)
365
367 """Slowly reduce the velocity to 0 to stop movement."""
368 velocities = xrange(velocity, VELOCITY_SLOW, -25)
369 if velocity < 0:
370 velocities = xrange(velocity, -VELOCITY_SLOW, 25)
371 for v in velocities:
372 self.drive(v, RADIUS_STRAIGHT)
373 time.sleep(0.05)
374 self.stop()
375
379
385
387 """Start looking for the dock and then dock."""
388
389
390 self.sci.start()
391 time.sleep(0.5)
392 self.sci.force_seeking_dock()
393
395
396 """Represents a Turtlebot robot."""
397
399 """
400 @param sensor_class: Sensor class to use for fetching and decoding sensor data.
401 """
402 super(Turtlebot, self).__init__()
403
404 - def start(self, tty='/dev/ttyUSB0', baudrate=57600):
407
409 """Start the robot's SCI interface and place it in safe or full mode."""
410 logging.info('sending control opcodes.')
411 self.passive()
412 if self.safe:
413 self.sci.safe()
414 else:
415 self.sci.full()
416 time.sleep(0.5)
417
419 """Enable or disable power to low side drivers.
420
421 'drivers' should be a list of booleans indicating which low side drivers
422 should be powered.
423
424 """
425 assert len(drivers) == 3, 'Expecting 3 low side driver power settings.'
426 byte = 0
427 for driver, power in enumerate(drivers):
428 byte += (2 ** driver) * int(power)
429 self.sci.low_side_drivers(byte)
430
432 """Enable or disable digital outputs."""
433 self.sci.digital_outputs(value)
434
436 """Do a soft reset of the Turtlebot."""
437 logging.info('sending soft reset.')
438 self.sci.soft_reset()
439 time.sleep(START_DELAY)
440 self.passive()
441