Skip to content

Programming the Pi 500+'s Per-Key RGB Keyboard Lighting

Summary

This chapter covers the Raspberry Pi 500+'s signature feature: per-key RGB lighting. It explains the keyboard matrix, key scan codes, and RGB LED zones, and how the keyboard's Python API and key-press/key-release events expose that hardware to code. It walks through lighting-effect programming -- color gradients, animation loops, and presets -- and reactive, event-driven lighting patterns with classroom-relevant applications such as a typing-speed indicator, a notification pattern, an accessibility lighting cue, and a Pomodoro timer. Students finishing this chapter will be able to write a Python program that lights individual keys in response to typing activity or a custom event.

Concepts Covered

This chapter covers the following 28 concepts from the learning graph:

  1. Per Key RGB Lighting
  2. Keyboard Matrix
  3. Key Scan Code
  4. Keyboard Firmware
  5. RGB LED Zone
  6. Lighting Effect Script
  7. Key Press Event
  8. Key Release Event
  9. Color Gradient Effect
  10. Typing Speed Indicator
  11. Notification Lighting Pattern
  12. Keyboard API
  13. Python Keyboard Library
  14. Custom Key Mapping
  15. Macro Key
  16. Lighting Animation Loop
  17. Reactive Lighting
  18. Ambient Lighting Mode
  19. Keyboard Shortcut
  20. USB HID Protocol
  21. Human Interface Device
  22. Keyboard Backlight Brightness
  23. Lighting Preset
  24. Event Driven Lighting
  25. Visual Feedback Design
  26. Accessibility Lighting Cue
  27. Game Status Indicator
  28. Pomodoro Timer Lighting

Prerequisites

This chapter builds on concepts from:


Let's Light Up Every Single Key!

Berry waving welcome Remember the NeoPixel strip from Chapter 5, where you controlled a whole line of LEDs together? The Pi 500+ takes that idea and puts one of those LEDs under every single key you can press. This chapter is where you learn to control every one of them individually, from Python. Let's build something berry bright!

Back in Chapter 5, a NeoPixel strip gave you a line of individually addressable LEDs to animate. The Raspberry Pi 500+ builds that same idea directly into its keyboard: every key has its own tiny RGB LED underneath it, controllable one key at a time from a Python program. This chapter walks up from the hardware that makes that possible, through the official Python library that controls it, to genuinely useful classroom effects — a typing speed indicator, a notification flash, an accessibility cue, a Pomodoro timer built entirely out of light.

How the Keyboard Sees a Key Press

Before lighting up a single key, it helps to understand how the keyboard notices you pressed one in the first place. A keyboard is one example of a human interface device (HID) — the general category of USB devices designed for a person to interact with a computer directly, which also includes mice and game controllers. Keyboards, along with most HID devices, communicate with a computer using the USB HID protocol, a standardized way of describing input events (like "key 42 was pressed") over a USB connection so that any operating system can understand any compliant device without needing custom drivers for each one.

Inside the keyboard itself, a small dedicated computer runs the keyboard firmware — the software built into the keyboard's own controller chip that scans for key presses, manages the RGB LEDs, and reports events to your Raspberry Pi OS over USB HID. To efficiently check 84 keys many times per second, the firmware doesn't wire each key to its own individual signal line. Instead, it arranges keys into a keyboard matrix: a grid of rows and columns where each key sits at one row/column intersection, letting the firmware detect a press by rapidly scanning each row and checking which columns respond. When a specific intersection is triggered, the firmware reports a key scan code — a numeric identifier for that specific physical key position, independent of what letter or symbol is printed on its keycap.

Now that matrix positions and scan codes have both been defined, the diagram below lets you explore how a specific key press turns into a specific, addressable identifier.

Diagram: Keyboard Matrix and Scan Code Explorer

Run the Keyboard Matrix and Scan Code Explorer MicroSim fullscreen

Keyboard Matrix and Scan Code Explorer (interactive diagram)

