Skip to content

Commit 20f2856

Browse files
committed
Documented AutoConfigBuilder.py
1 parent dc47562 commit 20f2856

File tree

1 file changed

+107
-11
lines changed

1 file changed

+107
-11
lines changed

FLLMaster/AutoConfigBuilder.py

Lines changed: 107 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env python3
22

3+
# Import stuff
34
from configparser import ConfigParser
45
from time import sleep
56
from ev3dev2.display import *
@@ -16,90 +17,143 @@ def configBuilder(configFile='robot.cfg'):
1617
"""
1718
Creates a robot config file based on user input and sensor/motor values
1819
"""
20+
# Make sure this is the global lego module, as it is used dynamicly later. Unsure if needed.
1921
global lego
22+
# Instantiate brick interface objects
2023
btn = Button()
2124
disp = Display()
2225
spkr = Sound()
26+
# Instantiate a ConfigParser object to writ and read the INI format config file
2327
config = ConfigParser()
28+
29+
# Create the config file sections
2430
config['Drivebase'] = {}
2531
config['Sensors'] = {}
2632
config['AuxMotors'] = {}
2733
config['Other'] = {}
34+
35+
# Used when detecting motors and sensors, as iterables
2836
outPorts = ['OUTPUT_A', 'OUTPUT_B', 'OUTPUT_C', 'OUTPUT_D']
2937
senTypes = ['Color', 'Gyro', 'Touch', 'Ultrasonic', 'Infrared']
3038

39+
# Ask the user if the robot is to be used for FLL
3140
disp.text_pixels("Is this robot for FLL?")
3241
disp.text_pixels("Y", False, 10, 110)
3342
disp.text_pixels("N", False, 150, 110)
3443
disp.update()
3544
spkr.beep()
45+
# Wait for a button press (the outer loop is to allow for selection retry if
46+
# an invalid button is pressed)
3647
while True:
3748
while not btn.any():
3849
pass
50+
# Get the list of currently pressed buttons
3951
selection = btn.buttons_pressed
52+
# Check if the first buton in the list is the left button
4053
if selection[0] == 'left':
54+
# If it is, set the ForFLL value to TRUE and exit the outer loop
4155
btn.wait_for_released('left')
4256
config['Other']['ForFLL'] = 'TRUE'
4357
break
4458
elif selection[0] == 'right':
59+
# Otherwise, check if the selection is right. If it is, set ForFLL to FLASE
60+
# and exit the outer loop
4561
btn.wait_for_released('right')
4662
config['Other']['ForFLL'] = 'FALSE'
4763
break
4864
else:
65+
# If both checks fail, play an error tone and go back to waiting for a button
4966
spkr.beep()
5067
spkr.beep('-f 380')
68+
# Clear the display for the next prompt
5169
disp.clear()
5270

53-
71+
# Auto-detect sensors
5472
for i in senTypes:
5573
try:
74+
# When a sensor object is instantiated without a port, it will find the first port with
75+
# that type of sensor connected. If that sensor is not connected, it will throw an error.
76+
# The getattr function is used to get the correct sensor type dynamically
5677
sensorClass = getattr(lego, i + 'Sensor')
78+
# Instantiate the current sensor type
5779
mySensor = sensorClass()
80+
# Get the port that the sensor is connected to
5881
port = str(mySensor.address)
82+
# sensor.adress will return sometheing like ev3:in1, so this replaces anything containing in# with INPUT_#
5983
port = re.sub('.*in([0-9]).*', 'INPUT_\g<1>', port)
84+
# Add port value to the config file
6085
config['Sensors'][i + 'Port'] = port
6186
except:
87+
# If the sensor instantiation failed, none of that kind of sensor are connected, so write the port value as None
6288
config['Sensors'][i + 'Port'] = 'None'
6389
finally:
90+
# Clear the object and class variables as they are reused every loop cycle
6491
mySensor = None
6592
sensorClass = None
6693

