Controll WS2812 RGB Strip via Rotary Encoder

0 136 Easy

In this instruction, a WS2812 RGB Strip (NeoPixel) is controlled by a 360° Rotary Encoder (I2C). The UNIHIKER display also shows the user which color values ​​are currently being used. This gives non-technical users a very simple interface and input option.

 

Note: In this tutorial, 5 LEDs are used. If you have a certain number of LEDs, you should think about an additional power supply for the RGB strip!

HARDWARE LIST
1 UNIHIKER
1 360 Degree Rotary Encoder
1 WS2812 RGB LED Strip
1 Gravity: 3 Pin PH2.0 to DuPont Male Connector Analog Cable
STEP 1
Connect everything

The individual connections are incredibly simple! Only the connection of the RGB strip (+, -, DIN) to the UNIHIKER should be made with some care, otherwise it can lead to problems. The RGB strip may also need to be cut to the appropriate length beforehand. Before doing so, disconnect the power connection from the UNIHIKER. You should always make connections while the power is turned off.

 

UNIHIKER and 360° Rotary Encoder

 

Connect the rotary encoder to the UNIHIKER via Gravity: 4Pin (I2C). You can freely choose which of the 2 options (on the UNIHIKER) you use (this has no influence on the code later because the address of the Rotary Encoder is predetermined).

 

UNIHIKER and RGB LED Strip

 

Connect the WS2812 RGB LED Strip to the UNIHIKER via Gravity: 3 Pin PH2.0. Make sure that the polarity for +, - and DIN is correct! In this example the PIN Interface 21 is used. In case you choose an other PIN on UNIHIKER, you need to adapt later the code. In the example picture below, I simply soldered the respective Dupont ends to the strip.

 

STEP 2
Create the application structure

For this step, you can connect the UNIHIKER device to your computer using the included USB cable. This should not cause any problems and should enable the development and later upload of the code to UNIHIKER.

 

The following file and folder structure is used in these instructions:

 

 

The root folder is named “NeoPixel”, inside is the file “main.py” and another folder “lib” with two more files “next_value_generator.py” and “rotary_encoder.py”.

 

The actual program logic is created in the “main.py” file. The both files inside the “lib” folder are classes which are imported into “main.py”. This has the advantage that the code remains much clearer, is better structured and the two classes can be reused for other projects. 

STEP 3
Develop the Python code

If you've followed my previous tutorials, you won't be surprised that Python is my personal favorite. But whatever...let's get started.

 

The first class in the file "next_value_generator.py" should simply return a value between 1 and 3. The number should increase with each call and return to 1 after the value 3. The idea behind it is that one of the RGB values ​​is always selected. The trigger is then the Rotary Encoder Button itself.

CODE
class NextValueGenerator:
    """
    Generates numbers class (1, 2, 3)
    """

    def __init__(self, start_value: int):
        """
        Class constructor
        :param start_value: starting value
        """
        if not 1 <= int(start_value) <= 3:
            start_value = 1

        self._current_value = int(start_value)

    def get_next_value(self) -> int:
        """
        Return value between 1 and 3
        :return: current value
        """
        self._current_value = (self._current_value % 3) + 1
        return self._current_value

The other file "rotary_encoder.py" contains the Python class for the Rotary Encoder itself. The original Python code for the Raspberry Pi is adapted here to the UNIHIKER. I was a bit lazy and didn't create any DocStrings. But that really shouldn't be a problem because the code is really very simple.

CODE
from pinpong.board import I2C


VISUAL_ROTARY_ENCODER_DEFAULT_I2C_ADDR = 0x54


