Note

Hello, welcome to the SunFounder Raspberry Pi & Arduino & ESP32 Enthusiasts Community on Facebook! Dive deeper into Raspberry Pi, Arduino, and ESP32 with fellow enthusiasts.

Why Join?

  • Expert Support: Solve post-sale issues and technical challenges with help from our community and team.

  • Learn & Share: Exchange tips and tutorials to enhance your skills.

  • Exclusive Previews: Get early access to new product announcements and sneak peeks.

  • Special Discounts: Enjoy exclusive discounts on our newest products.

  • Festive Promotions and Giveaways: Take part in giveaways and holiday promotions.

👉 Ready to explore and create with us? Click [here] and join today!

4.10 Hue Knob

Introduction

In this lesson, you will build a Hue Knob — an interactive color controller that uses a rotary encoder to adjust the hue of a circular WS2812 LED module. This WS2812 LED module contains 12 individually addressable WS2812 RGB LEDs, driven through the Fusion HAT+’s SPI-based NeoPixel interface. The external rotary encoder provides real-time user input via standard GPIO pins.

As you rotate the encoder, the 12 LEDs smoothly cycle through the full RGB color spectrum. Pressing the encoder’s built-in button resets the hue back to the starting value.


What You’ll Need

The following components are required for this project:

COMPONENT INTRODUCTION

PURCHASE LINK

Jumper Wires

BUY

Rotary Encoder Module

BUY

Circular WS2812 LED Module

-

Fusion HAT+

-

Raspberry Pi

-


Wiring Diagram

Refer to the wiring diagram for assembling the components:

../_images/4.10_hub_color_bb.png

Setup Steps

  1. Before running the code, you need to install the required library:

    This library provides the necessary functions to control NeoPixel LEDs using SPI communication.

    sudo pip3 install adafruit-circuitpython-neopixel-spi --break
    
  2. All example code used in this tutorial is available in the ai-lab-kit directory. Follow these steps to run the example:

    cd ~/ai-lab-kit/python/
    sudo python3 4.10_Hue_Knob.py
    
  3. When the script runs, the WS2812 module ring responds to the rotary encoder:

    • LEDs start in red (Hue 0°).

    • Rotate the encoder to smoothly cycle through the full RGB color wheel. Colors transition continuously (red → yellow → green → blue → purple → red). The terminal prints the current hue and RGB values.*

    • Press the encoder button to reset the hue back to 0° (red).

    • Press Ctrl+C to exit. All LEDs turn off before the program closes.


Code

Here is the Python script for the traffic light simulation:

#!/usr/bin/env python3

from fusion_hat.motor import Motor
from fusion_hat.pin import Pin, Mode, Pull
from fusion_hat.adc import ADC
from time import sleep, time
import math

BtnPin = Pin(22, mode=Mode.IN, pull=Pull.DOWN)
motor = Motor("M0")
thermistor = ADC("A3")

level = 0
currentTemp = None
markTemp = None

PRINT_INTERVAL = 1.0
_last_print = 0.0

button_event = False  # flag: button was pressed

def temperature(samples=5, delay=0.01):
   """Read thermistor multiple times and return averaged Celsius (float) or None."""
   vals = []
   for _ in range(samples):
      analogVal = thermistor.read()
      Vr = 3.3 * float(analogVal) / 4095.0
      if (3.3 - Vr) <= 0.1:
            return None
      Rt = 10000.0 * Vr / (3.3 - Vr)
      tempK = 1.0 / (((math.log(Rt / 10000.0)) / 3950.0) + (1.0 / (273.15 + 25.0)))
      vals.append(tempK - 273.15)
      sleep(delay)
   return sum(vals) / len(vals)

def motor_run(lv):
   lv = max(0, min(4, lv))
   motor.power(0 if lv == 0 else lv * 25)
   return lv

def changeLevel():
   """Button press: cycle level 0~4 and set a flag for main loop to print."""
   global level, button_event
   level = (level + 1) % 5
   button_event = True

BtnPin.when_activated = changeLevel

def main():
   global level, currentTemp, markTemp, _last_print, button_event

   markTemp = temperature()
   while True:
      currentTemp = temperature()
      if currentTemp is None:
            print("Sensor read failed. Please check the sensor.")
            sleep(0.5)
            continue

      # Handle button event in main loop (stable timing)
      if button_event:
            button_event = False
            markTemp = currentTemp
            print(f"[Button] Level -> {level} | Temp: {currentTemp:.2f} °C | Mark: {markTemp:.2f} °C")

      # Periodic temperature print
      now = time()
      if now - _last_print >= PRINT_INTERVAL:
            if markTemp is None:
               markTemp = currentTemp
            print(f"Temp: {currentTemp:.2f} °C | Mark: {markTemp:.2f} °C | Level: {level}")
            _last_print = now

      # Auto adjust level based on ±5°C
      if markTemp is None:
            markTemp = currentTemp

      if level != 0:
            diff = currentTemp - markTemp
            if diff <= -5:
               level = max(0, level - 1)
               markTemp = currentTemp
               print(f"[Auto] Temp down -> Level {level} (Temp: {currentTemp:.2f} °C)")
            elif diff >= 5:
               level = min(4, level + 1)
               markTemp = currentTemp
               print(f"[Auto] Temp up   -> Level {level} (Temp: {currentTemp:.2f} °C)")

      level = motor_run(level)
      sleep(0.5)

