This tutorial guides you through building a remote-controlled RGB LED strip using an ESP32 microcontroller and a UNIHIKER single-board computer. The project enables you to control the color and animation of the LED strip using an infrared (IR) remote control. The UNIHIKER handles the IR signal processing and communicates the commands to the ESP32 via Bluetooth Low Energy (BLE).
This project is a solid foundation for more advanced IoT applications, blending hardware interaction with wireless communication and graphical interfaces. Have fun!!!!
Hardware Overview:
- UNIHIKER Single-Board Computer (mandatory)
- 2x3A DC Motor Driver Carrier Board for UNIHIKER (mandatory)
- IR Remote Control (mandatory)
- ESP32 Microcontroller (mandatory)
- RGB LED Strip (mandatory)
- Jumper Cables M/M (mandatory)
- Soldering station (mandatory)
- Silicone Case for UNIHIKER (optional)
- USB 3.0 Type-C Cable (optional)
- 3D Printer (optional - You can find the STL file in the ZIP file)
Hardware Assembling:
Connect the UNIHIKER to the Carrier Board: Align the UNIHIKER with the carrier board's connectors and gently press it into place until fully seated. Read the documentation on this page, in case you need help!
Connect the RGB LED Strip to the ESP32: Connect the RGB LED strip's data pin to GPIO 23 on the ESP32. If you choose a different GPIO, you have to change the respective constant in MicroPython. Ensure the power and ground lines of the LED strip are connected to the corresponding pins on the ESP32.
Powering the Components: The UNIHIKER and ESP32 can be powered via USB or an external power supply. Ensure the power sources are suitable for your setup.
Printing the Shape: Use the STL file to print the BullDog shape.
Attach the RGB LEDs and solder the cables: Cut the RGB LED strip to the appropriate length, glue it into the bulldog shape and solder the jumper cables to the RGB LED strip.
```shell
# install esptool
$ pip3 install esptool
# get information about needed Firmware
$ esptool.py -p [SERIAL PORT] --chip auto chip_id
$ esptool.py -p [SERIAL PORT] --chip auto flash_id
# erase flash
$ esptool.py -p [SERIAL PORT] --chip auto erase_flash
# flash firmware (example Generic on ESP32)
$ esptool.py --chip esp32 --port [SERIAL PORT] --baud 460800 write_flash -z 0x1000 [BIN FILE]
```
With "rshell" you can easily copy the necessary MicroPython code to/from the ESP32. It also has the ability to invoke the regular REPL!
```shell
# install rshell
$ pip3 install rshell
# connect to ESP32
$ rshell -p [SERIAL PORT]
# copy from file (from local to ESP32)
$ rshell -p [SERIAL PORT] [LOCAL PATH] /pyboard/
```
Below is the MicroPython code that will run on the ESP32. It controls the RGB LED strip based on BLE commands received from the UNIHIKER. The file is named main.py.
Important:
Change the constant's LED_PIN and/or LED_NUMBER depending to your needs.
from machine import Pin
from micropython import const
from neopixel import NeoPixel
from ubluetooth import BLE, UUID, FLAG_WRITE
from utime import sleep, sleep_ms
LED_PIN = const(23)
LED_NUMBER = const(19)
_ADV_TYP_NAME = const(9)
_IRQ_CONNECT = const(1)
_IRQ_DISCONNECT = const(2)
_IRQ_WRITE = const(3)
_FLAG_READ = const(0x0002)
_FLAG_WRITE_NO_RESPONSE = const(0x0004)
_FLAG_WRITE = const(0x0008)
_FLAG_NOTIFY = const(0x0010)
_UART_UUID = UUID("6E400001-B5A3-F393-E0A9-E50E24DCCA9E")
_UART_TX = (
UUID("6E400003-B5A3-F393-E0A9-E50E24DCCA9E"),
_FLAG_READ | _FLAG_NOTIFY,
)
_UART_RX = (
UUID("6E400002-B5A3-F393-E0A9-E50E24DCCA9E"),
_FLAG_WRITE | _FLAG_WRITE_NO_RESPONSE,
)
_UART_SERVICE = (
_UART_UUID,
(_UART_TX, _UART_RX),
)
def fade_out(led: NeoPixel, delay: int = 50) -> None:
"""
Fades out the LED strip by progressively reducing the brightness.
:param led: The NeoPixel object representing the LED strip.
:type led: NeoPixel
:param delay: The delay (in milliseconds) between each step of the fading (default is 50 ms).
:type delay: int
:return: None
"""
for brightness in range(255, -1, -5):
for i in range(LED_NUMBER):
led[i] = (brightness, brightness, brightness)
led.write()
sleep_ms(delay)
led.fill((0, 0, 0))
led.write()
def circle_led(led: NeoPixel, direction: bool, delay: int = 50) -> None:
"""
Animates a circular movement of LED strip.
:param led: The NeoPixel object representing the LED strip.
:type led: NeoPixel
:param direction: A boolean value indicating the direction of the LED animation.
:type direction: bool
:param delay: The delay (in milliseconds) of the LED animation (default is 50 ms).
:type delay: int
:return: None
"""
global last_color
led.fill((0, 0, 0))
led.write()
if direction:
for i in range(LED_NUMBER):
led[i] = last_color
led.write()
sleep_ms(delay)
else:
for i in range(LED_NUMBER - 1, -1, -1):
led[i] = last_color
led.write()
sleep_ms(delay)
led.fill(last_color)
led.write()
def set_led_color(led: NeoPixel, color: tuple) -> None:
"""
Set the color for all LEDs of the LED strip.
:param led: A NeoPixel object representing the LED strip.
:type led: NeoPixel
:param color: A tuple representing the RGB color values to set the LED strip to.
:type color: tuple
:return: None
"""
global last_color
if last_color != color:
last_color = color
led.fill(color)
led.write()
def process_ble_msg(message: str) -> None:
"""
Process BLE message and perform corresponding actions.
:param message: The BLE message to be processed.
:type message: str
:return: None
"""
global nps
if str(message).lower() == 'left':
circle_led(led=nps, direction=True)
elif str(message).lower() == 'right':
circle_led(led=nps, direction=False)
else:
try:
tuple_value = tuple(map(int, message.split(',')))
set_led_color(led=nps, color=tuple_value)
except ValueError:
print(f"[WARN] Raw: {repr(message)} not implemented")
def ble_advertiser(name: str) -> None:
"""
Advertise BLE (Bluetooth Low Energy) devices with a given name.
:param name: The name of the BLE advertiser. It should be a string.
:type name: str
:return: None
"""
global ble
adv_data = b'\x02\x01\x06'
name = bytes(name, 'UTF-8')
adv_data = adv_data + bytearray((len(name) + 1, _ADV_TYP_NAME)) + name
print(f'[INFO] BLE advertiser:{adv_data}')
ble.gap_advertise(500, adv_data)
def ble_services() -> None:
"""
Perform BLE service registration.
:return: None
"""
global ble
global tx
global rx
services = (_UART_SERVICE,)
((tx, rx,),) = ble.gatts_register_services(services)
def ble_irq(event: int, data: tuple) -> None:
"""
Handles interrupts related to Bluetooth Low Energy (BLE) events.
:param event: An integer representing the event that occurred.
:type event: int
:param data: A tuple containing additional data related to the event (not used).
:type data: tuple
:return: None
"""
global ble
global rx
_ = data
if event == _IRQ_CONNECT:
print("[INFO] Central connected")
elif event == _IRQ_DISCONNECT:
print("[INFO] Central disconnected")
elif event == _IRQ_WRITE:
buffer = ble.gatts_read(rx)
msg = buffer.decode('UTF-8').strip()
print(f"[INFO] A central has written this: {msg}")
process_ble_msg(message=str(msg))
if __name__ == '__main__':
tx = None
rx = None
last_color = None
nps = NeoPixel(Pin(LED_PIN), LED_NUMBER)
set_led_color(led=nps, color=(50, 50, 50))
ble_name = 'ESP32'
ble = BLE()
ble.active(True)
ble_services()
ble.irq(ble_irq)
fade_out(led=nps)
while True:
ble_advertiser(name=ble_name)
sleep(5)
The following Python code runs on the UNIHIKER and controls the IR remote to BLE communication.
Important:
Depending to your IR Remote Control, change HEX values for constant's BTN_MAPPING, BTN_RIGHT and BTN_LEFT.
With "$ hcitool lescan" you can find out the address and adjust constant BLE_ADDRESS.
If you don't like the colors, look here for other options.
from pinpong.board import Board, Pin, IRRecv
from tkinter import Tk, Canvas
from bluepy.btle import Peripheral
DISPLAY_WIDTH: int = 240
DISPLAY_HEIGHT: int = 320
BTN_MAPPING: dict = {
'0xff9867': ('black', (0, 0, 0)),
'0xffa25d': ('white', (255, 255, 255)),
'0xff629d': ('red', (255, 0, 0)),
'0xffe21d': ('green', (0, 255, 0)),
'0xff22dd': ('blue', (0, 0, 255)),
'0xff02fd': ('cyan', (0, 255, 255)),
'0xffc23d': ('yellow', (255, 255, 0)),
'0xffe01f': ('magenta', (255, 0, 255)),
'0xffa857': ('olive', (128, 128, 0)),
'0xff906f': ('gold', (255, 215, 0))
}
BTN_RIGHT: str = '0xff5aa5'
BTN_LEFT: str = '0xff10ef'
BLE_ADDRESS: str = "08:3A:F2:B7:A8:4A"
BLE_CHARACTERISTIC_UUID: str = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
def change_canvas(color_name: str) -> None:
"""
Change the color of the canvas rectangle and text.
:param color_name: The name of the color to change to.
:type color_name: str
:return: None
"""
global canvas
global rect_id
global text_id
canvas.itemconfig(rect_id, fill=color_name)
canvas.itemconfig(text_id, text=color_name.capitalize())
def change_color(rgb: tuple, name: str) -> None:
"""
Change the color of the connected device using RGB values.
:param rgb: A tuple containing three integers representing the RGB values of the desired color.
:type rgb: tuple
:param name: The name of the color.
:type name: str
:return: None
"""
global ready
global device
global rx_char
if not ready:
return
color_data = f"{rgb[0]},{rgb[1]},{rgb[2]}".encode('utf-8')
try:
ready = False
if device is not None and rx_char is not None:
rx_char.write(color_data, withResponse=True)
change_canvas(color_name=name)
else:
print("[ERROR] Device or characteristic not initialized.")
except Exception as err_write:
print(f"[ERROR] {err_write}")
finally:
ready = True
def set_direction(direction: str) -> None:
"""
Sets the direction for the LED circle.
:param direction: The direction to set (left, right).
:type direction: str
:return: None
"""
global ready
if not ready:
return
direction_data = str(direction).encode('utf-8')
try:
ready = False
if device is not None and rx_char is not None:
rx_char.write(direction_data, withResponse=True)
else:
print("[ERROR] Device or characteristic not initialized.")
except Exception as err_write:
print(f"[ERROR] {err_write}")
finally:
ready = True
def ir_callback(data: int) -> None:
"""
Callback method for handling infrared data.
:param data: The received infrared data as an integer.
:type data: int
:return: None
"""
ir_hex = hex(int(data))
if ir_hex in BTN_MAPPING:
name = BTN_MAPPING[ir_hex][0]
rgb = BTN_MAPPING[ir_hex][1]
print(f'[INFO] BTN:{ir_hex} - Name:{name} - RGB:{rgb}')
change_color(rgb=rgb, name=name)
elif ir_hex in BTN_RIGHT:
print(f'[INFO] BTN:{ir_hex} - Name:right')
set_direction(direction='right')
elif ir_hex in BTN_LEFT:
print(f'[INFO] BTN:{ir_hex} - Name:left')
set_direction(direction='left')
else:
print(f'[WARN] BTN: {ir_hex} not mapped')
if __name__ == '__main__':
Board().begin()
ir = IRRecv(pin_obj=Pin(Pin.P14), callback=ir_callback)
device = Peripheral(BLE_ADDRESS)
rx_char = device.getCharacteristics(uuid=BLE_CHARACTERISTIC_UUID)[0]
ready = True
window = Tk()
window.geometry(f'{DISPLAY_WIDTH}x{DISPLAY_HEIGHT}+0+0')
canvas = Canvas(window, width=DISPLAY_WIDTH, height=DISPLAY_HEIGHT)
canvas.pack()
rect_width = DISPLAY_WIDTH - 10
rect_height = DISPLAY_HEIGHT - 100
rect_id = canvas.create_rectangle(10, 10, rect_width, rect_height, outline="black", fill="black", width=2)
txt_width = DISPLAY_WIDTH // 2
txt_height = DISPLAY_HEIGHT - 50
txt = 'Please select a color [0 - 9]'
font = ('Arial', 12)
text_id = canvas.create_text(txt_width, txt_height, text=txt, font=font, fill="black", anchor='center')
window.mainloop()
if device is not None:
device.disconnect()
As already mentioned, you can easily load the MicroPython code onto the ESP32 using rshell. Save the file as main.py on the ESP32 /pyboard/ directory, to run automatically on boot.
You can upload the Python code to the UNIHIKER via SCP and/or SMB. Use a tool like scp on Linux/macOS or WinSCP on Windows to upload the Python code to the UNIHIKER. Place the file in an appropriate directory for execution (eq. /root/ir_led.py).
Run the MicroPython code on the ESP32: Power up the ESP32 and ensure the main.py script is running. The ESP32 should start advertising BLE and await commands.
Run the Python code on the UNIHIKER: Start the application via touch display or command line.
Test the System: Use the IR remote to send commands to the ESP32 via the UNIHIKER. Observe the RGB LED strip changing colors or animating based on the commands.
Fix issues: In both code examples there are print() outputs, which give you the necessary information in the event of problems.
Expand Functionality: Add more remote control commands or functions, such as brightness control or different animation patterns.
Enhance User Interface: Improve the UNIHIKER's graphical interface, adding more interactive elements or feedback on the current state of the LED strip.
Additional Sensors: Integrate sensors such as a microphone to change LED patterns based on sound, or temperature sensors for reactive lighting.