Berry animation performance improvements: LUT and native push_pixels (#24094)

This commit is contained in:
s-hadinger 2025-11-05 20:46:35 +01:00 committed by GitHub
parent b2bc197054
commit ee1d867025
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 2623 additions and 2036 deletions

View File

@ -14,7 +14,7 @@ palette rainbow_with_white = [
]
# define a color attribute that cycles over time, cycle is 10 seconds
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=10s, transition_type=SINE)
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=0, transition_type=SINE)
# since strip_length is dynamic, we need to map it to a variable
set strip_len = strip_length()

View File

@ -14,7 +14,7 @@ palette rainbow_with_white = [
]
# define a color attribute that cycles over time, cycle is 10 seconds
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=10s, transition_type=SINE)
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=0, transition_type=SINE)
# define a gradient across the whole strip
animation back_pattern = palette_gradient_animation(color_source = rainbow_rich_color, shift_period = 5s)

View File

@ -331,7 +331,10 @@ Base interface for all color providers. Inherits from `ValueProvider`.
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| *(none)* | - | - | - | Base interface has no parameters |
| `brightness` | int | 255 | 0-255 | Overall brightness scaling for all colors |
**Static Methods**:
- `apply_brightness(color, brightness)` - Applies brightness scaling to a color (ARGB format). Only performs scaling if brightness is not 255 (full brightness). This is a static utility method that can be called without an instance.
**Factory**: N/A (base interface)
@ -342,6 +345,7 @@ Returns a single, static color. Inherits from `ColorProvider`.
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `color` | int | 0xFFFFFFFF | - | The solid color to return |
| *(inherits brightness from ColorProvider)* | | | | |
#### Usage Examples
@ -370,6 +374,7 @@ Cycles through a palette of colors with brutal switching. Inherits from `ColorPr
| `cycle_period` | int | 5000 | min: 0 | Cycle time in ms (0 = manual only) |
| `next` | int | 0 | - | Write 1 to move to next color manually, or any number to go forward or backwards by `n` colors |
| `palette_size` | int | 3 | read-only | Number of colors in the palette (automatically updated when palette changes) |
| *(inherits brightness from ColorProvider)* | | | | |
**Note**: The `get_color_for_value()` method accepts values in the 0-255 range for value-based color mapping.
@ -406,7 +411,7 @@ Generates colors from predefined palettes with smooth transitions and profession
| `palette` | bytes | rainbow palette | - | Palette bytes or predefined palette constant |
| `cycle_period` | int | 5000 | min: 0 | Cycle time in ms (0 = value-based only) |
| `transition_type` | int | animation.LINEAR | enum: [animation.LINEAR, animation.SINE] | LINEAR=constant speed, SINE=smooth ease-in/ease-out |
| `brightness` | int | 255 | 0-255 | Overall brightness scaling |
| *(inherits brightness from ColorProvider)* | | | | |
#### Available Predefined Palettes
@ -453,10 +458,11 @@ Creates breathing/pulsing color effects by modulating the brightness of a base c
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `base_color` | int | 0xFFFFFFFF | - | The base color to modulate (32-bit ARGB value) |
| `min_brightness` | int | 0 | 0-255 | Minimum brightness level |
| `max_brightness` | int | 255 | 0-255 | Maximum brightness level |
| `min_brightness` | int | 0 | 0-255 | Minimum brightness level (breathing effect) |
| `max_brightness` | int | 255 | 0-255 | Maximum brightness level (breathing effect) |
| `duration` | int | 3000 | min: 1 | Time for one complete breathing cycle in ms |
| `curve_factor` | int | 2 | 1-5 | Breathing curve shape (1=cosine wave, 2-5=curved breathing with pauses) |
| *(inherits brightness from ColorProvider)* | | | | Overall brightness scaling applied after breathing effect |
| *(inherits all OscillatorValueProvider parameters)* | | | | |
**Curve Factor Effects:**
@ -518,6 +524,7 @@ Combines multiple color providers with blending. Inherits from `ColorProvider`.
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `blend_mode` | int | 0 | enum: [0,1,2] | 0=overlay, 1=add, 2=multiply |
| *(inherits brightness from ColorProvider)* | | | | Overall brightness scaling applied to final composite color |
**Factory**: `animation.composite_color(engine)`

View File

@ -215,6 +215,125 @@ def render(frame, time_ms)
end
```
### Color Provider LUT Optimization
For color providers that perform expensive color calculations (like palette interpolation), the base `ColorProvider` class provides a Lookup Table (LUT) mechanism for caching pre-computed colors:
```berry
#@ solidify:MyColorProvider,weak
class MyColorProvider : animation.color_provider
# Instance variables (all should start with underscore)
var _cached_data # Your custom cached data
def init(engine)
super(self).init(engine) # Initializes _color_lut and _lut_dirty
self._cached_data = nil
end
# Mark LUT as dirty when parameters change
def on_param_changed(name, value)
super(self).on_param_changed(name, value)
if name == "palette" || name == "transition_type"
self._lut_dirty = true # Inherited from ColorProvider
end
end
# Rebuild LUT when needed
def _rebuild_color_lut()
# Allocate LUT (e.g., 129 entries * 4 bytes = 516 bytes)
if self._color_lut == nil
self._color_lut = bytes()
self._color_lut.resize(129 * 4)
end
# Pre-compute colors for values 0, 2, 4, ..., 254, 255
var i = 0
while i < 128
var value = i * 2
var color = self._compute_color_expensive(value)
self._color_lut.set(i * 4, color, 4)
i += 1
end
# Add final entry for value 255
var color_255 = self._compute_color_expensive(255)
self._color_lut.set(128 * 4, color_255, 4)
self._lut_dirty = false
end
# Update method checks if LUT needs rebuilding
def update(time_ms)
if self._lut_dirty || self._color_lut == nil
self._rebuild_color_lut()
end
return self.is_running
end
# Fast color lookup using LUT
def get_color_for_value(value, time_ms)
# Build LUT if needed (lazy initialization)
if self._lut_dirty || self._color_lut == nil
self._rebuild_color_lut()
end
# Map value to LUT index (divide by 2, special case for 255)
var lut_index = value >> 1
if value >= 255
lut_index = 128
end
# Retrieve pre-computed color from LUT
var color = self._color_lut.get(lut_index * 4, 4)
# Apply brightness scaling using static method (only if not 255)
var brightness = self.brightness
if brightness != 255
return animation.color_provider.apply_brightness(color, brightness)
end
return color
end
# Access LUT from outside (returns bytes() or nil)
# Inherited from ColorProvider: get_lut()
end
```
**LUT Benefits:**
- **5-10x speedup** for expensive color calculations
- **Reduced CPU usage** during rendering
- **Smooth animations** even with complex color logic
- **Memory efficient** (typically 516 bytes for 129 entries)
**When to use LUT:**
- Palette interpolation with binary search
- Complex color transformations
- Brightness calculations
- Any expensive per-pixel color computation
**LUT Guidelines:**
- Store colors at maximum brightness, apply scaling after lookup
- Use 2-step resolution (0, 2, 4, ..., 254, 255) to save memory
- Invalidate LUT when parameters affecting color calculation change
- Don't invalidate for brightness changes if brightness is applied post-lookup
**Brightness Handling:**
The `ColorProvider` base class includes a `brightness` parameter (0-255, default 255) and a static method for applying brightness scaling:
```berry
# Static method for brightness scaling (only scales if brightness != 255)
animation.color_provider.apply_brightness(color, brightness)
```
**Best Practices:**
- Store LUT colors at maximum brightness (255)
- Apply brightness scaling after LUT lookup using the static method
- Only call the static method if `brightness != 255` to avoid unnecessary overhead
- For inline performance-critical code, you can inline the brightness calculation instead of calling the static method
- Brightness changes do NOT invalidate the LUT since brightness is applied after lookup
## Parameter Access
### Direct Virtual Member Assignment

