4040from logging import getLogger
4141from os .path import abspath
4242from ev3dev2 import get_current_platform , Device , list_device_names
43+ from ev3dev2 .stopwatch import StopWatch
4344
4445log = getLogger (__name__ )
4546
@@ -83,9 +84,24 @@ class SpeedValue(object):
8384 :class:`SpeedDPS`, and :class:`SpeedDPM`.
8485 """
8586
87+ def __eq__ (self , other ):
88+ return self .to_native_units () == other .to_native_units ()
89+
90+ def __ne__ (self , other ):
91+ return not self .__eq__ (other )
92+
8693 def __lt__ (self , other ):
8794 return self .to_native_units () < other .to_native_units ()
8895
96+ def __le__ (self , other ):
97+ return self .to_native_units () <= other .to_native_units ()
98+
99+ def __gt__ (self , other ):
100+ return self .to_native_units () > other .to_native_units ()
101+
102+ def __ge__ (self , other ):
103+ return self .to_native_units () >= other .to_native_units ()
104+
89105 def __rmul__ (self , other ):
90106 return self .__mul__ (other )
91107
@@ -124,13 +140,13 @@ def __init__(self, native_counts):
124140 self .native_counts = native_counts
125141
126142 def __str__ (self ):
127- return str (self .native_counts ) + " counts/sec"
143+ return "{:.2f}" . format (self .native_counts ) + " counts/sec"
128144
129145 def __mul__ (self , other ):
130146 assert isinstance (other , (float , int )), "{} can only be multiplied by an int or float" .format (self )
131147 return SpeedNativeUnits (self .native_counts * other )
132148
133- def to_native_units (self , motor ):
149+ def to_native_units (self , motor = None ):
134150 """
135151 Return this SpeedNativeUnits as a number
136152 """
@@ -1774,6 +1790,46 @@ def _block(self):
17741790 self .wait_until_not_moving ()
17751791
17761792
1793+ # line follower classes
1794+ class LineFollowErrorLostLine (Exception ):
1795+ """
1796+ Raised when a line following robot has lost the line
1797+ """
1798+ pass
1799+
1800+
1801+ class LineFollowErrorTooFast (Exception ):
1802+ """
1803+ Raised when a line following robot has been asked to follow
1804+ a line at an unrealistic speed
1805+ """
1806+ pass
1807+
1808+
1809+ # line follower functions
1810+ def follow_for_forever (tank ):
1811+ """
1812+ ``tank``: the MoveTank object that is following a line
1813+ """
1814+ return True
1815+
1816+
1817+ def follow_for_ms (tank , ms ):
1818+ """
1819+ ``tank``: the MoveTank object that is following a line
1820+ ``ms`` : the number of milliseconds to follow the line
1821+ """
1822+ if not hasattr (tank , 'stopwatch' ) or tank .stopwatch is None :
1823+ tank .stopwatch = StopWatch ()
1824+ tank .stopwatch .start ()
1825+
1826+ if tank .stopwatch .value_ms >= ms :
1827+ tank .stopwatch = None
1828+ return False
1829+ else :
1830+ return True
1831+
1832+
17771833class MoveTank (MotorSet ):
17781834 """
17791835 Controls a pair of motors simultaneously, via individual speed setpoints for each motor.
@@ -1798,6 +1854,9 @@ def __init__(self, left_motor_port, right_motor_port, desc=None, motor_class=Lar
17981854 self .right_motor = self .motors [right_motor_port ]
17991855 self .max_speed = self .left_motor .max_speed
18001856
1857+ # color sensor used by follow_line()
1858+ self .cs = None
1859+
18011860 def _unpack_speeds_to_native_units (self , left_speed , right_speed ):
18021861 left_speed = self .left_motor ._speed_native_units (left_speed , "left_speed" )
18031862 right_speed = self .right_motor ._speed_native_units (right_speed , "right_speed" )
@@ -1921,6 +1980,125 @@ def on(self, left_speed, right_speed):
19211980 self .left_motor .run_forever ()
19221981 self .right_motor .run_forever ()
19231982
1983+ def follow_line (self ,
1984+ kp , ki , kd ,
1985+ speed ,
1986+ target_light_intensity = None ,
1987+ follow_left_edge = True ,
1988+ white = 60 ,
1989+ off_line_count_max = 20 ,
1990+ sleep_time = 0.01 ,
1991+ follow_for = follow_for_forever ,
1992+ ** kwargs
1993+ ):
1994+ """
1995+ PID line follower
1996+
1997+ ``kp``, ``ki``, and ``kd`` are the PID constants.
1998+
1999+ ``speed`` is the desired speed of the midpoint of the robot
2000+
2001+ ``target_light_intensity`` is the reflected light intensity when the color sensor
2002+ is on the edge of the line. If this is None we assume that the color sensor
2003+ is on the edge of the line and will take a reading to set this variable.
2004+
2005+ ``follow_left_edge`` determines if we follow the left or right edge of the line
2006+
2007+ ``white`` is the reflected_light_intensity that is used to determine if we have
2008+ lost the line
2009+
2010+ ``off_line_count_max`` is how many consecutive times through the loop the
2011+ reflected_light_intensity must be greater than ``white`` before we
2012+ declare the line lost and raise an exception
2013+
2014+ ``sleep_time`` is how many seconds we sleep on each pass through
2015+ the loop. This is to give the robot a chance to react
2016+ to the new motor settings. This should be something small such
2017+ as 0.01 (10ms).
2018+
2019+ ``follow_for`` is called to determine if we should keep following the
2020+ line or stop. This function will be passed ``self`` (the current
2021+ ``MoveTank`` object). Current supported options are:
2022+ - ``follow_for_forever``
2023+ - ``follow_for_ms``
2024+
2025+ ``**kwargs`` will be passed to the ``follow_for`` function
2026+
2027+ Example:
2028+
2029+ .. code:: python
2030+
2031+ from ev3dev2.motor import OUTPUT_A, OUTPUT_B, MoveTank, SpeedPercent, follow_for_ms
2032+ from ev3dev2.sensor.lego import ColorSensor
2033+
2034+ tank = MoveTank(OUTPUT_A, OUTPUT_B)
2035+ tank.cs = ColorSensor()
2036+
2037+ try:
2038+ # Follow the line for 4500ms
2039+ tank.follow_line(
2040+ kp=11.3, ki=0.05, kd=3.2,
2041+ speed=SpeedPercent(30),
2042+ follow_for=follow_for_ms,
2043+ ms=4500
2044+ )
2045+ except Exception:
2046+ tank.stop()
2047+ raise
2048+ """
2049+ assert self .cs , "ColorSensor must be defined"
2050+
2051+ if target_light_intensity is None :
2052+ target_light_intensity = self .cs .reflected_light_intensity
2053+
2054+ integral = 0.0
2055+ last_error = 0.0
2056+ derivative = 0.0
2057+ off_line_count = 0
2058+ speed_native_units = speed .to_native_units (self .left_motor )
2059+ MAX_SPEED = SpeedNativeUnits (self .max_speed )
2060+
2061+ while follow_for (self , ** kwargs ):
2062+ reflected_light_intensity = self .cs .reflected_light_intensity
2063+ error = target_light_intensity - reflected_light_intensity
2064+ integral = integral + error
2065+ derivative = error - last_error
2066+ last_error = error
2067+ turn_native_units = (kp * error ) + (ki * integral ) + (kd * derivative )
2068+
2069+ if not follow_left_edge :
2070+ turn_native_units *= - 1
2071+
2072+ left_speed = SpeedNativeUnits (speed_native_units - turn_native_units )
2073+ right_speed = SpeedNativeUnits (speed_native_units + turn_native_units )
2074+
2075+ if left_speed > MAX_SPEED :
2076+ log .info ("%s: left_speed %s is greater than MAX_SPEED %s" % (self , left_speed , MAX_SPEED ))
2077+ self .stop ()
2078+ raise LineFollowErrorTooFast ("The robot is moving too fast to follow the line" )
2079+
2080+ if right_speed > MAX_SPEED :
2081+ log .info ("%s: right_speed %s is greater than MAX_SPEED %s" % (self , right_speed , MAX_SPEED ))
2082+ self .stop ()
2083+ raise LineFollowErrorTooFast ("The robot is moving too fast to follow the line" )
2084+
2085+ # Have we lost the line?
2086+ if reflected_light_intensity >= white :
2087+ off_line_count += 1
2088+
2089+ if off_line_count >= off_line_count_max :
2090+ self .stop ()
2091+ raise LineFollowErrorLostLine ("we lost the line" )
2092+ else :
2093+ off_line_count = 0
2094+
2095+ if sleep_time :
2096+ time .sleep (sleep_time )
2097+
2098+ self .on (left_speed , right_speed )
2099+
2100+ self .stop ()
2101+
19242102
19252103class MoveSteering (MoveTank ):
19262104 """
@@ -2224,7 +2402,7 @@ def odometry_coordinates_log(self):
22242402
22252403 def odometry_start (self , theta_degrees_start = 90.0 ,
22262404 x_pos_start = 0.0 , y_pos_start = 0.0 ,
2227- SLEEP_TIME = 0.005 ): # 5ms
2405+ sleep_time = 0.005 ): # 5ms
22282406 """
22292407 Ported from:
22302408 http://seattlerobotics.org/encoder/200610/Article3/IMU%20Odometry,%20by%20David%20Anderson.htm
@@ -2254,8 +2432,8 @@ def _odometry_monitor():
22542432
22552433 # Have we moved?
22562434 if not left_ticks and not right_ticks :
2257- if SLEEP_TIME :
2258- time .sleep (SLEEP_TIME )
2435+ if sleep_time :
2436+ time .sleep (sleep_time )
22592437 continue
22602438
22612439 # log.debug("%s: left_ticks %s (from %s to %s)" %
@@ -2288,8 +2466,8 @@ def _odometry_monitor():
22882466 self .x_pos_mm += mm * math .cos (self .theta )
22892467 self .y_pos_mm += mm * math .sin (self .theta )
22902468
2291- if SLEEP_TIME :
2292- time .sleep (SLEEP_TIME )
2469+ if sleep_time :
2470+ time .sleep (sleep_time )
22932471
22942472 self .odometry_thread_id = None
22952473
0 commit comments