Introduction: GiggleBot Line Follower Using Python

This time around, we are programming in MicroPython the Dexter Industries GiggleBot to follow a black line using its built-in line follower sensor.

The GiggleBot has to be paired with a BBC micro:bit in order for it to be controlled appropriately.

If this tutorial is too advanced for you and programming the GiggleBot is too much for now, you can always go through the starter tutorial that shows you how the robot can be programmed in MakeCode here . The linked tutorial will walk you through the very basics.

Step 1: Required Components

The following hardware components are required:

  1. x3 AA batteries - in my case I'm using rechargeable batteries which have a lower voltage overall.
  2. A Dexter Industries GiggleBot robot for the micro:bit.
  3. A BBC micro:bit.

Of course, you also need a micro USB cable to program the BBC micro:bit - this cable generally comes within the BBC micro:bit's package or you can always use one that's used for charging (Android) smartphones.

Get the GiggleBot for the micro:bit here!

Step 2: Setup the Tracks

You will have to go through printing some tiles and designing your own tracks. You can use our own tiles so that you are 100% sure you are replicating our conditions. Or if you feel adventurous, you can use some black tape and make your own. Here's the PDF for the tiles we have used.

The above track is composed of the following number of different tiles:

  • 12 tiles of type #1.
  • 5 tiles of type #2.
  • 3 templates of tile type #5.
  • 3 templates of tile type #6 - here, you'll end up with one extra tile.

Next up, print them and cut them. Try placing them like in the above photo and keep in mind that on the right top side of the track, 2 tiles have to overlap one with another - this is expected in case you're wondering if you're doing something wrong.

Step 3: Setting Up the Environment

In order for you to be able to program the BBC micro:bit in MicroPython, you have to set up an editor for it (the Mu Editor) and set the GiggleBot MicroPython Runtime as its runtime. For that, you have to follow the instructions on this page . As of this moment, version v0.4.0 of the runtime is used.

Step 4: Programming the GiggleBot

Before getting down to it, the GiggleBot MicroPython runtime contains the classic runtime for the BBC micro:bit and other libraries to support the GiggleBot and other Dexter Industries Sensors.

After setting it up, open the following script in the Mu editor and click on Flash. This will flash the GiggleBot MicroPython Runtime and the script you've just opened up to your BBC micro:bit. The script is also shown down below.

Once the flashing process is done, stack the BBC micro:bit into the GiggleBot with the board's neopixels facing forward, place it on the track and switch it on.

Notice that in the script, the PID and other 2 constants (the speed setpoint and minimum speed constants) are already set.

Note: The following script might have missing whitespaces and this seems to be due to some issue in displaying GitHub Gists. Click on the gist to take you to its GitHub page where you can copy-paste the code.

GiggleBot PID Line Follower - Tuned w/ NeoPixels