View File

@ -258,14 +258,7 @@ class AnimationEngine
# Output frame buffer to LED strip
def _output_to_strip()
var i = 0
var strip_length = self.strip_length
var strip = self.strip
var pixels = self.frame_buffer.pixels
while i < strip_length
strip.set_pixel_color(i, pixels.get(i * 4, 4))
i += 1
end
self.strip.push_pixels_buffer_argb(self.frame_buffer.pixels)
self.strip.show()
end

View File

@ -139,7 +139,14 @@ class ColorCycleColorProvider : animation.color_provider
if (idx >= palette_size) idx = palette_size - 1 end
if (idx < 0) idx = 0 end
self.current_index = idx
return self._get_color_at_index(self.current_index)
var color = self._get_color_at_index(self.current_index)
# Apply brightness scaling
var brightness = self.brightness
if brightness != 255
return self.apply_brightness(color, brightness)
end
return color
end
# Auto-cycle mode: calculate which color to show based on time (brutal switching using integer math)
@ -151,9 +158,16 @@ class ColorCycleColorProvider : animation.color_provider
color_index = palette_size - 1
end
# Update current state and return the color
# Update current state and get the color
self.current_index = color_index
return self._get_color_at_index(color_index)
var color = self._get_color_at_index(color_index)
# Apply brightness scaling
var brightness = self.brightness
if brightness != 255
return self.apply_brightness(color, brightness)
end
return color
end
# Get a color based on a value (maps value to position in cycle)
@ -170,7 +184,12 @@ class ColorCycleColorProvider : animation.color_provider
end
if palette_size == 1
return self._get_color_at_index(0) # If only one color, just return it
var color = self._get_color_at_index(0) # If only one color, just return it
var brightness = self.brightness
if brightness != 255
return self.apply_brightness(color, brightness)
end
return color
end
# Clamp value to 0-255
@ -188,7 +207,14 @@ class ColorCycleColorProvider : animation.color_provider
color_index = palette_size - 1
end
return self._get_color_at_index(color_index)
var color = self._get_color_at_index(color_index)
# Apply brightness scaling
var brightness = self.brightness
if brightness != 255
return self.apply_brightness(color, brightness)
end
return color
end
# String representation of the provider