Type: interactive-diagram sim-id: keyboard-matrix-scan-code-explorer
Library: p5.js
Status: Specified

Learning objective: Students will explain (Bloom L2: Understand) how a keyboard matrix's row/column grid and a resulting scan code identify a specific pressed key.

Canvas: 700x420px, responsive — recompute key-grid cell size as a fraction of width inside windowResized() so the simplified keyboard grid scales without changing its row/column proportions.

Layout: a simplified 6-row keyboard grid (matching the real Pi 500+'s six rows referenced elsewhere in this book) drawn as evenly spaced rounded-rectangle keys, each labeled with a letter/symbol as on a real keyboard. A thin highlighted row-line and column-line sweep across the grid on a timer, visually representing the firmware's scanning process.

Controls: a createButton() labeled "Press a Random Key" that simulates a press at a random grid position; a createSlider() labeled "Scan Speed" (1-10) controlling how fast the row/column sweep animates.

Interaction: clicking any key directly (in addition to the random-press button) highlights that key, freezes the row/column sweep at its intersection, and opens an infobox showing that key's [row, column] matrix position and a generated scan code number, alongside a one-sentence reminder that the scan code identifies the physical position, not the printed letter. Clicking elsewhere clears the highlight and resumes scanning.

Implementation: p5.js. Store keys as a 2D array with row/column indices and printed labels. Compute a scan code as row * numColumns + column for demonstration purposes. Animate the sweep using frameCount modulo a period derived from the speed slider. Rectangle-based hit-testing in mousePressed().

Individually Addressable Light: Per-Key RGB

Once the firmware knows exactly which key was pressed, that same matrix addressing scheme is what makes per-key RGB lighting possible: the ability to set the color of each individual key's LED independently, rather than controlling the whole keyboard (or large sections of it) as a single unit the way a simpler backlit keyboard would. Sometimes, though, you want to treat a group of related keys as one unit anyway — an RGB LED zone is a named group of keys (like "the WASD keys" or "the function row") that a lighting effect targets together, even though each key underneath is still addressed individually.

Alongside color, every key's brightness can be controlled too. Keyboard backlight brightness is a single global setting controlling how intensely every lit key glows, independent of the color chosen for each one — useful for dimming the whole keyboard in a darkened classroom without changing any of the colors themselves.

Berry's Key Insight

Berry thinking Per-key RGB lighting and the NeoPixel strip from Chapter 5 are solving the exact same underlying problem — controlling many individually addressable LEDs from code — just packaged differently. Everything you already know about color and animation transfers directly; only the specific library and addressing scheme change.

Talking to the Keyboard from Python

Controlling the per-key LEDs from a Python program means going through an official software layer rather than talking to the keyboard firmware directly. A keyboard API is the set of functions a program is allowed to call to control the keyboard's behavior — reading its state, setting LED colors, changing presets — without needing to know how those requests are actually transmitted over USB HID underneath. On the Pi 500+, that API is provided by the Python keyboard library, a package called RPiKeyboardConfig, installed with pip install RPiKeyboardConfig, that wraps the low-level USB communication in ordinary Python method calls.

Any Python program you write to control the LEDs is a lighting effect script — a general term for a program whose purpose is to produce some specific lighting behavior, whether that's a single static color, an animation, or a reaction to something happening on the computer. The example below is a minimal lighting effect script that connects to the keyboard and sets two keys to specific colors.

from RPiKeyboardConfig import RPiKeyboardConfig

keyboard = RPiKeyboardConfig()          # connect to the keyboard over USB HID
keyboard.set_led_direct_effect()        # switch to direct, per-key LED control

keyboard.set_led_by_matrix(matrix=[2, 2], colour=(85, 255, 255))   # W key, green
keyboard.set_led_by_matrix(matrix=[3, 1], colour=(0, 255, 255))    # A key, red
keyboard.send_leds()                    # actually push the queued colors to the keys

keyboard.close()                        # release the USB HID connection

RPiKeyboardConfig() opens a connection to the keyboard's controller, auto-detecting it over USB HID. set_led_direct_effect() tells the firmware to hand LED control over to your script instead of running one of its own built-in effects. set_led_by_matrix(matrix=[row, col], colour=(h, s, v)) queues a color for one specific key, addressed by the same row/column matrix position introduced earlier in this chapter — colors are given as HSV (hue, saturation, value) rather than RGB, each on a 0–255 scale. Queued colors don't take effect until send_leds() is called, which pushes every queued change to the keyboard at once. Finally, close() cleanly releases the connection, which matters because the keyboard can only be controlled by one connection at a time.

Before the diagram below, notice the layering this example reveals: your lighting effect script calls the Python keyboard library, which is the keyboard API in this case, which communicates with the keyboard firmware, which finally lights an actual LED.

Diagram: Keyboard API Call Flow

Run the Keyboard API Call Flow MicroSim fullscreen

Keyboard API Call Flow (workflow diagram)

Type: workflow-diagram sim-id: keyboard-api-call-flow
Library: p5.js
Status: Specified

Learning objective: Students will analyze (Bloom L4: Analyze) the layers between a Python lighting effect script and an illuminated key, from the keyboard API down to the firmware.

Canvas: 700x360px, responsive — recompute the four-box horizontal layout as fractions of width inside windowResized(), stacking vertically below 480px wide.

Layout: four connected boxes left to right: "Your Lighting Effect Script (Python)" → "Python Keyboard Library / Keyboard API (RPiKeyboardConfig)" → "USB HID Protocol (over the USB cable)" → "Keyboard Firmware (sets the physical LED)." Each box uses a distinct color from the book's palette, with a small animated dot traveling left to right along the connecting arrows when triggered.

Controls: a createButton() labeled "Trace set_led_by_matrix() Call" that animates the dot traveling through all four boxes in sequence, pausing briefly at each one.

Interaction: clicking any box (independent of the animated trace) opens an infobox with a one-sentence explanation of that layer's job, matching the chapter's prose. A small always-visible caption below the diagram reads "Your script never talks to the firmware directly — the keyboard API is the layer that makes that safe and simple."

Implementation: p5.js. Store boxes as an array of {label, x, width, color, definition} objects. Animate the trace dot with linear interpolation between box centers over a fixed duration per segment, triggered by the button and paused at each box for a short dwell time. Rectangle-based hit-testing in mousePressed() for the static click interaction.

Berry's Tip

Berry sharing a tip HSV color values aren't the most intuitive to memorize by hand. Python's built-in colorsys module converts familiar RGB values (0-255 each) into the HSV tuples the keyboard API expects, so you can keep thinking in RGB and let one small helper function handle the conversion.

Presets and Animated Effects

Setting individual keys is useful, but most real lighting effects either save a configuration for later or change continuously over time. A lighting preset is a saved LED configuration stored in one of several numbered slots on the keyboard itself, so a specific effect (or a specific static color scheme) can be recalled instantly without re-running a script. Recalling one is as simple as calling keyboard.set_current_preset_index(3).

Effects that change continuously rely on a lighting animation loop: a program loop that repeatedly recalculates and re-sends LED colors, usually pausing briefly between updates, so the keyboard appears to animate smoothly over time rather than jumping between static states. One common animated effect is a color gradient effect, where colors blend smoothly from one hue to another across the keyboard, based on each key's physical position rather than every key sharing one identical color.

import time

def rainbow_wave(keyboard, duration=10):
    start = time.time()
    while time.time() - start < duration:
        for led in keyboard.get_leds():
            hue = (led.x * 3 + int(time.time() * 50)) % 256
            keyboard.set_led_by_idx(idx=led.idx, colour=(hue, 255, 255))
        keyboard.send_leds()
        time.sleep(0.03)

This function is a lighting animation loop: the while condition keeps it running for duration seconds, and each pass through the loop recalculates every LED's hue based on its x coordinate (creating the gradient effect across the keyboard) combined with the current time (creating motion), then sends the updated colors and pauses briefly with time.sleep(0.03) before repeating. An effect like this, running continuously regardless of what the user is doing, is an example of an ambient lighting mode: lighting meant to run as background atmosphere rather than to communicate a specific, timely piece of information.

Diagram: Lighting Animation Loop and Gradient Simulator

Run the Lighting Animation Loop and Gradient Simulator MicroSim fullscreen

Lighting Animation Loop and Gradient Simulator (MicroSim)

Type: microsim sim-id: lighting-animation-loop-simulator
Library: p5.js
Status: Specified

Learning objective: Students will analyze (Bloom L4: Analyze) how an animation loop's per-frame hue calculation, based on key position and time, produces a moving color gradient effect.

Canvas: 700x360px, responsive — recompute the simulated key grid's cell size as a fraction of width inside windowResized().

Layout: a simplified row of 20 keys rendered as colored squares, each key's color computed live from a formula matching the chapter's rainbow_wave code (hue = (x * factor + time * speed) % 256), animating a smooth traveling gradient across the row.

Controls: a createSlider() labeled "Gradient Spread (x factor)" (range 1-10, default 3) controlling how much each key's horizontal position affects its hue; a createSlider() labeled "Animation Speed" (range 0-100, default 50) controlling the time-based component; a createButton() labeled "Pause/Resume."

Interaction: dragging either slider updates the animation live, letting a student see how increasing "Gradient Spread" packs more color variety across the same 20 keys, while increasing "Animation Speed" makes the gradient visibly scroll faster. Clicking any individual key square pauses the animation and opens an infobox showing that key's current computed hue value and the formula used to compute it, tying the visual directly back to the code shown in the chapter.

Implementation: p5.js, recomputing each key's hue every frame with (x * spreadSlider.value() + frameCount * speedSlider.value() / 20) % 256, converted to RGB for fill() using colorMode(HSB). Square positions recalculated on windowResized(). Rectangle hit-testing for the click interaction.

Reacting to Typing: Events and Reactive Lighting

Ambient effects like the rainbow wave run the same way no matter what you do at the keyboard. Many of the most useful lighting effects instead respond directly to your typing, which means going back to an idea from Chapter 1: event-driven programming. A key press event is the specific moment a key transitions from up to down, and a key release event is the moment it transitions back from down to up — both are discrete, timestamped occurrences a program can detect and react to individually, rather than continuously polling the keyboard's whole state. Lighting built around responding to these events, rather than running on a fixed timer, is called event-driven lighting — the same event-driven programming pattern from Chapter 1, applied to LEDs instead of a robot's motors.

The most direct application of event-driven lighting is reactive lighting: an effect where pressing a specific key visibly changes that key's LED (or nearby keys') immediately, in direct response to the key press event itself, rather than on any independent schedule. The keyboard firmware includes several built-in reactive effects — for example, a key that briefly flashes bright white the instant it's pressed, then fades back to its base color over the next second or so.