94+
# Detect motors
95+
# Repeat test for each port
6796
for i in outPorts:
6897
try:
98+
# Instanitiate a Large Motor object at the current port. If there is a Lrge Motor at said port, this will suceed.
99+
# Otherwise, it will throw an error and exit the try block.
69100
motor = LargeMotor(eval(i))
101+
# Print where the motor was found (port A, B, C, or D), and ask the user what the motor is being used for.
70102
disp.text_pixels("Found a Large Motor at " + "port " + i[-1])
71103
disp.text_pixels("What kind of motor is this?", False, 0, 10)
72104
disp.text_pixels("Press left for left motor,", False, 0, 20)
73105
disp.text_pixels("right for right motor,", False, 0, 30)
74106
disp.text_pixels("up for Aux1, or down for Aux2", False, 0, 40)
75107
disp.update()
108+
# Beep to signal that user input is required
76109
spkr.beep()
110+
111+
# Loop is used to allow for invalid button presses to repeat selection
77112
while True:
113+
# Wait for any button to be pressed
78114
while not btn.any():
79115
pass
80-
selection = btn.buttons_pressed
81-
if selection[0] == 'left':
116+
# Store what it is (first button pressed, if multiple)
117+
selection = btn.buttons_pressed[0]
118+
if selection == 'left':
119+
# Wait for the button to be released, so the operation only occurs once
82120
btn.wait_for_released('left')
121+
# If left, store the current port as the left motor port
83122
config['Drivebase']['LeftMotorPort'] = i
123+
# Exit the loop, as this is a valid selection
84124
break
85-
elif selection[0] == 'right':
125+
elif selection == 'right':
126+
# Wait for the button to be released, so the operation only occurs once
86127
btn.wait_for_released('right')
128+
# If right, store the current port as the right motor port
87129
config['Drivebase']['RightMotorPort'] = i
130+
# Exit the loop, as this is a valid selection
88131
break
89-
elif selection[0] == 'up':
132+
elif selection == 'up':
133+
# Wait for the button to be released, so the operation only occurs once
90134
btn.wait_for_released('up')
135+
# If up, store the current port as the first auxillary motor port
91136
config['AuxMotors']['AuxMotor1'] = i
137+
# Exit the loop, as this is a valid selection
92138
break
93-
elif selection[0] == 'down':
139+
elif selection == 'down':
140+
# Wait for the button to be released, so the operation only occurs once
94141
btn.wait_for_released('down')
142+
# If down, store the current port as the second auxillary motor port
95143
config['AuxMotors']['AuxMotor2'] = i
144+
# Exit the loop, as this is a valid selection
96145
break
97146
else:
147+
# If the pressed button is not valid, play an error tone and repeat the selection menu
98148
spkr.beep()
99149
spkr.beep('-f 380')
150+
151+
# If a motor is not found, no action is required. Apparently 'try' doesn't work without an 'except'
100152
except:
101153
pass
102154

155+
# This works exactly the same as the Large Motor detection, except that it is detecting Medium Motors instead,
156+
# and there are no drivemotor selection options.
103157
for i in outPorts:
104158
try:
105159
motor = MediumMotor(eval(i))
@@ -112,12 +166,12 @@ def configBuilder(configFile='robot.cfg'):
112166
while True:
113167
while not btn.any():
114168
pass
115-
selection = btn.buttons_pressed
116-
if selection[0] == 'left':
169+
selection = btn.buttons_pressed[0]
170+
if selection == 'left':
117171
btn.wait_for_released('left')
118172
config['AuxMotors']['AuxMotor1'] = i
119173
break
120-
elif selection[0] == 'right':
174+
elif selection == 'right':
121175
btn.wait_for_released('right')
122176
config['AuxMotors']['AuxMotor2'] = i
123177
break
@@ -127,15 +181,19 @@ def configBuilder(configFile='robot.cfg'):
127181
except:
128182
pass
129183