View File

@ -14,6 +14,34 @@
#@ solidify:ColorProvider,weak
class ColorProvider : animation.value_provider
# LUT (Lookup Table) management for color providers
# Subclasses can use this to cache pre-computed colors for performance
# If a subclass doesn't use a LUT, this remains nil
var _color_lut # Color lookup table cache (bytes() object or nil)
var _lut_dirty # Flag indicating LUT needs rebuilding
# Parameter definitions
static var PARAMS = animation.enc_params({
"brightness": {"min": 0, "max": 255, "default": 255}
})
# Initialize the color provider
#
# @param engine: AnimationEngine - Reference to the animation engine (required)
def init(engine)
super(self).init(engine)
self._color_lut = nil
self._lut_dirty = true
end
# Get the color lookup table
# Returns the LUT bytes() object if the provider uses one, or nil otherwise
#
# @return bytes|nil - The LUT bytes object or nil
def get_lut()
return self._color_lut
end
# Produce a color value for any parameter name
# This is the main method that subclasses should override
#
@ -34,6 +62,31 @@ class ColorProvider : animation.value_provider
return self.produce_value("color", time_ms) # Default: use time-based color
end
# Static method to apply brightness scaling to a color
# Only performs scaling if brightness is not 255 (full brightness)
#
# @param color: int - Color in ARGB format (0xAARRGGBB)
# @param brightness: int - Brightness level (0-255)
# @return int - Color with brightness applied
static def apply_brightness(color, brightness)
# Skip scaling if brightness is full (255)
if brightness == 255
return color
end
# Extract RGB components (preserve alpha channel)
var r = (color >> 16) & 0xFF
var g = (color >> 8) & 0xFF
var b = color & 0xFF
# Scale each component by brightness
r = tasmota.scale_uint(r, 0, 255, 0, brightness)
g = tasmota.scale_uint(g, 0, 255, 0, brightness)
b = tasmota.scale_uint(b, 0, 255, 0, brightness)
# Reconstruct color with scaled brightness (preserve alpha)
return (color & 0xFF000000) | (r << 16) | (g << 8) | b
end
end

View File

@ -49,7 +49,12 @@ class CompositeColorProvider : animation.color_provider
end
if size(self.providers) == 1
return self.providers[0].produce_value(name, time_ms)
var color = self.providers[0].produce_value(name, time_ms)
var brightness = self.brightness
if brightness != 255
return self.apply_brightness(color, brightness)
end
return color
end
var result_color = self.providers[0].produce_value(name, time_ms)
@ -61,6 +66,11 @@ class CompositeColorProvider : animation.color_provider
i += 1
end
# Apply brightness scaling to final composite color
var brightness = self.brightness
if brightness != 255
return self.apply_brightness(result_color, brightness)
end
return result_color
end
@ -75,7 +85,12 @@ class CompositeColorProvider : animation.color_provider
end
if size(self.providers) == 1
return self.providers[0].get_color_for_value(value, time_ms)
var color = self.providers[0].get_color_for_value(value, time_ms)
var brightness = self.brightness
if brightness != 255
return self.apply_brightness(color, brightness)
end
return color
end
var result_color = self.providers[0].get_color_for_value(value, time_ms)
@ -87,6 +102,11 @@ class CompositeColorProvider : animation.color_provider
i += 1
end
# Apply brightness scaling to final composite color
var brightness = self.brightness
if brightness != 255
return self.apply_brightness(result_color, brightness)
end
return result_color
end

View File