Now that both ambient and reactive lighting have been defined, the table below summarizes when each is the better design choice.

Ambient Lighting Mode Reactive Lighting
Triggered by A continuous timer/loop A key press or release event
Changes when Constantly, regardless of typing Only when a relevant key event occurs
Communicates General mood or background status A specific, timely action just taken
Example from this chapter The rainbow wave gradient A key flashing white the instant it's pressed

Diagram: Reactive Event-Driven Lighting Explorer

Run the Reactive Event-Driven Lighting Explorer MicroSim fullscreen

Reactive Event-Driven Lighting Explorer (MicroSim)

Type: microsim sim-id: reactive-event-driven-lighting-explorer
Library: p5.js
Status: Specified

Learning objective: Students will analyze (Bloom L4: Analyze) how a key press event and a key release event each independently trigger a reactive lighting change.

Canvas: 700x360px, responsive — recompute the simulated keyboard row's cell size as a fraction of width inside windowResized().

Layout: a simulated row of 10 clickable keys. Clicking and holding a key simulates a key press event; releasing simulates a key release event. On press, that key's square instantly flashes bright white then fades to a chosen accent color over roughly one second; on release, a smaller secondary ripple effect briefly lights the two adjacent keys, visually distinguishing the two separate events.

Controls: a createSelect() dropdown labeled "Reactive Style" with two options, "Solid Reactive" (single key flashes) and "Ripple Reactive" (flash spreads outward to neighboring keys before fading), swapping which built-in-style effect is simulated.

