← Course overview
Week 11 of 12 · Systems

Full Integration

Stop treating each subsystem as its own script, and give the whole machine one coherent control loop.

~6 hrs Software

Concept A pre-wired bonnet, relocated with a ribbon cable

Up to now, each week's code has run mostly on its own. A real machine needs one control loop that knows what it's currently doing and what should happen next: idle, scanning, classifying, sorting, returning home. Writing that out explicitly as named states, with clear transitions between them, keeps the logic readable as it grows — the alternative, a pile of nested if statements tracking implicit state in scattered variables, gets unmanageable fast. The status display here isn't a raw ST7789 breakout wired pin-by-pin: it's the Adafruit 1.3″ TFT Bonnet (a self-contained board with its own 240×240 screen, a 4-direction joystick with a click button, and two front buttons, all pre-wired internally to a fixed set of GPIO pins the moment it's connected). The trade-off for that convenience is normally position — a bonnet is designed to plug directly onto the Pi's header, sitting flush against it. This build wants the screen and joystick facing the operator at the front of the enclosure instead, so a 40-pin GPIO ribbon cable runs from a stacking header on the Pi (which passes every pin straight through, so the Motor HAT can still occupy the header as before) out to wherever the bonnet is actually mounted — same signals, different location. Writing timestamped events to a log file, meanwhile, is the difference between guessing what went wrong and actually knowing, once the machine is running unattended.

Kit What you need this week

  • Everything built in previous weeks
  • Adafruit 1.3″ Color TFT Bonnet (240×240 + joystick + 2 buttons, product 4506)
  • A 40-pin GPIO ribbon cable + a stacking header/GPIO splitter, if mounting the bonnet away from the Pi like the reference build
Master Software Reference Manual

A complete, line-by-line interactive reference manual is available in the Software Architecture & Full Code Walkthrough Guide ↗.

  • LCD Menu System: 7-button tactile event dispatcher (Up/Down/Left/Right/Center/A/B) rendering to a 240×240 ST7789 framebuffer via Pillow (PIL).
  • Dual-Axis Calibration & Homing: 3-phase microswitch homing with back-off hysteresis compensation and kinematic bucket table lookup (possible_angles_slide = [45, 90, 135, 180, 225, 270]).
  • Normal Running Cycle: Full autonomous state machine with AS7341 8-channel spectral strobing, cosine similarity centroid classifier, and 3-consecutive empty detection auto-stop.
  • Interactive Source Code: Full syntax-highlighted code for main_loop.py (741 lines), skittles_classifier.py, motor_release.py, and skittle_centroids.json.

Wiring Diagrams

Raspberry Pi40-pin headerall 40 pinsStacking headerpasses every pinstraight throughMotor HAT stillMotor HATsits on top as before40-pin ribbon cableTFT Bonnet240×240 screen + joystick + 2 buttonsmounted at the front of the enclosureown fixed pinout — SPI, 4 joystick GPIOs,joystick click, 2 button GPIOs
A stacking header on the Pi passes every pin through unchanged, so the Motor HAT keeps its spot while a ribbon cable carries the same 40 signals out to the bonnet, relocated to the enclosure’s front panel.
Raspberry Pi 40-pin header (top view)13V325V3GPIO2 (SDA)45V5GPIO3 (SCL)6GND7GPIO4 — joystick click8GPIO14 (TXD)9GND10GPIO15 (RXD)11GPIO17 — joystick12GPIO1813GPIO27 — joystick14GND15GPIO22 — joystick16GPIO23 — joystick173V318GPIO2419GPIO10 (MOSI) — bonnet MOSI20GND21GPIO9 (MISO)22GPIO25 — bonnet D/C23GPIO11 (SCLK) — bonnet SCLK24GPIO8 (CE0) — bonnet CS25GND26GPIO7 (CE1)27ID_SD28ID_SC29GPIO5 — button A30GND31GPIO6 — button B32GPIO1233GPIO1334GND35GPIO1936GPIO1637GPIO26 — bonnet backlight38GPIO2039GND40GPIO21
powergroundused this weekunused GPIO
The bonnet's own fixed pinout — not a choice you make, unlike a raw breakout — all carried out to it over one ribbon cable via the Pi-side stacking header.

Build Step by step

Step 1

Wire the stacking header and ribbon cable

Fit a stacking header (one that passes every pin through) onto the Pi's 40-pin header so the Motor HAT still has a header to sit on, then run the GPIO ribbon cable from the stacking header's pass-through pins out to wherever the bonnet will be mounted.

Why not just plug the bonnet straight on?A bonnet is designed to do exactly that — and if your enclosure puts the Pi itself somewhere the operator can see and reach, there's no need for a ribbon cable at all. It's only needed here because this build wants the screen and joystick at the front of the enclosure, physically separate from where the Pi and Motor HAT actually sit.
Step 2

Connect the bonnet

Plug the far end of the ribbon cable into the bonnet. Because every one of its pins is pre-assigned by the board itself — SPI for the screen, four GPIOs for the joystick directions, one for its click, two more for the front buttons — there's no breadboard wiring for the display circuit itself, unlike a raw ST7789 breakout.

Step 3

Get “hello world” on screen

Display a simple message before wiring anything else in — isolating display problems now saves a lot of confusion later.

Step 4

Sketch the state machine on paper

Before writing code, draw the states (idle, scan, classify, sort, home) as circles and the events that move between them as arrows. This five-minute step prevents most of the tangled logic it's meant to avoid.

Step 5

The 8-Step Scan & Release Algorithm

Every candy sorted by the machine follows a deterministic 8-step physical and algorithmic sequence inside next_scan_and_release():

def next_scan_and_release(now_bucket):
    # 1. Update ST7789 LCD Framebuffer
    status_display(disp, draw, image, width, height, text=["Scanning", "Skittles"])

    # 2. Fire AS7341 Auxiliary Illuminator
    sensor.led = True

    # 3. Acquire 8-Channel Spectral Channels (F1 415nm -> F8 680nm)
    the_scan = [
        sensor.channel_415nm, sensor.channel_445nm, sensor.channel_480nm, sensor.channel_515nm,
        sensor.channel_555nm, sensor.channel_590nm, sensor.channel_630nm, sensor.channel_680nm
    ]

    # 4. Classify via Vector Sum-Normalization & Cosine Distance
    pred, score, ranked = classify_reading(*the_scan)

    # 5. Visual Telemetry (NeoPixels light up in predicted color) & Extinguish LED
    light_the_bar(pred)
    sensor.led = False

    # 6. Translate Color to Target Radial Cup Angle
    picked_bucket = which_slide(pred)  # Yellow->1, Green->2, Purple->3, Orange->4, Empty->0
    go_to_bucket(stepper_motor2, now_bucket, picked_bucket)

    # 7. Index Wheel 90° to Discharge Candy down the Chute
    rotate_to_angle(stepper_motor1, 90)

    # 8. De-energize Stepper Coils (Heat Prevention) & Return State
    stepper_motor1.release()
    off_light_bar()
    return picked_bucket, the_scan
Step 6

The Autonomous Continuous Repeat Loop

When the operator selects 1 - Sort All, the machine enters an autonomous feedback loop. It runs continuously until the hopper is empty, detecting the end of the batch using a 3-consecutive-empty heuristic:

# The Autonomous 'Sort All' Repeat Loop:
empty_count = 0
now_bucket = 0

while empty_count < 3:
    now_bucket, last_scan = next_scan_and_release(now_bucket)
    
    if now_bucket == 0:  # Empty pocket or unclassified reject
        empty_count += 1
    else:
        empty_count = 0  # Valid candy found, reset counter

# Graceful termination
status_display(disp, draw, image, width, height, text=["Finished", "Sorting"])
stepper_motor1.release()
stepper_motor2.release()
Why 3 consecutive empty pockets?A single empty pocket might just be an agitation hiccup from the hopper. Three in a row guarantees the feeder is genuinely empty, preventing the motor from grinding indefinitely.
Step 7

Add logging

Write a timestamped line to a text file on every state transition and every classification result, so a run can be reviewed afterwards without having watched it live.

Step 8

Add one custom exception

Wrap a risky hardware call (like a sensor read) so a glitch produces a clear, named error instead of a crash:

class SensorError(Exception):
    pass

try:
    reading = read_sensor()
except Exception as e:
    raise SensorError(f"Sensor read failed: {e}")
Step 9

Run a full cycle

Test the complete idle → scan → classify → sort → home loop for one real Skittle.

Make it yours
  • Show live status (“Scanning…”, “It's Yellow!”, “Sorting…”) on the LCD as each state runs, not just a result at the end.
  • The bonnet gives seven inputs (four joystick directions, its click, and two buttons) — use the spare ones for something beyond start/stop, like scrolling through a short settings menu (bucket mapping, brightness) before a run starts.

If it breaks Troubleshooting

Display shows nothingConfirm SPI is enabled (raspi-config) and check the ribbon cable is fully seated at both ends and the right way round (pin 1 to pin 1) — the bonnet's own pins are fixed, so a wrong-side connection is the far more likely cause than a pin-assignment mistake.
Joystick or a button does nothingConfirm which of the bonnet's fixed GPIOs that input uses in Adafruit's own pinout guide, then check with a simple polling script before assuming the main program's logic is at fault.
The state machine seems stuckAdd a print or log line at the top of every state so you can see exactly where it's looping.
A hardware call hangs foreverAdd a timeout around it, and always keep a physical power-cycle as the last resort recovery.
This week's checkpointOne Skittle can be scanned, classified, and sorted end-to-end through the integrated loop, with the result visible on the LCD.
Next week: Final assembly, calibration, and the live demo.