@ -33,38 +33,34 @@ import "./core/param_encoder" as encode_constraints
#@ solidify:RichPaletteColorProvider,weak
class RichPaletteColorProvider : animation.color_provider
# Non-parameter instance variables only
var slots_arr # Constructed array of timestamp slots, based on cycle_period
var value_arr # Constructed array of value slots (always 0-255 range)
var slots # Number of slots in the palette
var current_color # Current interpolated color (calculated during update)
var light_state # light_state instance for proper color calculations
var color_lut # Color lookup table cache (129 entries: 0, 2, 4, ..., 254, 255)
var lut_dirty # Flag indicating LUT needs rebuilding
var _brightness # Cached value for `self.brightness` used during render()
var _slots_arr # Constructed array of timestamp slots, based on cycle_period
var _value_arr # Constructed array of value slots (always 0-255 range)
var _slots # Number of slots in the palette
var _current_color # Current interpolated color (calculated during update)
var _light_state # light_state instance for proper color calculations
var _brightness # Cached value for `self.brightness` used during render()
# Parameter definitions
static var PARAMS = animation.enc_params({
"palette": {"type": "bytes", "default": nil}, # Palette bytes or predefined palette constant
"cycle_period": {"min": 0, "default": 5000}, # 5 seconds default, 0 = value-based only
"transition_type": {"enum": [animation.LINEAR, animation.SINE], "default": animation.LINEAR},
"brightness": {"min": 0, "max": 255, "default": 255}
"transition_type": {"enum": [animation.LINEAR, animation.SINE], "default": animation.LINEAR}
# brightness parameter inherited from ColorProvider base class
})
# Initialize a new RichPaletteColorProvider
#
# @param engine: AnimationEngine - Reference to the animation engine (required)
def init(engine)
super(self).init(engine) # Initialize parameter system
super(self).init(engine) # Initialize parameter system (also initializes LUT variables)
# Initialize non-parameter instance variables
self.current_color = 0xFFFFFFFF
self.slots = 0
self.color_lut = nil
self.lut_dirty = true
self._current_color = 0xFFFFFFFF
self._slots = 0
# Create light_state instance for proper color calculations (reuse from Animate_palette)
import global
self.light_state = global.light_state(global.light_state.RGB)
self._light_state = global.light_state(global.light_state.RGB)
# We need to register this value provider to receive 'update()'
engine.add(self)
@ -77,7 +73,7 @@ class RichPaletteColorProvider : animation.color_provider
def on_param_changed(name, value)
super(self).on_param_changed(name, value)
if name == "cycle_period" || name == "palette"
if (self.slots_arr != nil) || (self.value_arr != nil)
if (self._slots_arr != nil) || (self._value_arr != nil)
# only if they were already computed
self._recompute_palette()
end
@ -85,8 +81,9 @@ class RichPaletteColorProvider : animation.color_provider
# Mark LUT as dirty when palette or transition_type changes
# Note: brightness changes do NOT invalidate LUT since brightness is applied after lookup
if name == "palette" || name == "transition_type"
self.lut_dirty = true
self._lut_dirty = true
end
# Brightness changes do NOT invalidate LUT - brightness is applied after lookup
end
# Start/restart the animation cycle at a specific time
@ -95,7 +92,7 @@ class RichPaletteColorProvider : animation.color_provider
# @return self for method chaining
def start(time_ms)
# Compute arrays if they were not yet initialized
if (self.slots_arr == nil) && (self.value_arr == nil)
if (self._slots_arr == nil) && (self._value_arr == nil)
self._recompute_palette()
end
super(self).start(time_ms)
@ -123,25 +120,25 @@ class RichPaletteColorProvider : animation.color_provider
# Compute slots_arr based on 'cycle_period'
var cycle_period = self.cycle_period
var palette_bytes = self._get_palette_bytes()
self.slots = size(palette_bytes) / 4
self._slots = size(palette_bytes) / 4
# Recompute palette with new cycle period (only if > 0 for time-based cycling)
if cycle_period > 0 && palette_bytes != nil
self.slots_arr = self._parse_palette(0, cycle_period - 1)
self._slots_arr = self._parse_palette(0, cycle_period - 1)
else
self.slots_arr = nil
self._slots_arr = nil
end
# Compute value_arr for value-based mode (always 0-255 range)
if self._get_palette_bytes() != nil
self.value_arr = self._parse_palette(0, 255)
self._value_arr = self._parse_palette(0, 255)
else
self.value_arr = nil
self._value_arr = nil
end
# Set initial color
if self.slots > 0
self.current_color = self._get_color_at_index(0)
if self._slots > 0
self._current_color = self._get_color_at_index(0)
end
return self
@ -155,7 +152,7 @@ class RichPaletteColorProvider : animation.color_provider
def _parse_palette(min, max)
var palette_bytes = self._get_palette_bytes()
var arr = []
var slots = self.slots
var slots = self._slots
arr.resize(slots)
# Check if we have slots or values (exact logic from Animate_palette)
@ -191,7 +188,7 @@ class RichPaletteColorProvider : animation.color_provider
# Get color at a specific index (simplified)
def _get_color_at_index(idx)
if idx < 0 || idx >= self.slots
if idx < 0 || idx >= self._slots
return 0xFFFFFFFF
end
@ -245,7 +242,7 @@ class RichPaletteColorProvider : animation.color_provider
# @return bool - True if object is still running, false if completed
def update(time_ms)
# Rebuild LUT if dirty
if self.lut_dirty || self.color_lut == nil
if self._lut_dirty || self._color_lut == nil
self._rebuild_color_lut()
end
@ -264,12 +261,12 @@ class RichPaletteColorProvider : animation.color_provider
# Ensure time_ms is valid and initialize start_time if needed
time_ms = self._fix_time_ms(time_ms)
if (self.slots_arr == nil) && (self.value_arr == nil)
if (self._slots_arr == nil) && (self._value_arr == nil)
self._recompute_palette()
end
var palette_bytes = self._get_palette_bytes()
if palette_bytes == nil || self.slots < 2
if palette_bytes == nil || self._slots < 2
return 0xFFFFFFFF
end
@ -284,7 +281,7 @@ class RichPaletteColorProvider : animation.color_provider
var g = (bgrt0 >> 16) & 0xFF
var b = (bgrt0 >> 24) & 0xFF
# Apply brightness scaling
# Apply brightness scaling (inline for speed)
if brightness != 255
r = tasmota.scale_uint(r, 0, 255, 0, brightness)
g = tasmota.scale_uint(g, 0, 255, 0, brightness)
@ -292,7 +289,7 @@ class RichPaletteColorProvider : animation.color_provider
end
var final_color = (0xFF << 24) | (r << 16) | (g << 8) | b
self.current_color = final_color
self._current_color = final_color
return final_color
end
@ -301,17 +298,17 @@ class RichPaletteColorProvider : animation.color_provider
var past = elapsed % cycle_period
# Find slot (exact algorithm from Animate_palette)
var slots = self.slots
var slots = self._slots
var idx = slots - 2
while idx > 0
if past >= self.slots_arr[idx] break end
if past >= self._slots_arr[idx] break end
idx -= 1
end
var bgrt0 = palette_bytes.get(idx * 4, 4)
var bgrt1 = palette_bytes.get((idx + 1) * 4, 4)
var t0 = self.slots_arr[idx]
var t1 = self.slots_arr[idx + 1]
var t0 = self._slots_arr[idx]
var t1 = self._slots_arr[idx + 1]
# Use interpolation based on transition_type (LINEAR or SINE)
var r = self._interpolate(past, t0, t1, (bgrt0 >> 8) & 0xFF, (bgrt1 >> 8) & 0xFF)
@ -319,7 +316,7 @@ class RichPaletteColorProvider : animation.color_provider
var b = self._interpolate(past, t0, t1, (bgrt0 >> 24) & 0xFF, (bgrt1 >> 24) & 0xFF)
# Use light_state for proper brightness calculation (from Animate_palette)
var light_state = self.light_state
var light_state = self._light_state
light_state.set_rgb((bgrt0 >> 8) & 0xFF, (bgrt0 >> 16) & 0xFF, (bgrt0 >> 24) & 0xFF)
var bri0 = light_state.bri
light_state.set_rgb((bgrt1 >> 8) & 0xFF, (bgrt1 >> 16) & 0xFF, (bgrt1 >> 24) & 0xFF)
@ -332,7 +329,7 @@ class RichPaletteColorProvider : animation.color_provider
g = light_state.g
b = light_state.b
# Apply brightness scaling (from Animate_palette)
# Apply brightness scaling (inline for speed)
if brightness != 255
r = tasmota.scale_uint(r, 0, 255, 0, brightness)
g = tasmota.scale_uint(g, 0, 255, 0, brightness)
@ -341,7 +338,7 @@ class RichPaletteColorProvider : animation.color_provider
# Create final color in ARGB format
var final_color = (0xFF << 24) | (r << 16) | (g << 8) | b
self.current_color = final_color
self._current_color = final_color
return final_color
end
@ -369,14 +366,14 @@ class RichPaletteColorProvider : animation.color_provider
# - Little-endian format (native Berry integer representation)
def _rebuild_color_lut()
# Ensure palette arrays are initialized
if self.value_arr == nil
if self._value_arr == nil
self._recompute_palette()
end
# Allocate LUT if needed (129 entries * 4 bytes = 516 bytes)
if self.color_lut == nil
self.color_lut = bytes()
self.color_lut.resize(129 * 4)
if self._color_lut == nil
self._color_lut = bytes()
self._color_lut.resize(129 * 4)
end
# Pre-compute colors for values 0, 2, 4, ..., 254 at max brightness
@ -386,49 +383,49 @@ class RichPaletteColorProvider : animation.color_provider
var color = self._get_color_for_value_uncached(value, 0)
# Store color using efficient bytes.set()
self.color_lut.set(i * 4, color, 4)
self._color_lut.set(i * 4, color, 4)
i += 1
end
# Add final entry for value 255 at max brightness
var color_255 = self._get_color_for_value_uncached(255, 0)
self.color_lut.set(128 * 4, color_255, 4)
self._color_lut.set(128 * 4, color_255, 4)
self.lut_dirty = false
self._lut_dirty = false
end
# Get color for a specific value WITHOUT using cache (internal method)
# This is the original implementation moved to a separate method
# Colors are returned at MAXIMUM brightness (255) - brightness scaling applied separately
#
# @param value: int/float - Value to map to a color (0-255 range)
# @param time_ms: int - Current time in milliseconds (ignored for value-based color)
# @return int - Color in ARGB format
# @return int - Color in ARGB format at maximum brightness
def _get_color_for_value_uncached(value, time_ms)
if (self.slots_arr == nil) && (self.value_arr == nil)
if (self._slots_arr == nil) && (self._value_arr == nil)
self._recompute_palette()
end
var palette_bytes = self._get_palette_bytes()
var brightness = self.brightness
# Find slot (exact algorithm from Animate_palette.set_value)
var slots = self.slots
var slots = self._slots
var idx = slots - 2
while idx > 0
if value >= self.value_arr[idx] break end
if value >= self._value_arr[idx] break end
idx -= 1
end
var bgrt0 = palette_bytes.get(idx * 4, 4)
var bgrt1 = palette_bytes.get((idx + 1) * 4, 4)
var t0 = self.value_arr[idx]
var t1 = self.value_arr[idx + 1]
var t0 = self._value_arr[idx]
var t1 = self._value_arr[idx + 1]
# Use interpolation based on transition_type (LINEAR or SINE)
var r = self._interpolate(value, t0, t1, (bgrt0 >> 8) & 0xFF, (bgrt1 >> 8) & 0xFF)
var g = self._interpolate(value, t0, t1, (bgrt0 >> 16) & 0xFF, (bgrt1 >> 16) & 0xFF)
var b = self._interpolate(value, t0, t1, (bgrt0 >> 24) & 0xFF, (bgrt1 >> 24) & 0xFF)
# Create final color in ARGB format
# Create final color in ARGB format at maximum brightness
return (0xFF << 24) | (r << 16) | (g << 8) | b
end
@ -448,7 +445,7 @@ class RichPaletteColorProvider : animation.color_provider
#
# Brightness handling:
# - LUT stores colors at maximum brightness (255)
# - Actual brightness scaling applied here after lookup
# - Actual brightness scaling applied here after lookup using static method
# - This allows brightness to change dynamically without invalidating LUT
#
# @param value: int/float - Value to map to a color (0-255 range)
@ -469,7 +466,7 @@ class RichPaletteColorProvider : animation.color_provider
# Retrieve color from LUT using efficient bytes.get()
# This color is at maximum brightness (255)
var color = self.color_lut.get(lut_index * 4, 4)
var color = self._color_lut.get(lut_index * 4, 4)
# Apply brightness scaling if not at maximum
var brightness = self._brightness
@ -521,7 +518,7 @@ class RichPaletteColorProvider : animation.color_provider
# String representation
def tostring()
try
return f"RichPaletteColorProvider(slots={self.slots}, cycle_period={self.cycle_period})"
return f"RichPaletteColorProvider(slots={self._slots}, cycle_period={self.cycle_period})"
except ..
return "RichPaletteColorProvider(uninitialized)"
end

