@@ -1790,6 +1790,15 @@ def _block(self):
17901790 self .wait_until_not_moving ()
17911791
17921792
1793+ # follow gyro angle classes
1794+ class FollowGyroAngleErrorTooFast (Exception ):
1795+ """
1796+ Raised when a gyro following robot has been asked to follow
1797+ an angle at an unrealistic speed
1798+ """
1799+ pass
1800+
1801+
17931802# line follower classes
17941803class LineFollowErrorLostLine (Exception ):
17951804 """
@@ -1854,8 +1863,23 @@ def __init__(self, left_motor_port, right_motor_port, desc=None, motor_class=Lar
18541863 self .right_motor = self .motors [right_motor_port ]
18551864 self .max_speed = self .left_motor .max_speed
18561865
1857- # color sensor used by follow_line()
1858- self .cs = None
1866+ # color sensor used by follow_line()
1867+ @property
1868+ def cs (self ):
1869+ return self ._cs
1870+
1871+ @cs .setter
1872+ def cs (self , cs ):
1873+ self ._cs = cs
1874+
1875+ # gyro sensor used by follow_gyro_angle()
1876+ @property
1877+ def gyro (self ):
1878+ return self ._gyro
1879+
1880+ @gyro .setter
1881+ def gyro (self , gyro ):
1882+ self ._gyro = gyro
18591883
18601884 def _unpack_speeds_to_native_units (self , left_speed , right_speed ):
18611885 left_speed = self .left_motor ._speed_native_units (left_speed , "left_speed" )
@@ -2046,10 +2070,10 @@ def follow_line(self,
20462070 tank.stop()
20472071 raise
20482072 """
2049- assert self .cs , "ColorSensor must be defined"
2073+ assert self ._cs , "ColorSensor must be defined"
20502074
20512075 if target_light_intensity is None :
2052- target_light_intensity = self .cs .reflected_light_intensity
2076+ target_light_intensity = self ._cs .reflected_light_intensity
20532077
20542078 integral = 0.0
20552079 last_error = 0.0
@@ -2059,7 +2083,7 @@ def follow_line(self,
20592083 MAX_SPEED = SpeedNativeUnits (self .max_speed )
20602084
20612085 while follow_for (self , ** kwargs ):
2062- reflected_light_intensity = self .cs .reflected_light_intensity
2086+ reflected_light_intensity = self ._cs .reflected_light_intensity
20632087 error = target_light_intensity - reflected_light_intensity
20642088 integral = integral + error
20652089 derivative = error - last_error
@@ -2099,6 +2123,183 @@ def follow_line(self,
20992123
21002124 self .stop ()
21012125
2126+ def calibrate_gyro (self ):
2127+ """
2128+ Calibrates the gyro sensor.
2129+
2130+ NOTE: This takes 1sec to run
2131+ """
2132+ assert self ._gyro , "GyroSensor must be defined"
2133+
2134+ for x in range (2 ):
2135+ self ._gyro .mode = 'GYRO-RATE'
2136+ self ._gyro .mode = 'GYRO-ANG'
2137+ time .sleep (0.5 )
2138+
2139+ def follow_gyro_angle (self ,
2140+ kp , ki , kd ,
2141+ speed ,
2142+ target_angle = 0 ,
2143+ sleep_time = 0.01 ,
2144+ follow_for = follow_for_forever ,
2145+ ** kwargs
2146+ ):
2147+ """
2148+ PID gyro angle follower
2149+
2150+ ``kp``, ``ki``, and ``kd`` are the PID constants.
2151+
2152+ ``speed`` is the desired speed of the midpoint of the robot
2153+
2154+ ``target_angle`` is the angle we want to maintain
2155+
2156+ ``sleep_time`` is how many seconds we sleep on each pass through
2157+ the loop. This is to give the robot a chance to react
2158+ to the new motor settings. This should be something small such
2159+ as 0.01 (10ms).
2160+
2161+ ``follow_for`` is called to determine if we should keep following the
2162+ desired angle or stop. This function will be passed ``self`` (the current
2163+ ``MoveTank`` object). Current supported options are:
2164+ - ``follow_for_forever``
2165+ - ``follow_for_ms``
2166+
2167+ ``**kwargs`` will be passed to the ``follow_for`` function
2168+
2169+ Example:
2170+
2171+ .. code:: python
2172+
2173+ from ev3dev2.motor import OUTPUT_A, OUTPUT_B, MoveTank, SpeedPercent, follow_for_ms
2174+ from ev3dev2.sensor.lego import GyroSensor
2175+
2176+ # Instantiate the MoveTank object
2177+ tank = MoveTank(OUTPUT_A, OUTPUT_B)
2178+
2179+ # Initialize the tank's gyro sensor
2180+ tank.gyro = GyroSensor()
2181+
2182+ try:
2183+ # Calibrate the gyro to eliminate drift, and to initialize the current angle as 0
2184+ tank.calibrate_gyro()
2185+
2186+ # Follow the line for 4500ms
2187+ tank.follow_gyro_angle(
2188+ kp=11.3, ki=0.05, kd=3.2,
2189+ speed=SpeedPercent(30),
2190+ target_angle=0
2191+ follow_for=follow_for_ms,
2192+ ms=4500
2193+ )
2194+ except FollowGyroAngleErrorTooFast:
2195+ tank.stop()
2196+ raise
2197+ """
2198+ assert self ._gyro , "GyroSensor must be defined"
2199+
2200+ integral = 0.0
2201+ last_error = 0.0
2202+ derivative = 0.0
2203+ speed_native_units = speed .to_native_units (self .left_motor )
2204+ MAX_SPEED = SpeedNativeUnits (self .max_speed )
2205+
2206+ assert speed_native_units <= MAX_SPEED , "Speed exceeds the max speed of the motors"
2207+
2208+ while follow_for (self , ** kwargs ):
2209+ current_angle = self ._gyro .angle
2210+ error = current_angle - target_angle
2211+ integral = integral + error
2212+ derivative = error - last_error
2213+ last_error = error
2214+ turn_native_units = (kp * error ) + (ki * integral ) + (kd * derivative )
2215+
2216+ left_speed = SpeedNativeUnits (speed_native_units - turn_native_units )
2217+ right_speed = SpeedNativeUnits (speed_native_units + turn_native_units )
2218+
2219+ if abs (left_speed ) > MAX_SPEED :
2220+ log .info ("%s: left_speed %s is greater than MAX_SPEED %s" %
2221+ (self , left_speed , MAX_SPEED ))
2222+ self .stop ()
2223+ raise FollowGyroAngleErrorTooFast (
2224+ "The robot is moving too fast to follow the angle" )
2225+
2226+ if abs (right_speed ) > MAX_SPEED :
2227+ log .info ("%s: right_speed %s is greater than MAX_SPEED %s" %
2228+ (self , right_speed , MAX_SPEED ))
2229+ self .stop ()
2230+ raise FollowGyroAngleErrorTooFast (
2231+ "The robot is moving too fast to follow the angle" )
2232+
2233+ if sleep_time :
2234+ time .sleep (sleep_time )
2235+
2236+ self .on (left_speed , right_speed )
2237+
2238+ self .stop ()
2239+
2240+ def turn_to_angle_gyro (self ,
2241+ speed ,
2242+ target_angle = 0 ,
2243+ wiggle_room = 2 ,
2244+ sleep_time = 0.01
2245+ ):
2246+ """
2247+ Pivot Turn
2248+
2249+ ``speed`` is the desired speed of the midpoint of the robot
2250+
2251+ ``target_angle`` is the target angle we want to pivot to
2252+
2253+ ``wiggle_room`` is the +/- angle threshold to control how accurate the turn should be
2254+
2255+ ``sleep_time`` is how many seconds we sleep on each pass through
2256+ the loop. This is to give the robot a chance to react
2257+ to the new motor settings. This should be something small such
2258+ as 0.01 (10ms).
2259+
2260+ Example:
2261+
2262+ .. code:: python
2263+
2264+ from ev3dev2.motor import OUTPUT_A, OUTPUT_B, MoveTank, SpeedPercent
2265+ from ev3dev2.sensor.lego import GyroSensor
2266+
2267+ # Instantiate the MoveTank object
2268+ tank = MoveTank(OUTPUT_A, OUTPUT_B)
2269+
2270+ # Initialize the tank's gyro sensor
2271+ tank.gyro = GyroSensor()
2272+
2273+ # Calibrate the gyro to eliminate drift, and to initialize the current angle as 0
2274+ tank.calibrate_gyro()
2275+
2276+ # Pivot 30 degrees
2277+ tank.turn_to_angle_gyro(
2278+ speed=SpeedPercent(5),
2279+ target_angle(30)
2280+ )
2281+ """
2282+ assert self ._gyro , "GyroSensor must be defined"
2283+
2284+ speed_native_units = speed .to_native_units (self .left_motor )
2285+ target_reached = False
2286+
2287+ while not target_reached :
2288+ current_angle = self ._gyro .angle
2289+ if abs (current_angle - target_angle ) <= wiggle_room :
2290+ target_reached = True
2291+ self .stop ()
2292+ elif (current_angle > target_angle ):
2293+ left_speed = SpeedNativeUnits (- 1 * speed_native_units )
2294+ right_speed = SpeedNativeUnits (speed_native_units )
2295+ else :
2296+ left_speed = SpeedNativeUnits (speed_native_units )
2297+ right_speed = SpeedNativeUnits (- 1 * speed_native_units )
2298+
2299+ if sleep_time :
2300+ time .sleep (sleep_time )
2301+
2302+ self .on (left_speed , right_speed )
21022303
21032304class MoveSteering (MoveTank ):
21042305 """
0 commit comments