184+
# Create a MoveTank object with the detected drive motors, as it is needed for the upcoming tests.
130185
mtrs = MoveTank(eval(config['Drivebase']['LeftMotorPort']), eval(config['Drivebase']['RightMotorPort']))
131186

187+
# A Foward-Facing Ultrasonic sensor is the only way to allow for calculating the wheel circumfrence.
132188
if config['Sensors']['UltrasonicPort'] is not 'None':
189+
# Ask the user if the detected Ultrasonic sensor is facing fowards
133190
us = UltrasonicSensor(eval(config['Sensors']['UltrasonicPort']))
134191
disp.text_pixels("Does the Ultrasonic sensor")
135192
disp.text_pixels("face foward?", False, 0, 10)
136193
disp.text_pixels("Y", False, 10, 110)
137194
disp.text_pixels("N", False, 150, 110)
138195
disp.update()
196+
# Same selection system as before
139197
while not btn.any():
140198
pass
141199
selection = btn.buttons_pressed[0]
@@ -150,39 +208,54 @@ def configBuilder(configFile='robot.cfg'):
150208
usNav = False
151209
us = None
152210

211+
# Using the variable usNav set previously, check if the Ultrasonic sensor faces fowards.
153212
if usNav:
213+
# Ask the user to place the robot facing a wall, then wait for a button press.
154214
disp.text_pixels("Place the robot facing a wall, ")
155215
disp.text_pixels("and press any button.", False, 0, 10)
156216
disp.update()
157217
while not btn.any():
158218
pass
219+
# Set up some variables for averaging
159220
circsum = 0
160221
sign = 1
161222
tests = 3
223+
# Repeat the wheelcircumfrence test multiple times to get a more accurate average
162224
for j in range(tests):
225+
# Determine if this is an even or odd loop cycle. This is used for calculating MotorsInverted,
226+
# as the motors are run backwards on odd cycles
163227
if j / 2 == round(j / 2):
164228
parity = 1
165229
else:
166230
parity = -1
231+
# More variables for averaging
167232
usSum = 0
168233
readings = 5
234+
# Take multiple sensor readings of how far away from the wall the robot is, and average them
169235
for i in range(readings):
170236
usSum += us.distance_centimeters
171237
sleep(0.1)
172238
stavg = round(usSum / readings, 2)
239+
# Reset averaging variable
173240
usSum = 0
174-
print(stavg, file=stderr)
241+
# Move the robot one wheel rotation, foward on even loop cycles, or backward on odd
175242
mtrs.on_for_rotations(parity * 25, parity * 25, 1)
243+
# Take multiple sensor readings of how far away from the wall the robot is, and average them
176244
for i in range(readings):
177245
usSum += us.distance_centimeters
178246
sleep(0.1)
179247
enavg = round(usSum / readings, 2)
180-
print(enavg, file=stderr)
248+
# The absolute difference between the starting average and the ending average is the wheel circumfrence;
249+
# however, this is a cumulative sum of all wheel circumfrence measurements for averaging.
181250
circsum += abs(enavg - stavg)
251+
# Isolate the sign (-x or x to -1 or 1) of the wheel circumfrence (Used for MotorsInverted)
182252
sign = (enavg - stavg) / abs(enavg - stavg)
253+
# Calculate the average wheel circumfrence
183254
avg = round(circsum / tests, 2)
255+
# Write to config file variable
184256
config['Drivebase']['WheelCircumfrence'] = str(avg)
185257

258+
# Set MotorsInverted in the config file, and set the 'polarity' variable (for inverting motor commands if necessary)
186259
polarity = 'normal'
187260
if sign < 0:
188261
config['Drivebase']['MotorsInverted'] = 'FALSE'
@@ -191,31 +264,49 @@ def configBuilder(configFile='robot.cfg'):
191264
config['Drivebase']['MotorsInverted'] = 'TRUE'
192265
polarity = 'inversed'
193266