View File

@ -22,7 +22,12 @@ class StaticColorProvider : animation.color_provider
# @param time_ms: int - Current time in milliseconds (ignored)
# @return int - Color in ARGB format (0xAARRGGBB)
def produce_value(name, time_ms)
return self.color
var color = self.color
var brightness = self.brightness
if brightness != 255
return self.apply_brightness(color, brightness)
end
return color
end
# Get the solid color for a value (ignores the value)
@ -31,7 +36,12 @@ class StaticColorProvider : animation.color_provider
# @param time_ms: int - Current time in milliseconds (ignored)
# @return int - Color in ARGB format (0xAARRGGBB)
def get_color_for_value(value, time_ms)
return self.color
var color = self.color
var brightness = self.brightness
if brightness != 255
return self.apply_brightness(color, brightness)
end
return color
end
# String representation of the provider

View File

@ -202,7 +202,7 @@ class RichPaletteAnimationTest
# Test value-based color generation (now always 0-255 range)
provider.start()
provider.update()
print(f"{provider.slots_arr=} {provider.value_arr=}")
print(f"{provider._slots_arr=} {provider._value_arr=}")
var color_0 = provider.get_color_for_value(0, 0)
var color_128 = provider.get_color_for_value(128, 0)
var color_255 = provider.get_color_for_value(255, 0)