from microbit import*
from gigglebot import*
from utime import sleep_ms, ticks_us
import ustruct
# initialize GB neopixels
neo = init()
# timing
update_rate =50
# gains/constants (assuming the battery voltage is around 4.0 volts)
Kp =25.0
Ki =0.5
Kd =35.0
trigger_point =0.3
min_speed_percent =0.3
base_speed =70
setpoint =0.5
last_position = setpoint
integral =0.0
run_neopixels =True
center_pixel =5# where the center pixel of the smile is located on the GB
# turquoise = tuple(map(lambda x: int(x / 5), (64, 224, 208))) # color to use to draw the error with the neopixels
# turquoise = (12, 44, 41) # which is exactly the above turquoise commented above this
error_width_per_pixel =0.5/3# max error divided by the number of segments between each neopixel
defupper_bound_linear_speed_reducer(abs_error, trigger_point, upper_bound, smallest_motor_power, highest_motor_power):
global base_speed
if abs_error >= trigger_point:
# x0 = 0.0
# y0 = 0.0
# x1 = upper_bound - trigger_point
# y1 = 1.0
# x = abs_error - trigger_point
# y = y0 + (x - x0) * (y1 - y0) / (x1 - x0)
# same as
y = (abs_error - trigger_point) / (upper_bound - trigger_point)
motor_power = base_speed * (smallest_motor_power + (1- y) * (highest_motor_power - smallest_motor_power))
return motor_power
else:
return base_speed * highest_motor_power
run =False
previous_error =0
whileTrue:
# if button a is pressed then start following
if button_a.is_pressed():
run =True
# but if button b is pressed stop the line follower
if button_b.is_pressed():
run =False
integral =0.0
previous_error =0.0
pixels_off()
stop()
sleep_ms(500)
if run isTrue:
# read the line sensors
start_time = ticks_us()
right, left = read_sensor(LINE_SENSOR, BOTH)
# line is on the left when position < 0.5
# line is on the right when position > 0.5
# line is in the middle when position = 0.5
# it's a weighted arithmetic mean
try:
position = right /float(left + right)
exceptZeroDivisionError:
position =0.5
# the range has to be (0, 1) and not [0, 1]
if position ==0: position =0.001
if position ==1: position =0.999
# use a PD controller
error = position - setpoint
integral += error
correction = Kp * error + Ki * integral + Kd * (error - previous_error)
previous_error = error
# calculate motor speeds
motor_speed = upper_bound_linear_speed_reducer(abs(error), setpoint * trigger_point, setpoint, min_speed_percent, 1.0)
leftMotorSpeed = motor_speed + correction
rightMotorSpeed = motor_speed - correction
# light up the neopixels according to the given error
if run_neopixels isTrueand total_counts %3==0:
for i inb'\x00\x01\x02\x03\x04\x05\x06\x07\x08':
neo[i] = (0, 0, 0)
for i inb'\x00\x01\x02\x03':
ifabs(error) > error_width_per_pixel * i:
if error <0:
# neo[center_pixel + i] = turquoise
neo[center_pixel + i] = (12, 44, 41)
else:
# neo[center_pixel - i] = turquoise
neo[center_pixel + i] = (12, 44, 41)
else:
percent =1- (error_width_per_pixel * i -abs(error)) / error_width_per_pixel
# light up the current pixel
if error <0:
# neo[center_pixel + i] = tuple(map(lambda x: int(x * percentage), turquoise))
neo[center_pixel + i] = (int(64* percent /5), int(224* percent /5), int(208* percent /5))
else:
# neo[center_pixel - i] = tuple(map(lambda x: int(x * percentage), turquoise))
neo[center_pixel - i] = (int(64* percent /5), int(224* percent /5), int(208* percent /5))
break
neo.show()
try:
# clip the motor speeds
if leftMotorSpeed >100:
leftMotorSpeed =100
rightMotorSpeed = rightMotorSpeed - leftMotorSpeed +100
if rightMotorSpeed >100:
rightMotorSpeed =100
leftMotorSpeed = leftMotorSpeed - rightMotorSpeed +100
if leftMotorSpeed <-100:
leftMotorSpeed =-100
if rightMotorSpeed <-100:
rightMotorSpeed =-100
# actuate the motors
set_speed(leftMotorSpeed, rightMotorSpeed)
drive()
# print((error, motor_speed))
except:
# in case we get into some unfixable issue
pass
# and maintain the loop frequency
end_time = ticks_us()
delay_diff = (end_time - start_time) /1000
if1000.0/ update_rate - delay_diff >0:
sleep(1000.0/ update_rate - delay_diff)

Step 5: Letting It Run

There are 2 buttons on the BBC micro:bit: button A and button B:

  • Pressing on button A sets the GiggleBot to follow the line (if there's one).
  • Pressing on button B stops the GiggleBot and resets everything so that you can use it again.

It's highly advised to not lift the GiggleBot while it's following a line and then put it back on it because the error that's calculating could accumulate and totally mess the robot's route. If you want to lift it, press on button B and then when you put it back press A again.