2525if sys .version_info < (3 ,4 ):
2626 raise SystemError ('Must be using Python 3.4 or higher' )
2727
28+ import math
2829import select
2930import time
31+ import _thread
3032
3133# python3 uses collections
3234# micropython uses ucollections
3638 from ucollections import OrderedDict
3739
3840from logging import getLogger
39- from math import atan2 , degrees as math_degrees , sqrt , pi
4041from os .path import abspath
4142from 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
21862361class 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
0 commit comments