View File

@ -42,7 +42,7 @@ var b = color_at_255 & 0xFF
log(f" RGB({r:3d}, {g:3d}, {b:3d}) = 0x{color_at_255:08X}")
# Verify LUT is not dirty
log(f"LUT dirty after initial build: {provider.lut_dirty}")
log(f"LUT dirty after initial build: {provider._lut_dirty}")
log("")
# Change brightness multiple times and verify LUT stays valid
@ -50,7 +50,7 @@ var brightness_values = [200, 150, 100, 50, 255]
for brightness : brightness_values
provider.brightness = brightness
log(f"Changed brightness to {brightness}")
log(f" LUT dirty: {provider.lut_dirty}")
log(f" LUT dirty: {provider._lut_dirty}")
var color = provider.get_color_for_value(128, 0)
r = (color >> 16) & 0xFF
@ -148,25 +148,25 @@ rebuild_provider.cycle_period = 0
# Force initial build
rebuild_provider.get_color_for_value(128, 0)
log(f"After initial build: lut_dirty = {rebuild_provider.lut_dirty}")
log(f"After initial build: lut_dirty = {rebuild_provider._lut_dirty}")
# Change brightness - should NOT trigger rebuild
rebuild_provider.brightness = 100
log(f"After brightness change: lut_dirty = {rebuild_provider.lut_dirty}")
log(f"After brightness change: lut_dirty = {rebuild_provider._lut_dirty}")
rebuild_provider.get_color_for_value(128, 0)
log(f"After lookup with new brightness: lut_dirty = {rebuild_provider.lut_dirty}")
log(f"After lookup with new brightness: lut_dirty = {rebuild_provider._lut_dirty}")
# Change palette - SHOULD trigger rebuild
rebuild_provider.palette = bytes("00FF0000" "FFFFFF00")
log(f"After palette change: lut_dirty = {rebuild_provider.lut_dirty}")
log(f"After palette change: lut_dirty = {rebuild_provider._lut_dirty}")
rebuild_provider.get_color_for_value(128, 0)
log(f"After lookup with new palette: lut_dirty = {rebuild_provider.lut_dirty}")
log(f"After lookup with new palette: lut_dirty = {rebuild_provider._lut_dirty}")
# Change transition_type - SHOULD trigger rebuild
rebuild_provider.transition_type = animation.SINE
log(f"After transition_type change: lut_dirty = {rebuild_provider.lut_dirty}")
log(f"After transition_type change: lut_dirty = {rebuild_provider._lut_dirty}")
rebuild_provider.get_color_for_value(128, 0)
log(f"After lookup with new transition: lut_dirty = {rebuild_provider.lut_dirty}")
log(f"After lookup with new transition: lut_dirty = {rebuild_provider._lut_dirty}")
log("")
log("=== All tests completed successfully ===")