try:
   main()
except KeyboardInterrupt:
   print("\nExiting...")
finally:
   motor.stop()
   sleep(0.1)

Understanding the Code

  1. NeoPixel Initialization

    • The script uses SPI to control a NeoPixel LED ring/strip (LED_COUNT = 12).

    • auto_write=False is enabled so the LEDs update only when strip.show() is called, which prevents flicker and improves performance.

    • The LEDs are cleared at startup using strip.fill(0) and strip.show().

  2. Rotary Encoder Hardware Setup

    • Three GPIO pins are used: - CLK_PIN and DT_PIN provide the quadrature signals for rotation direction and steps. - SW_PIN is the encoder button input.

    • All pins use internal pull-ups (Pull.UP), and the button is active LOW (pressed = 0).

  3. Key Parameters (Tuning Behavior)

    • DETENTS_PER_CYCLE defines how many physical “clicks” are needed to complete a full hue rotation (0–360°). - A larger value gives finer color control.

    • TRANSITIONS_PER_DETENT converts raw quadrature transitions into “one detent = one count”. - Many encoders output 2 transitions per detent, but some produce 4. Adjusting this value improves accuracy.

  4. hue_to_rgb()

    • Converts a hue value in the range 0.0 ~ 1.0 into an RGB tuple (R, G, B) with values 0 ~ 255.

    • This makes it easy to generate smooth color changes using the HSV color model.

  5. apply_color_from_detent()

    • Maps the encoder detent count to a hue index using modulo: - hue_idx = detent % DETENTS_PER_CYCLE

    • Converts the hue into an RGB color and updates all LEDs with strip.fill(color) followed by strip.show().

    • Uses last_hue_idx to avoid refreshing the LEDs when the calculated hue does not change, which reduces unnecessary updates.

    • Prints the current hue angle, detent value, and RGB color for debugging/feedback.

  6. reset_all()

    • Resets the internal counter state: - raw (raw transition count) - last_detent (last displayed detent) - last_hue_idx (last displayed hue index)

    • Calls apply_color_from_detent(0) to immediately restore the LEDs to the starting color (Hue = 0°).

  7. Main Loop (Polling + Direction Detection)

    • The program polls CLK continuously and checks for changes: - If CLK changes, the encoder moved one step in the quadrature sequence.

    • Direction is determined by comparing DT to CLK: - dt.value() != c means one rotation direction (increment) - otherwise decrement

    • The raw transition count is converted into detents: - detent = raw // TRANSITIONS_PER_DETENT

    • The LED color is updated only when the detent value changes.

  8. Button Reset and Debounce

    • When the button is pressed (sw.value() == 0), the program calls reset_all().

    • A short debounce delay is applied, and the script waits until the button is released to prevent multiple resets from a single press.

  9. Clean Exit

    • Pressing Ctrl + C exits the program.

    • In the finally block, all LEDs are turned off (strip.fill(0) and strip.show()) so the hardware is left in a safe state.

Troubleshooting

  • LEDs do not light up

    • Check wiring to the WS2812 LED module.

    • Ensure the Fusion HAT+ SPI NeoPixel interface is enabled.

    • Make sure you are using a supported WS2812/WS2812B LED module.

  • Colors change too quickly or too slowly

    • Adjust STEPS_PER_CYCLE to refine or increase sensitivity.

  • Button press does not reset hue

    • Verify SW is connected to GPIO27.

    • Ensure the pin is configured with pull=Pin.PULL_UP.

  • Script exits immediately

    • Confirm pause() is imported from signal.

    • Make sure no other process is using SPI.

Try It Yourself

Want to extend this project further? Try these ideas:

  1. Add Brightness Control

    Use another variable (e.g., encoder press + rotation) to adjust LED brightness from 0–255.

  2. Add Multiple Display Modes

    Press the encoder to cycle through modes:

    • Solid color (default)

    • Rainbow animation

    • Breathing effect

    • Color wipe

  3. Add a Power Toggle

    Long-press the encoder button to turn the LED ring on/off.

  4. Make Hue Change Smoother

    Increase STEPS_PER_CYCLE or add interpolation for buttery-smooth transitions.

  5. Show Direction Feedback

    Glow one LED green when rotating clockwise, red when counterclockwise.

These small extensions turn the simple “Hue Knob” into a versatile RGB control interface.