Interaction: each simulated press/release logs a one-line timestamped entry to a small event log below the keyboard row, explicitly labeled "Key Press Event" or "Key Release Event," reinforcing that these are two distinct, separately detectable moments. Clicking a log entry re-highlights the corresponding key.

Implementation: p5.js, using mousePressed()/mouseReleased() on each key's bounding box to fire the two separate simulated events. Fade animation implemented with a per-key "time since triggered" value decremented each frame and mapped to brightness. Event log stored as a capped array of strings rendered with text().

Berry's Key Insight

Berry thinking Polling versus interrupts, from Chapter 1, has a direct echo here: you could constantly poll get_switch_matrix_state() in a loop to notice key changes yourself, or let event-driven lighting handle it. Just like with sensors, reacting to events tends to be simpler to write and more responsive than polling for the same information.

Designing Lighting That Communicates: Classroom-Ready Effects

All of the pieces so far — colors, presets, animation loops, and events — exist to serve one goal: using light to tell a person something useful, quickly. Visual feedback design is the practice of choosing colors, timing, and patterns so that a light-based signal is clear, quickly recognizable, and not easily confused with a different signal, the same design thinking behind a traffic light or a smoke detector's blinking status LED. Every classroom effect below is really an exercise in visual feedback design, applied to a specific situation.