View File

@ -51,11 +51,11 @@ provider.produce_value("color", 0)
# Debug: Check palette
log(f"Palette size: {size(provider.palette)} bytes")
log(f"Slots: {provider.slots}")
log(f"Slots: {provider._slots}")
log("Range: 0 to 255 (fixed)")
# Force LUT rebuild
provider.lut_dirty = true
provider._lut_dirty = true
# Test key values
var test_values = [0, 2, 4, 50, 100, 150, 200, 254, 255]
@ -75,19 +75,19 @@ log("")
log("Test 2: LUT invalidation on parameter changes")
log("----------------------------------------------")
provider.lut_dirty = false
log(f"Initial lut_dirty: {provider.lut_dirty}")
provider._lut_dirty = false
log(f"Initial _lut_dirty: {provider._lut_dirty}")
provider.brightness = 200
log(f"After brightness change: lut_dirty = {provider.lut_dirty}")
log(f"After brightness change: _lut_dirty = {provider._lut_dirty}")
provider.lut_dirty = false
provider._lut_dirty = false
provider.transition_type = animation.SINE
log(f"After transition_type change: lut_dirty = {provider.lut_dirty}")
log(f"After transition_type change: _lut_dirty = {provider._lut_dirty}")
provider.lut_dirty = false
provider._lut_dirty = false
provider.palette = bytes("00FF0000" "FFFFFF00" "FF00FF00")
log(f"After palette change: lut_dirty = {provider.lut_dirty}")
log(f"After palette change: _lut_dirty = {provider._lut_dirty}")
log("")

