Skip to content

Commit b6794a9

Browse files
authored
Merge pull request #49 from Open-STEM/SpeedControlBugFix
Speed control bug fix, Straight and Turn retuned
2 parents f6228dc + ae5b217 commit b6794a9

File tree

7 files changed

+94
-84
lines changed

7 files changed

+94
-84
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
.vscode/
22
.git/
33
docs/_build/
4-
4+
.micropico
5+
.picowgo
56
secrets.json

.picowgo

Lines changed: 0 additions & 3 deletions
This file was deleted.

Examples/drive_examples.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def circle():
3838
# Follow the perimeter of a square with variable sidelength
3939
def square(sidelength):
4040
for sides in range(4):
41-
drivetrain.straight(sidelength, 80)
41+
drivetrain.straight(sidelength, 0.8)
4242
drivetrain.turn(90)
4343
# Alternatively:
4444
# polygon(sidelength, 4)

XRPLib/differential_drive.py

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -145,24 +145,26 @@ def straight(self, distance: float, max_effort: float = 0.5, timeout: float = No
145145
distance *= -1
146146

147147
time_out = Timeout(timeout)
148-
startingLeft = self.get_left_encoder_position()
149-
startingRight = self.get_right_encoder_position()
148+
starting_left = self.get_left_encoder_position()
149+
starting_right = self.get_right_encoder_position()
150150

151151

152152
if main_controller is None:
153153
main_controller = PID(
154-
kp = 0.075,
155-
kd = 0.005,
156-
minOutput = 0.25,
157-
maxOutput = max_effort,
158-
tolerance = 0.1,
159-
toleranceCount = 3,
154+
kp = 0.1,
155+
ki = 0.04,
156+
kd = 0.06,
157+
min_output = 0.25,
158+
max_output = 0.5,
159+
max_integral = 10,
160+
tolerance = 0.25,
161+
tolerance_count = 3,
160162
)
161163

162164
# Secondary controller to keep encoder values in sync
163165
if secondary_controller is None:
164166
secondary_controller = PID(
165-
kp = 0.025, kd=0.0025,
167+
kp = 0.075, kd=0.005,
166168
)
167169

168170
if self.imu is not None:
@@ -174,13 +176,13 @@ def straight(self, distance: float, max_effort: float = 0.5, timeout: float = No
174176
while True:
175177

176178
# calculate the distance traveled
177-
leftDelta = self.get_left_encoder_position() - startingLeft
178-
rightDelta = self.get_right_encoder_position() - startingRight
179-
distTraveled = (leftDelta + rightDelta) / 2
179+
left_delta = self.get_left_encoder_position() - starting_left
180+
right_delta = self.get_right_encoder_position() - starting_right
181+
dist_traveled = (left_delta + right_delta) / 2
180182

181183
# PID for distance
182-
distanceError = distance - distTraveled
183-
effort = main_controller.update(distanceError)
184+
distance_error = distance - dist_traveled
185+
effort = main_controller.update(distance_error)
184186

185187
if main_controller.is_done() or time_out.is_done():
186188
break
@@ -190,7 +192,7 @@ def straight(self, distance: float, max_effort: float = 0.5, timeout: float = No
190192
# record current heading to maintain it
191193
current_heading = self.imu.get_yaw()
192194
else:
193-
current_heading = ((rightDelta-leftDelta)/2)*360/(self.track_width*math.pi)
195+
current_heading = ((right_delta-left_delta)/2)*360/(self.track_width*math.pi)
194196

195197
headingCorrection = secondary_controller.update(initial_heading - current_heading)
196198

@@ -206,12 +208,12 @@ def straight(self, distance: float, max_effort: float = 0.5, timeout: float = No
206208
def turn(self, turn_degrees: float, max_effort: float = 0.5, timeout: float = None, main_controller: Controller = None, secondary_controller: Controller = None, use_imu:bool = True) -> bool:
207209
"""
208210
Turn the robot some relative heading given in turnDegrees, and exit function when the robot has reached that heading.
209-
Speed is bounded from -1 (turn counterclockwise the relative heading at full speed) to 1 (turn clockwise the relative heading at full speed)
211+
effort is bounded from -1 (turn counterclockwise the relative heading at full speed) to 1 (turn clockwise the relative heading at full speed)
210212
Uses the IMU to determine the heading of the robot and P control for the motor controller.
211213
212214
:param turnDegrees: The number of angle for the robot to turn (In Degrees)
213215
:type turnDegrees: float
214-
:param max_effort: The max effort for which the robot to travel (Bounded from -1 to 1). Default is half effort forward.
216+
:param max_effort: The max speed for which the robot to travel (Bounded from -1 to 1)
215217
:type max_effort: float
216218
:param timeout: The amount of time before the robot stops trying to turn and continues to the next step (In Seconds)
217219
:type timeout: float
@@ -226,27 +228,29 @@ def turn(self, turn_degrees: float, max_effort: float = 0.5, timeout: float = No
226228
"""
227229

228230
if max_effort < 0:
229-
max_effort *= -1
230-
turn_degrees *= -1
231+
max_effort = -max_effort
232+
turn_degrees = -turn_degrees
231233

232234
time_out = Timeout(timeout)
233-
startingLeft = self.get_left_encoder_position()
234-
startingRight = self.get_right_encoder_position()
235+
starting_left = self.get_left_encoder_position()
236+
starting_right = self.get_right_encoder_position()
235237

236238
if main_controller is None:
237239
main_controller = PID(
238-
kp = .015,
239-
kd = 0.0012,
240-
minOutput = 0.25,
241-
maxOutput = max_effort,
242-
tolerance = 0.5,
243-
toleranceCount = 3
240+
kp = 0.02,
241+
ki = 0.001,
242+
kd = 0.00165,
243+
min_output = 0.35,
244+
max_output = 0.5,
245+
max_integral = 75,
246+
tolerance = 1,
247+
tolerance_count = 3
244248
)
245249

246250
# Secondary controller to keep encoder values in sync
247251
if secondary_controller is None:
248252
secondary_controller = PID(
249-
kp = 0.002,
253+
kp = 1.0,
250254
)
251255

252256
if use_imu and (self.imu is not None):
@@ -255,25 +259,25 @@ def turn(self, turn_degrees: float, max_effort: float = 0.5, timeout: float = No
255259
while True:
256260

257261
# calculate encoder correction to minimize drift
258-
leftDelta = self.get_left_encoder_position() - startingLeft
259-
rightDelta = self.get_right_encoder_position() - startingRight
260-
encoderCorrection = secondary_controller.update(leftDelta + rightDelta)
262+
left_delta = self.get_left_encoder_position() - starting_left
263+
right_delta = self.get_right_encoder_position() - starting_right
264+
encoder_correction = secondary_controller.update(left_delta + right_delta)
261265

262266
if use_imu and (self.imu is not None):
263267
# calculate turn error (in degrees) from the imu
264-
turnError = turn_degrees - self.imu.get_yaw()
268+
turn_error = turn_degrees - self.imu.get_yaw()
265269
else:
266270
# calculate turn error (in degrees) from the encoder counts
267-
turnError = turn_degrees - ((rightDelta-leftDelta)/2)*360/(self.track_width*math.pi)
271+
turn_error = turn_degrees - ((right_delta-left_delta)/2)*360/(self.track_width*math.pi)
268272

269273
# Pass the turn error to the main controller to get a turn speed
270-
turnSpeed = main_controller.update(turnError)
274+
turn_speed = main_controller.update(turn_error)
271275

272276
# exit if timeout or tolerance reached
273277
if main_controller.is_done() or time_out.is_done():
274278
break
275279

276-
self.set_effort(-turnSpeed - encoderCorrection, turnSpeed - encoderCorrection)
280+
self.set_effort(-turn_speed - encoder_correction, turn_speed - encoder_correction)
277281

278282
time.sleep(0.01)
279283

XRPLib/encoded_motor.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,21 +115,23 @@ def get_speed(self) -> float:
115115
def set_speed(self, speed_rpm: float = None):
116116
"""
117117
Sets target speed (in rpm) to be maintained passively
118-
Call with no parameters to turn off speed control
118+
Call with no parameters or 0 to turn off speed control
119119
120120
:param target_speed_rpm: The target speed for the motor in rpm, or None
121121
:type target_speed_rpm: float, or None
122122
"""
123-
if speed_rpm is None:
123+
if speed_rpm is None or speed_rpm == 0:
124124
self.target_speed = None
125125
self.set_effort(0)
126126
self.speed = 0
127+
self.updateTimer.deinit()
127128
return
128129
# If the update timer is not running, start it at 50 Hz (20ms updates)
129130
self.updateTimer.init(period=20, callback=lambda t:self._update())
130131
# Convert from rev per min to counts per 20ms (60 sec/min, 50 Hz)
131132
self.target_speed = speed_rpm*self._encoder.resolution/(60*50)
132133
self.speedController.clear_history()
134+
self.prev_position = self.get_position_counts()
133135

134136
def set_speed_controller(self, new_controller: Controller):
135137
"""

XRPLib/pid.py

Lines changed: 47 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -11,43 +11,45 @@ def __init__(self,
1111
kp = 1.0,
1212
ki = 0.0,
1313
kd = 0.0,
14-
minOutput = 0.0,
15-
maxOutput = 1.0,
16-
maxDerivative = None,
14+
min_output = 0.0,
15+
max_output = 1.0,
16+
max_derivative = None,
17+
max_integral = None,
1718
tolerance = 0.1,
18-
toleranceCount = 1
19+
tolerance_count = 1
1920
):
2021
"""
2122
:param kp: proportional gain
2223
:param ki: integral gain
2324
:param kd: derivative gain
24-
:param minOutput: minimum output
25-
:param maxOutput: maximum output
26-
:param maxDerivative: maximum derivative (change per second)
25+
:param min_output: minimum output
26+
:param max_output: maximum output
27+
:param max_derivative: maximum derivative (change per second)
28+
:param max_integral: maximum integral windup allowed (will cap integral at this value)
2729
:param tolerance: tolerance for exit condition
28-
:param numTimesInTolerance: number of times in tolerance for exit condition
30+
:param tolerance_count: number of times the error needs to be within tolerance for is_done to return True
2931
"""
3032
self.kp = kp
3133
self.ki = ki
3234
self.kd = kd
33-
self.minOutput = minOutput
34-
self.maxOutput = maxOutput
35-
self.maxDerivative = maxDerivative
35+
self.min_output = min_output
36+
self.max_output = max_output
37+
self.max_derivative = max_derivative
38+
self.max_integral = max_integral
3639
self.tolerance = tolerance
37-
self.toleranceCount = toleranceCount
40+
self.tolerance_count = tolerance_count
3841

39-
self.prevError = 0
40-
self.prevIntegral = 0
41-
self.prevOutput = 0
42+
self.prev_error = 0
43+
self.prev_integral = 0
44+
self.prev_output = 0
4245

43-
self.startTime = None
44-
self.prevTime = None
46+
self.start_time = None
47+
self.prev_time = None
4548

4649
# number of actual times in tolerance
4750
self.times = 0
4851

4952
def _handle_exit_condition(self, error: float):
50-
5153
if abs(error) < self.tolerance:
5254
# if error is within tolerance, increment times in tolerance
5355
self.times += 1
@@ -65,43 +67,47 @@ def update(self, error: float) -> float:
6567
:return: The system output from the controller, to be used as an effort value or for any other purpose
6668
:rtype: float
6769
"""
68-
currentTime = time.ticks_ms()
69-
if self.prevTime is None:
70+
current_time = time.ticks_ms()
71+
if self.prev_time is None:
7072
# First update after instantiation
71-
self.startTime = currentTime
73+
self.start_time = current_time
7274
timestep = 0.01
7375
else:
7476
# get time delta in seconds
75-
timestep = time.ticks_diff(currentTime, self.prevTime) / 1000
76-
self.prevTime = currentTime # cache time for next update
77+
timestep = time.ticks_diff(current_time, self.prev_time) / 1000
78+
self.prev_time = current_time # cache time for next update
7779

7880
self._handle_exit_condition(error)
7981

80-
integral = self.prevIntegral + error * timestep
81-
derivative = (error - self.prevError) / timestep
82+
integral = self.prev_integral + error * timestep
83+
84+
if self.max_integral is not None:
85+
integral = max(-self.max_integral, min(self.max_integral, integral))
86+
87+
derivative = (error - self.prev_error) / timestep
8288

8389
# derive output
84-
output = self.kp * error + self.ki * integral + self.kd * derivative
85-
self.prevError = error
86-
self.prevIntegral = integral
90+
output = self.kp * error + self.ki * integral - self.kd * derivative
91+
self.prev_error = error
92+
self.prev_integral = integral
8793

8894
# Bound output by minimum
8995
if output > 0:
90-
output = max(self.minOutput, output)
96+
output = max(self.min_output, output)
9197
else:
92-
output = min(-self.minOutput, output)
98+
output = min(-self.min_output, output)
9399

94100
# Bound output by maximum
95-
output = max(-self.maxOutput, min(self.maxOutput, output))
101+
output = max(-self.max_output, min(self.max_output, output))
96102

97103
# Bound output by maximum acceleration
98-
if self.maxDerivative is not None:
99-
lowerBound = self.prevOutput - self.maxDerivative * timestep
100-
upperBound = self.prevOutput + self.maxDerivative * timestep
101-
output = max(lowerBound, min(upperBound, output))
104+
if self.max_derivative is not None:
105+
lower_bound = self.prev_output - self.max_derivative * timestep
106+
upper_bound = self.prev_output + self.max_derivative * timestep
107+
output = max(lower_bound, min(upper_bound, output))
102108

103109
# cache output for next update
104-
self.prevOutput = output
110+
self.prev_output = output
105111

106112
return output
107113

@@ -110,11 +116,11 @@ def is_done(self) -> bool:
110116
:return: if error is within tolerance for numTimesInTolerance consecutive times, or timed out
111117
:rtype: bool
112118
"""
113-
114-
return self.times >= self.toleranceCount
119+
return self.times >= self.tolerance_count
115120

116121
def clear_history(self):
117-
self.prevError = 0
118-
self.prevIntegral = 0
119-
self.prevOutput = 0
122+
self.prev_error = 0
123+
self.prev_integral = 0
124+
self.prev_output = 0
125+
self.prev_time = None
120126
self.times = 0

XRPLib/resetbot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# using the EncodedMotor since the default drivetrain uses the IMU and takes 3 seconds to init
1212
for i in range(4):
1313
motor = EncodedMotor.get_default_encoded_motor(i+1)
14-
motor.set_effort(0)
14+
motor.set_speed(0)
1515
motor.reset_encoder_position()
1616

1717
# Turn off the on-board LED

0 commit comments

Comments
 (0)