class RotaryEncoder:

    VISUAL_ROTARY_ENCODER_PID = 0x01F6
    VISUAL_ROTARY_ENCODER_PID_MSB_REG = 0x00
    VISUAL_ROTARY_ENCODER_COUNT_MSB_REG = 0x08
    VISUAL_ROTARY_ENCODER_KEY_STATUS_REG = 0x0A
    VISUAL_ROTARY_ENCODER_GAIN_REG = 0x0B

    def __init__(self, i2c_addr=VISUAL_ROTARY_ENCODER_DEFAULT_I2C_ADDR, bus=0):
        self._addr = i2c_addr

        try:
            self._i2c = I2C(bus)
        except Exception as err:
            print(f'Could not initialize i2c! bus: {bus}, error: {err}')

    def _write_reg(self, reg, data) -> None:
        if isinstance(data, int):
            data = [data]

        try:
            self._i2c.writeto_mem(self._addr, reg, bytearray(data))
        except Exception as err:
            print(f'Write issue: {err}')

    def _read_reg(self, reg, length) -> bytes:
        try:
            result = self._i2c.readfrom_mem(self._addr, reg, length)
        except Exception as err:
            print(f'Read issue: {err}')
            result = [0, 0]

        return result

    def _device_info(self) -> str:
        data = self._read_reg(self.VISUAL_ROTARY_ENCODER_PID_MSB_REG, 8)

        pid = (data[0] << 8) | data[1]
        vid = (data[2] << 8) | data[3]
        version = (data[4] << 8) | data[5]
        i2c_addr = data[7]

        return f'PID: {pid}, VID: {vid}, Version: {version}, I2C: {i2c_addr}'

    def set_gain_coefficient(self, gain_value: int) -> None:
        if 0x01 <= int(gain_value) <= 0x33:
            self._write_reg(self.VISUAL_ROTARY_ENCODER_GAIN_REG, int(gain_value))

    def set_encoder_value(self, value: int) -> None:
        if 0x0000 <= int(value) <= 0x3FF:
            temp_buf = [(int(value) & 0xFF00) >> 8, int(value) & 0x00FF]
            self._write_reg(self.VISUAL_ROTARY_ENCODER_COUNT_MSB_REG, temp_buf)

    def coefficient(self) -> int:
        return self._read_reg(self.VISUAL_ROTARY_ENCODER_GAIN_REG, 1)[0]

    def value(self) -> int:
        data = self._read_reg(self.VISUAL_ROTARY_ENCODER_COUNT_MSB_REG, 2)
        return (data[0] << 8) | data[1]

    def __str__(self):
        return self._device_info()

    def __bool__(self):
        if 1 == self._read_reg(self.VISUAL_ROTARY_ENCODER_KEY_STATUS_REG, 1)[0]:
            self._write_reg(self.VISUAL_ROTARY_ENCODER_KEY_STATUS_REG, 0)
            return True
        else:
            return False

Finally, the code for “main.py”. Various imports are made here (including the two classes from lib) and the logic for the application is created. The value of the constant NEOPIXEL_NUM: int = 5 should be adjusted to the respective number of LEDs. If you use a different UNIHIKER PIN, adjust it! To do this, look for this place in the code: led = NeoPixel(Pin(Pin.P21), NEOPIXEL_NUM). As already described, Pin.P21 is used in the example.

CODE
from time import sleep
from tkinter import Tk, Canvas

from pinpong.board import Board, Pin, NeoPixel

from lib.next_value_generator import NextValueGenerator
from lib.rotary_encoder import RotaryEncoder


SCREEN_WIDTH: int = 240
SCREEN_HEIGHT: int = 320
DELAY_SECONDS: float = .25
NEOPIXEL_NUM: int = 5
DEFAULT_FONT: tuple = ("Helvetica", 10)