267+
# Invert motor commands if necessary
194268
mtrs.set_polarity(polarity)
269+
# Ask the user to place the robot in a clear area, and wait for a button press
195270
disp.text_pixels("Place robot in clear area,")
196271
disp.text_pixels("and press any button.", False, 0, 10)
197272
disp.update()
198273
while not btn.any():
199274
pass
275+
# Instantiate a Gyro Sensor object as the degrees turned is necessary for calculating the Width Between Wheels
200276
gs = GyroSensor(eval(config['Sensors']['GyroPort']))
277+
# Create variables for averaging
201278
widthbetweenwheelssum = 0
202279
ang = 0
280+
# Repeat the trial multiple times and average the results
203281
for i in range(tests):
282+
# Reset the reported gyro angle to zero
204283
gs._ensure_mode(gs.MODE_GYRO_RATE)
205284
gs._ensure_mode(gs.MODE_GYRO_ANG)
285+
# Turn in place for one wheel rotation
206286
mtrs.on_for_degrees(25, -25, 360)
287+
# Wait for the robot to settle
207288
sleep(0.5)
289+
# Read the current angle of the robot
208290
ang = gs.angle
291+
# Calculate the width between the robot's wheels using the formula for arc length (ArcLength = (π * d * Θ) / 360) solved for d,
292+
# the diameter of the circle, which is the width between wheels. Absolute value is used so the direction of turn does not matter.
209293
wbw = abs((360 * float(config['Drivebase']['WheelCircumfrence'])) / (ang * math.pi))
294+
# Add the calculated value to the running total (used for averaging)
210295
widthbetweenwheelssum += wbw
296+
# Calculate the average WBW value, round it to remove floating point errors, and store in the ConfigParser object
211297
config['Drivebase']['WidthBetweenWheels'] = str(round(widthbetweenwheelssum / tests, 2))
212298

299+
# The motor move command previously made the robot turn right. If the gyro reported this as a turn towards positive, it is not inverted.
300+
# Otherwise, the gyro is inverted.
213301
if ang > 0:
214302
config['Drivebase']['GyroInverted'] = 'FALSE'
215303
elif ang < 0:
216304
config['Drivebase']['GyroInverted'] = 'TRUE'
217305

218306
else:
307+
# If the robot does not have an ultrasonic sensor that faces fowards, the robot cannot calculate the wheel circumfrence, which is required
308+
# to calculate widthbetweenwheels. The robot also cannot calculate MotorsInverted or GyroInverted. Therefore, empty valuse are written to
309+
# those parts of the config file, and the user is notified that they need to manually fill in that information.
219310
spkr.beep()
220311
spkr.beep()
221312
config['Drivebase']['WheelCircumfrence'] = ''
@@ -230,6 +321,9 @@ def configBuilder(configFile='robot.cfg'):
230321
while not btn.any():
231322
pass
232323

324+
# Write placeholder values for the PID gains, so those functions may be usable, if poorly.
325+
# Because the WheelCircumfrence is calculated, the wheelcircumfrence value will include any gearing,
326+
# so the effective gear ratio is one.
233327
config['Drivebase']['GearRatio'] = '1'
234328
config['Drivebase']['kp'] = '1'
235329
config['Drivebase']['ki'] = '0'
@@ -238,6 +332,7 @@ def configBuilder(configFile='robot.cfg'):
238332
config['Sensors']['kiLine'] = '0'
239333
config['Sensors']['kdLine'] = '2'
240334

335+
# Tell the user that the PID values may (will) need to be tuned.
241336
spkr.beep()
242337
spkr.beep()
243338
disp.text_pixels("The PID gains in the config")
@@ -248,6 +343,7 @@ def configBuilder(configFile='robot.cfg'):
248343
while not btn.any():
249344
pass
250345

346+
# Write all the values stored in the ConfigParser object named config to a file in INI format, with the name passed by configFile.
251347
with open(configFile, 'w') as configfile:
252348
config.write(configfile)
253349

0 commit comments

Comments
 (0)