an introduction to programming robotic vacuums
by Eric Wood
eric@ericwood.org
@eric_b_wood
[velocity (2 bytes)][radius (2 bytes)]
Goal: drive in reverse at 200 mm/s while turning at a radius of 500mm
Velocity = -200 = 0xFF38 = [0xFF][0x38] Radius = -500 = 0x01F4 = [0x01][0xF4]
Bytes sent over serial:
[137][255][56][1][244]
require 'serialport'
# port is very OS/driver dependent...
# Typically, on *nix you're want this:
port = '/dev/ttyusbserial'
# baud:
# 115200 for Roomba 5xx
# 57600 for older (and iRobot Create)
baud = 115200
@serial = SerialPort.new(port, baud)
@serial.write('hello!')
Introducing: Array.pack
"Packs the contents of arr into a binary sequence according to the directives in aTemplateString"
What we want: "C"
8-bit unsigned integer (unsigned char)
>> [128].pack('C')
# => "\x80"
"C" is the directive for:
8-bit unsigned integer (unsigned char)
# Converts input data (an array) into bytes before
# sending it over the serial connection.
def write_chars(data)
data.map! do |c|
if c.class == String
result = c.bytes.to_a.map { |b| [b].pack("C") }
else
result = [c].pack("C")
end
result
end
data = data.flatten.join
@serial.write(data)
@serial.flush
end
# Convert integer to two's complement signed 16 bit integer
def convert_int(int)
[int].pack('s').reverse
end
def drive(velocity, radius)
raise RangeError if velocity < -500 || velocity > 500
raise RangeError if (radius < -2000 || radius > 2000) && radius != 0xFFFF
velocity = convert_int(velocity)
radius = convert_int(radius)
write_chars([DRIVE, velocity, radius])
end
Example:
get the distance travelled (19) and bumper (7)
[149][2][19][7]
# Get sensors by list
# Array entry can be packet ID or symbol
def get_sensors_list(sensors)
# convert from symbols to IDs
sensors.map! { |l|
l.class == Symbol ? SENSOR_SYMBOLS.find_index(l) : l
}
# request sensor data!
request = [Constants::QUERY_LIST, sensors.length] + sensors
write_chars(request)
raw_data = ""
sensors.each do |id|
raw_data << @serial.read(SENSOR_PACKET_SIZE[id])
end
sensor_bytes_to_packets(raw_data, sensors)
end
# Human readable packet names
# truncated for presentation, TOO MANY TO LIST!
SENSOR_SYMBOLS = [:ignore, :bumps_and_wheel_drops,:wall,:cliff_left,:cliff_front_left,
:cliff_front_right,:cliff_right,:virtual_wall,:wheel_overcurrents]
SENSOR_PACKET_SIZE = [0, 0, 0, 1, 1, 1, 1] # pretend this has everything
SENSOR_PACKET_SIGNEDNESS = [:na, :na, :na, :signed, :unsigned] #...
# map to appropriate classes for conversion...
SENSOR_PACKET_VALUE = {
wall: Boolean,
cliff_left: Boolean,
cliff_front_left: Boolean,
charging_state: ChargingState,
oi_mode: OIMode,
charging_sources_available: ChargingSourceAvailable,
light_bumper: LightBumper,
wheel_overcurrents: WheelOvercurrents,
bumps_and_wheel_drops: BumpsAndWheelDrops,
infrared_character_omni: InfraredCharacter,
infrared_character_left: InfraredCharacter,
infrared_character_right: InfraredCharacter
}
def sensor_bytes_to_packets(bytes, packets)
# template string for unpacking the data
pack = ''
packets.each do |packet|
size = SENSOR_PACKET_SIZE[packet]
signedness = SENSOR_PACKET_SIGNEDNESS[packet]
case size
when 1 # 8 bit (big endian)
case signedness
when :signed
pack << 'c'
when :unsigned
pack << 'C'
end
when 2 # 16 bit (big endian)
case signedness
when :signed
pack << 's>'
when :unsigned
pack << 'S>'
end
end
end
data = bytes.unpack(pack)
# CONTINUED ON NEXT SLIDE!
end
results = {}
packets.each_with_index do |packet,index|
packet_name = SENSOR_SYMBOLS[packet]
unless packet_name == :ignore
value = data[index]
# map to native Ruby type
converter = SENSOR_PACKET_VALUE[packet_name]
value = converter.convert(value) if converter
results[packet_name] = value
end
end
results
class BumpsAndWheelDrops
def self.convert(v)
h = {}
h[:bump_right] = v & 0b0001 > 0
h[:bump_left] = v & 0b0010 > 0
h[:wheel_drop_right] = v & 0b0100 > 0
h[:wheel_drop_left] = v & 0b1000 > 0
h
end
end
It'd be cool if we could make it look something like this:
require 'rumba'
Roomba.new('/dev/tty.usbserial') do
safe_mode
forward 1.meter
rotate :left
rotate -90 # degrees
rotate :right
rotate 90
backward 1.meter
# access to any methods in the Roomba class here!
end
def initialize(port, baud=57600, &block)
# Snip...
# initialize the "DSL" here!
if block_given?
instance_eval(&block)
# clean up after ourselves (this is a Roomba, after all!)
self.power_off
end
end
# move both wheels at the same speed in a certain direction!
# NOTE THAT THIS BLOCKS UNTIL COMPLETE
def straight_distance(distance, speed: DEFAULT_SPEED)
total = 0
straight(speed)
loop do
total += get_sensor(:distance).abs
break if total >= distance
end
halt
end
# distance is in mm!
def forward(distance, speed: DEFAULT_SPEED)
straight_distance(distance, speed: speed)
end
# distance is in mm!
def backward(distance, speed: DEFAULT_SPEED)
straight_distance(distance, speed: -speed)
end
It wouldn't be Ruby unless we monkeypatched built-ins!
# MEASUREMENT HELPERS
class Fixnum
def inches
25.4 * self
end
alias_method :inch, :inches
def feet
self.inches * 12
end
alias_method :foot, :feet
def meters
self * 1000
end
alias_method :meter, :meters
end