View File

@ -12,6 +12,7 @@
extern int be_tasmotaled_call_native(bvm *vm);
extern int be_leds_blend_color(bvm *vm);
extern int be_leds_apply_bri_gamma(bvm *vm);
extern int be_leds_set_pixels(bvm *vm);
/* @const_object_info_begin
class be_class_Leds_ntv (scope: global, name: Leds_ntv, strings: weak) {
@ -29,6 +30,8 @@ class be_class_Leds_ntv (scope: global, name: Leds_ntv, strings: weak) {
blend_color, static_func(be_leds_blend_color)
apply_bri_gamma, static_func(be_leds_apply_bri_gamma)
set_pixels, static_func(be_leds_set_pixels)
}
@const_object_info_end */

View File

@ -131,6 +131,21 @@ class Leds : Leds_ntv
def dirty() ## DEPRECATED
self.call_native(5)
end
# push_pixels
#
# Pushes a bytes() buffer of 0xAARRGGBB colors, without bri nor gamma correction
#
def push_pixels_buffer_argb(pixels)
# Leds.set_pixels(buffer:bytes, pixels_buffer:comptr, pixels_count:int, [pixel_size:int = 3, bri:int (0..255) = 255, gamma:bool = true]) -> void
self.set_pixels(pixels,
self.call_native(6), # address of buffer in memory
self.pixel_count(),
self.pixel_size(),
self.get_bri(),
self.get_gamma())
end
def pixels_buffer(old_buf)
var buf = self.call_native(6) # address of buffer in memory
var sz = self.pixel_size() * self.pixel_count()

File diff suppressed because it is too large Load Diff

View File

@ -325,6 +325,77 @@ extern "C" {
}
be_raise(vm, kTypeError, nullptr);
}
// Leds.set_pixels(buffer:bytes, pixels_buffer:comptr, pixels_count:int, [pixel_size:int = 3, bri:int (0..255) = 255, gamma:bool = true]) -> void
//
// Copies the pixels from the buffer to the driver, from 0xAARRGGBB buffer
// to either 0xRRGGBB or 0xWWRRGGBB, applying brightness (0..255) and
// optional gamma correction
int32_t be_leds_set_pixels(bvm *vm);
int32_t be_leds_set_pixels(bvm *vm) {
int32_t top = be_top(vm); // Get the number of arguments
if (top >= 3 && be_isbytes(vm, 1) && be_iscomptr(vm, 2) && be_isint(vm, 3)) {
// Get parameters
// arg1: buffer:bytes
size_t buffer_length;
uint32_t * buffer = (uint32_t*) be_tobytes(vm, 1, &buffer_length);
// arg2: pixels:comptr
uint8_t * pixels = (uint8_t*) be_tocomptr(vm, 2);
// arg3: pixels_count:int
int32_t pixels_count = be_toint(vm, 3);
// arg4: pixel_size:int
size_t pixel_size = 3; // 0xRRGGBB
if (top >= 4 && be_isint(vm, 4)) {
pixel_size = be_toint(vm, 4);
}
if (pixel_size < 3) { pixel_size = 3; }
if (pixel_size > 4) { pixel_size = 4; }
// arg5: bri:int (0..255)
int32_t bri255 = 255;
if (top >= 3 && be_isint(vm, 5)) {
bri255 = be_toint(vm, 5);
}
if (bri255 < 0) { bri255 = 0; }
if (bri255 > 255) { bri255 = 255; }
// arg6: gamma:bool
bool gamma = true;
if (top >= 6 && be_isbool(vm, 6)) {
gamma = be_tobool(vm, 6);
}
// now do the stuff
uint8_t * p = pixels;
for (uint32_t i = 0; i < pixels_count; i++) {
uint32_t argb = buffer[i]; // get 0xAARRGGBB, we drop the AA alpha channel
uint8_t r = (argb >> 16) & 0xFF;
uint8_t g = (argb >> 8) & 0xFF;
uint8_t b = (argb ) & 0xFF;
uint8_t w = 0; // for now we set white channel to zero
// apply brigthness
if (bri255 < 255) {
r = changeUIntScale(bri255, 0, 255, 0, r);
g = changeUIntScale(bri255, 0, 255, 0, g);
b = changeUIntScale(bri255, 0, 255, 0, b);
}
// apply gamma
if (gamma) {
r = ledGamma(r);
g = ledGamma(g);
b = ledGamma(b);
}
// set pixel in strip
if (pixel_size == 4) { *p++ = 0;}
*p++ = r;
*p++ = g;
*p++ = b;
}
be_return_nil(vm);
}
be_raise(vm, kTypeError, nullptr);
}
}
#endif // USE_WS2812