A typing speed indicator maps a calculated typing speed (often words per minute) to a color across the whole keyboard — slow typing shown as blue, fast typing shifting toward red, for instance — giving a continuously updating, at-a-glance sense of pace. A notification lighting pattern flashes the keyboard (or a specific set of keys) a distinct color a fixed number of times when a chosen event occurs, such as a new message arriving, similar in spirit to a phone's notification light. An accessibility lighting cue uses a deliberately chosen, high-contrast color and timing pattern to communicate information that might otherwise rely on sound alone — useful for a student who is deaf or hard of hearing, or simply working in a noisy classroom where an audio alert would be missed. A game status indicator repurposes the keyboard as an in-game display, for example lighting the WASD movement keys red when a game character's health drops low. A Pomodoro timer lighting effect gradually shifts a color, or lights a shrinking bar of keys, to show how much time remains in a work or break interval from the well-known Pomodoro time-management technique.

The notification effect below flashes every key a chosen color a set number of times, then restores whatever preset was active before the script ran.

import time

def flash_keyboard(keyboard, color, times=3, delay=0.2):
    for _ in range(times):
        for led in keyboard.get_leds():
            keyboard.set_led_by_idx(idx=led.idx, colour=color)
        keyboard.send_leds()
        time.sleep(delay)
        keyboard.rgb_clear()
        keyboard.send_leds()
        time.sleep(delay)

get_leds() returns every LED on the keyboard, and the inner loop sets all of them to the same color before send_leds() pushes the change. rgb_clear() turns every LED back off, and the outer for _ in range(times) loop repeats that on/off cycle the requested number of times, producing a clean, unmistakable flash pattern — exactly the notification lighting pattern described above.

Before the diagram below, it's worth pointing out that all five classroom effects reuse the same small set of building blocks from earlier in this chapter — a lighting animation loop, event-driven lighting, or both — just aimed at a different real-world signal.

Diagram: Classroom Lighting Effects Picker

Run the Classroom Lighting Effects Picker MicroSim fullscreen

Classroom Lighting Effects Picker (interactive infographic)

