Skip to content

Commit 73b7b71

Browse files
authored
MoveDifferential odometry support (#615)
* MoveDifferential odometry support, tracks robot's (x,y) position
1 parent e41fca4 commit 73b7b71

File tree

3 files changed

+226
-9
lines changed

3 files changed

+226
-9
lines changed

debian/changelog

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
python-ev3dev2 (2.0.0~beta4) UNRELEASED; urgency=medium
22

33
[Daniel Walton]
4+
* MoveDifferential odometry support, tracks robot's (x,y) position
45
* Display support via raspberry pi HDMI
56
* Update tests/README to include sphinx-build instructions
67
* LED animation support, brickpi3 LED corrected from BLUE to AMBER

ev3dev2/motor.py

Lines changed: 183 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525
if sys.version_info < (3,4):
2626
raise SystemError('Must be using Python 3.4 or higher')
2727

28+
import math
2829
import select
2930
import time
31+
import _thread
3032

3133
# python3 uses collections
3234
# micropython uses ucollections
@@ -36,7 +38,6 @@
3638
from ucollections import OrderedDict
3739

3840
from logging import getLogger
39-
from math import atan2, degrees as math_degrees, sqrt, pi
4041
from os.path import abspath
4142
from ev3dev2 import get_current_platform, Device, list_device_names
4243

@@ -2013,6 +2014,9 @@ class MoveDifferential(MoveTank):
20132014
- drive in an arc (clockwise or counter clockwise) of a specified radius
20142015
for a specified distance
20152016
2017+
Odometry can be use to enable driving to specific coordinates and
2018+
rotating to a specific angle.
2019+
20162020
New arguments:
20172021
20182022
wheel_class - Typically a child class of :class:`ev3dev2.wheel.Wheel`. This is used to
@@ -2056,6 +2060,21 @@ class MoveDifferential(MoveTank):
20562060
# Drive in arc to the right along an imaginary circle of radius 150 mm.
20572061
# Drive for 700 mm around this imaginary circle.
20582062
mdiff.on_arc_right(SpeedRPM(80), 150, 700)
2063+
2064+
# Enable odometry
2065+
mdiff.odometry_start()
2066+
2067+
# Use odometry to drive to specific coordinates
2068+
mdiff.on_to_coordinates(SpeedRPM(40), 300, 300)
2069+
2070+
# Use odometry to go back to where we started
2071+
mdiff.on_to_coordinates(SpeedRPM(40), 0, 0)
2072+
2073+
# Use odometry to rotate in place to 90 degrees
2074+
mdiff.turn_to_angle(SpeedRPM(40), 90)
2075+
2076+
# Disable odometry
2077+
mdiff.odometry_stop()
20592078
"""
20602079

20612080
def __init__(self, left_motor_port, right_motor_port,
@@ -2067,10 +2086,17 @@ def __init__(self, left_motor_port, right_motor_port,
20672086
self.wheel_distance_mm = wheel_distance_mm
20682087

20692088
# The circumference of the circle made if this robot were to rotate in place
2070-
self.circumference_mm = self.wheel_distance_mm * pi
2089+
self.circumference_mm = self.wheel_distance_mm * math.pi
20712090

20722091
self.min_circle_radius_mm = self.wheel_distance_mm / 2
20732092

2093+
# odometry variables
2094+
self.x_pos_mm = 0.0 # robot X position in mm
2095+
self.y_pos_mm = 0.0 # robot Y position in mm
2096+
self.odometry_thread_run = False
2097+
self.odometry_thread_id = None
2098+
self.theta = 0.0
2099+
20742100
def on_for_distance(self, speed, distance_mm, brake=True, block=True):
20752101
"""
20762102
Drive distance_mm
@@ -2091,9 +2117,9 @@ def _on_arc(self, speed, radius_mm, distance_mm, brake, block, arc_right):
20912117

20922118
# The circle formed at the halfway point between the two wheels is the
20932119
# circle that must have a radius of radius_mm
2094-
circle_outer_mm = 2 * pi * (radius_mm + (self.wheel_distance_mm / 2))
2095-
circle_middle_mm = 2 * pi * radius_mm
2096-
circle_inner_mm = 2 * pi * (radius_mm - (self.wheel_distance_mm / 2))
2120+
circle_outer_mm = 2 * math.pi * (radius_mm + (self.wheel_distance_mm / 2))
2121+
circle_middle_mm = 2 * math.pi * radius_mm
2122+
circle_inner_mm = 2 * math.pi * (radius_mm - (self.wheel_distance_mm / 2))
20972123

20982124
if arc_right:
20992125
# The left wheel is making the larger circle and will move at 'speed'
@@ -2159,7 +2185,8 @@ def _turn(self, speed, degrees, brake=True, block=True):
21592185
# The number of rotations to move distance_mm
21602186
rotations = distance_mm/self.wheel.circumference_mm
21612187

2162-
log.debug("%s: turn() degrees %s, distance_mm %s, rotations %s, degrees %s" % (self, degrees, distance_mm, rotations, degrees))
2188+
log.debug("%s: turn() degrees %s, distance_mm %s, rotations %s, degrees %s" %
2189+
(self, degrees, distance_mm, rotations, degrees))
21632190

21642191
# If degrees is positive rotate clockwise
21652192
if degrees > 0:
@@ -2182,6 +2209,154 @@ def turn_left(self, speed, degrees, brake=True, block=True):
21822209
"""
21832210
self._turn(speed, abs(degrees) * -1, brake, block)
21842211

2212+
def odometry_coordinates_log(self):
2213+
log.debug("%s: odometry angle %s at (%d, %d)" %
2214+
(self, math.degrees(self.theta), self.x_pos_mm, self.y_pos_mm))
2215+
2216+
def odometry_start(self, theta_degrees_start=90.0,
2217+
x_pos_start=0.0, y_pos_start=0.0,
2218+
SLEEP_TIME=0.005): # 5ms
2219+
"""
2220+
Ported from:
2221+
http://seattlerobotics.org/encoder/200610/Article3/IMU%20Odometry,%20by%20David%20Anderson.htm
2222+
2223+
A thread is started that will run until the user calls odometry_stop()
2224+
which will set odometry_thread_run to False
2225+
"""
2226+
2227+
def _odometry_monitor():
2228+
left_previous = 0
2229+
right_previous = 0
2230+
self.theta = math.radians(theta_degrees_start) # robot heading
2231+
self.x_pos_mm = x_pos_start # robot X position in mm
2232+
self.y_pos_mm = y_pos_start # robot Y position in mm
2233+
TWO_PI = 2 * math.pi
2234+
2235+
while self.odometry_thread_run:
2236+
2237+
# sample the left and right encoder counts as close together
2238+
# in time as possible
2239+
left_current = self.left_motor.position
2240+
right_current = self.right_motor.position
2241+
2242+
# determine how many ticks since our last sampling
2243+
left_ticks = left_current - left_previous
2244+
right_ticks = right_current - right_previous
2245+
2246+
# Have we moved?
2247+
if not left_ticks and not right_ticks:
2248+
if SLEEP_TIME:
2249+
time.sleep(SLEEP_TIME)
2250+
continue
2251+
2252+
# log.debug("%s: left_ticks %s (from %s to %s)" %
2253+
# (self, left_ticks, left_previous, left_current))
2254+
# log.debug("%s: right_ticks %s (from %s to %s)" %
2255+
# (self, right_ticks, right_previous, right_current))
2256+
2257+
# update _previous for next time
2258+
left_previous = left_current
2259+
right_previous = right_current
2260+
2261+
# rotations = distance_mm/self.wheel.circumference_mm
2262+
left_rotations = float(left_ticks / self.left_motor.count_per_rot)
2263+
right_rotations = float(right_ticks / self.right_motor.count_per_rot)
2264+
2265+
# convert longs to floats and ticks to mm
2266+
left_mm = float(left_rotations * self.wheel.circumference_mm)
2267+
right_mm = float(right_rotations * self.wheel.circumference_mm)
2268+
2269+
# calculate distance we have traveled since last sampling
2270+
mm = (left_mm + right_mm) / 2.0
2271+
2272+
# accumulate total rotation around our center
2273+
self.theta += (right_mm - left_mm) / self.wheel_distance_mm
2274+
2275+
# and clip the rotation to plus or minus 360 degrees
2276+
self.theta -= float(int(self.theta/TWO_PI) * TWO_PI)
2277+
2278+
# now calculate and accumulate our position in mm
2279+
self.x_pos_mm += mm * math.cos(self.theta)
2280+
self.y_pos_mm += mm * math.sin(self.theta)
2281+
2282+
if SLEEP_TIME:
2283+
time.sleep(SLEEP_TIME)
2284+
2285+
self.odometry_thread_id = None
2286+
2287+
self.odometry_thread_run = True
2288+
self.odometry_thread_id = _thread.start_new_thread(_odometry_monitor, ())
2289+
2290+
def odometry_stop(self):
2291+
"""
2292+
Signal the odometry thread to exit and wait for it to exit
2293+
"""
2294+
2295+
if self.odometry_thread_id:
2296+
self.odometry_thread_run = False
2297+
2298+
while self.odometry_thread_id:
2299+
pass
2300+
2301+
def turn_to_angle(self, speed, angle_target_degrees, brake=True, block=True):
2302+
"""
2303+
Rotate in place to `angle_target_degrees` at `speed`
2304+
"""
2305+
assert self.odometry_thread_id, "odometry_start() must be called to track robot coordinates"
2306+
2307+
# Make both target and current angles positive numbers between 0 and 360
2308+
if angle_target_degrees < 0:
2309+
angle_target_degrees += 360
2310+
2311+
angle_current_degrees = math.degrees(self.theta)
2312+
2313+
if angle_current_degrees < 0:
2314+
angle_current_degrees += 360
2315+
2316+
# Is it shorter to rotate to the right or left
2317+
# to reach angle_target_degrees?
2318+
if angle_current_degrees > angle_target_degrees:
2319+
turn_right = True
2320+
angle_delta = angle_current_degrees - angle_target_degrees
2321+
else:
2322+
turn_right = False
2323+
angle_delta = angle_target_degrees - angle_current_degrees
2324+
2325+
if angle_delta > 180:
2326+
angle_delta = 360 - angle_delta
2327+
turn_right = not turn_right
2328+
2329+
log.debug("%s: turn_to_angle %s, current angle %s, delta %s, turn_right %s" %
2330+
(self, angle_target_degrees, angle_current_degrees, angle_delta, turn_right))
2331+
self.odometry_coordinates_log()
2332+
2333+
if turn_right:
2334+
self.turn_right(speed, angle_delta, brake, block)
2335+
else:
2336+
self.turn_left(speed, angle_delta, brake, block)
2337+
2338+
self.odometry_coordinates_log()
2339+
2340+
def on_to_coordinates(self, speed, x_target_mm, y_target_mm, brake=True, block=True):
2341+
"""
2342+
Drive to (`x_target_mm`, `y_target_mm`) coordinates at `speed`
2343+
"""
2344+
assert self.odometry_thread_id, "odometry_start() must be called to track robot coordinates"
2345+
2346+
# stop moving
2347+
self.off(brake='hold')
2348+
2349+
# rotate in place so we are pointed straight at our target
2350+
x_delta = x_target_mm - self.x_pos_mm
2351+
y_delta = y_target_mm - self.y_pos_mm
2352+
angle_target_radians = math.atan2(y_delta, x_delta)
2353+
angle_target_degrees = math.degrees(angle_target_radians)
2354+
self.turn_to_angle(speed, angle_target_degrees, brake=True, block=True)
2355+
2356+
# drive in a straight line to the target coordinates
2357+
distance_mm = math.sqrt(pow(self.x_pos_mm - x_target_mm, 2) + pow(self.y_pos_mm - y_target_mm, 2))
2358+
self.on_for_distance(speed, distance_mm, brake, block)
2359+
21852360

21862361
class MoveJoystick(MoveTank):
21872362
"""
@@ -2213,8 +2388,8 @@ def on(self, x, y, radius=100.0):
22132388
self.off()
22142389
return
22152390

2216-
vector_length = sqrt(x*x + y*y)
2217-
angle = math_degrees(atan2(y, x))
2391+
vector_length = math.sqrt((x * x) + (y * y))
2392+
angle = math.degrees(math.atan2(y, x))
22182393

22192394
if angle < 0:
22202395
angle += 360

utils/move_differential.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from ev3dev2.motor import OUTPUT_A, OUTPUT_B, MoveDifferential, SpeedRPM
88
from ev3dev2.wheel import EV3Tire
9+
from ev3dev2.unit import DistanceFeet
910
from math import pi
1011
import logging
1112
import sys
@@ -22,7 +23,7 @@
2223
ONE_FOOT_CICLE_CIRCUMFERENCE_MM = 2 * pi * ONE_FOOT_CICLE_RADIUS_MM
2324

2425
# Testing with RileyRover
25-
# http://www.damienkee.com/home/2013/8/2/rileyrover-ev3-classroom-robot-design.html
26+
# http://www.damienkee.com/rileyrover-ev3-classroom-robot-design/
2627
#
2728
# The centers of the wheels are 16 studs apart but this is not the
2829
# "effective" wheel seperation. Test drives of circles with
@@ -43,3 +44,43 @@
4344
# Test turning in place
4445
#mdiff.turn_right(SpeedRPM(40), 180)
4546
#mdiff.turn_left(SpeedRPM(40), 180)
47+
48+
# Test odometry
49+
#mdiff.odometry_start()
50+
#mdiff.odometry_coordinates_log()
51+
52+
#mdiff.turn_to_angle(SpeedRPM(40), 0)
53+
#mdiff.on_for_distance(SpeedRPM(40), DistanceFeet(2).mm)
54+
#mdiff.turn_right(SpeedRPM(40), 180)
55+
#mdiff.turn_left(SpeedRPM(30), 90)
56+
#mdiff.on_arc_left(SpeedRPM(80), ONE_FOOT_CICLE_RADIUS_MM, ONE_FOOT_CICLE_CIRCUMFERENCE_MM)
57+
58+
# Drive in a quarter arc to the right then go back to where you started
59+
#log.info("turn on arc to the right")
60+
#mdiff.on_arc_right(SpeedRPM(40), ONE_FOOT_CICLE_RADIUS_MM, ONE_FOOT_CICLE_CIRCUMFERENCE_MM / 4)
61+
#mdiff.odometry_coordinates_log()
62+
#log.info("\n\n\n\n")
63+
#log.info("go back to (0, 0)")
64+
#mdiff.odometry_coordinates_log()
65+
#mdiff.on_to_coordinates(SpeedRPM(40), 0, 0)
66+
#mdiff.turn_to_angle(SpeedRPM(40), 90)
67+
68+
# Drive in a rectangle
69+
#mdiff.turn_to_angle(SpeedRPM(40), 120)
70+
#mdiff.on_to_coordinates(SpeedRPM(40), 0, DistanceFeet(1).mm)
71+
#mdiff.on_to_coordinates(SpeedRPM(40), DistanceFeet(2).mm, DistanceFeet(1).mm)
72+
#mdiff.on_to_coordinates(SpeedRPM(40), DistanceFeet(2).mm, 0)
73+
#mdiff.on_to_coordinates(SpeedRPM(40), 0, 0)
74+
#mdiff.turn_to_angle(SpeedRPM(40), 90)
75+
76+
77+
# Use odometry to drive to specific coordinates
78+
#mdiff.on_to_coordinates(SpeedRPM(40), 600, 300)
79+
80+
# Now go back to where we started and rotate in place to 90 degrees
81+
#mdiff.on_to_coordinates(SpeedRPM(40), 0, 0)
82+
#mdiff.turn_to_angle(SpeedRPM(40), 90)
83+
84+
85+
#mdiff.odometry_coordinates_log()
86+
#mdiff.odometry_stop()

0 commit comments

Comments
 (0)