def draw_squares(target_canvas, red_height: int, green_height: int, blue_height: int, current_pos: int) -> None:
    """
    Draw squares and text on target canvas
    :param target_canvas: target tkinter canvas
    :param red_height: red height (between 0 and 255)
    :param green_height: green height (between 0 and 255)
    :param blue_height: blue height (between 0 and 255)
    :param current_pos: current position (between 1 and 3)
    :return: None
    """
    target_canvas.delete("all")

    font_configs = {
        1: (16, "bold"),
        2: (16, "bold"),
        3: (16, "bold")
    }

    if not 0 <= int(red_height) <= 255:
        red_height = 0

    if not 0 <= int(green_height) <= 255:
        green_height = 0

    if not 0 <= int(blue_height) <= 255:
        blue_height = 0

    if not 1 <= int(current_pos) <= 3:
        current_pos = 1

    target_canvas.create_rectangle(20, SCREEN_HEIGHT - int(red_height), 60, SCREEN_HEIGHT, fill="red")
    red_txt = target_canvas.create_text(40, 25, text=str(red_height), font=DEFAULT_FONT)

    target_canvas.create_rectangle(100, SCREEN_HEIGHT - int(green_height), 140, SCREEN_HEIGHT, fill="green")
    green_txt = target_canvas.create_text(120, 25, text=str(green_height), font=DEFAULT_FONT)

    target_canvas.create_rectangle(180, SCREEN_HEIGHT - int(blue_height), 220, SCREEN_HEIGHT, fill="blue")
    blue_txt = target_canvas.create_text(200, 25, text=str(blue_height), font=DEFAULT_FONT)

    for txt, pos in zip([red_txt, green_txt, blue_txt], range(1, 4)):
        font = DEFAULT_FONT if pos != current_pos else ("Helvetica", *font_configs[current_pos])
        target_canvas.itemconfig(txt, font=font)


def map_value(value: int, in_min: int, in_max: int, out_min: int, out_max: int) -> int:
    """
    Map value between in_min, in_max and out_min, out_max
    :param value: value to map
    :param in_min: min value
    :param in_max: max value
    :param out_min: min value
    :param out_max: max value
    :return: number for mapped value
    """
    return (value - in_min) * (out_max - out_min) // (in_max - in_min) + out_min


if __name__ == '__main__':
    red, green, blue = 0, 0, 0
    button_pressed = False
    position = 1

    Board().begin()
    led = NeoPixel(Pin(Pin.P21), NEOPIXEL_NUM)
    rotary_encoder = RotaryEncoder()
    generator = NextValueGenerator(start_value=position)

    window = Tk()
    window.geometry(f'{SCREEN_WIDTH}x{SCREEN_HEIGHT}+0+0')
    canvas = Canvas(window, width=SCREEN_WIDTH, height=SCREEN_HEIGHT)
    canvas.pack()

    while True:
        rotary_value = rotary_encoder.value()

        if rotary_encoder and not button_pressed:
            position = generator.get_next_value()
            button_pressed = True
        elif not rotary_encoder:
            button_pressed = False

        mapped_value = map_value(value=rotary_value, in_min=0, in_max=1023, out_min=0, out_max=255)

        if position == 1:
            red = mapped_value
        elif position == 2:
            green = mapped_value
        elif position == 3:
            blue = mapped_value

        for i in range(NEOPIXEL_NUM):
            led[i] = (red, green, blue)

        sleep(DELAY_SECONDS)

        draw_squares(target_canvas=canvas, red_height=red, green_height=green, blue_height=blue, current_pos=position)

        window.update_idletasks()
        window.update()
STEP 4
Upload to UNIHIKER

There are various (very simple) ways to load the folders and files onto the UNIHIKER. If you don't know them yet, find out more on this page! Here is a simple example with USB and SCP.

CODE
# upload all files and folders via scp (password: dfrobot)
$ scp -r NeoPixel root@10.1.2.3:/root/
STEP 5
Run application

Complete! The application can be started and you can set the color values ​​with the Rotary Encoder. By turning the respective values ​​are indicated and by pressing the respective value can be selected. The relevant information is shown on the UNIHIKER display.

 

STEP 6
Extensions

Take a close look at the code and try to understand it. Think of great projects yourself and develop them using the basics you learned here. Then share this with the community. If you enjoyed this guide, leave a like or comment and I'll make more like it!

License
All Rights
Reserved
licensBg
0