Type: interactive-infographic sim-id: classroom-lighting-effects-picker
Library: p5.js
Template: https://github.com/dmccreary/moving-rainbow/tree/main/docs/sims/brightness-envelope-comparison
Status: Specified

Learning objective: Students will evaluate (Bloom L5: Evaluate) which lighting design — typing speed indicator, notification pattern, accessibility cue, game status indicator, or Pomodoro timer lighting — best fits a given classroom visual-feedback scenario.

Canvas: 700x460px, responsive — recompute the five-card grid layout as fractions of width inside windowResized(), reflowing from a 5-across row to a 2-column grid below 600px wide.

Layout: five labeled cards, one per classroom effect (Typing Speed Indicator, Notification Lighting Pattern, Accessibility Lighting Cue, Game Status Indicator, Pomodoro Timer Lighting), each showing a small animated preview using a simplified 5-key strip (e.g., the Pomodoro card slowly shrinks a lit segment; the notification card flashes on a timer; the accessibility card pulses a single, deliberately high-contrast color).

Controls: a createSelect() dropdown labeled "Classroom Scenario" with five preset scenario descriptions (e.g., "A student wants to know their typing speed while practicing," "The class needs a silent alert when a shared robot's battery is low"), each highlighting the one card judged the best fit when selected.

Interaction: clicking any card opens an infobox with the one-sentence definition of that effect matching the chapter's prose, plus the specific classroom scenario it's built for. Selecting a scenario from the dropdown and then clicking the highlighted card triggers a brief "Correct fit!" confirmation animation, giving the interaction a light self-check quality without being a formal quiz.

Implementation: p5.js. Store the five effects as an array of {name, definition, scenario, previewFn} objects, each with its own small animated preview function called every frame. Card layout recalculated on resize; hit-testing via rectangle bounds in mousePressed().

Berry's Gentle Warning

Berry warning Rapid, high-contrast flashing (like a fast notification pattern) can be genuinely uncomfortable, or worse, for people with photosensitive conditions. When you design a notification lighting pattern for a shared classroom device, keep flashes slow and few, and consider a gentler pulse instead of a hard flash as the default.

Keys That Do More: Mapping and Macros

Two more keyboard features round out this chapter, and while they're not about RGB color directly, they use the same matrix and firmware ideas you've now learned. Custom key mapping is reassigning which character or action a specific physical key position produces, independent of what's printed on its keycap — the same matrix position from earlier in this chapter can be told to send a completely different scan code than its label suggests. A macro key takes that a step further: it's a single key programmed to send an entire sequence of keystrokes or trigger a multi-step action when pressed once, useful for anything you find yourself typing or clicking repeatedly.

Both of these build on a simpler, more familiar idea you already use every day: a keyboard shortcut, a combination of keys pressed together (like Ctrl+C) that triggers a specific software action rather than typing a character. Custom key mapping and macro keys essentially let you create new shortcuts of your own, or change what existing ones do, using the exact same matrix and event mechanisms this chapter has covered from the ground up.

You've Got This!

Berry encouraging you If your first script leaves the keyboard stuck showing the wrong colors, don't panic — that's a completely normal part of experimenting with direct LED control. Keep a "reset" script handy that calls rgb_clear() and restores your saved preset, and you can always get back to a known-good state in one command.

Bringing It Together

You've now taken the Pi 500+'s signature feature apart and put it back together in code — from the keyboard matrix and scan codes that notice a key press, through the Python keyboard library that gives you per-key control, to real, classroom-ready effects that communicate something useful the instant you see them. The next chapters shift to the most capable hardware tier in this book: the Raspberry Pi 5, where the same Python skills you've been building scale up to real-time camera and audio AI, running on genuinely powerful hardware.

You Unlocked a Superpower!

Berry celebrating That's berry impressive — every single key under your fingers now answers to your Python code. From a single lit key to a full reactive typing-speed display, you've built real visual feedback design, not just pretty colors. STEM is our superpower! Let's build something — see you in the next chapter!

See Annotated References