Welcome to the Final Tutorial
Throughout this practical tutorial series, you’ve progressively developed your Python programming skills by controlling real electronic hardware through the Robo-Tx API.
You started with relatively simple tasks such as responding to button presses and reading an analogue input. You then progressed to applications involving sensors, mathematical calculations, LCD output, timing and multiple forms of user interaction.
This final instalment brings many of those ideas together to create a fully interactive electronic metronome.
The project uses:
- the slider to set the tempo
- the button to start and stop the metronome
- a longer button press to select the time signature
- the beeper to produce the beats
- the LED to identify the down-beat
- the LCD to display the current beat, time signature and tempo
The result is a small but reasonably sophisticated application that demonstrates how Python can coordinate several physical inputs and outputs simultaneously.
Before You Begin
If you haven’t already prepared your hardware and Python environment, first follow the setup instructions in the GitHub repository. The Python example discussed in this tutorial (3-metronome.py) can be found at the same repository.
Don’t have an All-in-One Starter Kit? The Python Programming: Robotics Foundation Course provides supervised tuition together with remote access to the same All-in-One Starter kit, allowing you to learn challenging Robo-Tx API and Python programming concepts from anywhere.
What Is a Metronome?
A metronome produces regularly spaced beats to help musicians maintain a consistent tempo. Tempo is normally expressed in beats per minute (BPM).
For example:
- 60 BPM → one beat every second
- 120 BPM → two beats every second
- 240 BPM → four beats every second
The All-in-One Starter Kit’s slider is used to select a tempo between 60 and 240 BPM.
The metronome also supports three time signatures: 2/4, 3/4 and 4/4. The first beat of each group is called the down-beat. This program distinguishes the down-beat from the other beats by producing a longer beep and flashing the LED. The remaining beats receive shorter beeps.
The Hardware Interface
The program uses four components from the All-in-One Starter Kit:
led = all_in_one_kit.Switch2
tempo_bpm = all_in_one_kit.Analog.A0
beeper = all_in_one_kit.Trigger
display = all_in_one_kit.Display
| Python variable | Hardware | Purpose |
| led | LED | Indicates the down-beat |
| tempo_bpm | Slider | Selects tempo |
| beeper | Beeper | Produces beats |
| display | 16×2 LCD | Displays status |
This illustrates one of the key advantages of the Robo-Tx API: Python code can interact with the physical components through meaningful objects and properties rather than having to deal with low-level Arduino communication.
Converting the Slider into BPM
The slider produces a raw analogue value between 0 and 1023. The program converts this into a BPM value in the range of 60 to 240:
def convert_to_bpm(value):
return ((180 * value) / 1023) + 60
By registering the converter with the Robo-Tx API as demonstrated in a previous tutorial, the application can simply use the Value property of tempo_bpm to obtain the BPM:
tempo_bpm.Value
This keeps the application logic easier to understand and also demonstrates an important software-development principle: keep data conversion close to the source of the data, rather than repeatedly embedding conversion calculations throughout the application.
Representing the Time Signature
The time signature is represented by the variable signature, whose valid range is from 2 to 4. On the LCD the time signature is represented as 2/4, 3/4 and 4/4. A long press and release of the button cycles through the time signatures:
elif input_event == Input.BUTTON_1_SUSTAIN_RELEASED:
signature += 1
if signature > 4:
signature = 2
display.PrintAt(6, 0, f"{signature}/4")
Keeping Track of the Metronome
Several variables maintain the state of the application:
beat_counter = 0
beat_interval = 0
last_beat_time = datetime.now()
metronome_active = False
beat_counter records the current beat within the bar.
- For 4/4 time, it cycles: 1 → 2 → 3 → 4 → 1 → …
- For 3/4: 1 → 2 → 3 → 1 → …
- For 2/4: 1 → 2 → 1 → …
beat_interval stores the time between beats in milliseconds.
last_beat_time records when the previous beat occurred.
metronome_active indicates whether the metronome is currently running.
Together, these variables provide the information required to control the timing and behaviour of the application.
Calculating the Beat Interval
The program calculates the interval (in milliseconds) between beats using:
beat_interval = 240_000 / (
int(tempo_bpm.Value) * (4 if signature <= 4 else 8)
)
At first glance, this looks complicated. The important concept is that the interval between beats is related to the selected BPM.
For example, at 60 BPM, a quarter-note beat occurs once per second (1000 milliseconds). At 120 BPM, 2 beats per second (500 milliseconds interval).
Where the does the value 240000 come from? There are 60000 milliseconds in a minute. The factor of four in the calculation accounts for the relationship between the selected tempo and the quarter-note basis used by the metronome (2/4, 3/4 and 4/4).
The conditional expression also leaves the code prepared to accommodate an eighth-note-based signature in the future.
(4 if signature <= 4 else 8)
This is an example of designing code with a little flexibility beyond the immediate requirements.
Starting and Stopping the Metronome
A quick button release toggles the running state:
if input_event == Input.BUTTON_1_RELEASED:
metronome_active = not metronome_active
beat_counter = 0
last_beat_time = datetime.now() - timedelta(
milliseconds=beat_interval
)
The beat counter is reset and the timing reference is adjusted. Why set last_beat_time to a value one beat interval in the past? Normally, last_beat_time represents when the previous beat occurred. When the metronome is started, there hasn’t actually been a previous beat. By setting the imaginary previous beat to one complete beat interval in the past, the program is requesting the next beat to be immediate. This allows the metronome to start promptly rather than waiting for a complete interval before producing its first beat.
Measuring Elapsed Time
The program determines how long it has been since the previous beat:
time_diff = (
datetime.now() - last_beat_time
).total_seconds() * 1000
The result is converted into milliseconds. The program can then ask:
if metronome_active and time_diff >= beat_interval:
In other words, is the metronome running, and has enough time passed for another beat? If both conditions are true, another beat is generated.
The program contains an especially useful bit of timing logic for minimising time drift:
adjustment = time_diff - beat_interval
last_beat_time = datetime.now() - timedelta(
milliseconds=adjustment
)
Suppose the beat should occur after 500 ms but the program checks after 507 ms. If the current time is simply recorded as the new reference, the next beat will also be scheduled 500 ms from this slightly late point. The error can accumulate. Instead, the program compensates for the additional seven milliseconds. This helps prevent timing errors from accumulating over time.
This is a good example of how real-world software sometimes needs to account for the fact that computers don’t execute instructions at perfectly predictable times.
Identifying the Down-Beat
The program determines whether the next beat is the first beat of the bar:
if beat_counter < 1 or beat_counter >= signature:
beat_counter = 1
The condition handles both:
- the first beat when the metronome starts
- the transition from the final beat back to beat 1
When the down-beat occurs, the program performs two actions: the beeper produces a longer 100 ms pulse and the LED is illuminated for 0.1 seconds. This combination provides both an audible and visual indication of the beginning of each bar. For the remaining beats, a shorter beep is produced.
What Makes This a Good Project to End the Tutorial Series?
The metronome brings together almost everything you’ve encountered so far.
Inputs
- analogue slider
- button events
Processing
- analogue conversion
- arithmetic
- conditional statements
- counters
- elapsed-time calculations
- state management
Outputs
- LCD
- beeper
- LED
Python features
- functions
- variables
- Boolean logic
- conditional statements
- loops
- object properties
- object methods
- datetime
- timedelta
- string formatting
The project therefore demonstrates how individual programming techniques can be combined to create a complete interactive application.
Challenge Exercises
The supplied program is a working solution, but don’t stop there. Try modifying it.
- Add a 6/8 Time Signature – extend the program so that the available signatures include: 2/4, 3/4, 4/4, 6/8. The existing beat interval calculation has deliberately been structured to allow for an eighth-note-based signature.
- Change the Tempo Range – change the convert_to_bpm() function so that the slider controls a different tempo range, 40-200 BPM.
Continue Your Practical Python Learning
If you’ve enjoyed using Python to control real hardware and want to develop these skills further, find out more about the Python Programming: Robotics Foundation Course.
The course builds on Python programming fundamentals and applies them to practical robotics and electronics projects using the Robo-Tx API and the Elecrow All-in-One Starter Kit.