STARLEAP / SPIKE PRIME PYTHON / 2026–27

Build 2026.09.22-090630-0400

On this page

Project 22: Smart Bike Route Data Recorder

You need: a LEGO SPIKE Prime set, the Smart Bike (Bike and Biker), motors C and E, support wheels, the SPIKE App Python editor, and a wide, low, stable slope board on the floor. Finish Project 17's axis and sign calibration first.

Record a short series of slope readings, find the lowest and highest values, then compare two routes. Start with stationary tests. Powered movement comes only after your teacher checks the readings. This records tilt; it does not measure electrical power, energy, or distance.

Create a fresh project. Every variable needed here is defined below. Gray lines are already there; dark lines are the edit. Stop the program before each change.

Step 1: Read One Angle

Name your project Bike Route Recorder and type:

CODE
from hub import motion_sensor
import runloop

tilt_axis = 1

async def main():
    for sample in range(20):
        angle = motion_sensor.tilt_angles()[tilt_axis] / 10
        print(angle)
        await runloop.sleep_ms(250)

runloop.run(main())

Use your tested tilt_axis from Project 17, which may be 2 instead of 1. Run the program with the motors still. Gently raise and lower the front. It takes 20 readings with a quarter-second pause after each. That is roughly five seconds, plus the time spent reading and printing.

Step 2: Calibrate Level And Sign

Add your tested sign beside the axis. Use 1 or -1 so front-up will be positive:

EDIT VIEW · Gray = already there; dark = add or change
tilt_axis = 1
tilt_sign = 1

Replace main():

EDIT VIEW · Gray = already there; dark = add or change
async def main():
    baseline = motion_sensor.tilt_angles()[tilt_axis] / 10
    for sample in range(20):
        angle = motion_sensor.tilt_angles()[tilt_axis] / 10
        slope = (angle - baseline) * tilt_sign
        print(slope)
        await runloop.sleep_ms(250)

Run the program starting on level ground. Front-up should be positive, back-up negative, and level close to zero. Stop and fix the axis or sign if those observations disagree. Do not move the baseline assignment inside the loop.

Step 3: Save The Readings In A List

Insert an empty list after the baseline:

EDIT VIEW · Gray = already there; dark = add or change
    baseline = motion_sensor.tilt_angles()[tilt_axis] / 10
    samples = []
    for sample in range(20):

Add one line immediately after calculating slope:

EDIT VIEW · Gray = already there; dark = add or change
        slope = (angle - baseline) * tilt_sign
        samples.append(slope)
        print(slope)

After the loop, at four spaces, add:

CODE
    print("saved readings")
    print(samples)

Run the program. You should see individual readings while it runs and a bracketed list at the end. append() adds one value to the end of the list. The list starts fresh each run.

Step 4: Count What Was Saved

Add this after print(samples), outside the loop:

CODE
    print("sample count")
    print(len(samples))

Run the program. The count should be 20. len() gives the number of saved values, not the largest angle. Keep the loop count at 20 for the rest of the lesson.

Step 5: Find The Lowest And Highest Slopes

Add these lines after the sample count:

CODE
    if len(samples) > 0:
        print("lowest slope")
        print(min(samples))
        print("highest slope")
        print(max(samples))

Run the program while gently tilting both ways. A downhill reading can be the lowest because it is negative. max() finds the most positive reading, not necessarily the steepest tilt. The if avoids asking for a minimum or maximum of an empty list.

Step 6: Find The Steepest Tilt In Either Direction

Add this inside the summary's if, after print(max(samples)):

CODE
        steepest = max(abs(min(samples)), abs(max(samples)))
        print("steepest tilt size")
        print(steepest)

For example, if the lowest reading is -12 and the highest is 7, the steepest size is 12. abs() removes each sign before the outer max() compares their sizes.

Run the program. Make the downhill tilt larger than the uphill tilt. Check that the steepest value follows the larger size, not just the positive reading.

Step 7: Stop Sampling At A Large Tilt

Add this definition beside your other top-level settings:

EDIT VIEW · Gray = already there; dark = add or change
tilt_sign = 1
tilt_limit = 20

Keep your own tested sign. Inside the loop, insert this after print(slope) and before the pause:

EDIT VIEW · Gray = already there; dark = add or change
        print(slope)
        if abs(slope) > tilt_limit:
            print("tilt limit: recording ended")
            break
        await runloop.sleep_ms(250)

Run the program and gently tilt past 20 degrees while holding the stationary bike by its frame. break exits the recording loop, then the summary still runs. The out-of-range reading was appended before the check, so it appears in the count and summary. Exactly 20 does not pass >.

Step 8: Print Rows For A Graph

Add this after the whole summary block, at four spaces inside main():

CODE
    print("sample,slope_degrees")
    for index in range(len(samples)):
        print(str(index) + "," + str(samples[index]))

Run the program. index starts at zero; samples[index] selects that saved reading. str() converts each number to text, and + joins it with a comma into one row. Copy only the header and rows into a spreadsheet if available, or plot the values on paper.

Use sample number on the horizontal axis and slope degrees on the vertical axis. These rows are not exact timestamps or distance measurements.

Step 9: Test A Motor Pulse Separately

Save the recorder. Create a separate Python project named Bike Recorder Motor Test:

CODE
from hub import port
import motor_pair
import runloop

bike_motor = port.C
rider_motor = port.E

async def main():
    motor_pair.pair(motor_pair.PAIR_1, bike_motor, rider_motor)
    await motor_pair.move_for_time(
        motor_pair.PAIR_1, 200, 0, velocity=120)
    motor_pair.stop(motor_pair.PAIR_1)

runloop.run(main())

bike_motor names the bike motor on C; rider_motor names the rider motor on E. Put these variables after the imports so commands can use names instead of port letters.

Run the program once on a clear, level floor. It should make a short forward pulse and stop. Have your teacher check the build, direction, support wheels, and room for 20 pulses before combining the programs. Fix a failed motor test before continuing.

Step 10: Add Short Rides Between Readings

Reopen Bike Route Recorder. Replace the first import and add the motor-pair import. Keep import runloop with the imports. Add the two port variables just below all imports, before tilt_axis:

EDIT VIEW · Gray = already there; dark = add or change
from hub import motion_sensor, port
import motor_pair
import runloop

bike_motor = port.C
rider_motor = port.E

At the start of main(), before the baseline, insert:

EDIT VIEW · Gray = already there; dark = add or change
async def main():
    motor_pair.pair(motor_pair.PAIR_1, bike_motor, rider_motor)
    motor_pair.stop(motor_pair.PAIR_1)
    baseline = motion_sensor.tilt_angles()[tilt_axis] / 10

Replace only the loop's await runloop.sleep_ms(250) with these lines. Keep them after the tilt check and its break:

CODE
        await motor_pair.move_for_time(
            motor_pair.PAIR_1, 200, 0, velocity=120)
        await runloop.sleep_ms(50)

Add an explicit stop after the loop, before print("saved readings"):

EDIT VIEW · Gray = already there; dark = add or change
    motor_pair.stop(motor_pair.PAIR_1)
    print("saved readings")

Before you run: Start level, with the whole short route clear. Use only gentle slopes and a level starting approach. Keep the support wheels and the floor underneath the board. Do not touch hub buttons on a moving bike.

Run the program. It records an angle, checks it, then makes a short pulse. At most 20 pulses run; a large tilt ends the loop before the next pulse. The guard cannot detect a change during a pulse and is not continuous tip protection. Use the app's stop control if needed.

Step 11: Compare Two Routes

Run once on a flat route. Then run on a gentle route with a level start and a low rise. Keep the speed, pulse length, sample count, axis and sign unchanged. Recalibrate by starting each run level.

Save the printed rows from each run and sketch or graph them. Mark the highest and lowest values. A single reading is a sample: a bump between samples might be missed. The last reading happens before the last pulse; this is a series of sampled positions, not a continuous trace of every part of the ride.

Fix it: If every result is near zero on a clear slope, check the chosen axis. If uphill is negative, check the sign. If the list only contains one value, check that samples = [] is above the loop. If the motor still moves after a guard message, check that the movement is below break and outside the if block.

Teacher Check

Show your teacher:

Save and stop the program. Your data describes orientation; do not label requested motor speed or tilt as measured electrical power.