from machine import Pin, PWM, Timer import time class RGBLED: """ MicroPython driver for 4-pin RGB LEDs on ESP32 / Heltec boards. Supports state tracking, color setting, brightness scaling, state toggling, and non-blocking blinking via machine.Timer. """ RED = (255, 0, 0) GREEN = (0, 255, 0) BLUE = (0, 0, 255) YELLOW = (255, 120, 0) WHITE_YELLOW = (150, 30, 0) ORANGE = (255, 50, 0) WHITE = (255, 255, 255) OFF = (0, 0, 0) def __init__(self, red_pin, green_pin, blue_pin, common_anode=False, freq=1000, timer_id=1): """ :param red_pin: GPIO pin number for Red channel :param green_pin: GPIO pin number for Green channel :param blue_pin: GPIO pin number for Blue channel :param common_anode: Set True if cathode is connected to 3.3V instead of GND :param freq: PWM frequency in Hz (default 1000Hz) :param timer_id: Hardware/software timer ID for non-blocking blinks (-1 uses soft timers on ESP32). """ self._r_pwm = PWM(Pin(red_pin, Pin.OUT), freq=freq) self._g_pwm = PWM(Pin(green_pin, Pin.OUT), freq=freq) self._b_pwm = PWM(Pin(blue_pin, Pin.OUT), freq=freq) self._common_anode = common_anode # State tracking variables self._color = (0, 0, 0) # Current (R, G, B) tuple [0-255] self._brightness = 1.0 # Brightness factor [0.0 to 1.0] self._is_on = True # Master power state # Blink state variables self._timer = Timer(timer_id) self._is_blinking = False self._apply() def _apply(self): """Recalculates and applies PWM duty cycles based on state.""" if not self._is_on: r, g, b = 0, 0, 0 else: r = int(self._color[0] * self._brightness) g = int(self._color[1] * self._brightness) b = int(self._color[2] * self._brightness) for pwm, val in ((self._r_pwm, r), (self._g_pwm, g), (self._b_pwm, b)): # Clamp value between 0 and 255 val = max(0, min(255, val)) # Convert 8-bit (0-255) to MicroPython's 16-bit PWM duty (0-65535) duty = int((val / 255.0) * 65535) if self._common_anode: duty = 65535 - duty pwm.duty_u16(duty) # --- Properties and Setters --- @property def color(self): """Returns the active RGB tuple (R, G, B).""" return self._color @color.setter def color(self, rgb_tuple): """Sets the RGB color tuple (e.g., (255, 128, 0)).""" if isinstance(rgb_tuple, (tuple, list)) and len(rgb_tuple) == 3: self._color = tuple(rgb_tuple) self._apply() else: raise ValueError("Color must be a tuple of 3 integers: (R, G, B)") @property def brightness(self): """Returns the current brightness level (0.0 to 1.0).""" return self._brightness @brightness.setter def brightness(self, level): """Sets brightness level from 0.0 (0%) to 1.0 (100%).""" self._brightness = max(0.0, min(1.0, float(level))) self._apply() @property def is_on(self): """Returns True if the LED is currently powered on.""" return self._is_on @property def is_blinking(self): return self._is_blinking # --- Basic Control Methods --- def set_rgb(self, r, g, b): """Alternative setter for individual R, G, B integer values.""" self.color = (r, g, b) def on(self): """Turns the LED on using its stored color and brightness.""" self._is_on = True self._apply() def off(self): """Turns the LED off without resetting the active color state.""" self._is_on = False self._apply() def toggle(self): """Toggles between ON and OFF states.""" self._is_on = not self._is_on self._apply() # --- Non-Blocking Blinking Methods --- def _timer_callback(self, t): """Internal callback executed by machine.Timer.""" self.toggle() def blink_on(self, interval_ms=500): """Starts background blinking at the specified interval in milliseconds.""" if self._is_blinking: self._timer.deinit() self._is_blinking = True self.on() # Ensure initial state is on self._timer.init( period=interval_ms, mode=Timer.PERIODIC, callback=self._timer_callback ) def blink_off(self): """Stops blinking and returns control to steady state.""" if self._is_blinking: self._timer.deinit() self._is_blinking = False def blink_toggle(self, interval_ms=500): """Toggles blinking state (starts if stopped, stops if active).""" if self._is_blinking: self.blink_off() else: self.blink_on(interval_ms) def deinit(self): """Releases the hardware PWM pins and timer when finished.""" self._r_pwm.deinit() self._g_pwm.deinit() self._b_pwm.deinit()