Berry animation improvements (#24162)

This commit is contained in:
s-hadinger 2025-11-26 11:48:33 +01:00 committed by GitHub
parent 07a1a982cd
commit ec32b83f75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 14167 additions and 13098 deletions

View File

@ -377,6 +377,26 @@ SUCCESS
SUCCESS
```
## demo_value_meter.anim
**Status:** ✅ Success
## Symbol Table
| Symbol | Type | Builtin | Dangerous | Takes Args |
|---------------------------|----------------------------|---------|-----------|------------|
| `back_pattern` | animation | | | |
| `closure_value` | value_provider_constructor | ✓ | ⚠️ | ✓ |
| `palette_meter_animation` | animation_constructor | ✓ | ⚠️ | ✓ |
| `rainbow_with_white` | palette | | | |
| `rand_meter` | user_function | | | ✓ |
### Compilation Output
```
SUCCESS
```
## disco_strobe.anim
**Status:** ✅ Success
@ -702,34 +722,6 @@ SUCCESS
SUCCESS
```
## plasma_wave.anim
**Status:** ✅ Success
## Symbol Table
| Symbol | Type | Builtin | Dangerous | Takes Args |
|--------------------------|----------------------------|---------|-----------|------------|
| `SINE` | constant | ✓ | | |
| `beacon_animation` | animation_constructor | ✓ | ⚠️ | ✓ |
| `plasma_base` | animation | | | |
| `plasma_colors` | palette | | | |
| `plasma_wave1` | animation | | | |
| `plasma_wave2` | animation | | | |
| `plasma_wave3` | animation | | | |
| `rich_palette_animation` | animation_constructor | ✓ | ⚠️ | ✓ |
| `rich_palette` | color_constructor | ✓ | ⚠️ | ✓ |
| `smooth` | value_provider_constructor | ✓ | ⚠️ | ✓ |
| `wave1_pattern` | color | | | |
| `wave2_pattern` | color | | | |
| `wave3_pattern` | color | | | |
### Compilation Output
```
SUCCESS
```
## palette_demo.anim
**Status:** ✅ Success
@ -789,6 +781,34 @@ SUCCESS
SUCCESS
```
## plasma_wave.anim
**Status:** ✅ Success
## Symbol Table
| Symbol | Type | Builtin | Dangerous | Takes Args |
|--------------------------|----------------------------|---------|-----------|------------|
| `SINE` | constant | ✓ | | |
| `beacon_animation` | animation_constructor | ✓ | ⚠️ | ✓ |
| `plasma_base` | animation | | | |
| `plasma_colors` | palette | | | |
| `plasma_wave1` | animation | | | |
| `plasma_wave2` | animation | | | |
| `plasma_wave3` | animation | | | |
| `rich_palette_animation` | animation_constructor | ✓ | ⚠️ | ✓ |
| `rich_palette` | color_constructor | ✓ | ⚠️ | ✓ |
| `smooth` | value_provider_constructor | ✓ | ⚠️ | ✓ |
| `wave1_pattern` | color | | | |
| `wave2_pattern` | color | | | |
| `wave3_pattern` | color | | | |
### Compilation Output
```
SUCCESS
```
## police_lights.anim
**Status:** ✅ Success
@ -1236,8 +1256,8 @@ SUCCESS
## Summary
- **Total files processed:** 50
- **Successfully compiled:** 47
- **Total files processed:** 51
- **Successfully compiled:** 48
- **Failed to compile:** 3
### Successful Files
@ -1257,6 +1277,7 @@ SUCCESS
- ✅ demo_shutter_rainbow_central.anim
- ✅ demo_shutter_rainbow_leftright.anim
- ✅ demo_shutter_rainbow2.anim
- ✅ demo_value_meter.anim
- ✅ disco_strobe.anim
- ✅ fire_flicker.anim
- ✅ heartbeat_pulse.anim
@ -1267,9 +1288,9 @@ SUCCESS
- ✅ meteor_shower.anim
- ✅ neon_glow.anim
- ✅ ocean_waves.anim
- ✅ plasma_wave.anim
- ✅ palette_demo.anim
- ✅ palette_showcase.anim
- ✅ plasma_wave.anim
- ✅ police_lights.anim
- ✅ property_assignment_demo.anim
- ✅ rainbow_cycle.anim

View File

@ -0,0 +1,74 @@
# Generated Berry code from Animation DSL
# Source: demo_value_meter.anim
#
# This file was automatically generated by compile_all_examples.sh
# Do not edit manually - changes will be overwritten
import animation
# Pattern of colors in the background based on palette, rotating over 5 s
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
# Berry code block
def rand_meter(time_ms, self)
import math
var r = math.rand() % 101
return r
end
# End berry code block
# External function declaration: rand_meter
animation.register_user_function("rand_meter", rand_meter)
# define a palette of rainbow colors including white with constant brightness
var rainbow_with_white_ = bytes(
"FFFC0000" # Red
"FFFF8000" # Orange
"FFFFFF00" # Yellow
"FF00FF00" # Green
"FF00FFFF" # Cyan
"FF0080FF" # Blue
"FF8000FF" # Violet
"FFCCCCCC" # White
"FFFC0000" # Red - need to add the first color at last position to ensure roll-over
)
# define a gradient across the whole strip
var back_pattern_ = animation.palette_meter_animation(engine)
back_pattern_.value_func = animation.create_closure_value(engine, def (engine) return animation.get_user_function('rand_meter')(engine) end)
engine.add(back_pattern_)
engine.run()
#- Original DSL source:
# Pattern of colors in the background based on palette, rotating over 5 s
berry """
def rand_meter(time_ms, self)
import math
var r = math.rand() % 101
return r
end
"""
extern function rand_meter
# define a palette of rainbow colors including white with constant brightness
palette rainbow_with_white = [
0xFC0000 # Red
0xFF8000 # Orange
0xFFFF00 # Yellow
0x00FF00 # Green
0x00FFFF # Cyan
0x0080FF # Blue
0x8000FF # Violet
0xCCCCCC # White
0xFC0000 # Red - need to add the first color at last position to ensure roll-over
]
# define a gradient across the whole strip
animation back_pattern = palette_meter_animation(value_func = rand_meter)
run back_pattern
-#

View File

@ -0,0 +1,29 @@
# Pattern of colors in the background based on palette, rotating over 5 s
berry """
def rand_meter(time_ms, self)
import math
var r = math.rand() % 101
return r
end
"""
extern function rand_meter
# define a palette of rainbow colors including white with constant brightness
palette rainbow_with_white = [
0xFC0000 # Red
0xFF8000 # Orange
0xFFFF00 # Yellow
0x00FF00 # Green
0x00FFFF # Cyan
0x0080FF # Blue
0x8000FF # Violet
0xCCCCCC # White
0xFC0000 # Red - need to add the first color at last position to ensure roll-over
]
# define a gradient across the whole strip
animation back_pattern = palette_meter_animation(value_func = rand_meter)
run back_pattern

View File

@ -13,7 +13,7 @@ palette rainbow_with_white = [
0xFC0000 # Red - need to add the first color at last position to ensure roll-over
]
# define a color attribute that cycles over time, cycle is 10 seconds
# define a color attribute cycles color in space
color rainbow_rich_color = rich_palette(palette=rainbow_with_white, cycle_period=0, transition_type=SINE)
# define a gradient across the whole strip

View File

@ -8,6 +8,8 @@ def rand_meter(time_ms, self)
end
"""
extern function rand_meter
# define a palette of rainbow colors including white with constant brightness
palette rainbow_with_white = [
0xFC0000 # Red
@ -21,7 +23,10 @@ palette rainbow_with_white = [
0xFC0000 # Red - need to add the first color at last position to ensure roll-over
]
# define a color attribute cycles color in space
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_meter_animation(value_func = rand_meter)
animation back_pattern = palette_meter_animation(color_source = rainbow_rich_color, value_func = rand_meter)
run back_pattern

View File

@ -22,7 +22,8 @@ ParameterizedObject (base class with parameter management and playable interface
│ ├── BeaconAnimation (pulse at specific position)
│ ├── CrenelPositionAnimation (crenel/square wave pattern)
│ ├── BreatheAnimation (breathing effect)
│ ├── PalettePatternAnimation (base for palette-based animations)
│ ├── PaletteGradientAnimation (gradient patterns with palette colors)
│ │ └── PaletteMeterAnimation (meter/bar patterns)
│ ├── CometAnimation (moving comet with tail)
│ ├── FireAnimation (realistic fire effect)
│ ├── TwinkleAnimation (twinkling stars effect)
@ -243,11 +244,11 @@ Generates oscillating values using various waveforms. Inherits from `ValueProvid
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `min_value` | int | 0 | - | Minimum oscillation value |
| `max_value` | int | 100 | - | Maximum oscillation value |
| `max_value` | int | 255 | - | Maximum oscillation value |
| `duration` | int | 1000 | min: 1 | Oscillation period in milliseconds |
| `form` | int | 1 | enum: [1,2,3,4,5,6,7,8,9] | Waveform type |
| `phase` | int | 0 | 0-100 | Phase shift percentage |
| `duty_cycle` | int | 50 | 0-100 | Duty cycle for square/triangle waves |
| `phase` | int | 0 | 0-255 | Phase shift in 0-255 range (mapped to duration) |
| `duty_cycle` | int | 127 | 0-255 | Duty cycle for square/triangle waves in 0-255 range |
**Waveform Constants**:
- `1` (SAWTOOTH) - Linear ramp from min to max
@ -301,6 +302,18 @@ The ClosureValueProvider includes built-in mathematical helper methods that can
- **Cosine Behavior**: Matches oscillator COSINE waveform (starts at minimum, not maximum)
- **Scale Function**: Uses `tasmota.scale_int()` for efficient integer scaling
#### Closure Signature
Closures used with ClosureValueProvider must follow this signature:
```berry
def closure_func(engine, param_name, time_ms)
# engine: AnimationEngine reference
# param_name: Name of the parameter being computed
# time_ms: Current time in milliseconds
return computed_value
end
```
#### Usage in Computed Values
These methods are automatically available in DSL computed expressions:
@ -1029,51 +1042,23 @@ animation strobe = wave_animation(
### PalettePatternAnimation
### PaletteGradientAnimation
Applies colors from a color provider to specific patterns using an efficient bytes() buffer. Inherits from `Animation`.
Creates shifting gradient patterns with palette colors. Inherits from `Animation`.
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `color_source` | instance | nil | - | Color provider for pattern mapping |
| `pattern_func` | function | nil | - | Function that generates pattern values (0-255) for each pixel |
| `shift_period` | int | 0 | min: 0 | Time for one complete shift cycle in ms (0 = static gradient) |
| `spatial_period` | int | 0 | min: 0 | Spatial period in pixels (0 = full strip length) |
| `phase_shift` | int | 0 | 0-255 | Phase shift in 0-255 range (mapped to spatial period) |
| *(inherits all Animation parameters)* | | | | |
**Implementation Details:**
- Uses `bytes()` buffer for efficient storage of per-pixel values
- Pattern function should return values in 0-255 range
- Color source receives values in 0-255 range via `get_color_for_value(value, time_ms)`
- Buffer automatically resizes when strip length changes
**Factory**: `animation.palette_pattern_animation(engine)`
### PaletteWaveAnimation
Creates sine wave patterns with palette colors. Inherits from `PalettePatternAnimation`.
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `wave_period` | int | 5000 | min: 1 | Wave animation period in ms |
| `wave_length` | int | 10 | min: 1 | Wave length in pixels |
| *(inherits all PalettePatternAnimation parameters)* | | | | |
**Pattern Generation:**
- Generates sine wave values in 0-255 range using `tasmota.sine_int()`
- Wave position advances based on `wave_period` timing
- Each pixel's value calculated as: `sine_value = tasmota.scale_int(sine_int(angle), -4096, 4096, 0, 255)`
**Factory**: `animation.palette_wave_animation(engine)`
### PaletteGradientAnimation
Creates shifting gradient patterns with palette colors. Inherits from `PalettePatternAnimation`.
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `shift_period` | int | 10000 | min: 0 | Time for one complete shift cycle in ms (0 = static gradient) |
| `spatial_period` | int | 0 | min: 0 | Spatial period in pixels (0 = full strip length) |
| `phase_shift` | int | 0 | 0-100 | Phase shift as percentage of spatial period |
| *(inherits all PalettePatternAnimation parameters)* | | | | |
- Optimized LUT (Lookup Table) support for color providers
**Pattern Generation:**
- Generates linear gradient values in 0-255 range across the specified spatial period
@ -1084,23 +1069,27 @@ Creates shifting gradient patterns with palette colors. Inherits from `PalettePa
- `0`: Gradient spans the full strip length (single gradient across entire strip)
- `> 0`: Gradient repeats every N pixels
- **phase_shift**: Shifts the gradient pattern spatially by a percentage of the spatial period
- Each pixel's value calculated as: `value = tasmota.scale_uint(spatial_position, 0, spatial_period-1, 0, 255)`
- Each pixel's value calculated using optimized fixed-point arithmetic
**Factory**: `animation.palette_gradient_animation(engine)`
### PaletteMeterAnimation
Creates meter/bar patterns based on a value function. Inherits from `PalettePatternAnimation`.
Creates meter/bar patterns based on a value function. Inherits from `PaletteGradientAnimation`.
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `value_func` | function | nil | - | Function that provides meter values (0-100 range) |
| *(inherits all PalettePatternAnimation parameters)* | | | | |
| `value_func` | function | nil | - | Function that provides meter values (0-255 range) |
| *(inherits all PaletteGradientAnimation parameters)* | | | | |
**Pattern Generation:**
- Value function returns percentage (0-100) representing meter level
- Value function signature: `value_func(engine, time_ms, self)` where:
- `engine`: AnimationEngine reference
- `time_ms`: Elapsed time since animation start
- `self`: Reference to the animation instance
- Value function returns value in 0-255 range representing meter level
- Pixels within meter range get value 255, others get value 0
- Meter position calculated as: `position = tasmota.scale_uint(value, 0, 100, 0, strip_length)`
- Meter position calculated as: `position = tasmota.scale_uint(value, 0, 255, 0, strip_length)`
**Factory**: `animation.palette_meter_animation(engine)`

View File

@ -59,6 +59,7 @@ The following keywords are reserved and cannot be used as identifiers:
- `set` - Variable assignment
- `import` - Import Berry modules
- `berry` - Embed arbitrary Berry code
- `extern` - Declare external Berry functions for DSL use
**Definition Keywords:**
- `color` - Color definition
@ -295,6 +296,70 @@ print("Animation configured")
run pulse
```
### External Function Declarations
The `extern` keyword declares Berry functions defined in `berry` code blocks so they can be used in DSL expressions and computed parameters:
```berry
# Define the function in a berry block
berry """
def rand_meter(time_ms, self)
import math
var r = math.rand() % 101
return r
end
def breathing_effect(base_value, amplitude)
import math
var time_factor = (tasmota.millis() / 1000) % 4 # 4-second cycle
var breath = math.sin(time_factor * math.pi / 2)
return int(base_value + breath * amplitude)
end
"""
# Declare functions as external so DSL knows about them
extern function rand_meter
extern function breathing_effect
# Now they can be used in DSL expressions
animation back_pattern = palette_meter_animation(value_func = rand_meter)
animation breathing_light = solid(color=blue)
breathing_light.opacity = breathing_effect
run back_pattern
```
**External Function Features:**
- Functions must be declared with `extern function function_name` after being defined in `berry` blocks
- Can be used with or without parentheses: `rand_meter` or `rand_meter()`
- Automatically receive `engine` as the first parameter when called from DSL
- Can be used in computed parameters and property assignments
- Support the same signature pattern as registered user functions
**Function Signature Requirements:**
External functions should follow the user function pattern where the first parameter is the engine:
```berry
berry """
def my_external_func(engine, param1, param2)
# engine is automatically provided by DSL
# param1, param2 are user-provided parameters
return computed_value
end
"""
extern function my_external_func
# Usage in DSL (engine parameter is automatic)
animation test = solid(color=red)
test.opacity = my_external_func # Called as my_external_func(engine)
```
**Comparison with User Functions:**
- **External functions**: Defined in `berry` blocks within the same DSL file, declared with `extern function`
- **User functions**: Defined in separate Berry modules, imported with `import`, registered with `animation.register_user_function()`
- Both use the same calling convention and can be used interchangeably in DSL expressions
## Color Definitions
The `color` keyword defines static colors or color providers:
@ -1449,11 +1514,13 @@ statement = import_stmt
| property_assignment
| sequence
| template_def
| external_stmt
| execution_stmt ;
(* Import and Configuration *)
import_stmt = "import" identifier ;
config_stmt = variable_assignment ;
external_stmt = "extern" "function" identifier ;
(* strip_config = "strip" "length" number ; -- TEMPORARILY DISABLED *)
variable_assignment = "set" identifier "=" expression ;
@ -1607,6 +1674,7 @@ This applies to:
- Execution statements
- **Template animations**: Reusable animation classes with parameters extending `engine_proxy`
- User-defined functions (with engine-first parameter pattern) - see **[User Functions Guide](USER_FUNCTIONS.md)**
- **External function declarations**: `extern` keyword to declare Berry functions defined in `berry` blocks for DSL use
- **User functions in computed parameters**: User functions can be used in arithmetic expressions alongside mathematical functions
- **Flexible parameter syntax**: Commas optional when parameters are on separate lines
- **Computed values**: Arithmetic expressions with value providers automatically create closures

View File

@ -31,7 +31,7 @@ class GradientAnimation : animation.animation
self.phase_offset = 0
# Initialize with default strip length from engine
var strip_length = self.engine.get_strip_length()
var strip_length = self.engine.strip_length
self.current_colors.resize(strip_length)
# Initialize colors to black
@ -47,7 +47,7 @@ class GradientAnimation : animation.animation
super(self).on_param_changed(name, value)
# TODO maybe be more specific on attribute name
# Handle strip length changes from engine
var current_strip_length = self.engine.get_strip_length()
var current_strip_length = self.engine.strip_length
if size(self.current_colors) != current_strip_length
self.current_colors.resize(current_strip_length)
var i = size(self.current_colors)
@ -92,7 +92,7 @@ class GradientAnimation : animation.animation
# Cache parameter values for performance
var gradient_type = self.gradient_type
var color_param = self.color
var strip_length = self.engine.get_strip_length()
var strip_length = self.engine.strip_length
# Ensure current_colors array matches strip length
if size(self.current_colors) != strip_length
@ -205,7 +205,7 @@ class GradientAnimation : animation.animation
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var strip_length = self.engine.get_strip_length()
var strip_length = self.engine.strip_length
var i = 0
while i < strip_length && i < frame.width
if i < size(self.current_colors)
@ -271,4 +271,7 @@ def gradient_two_color_linear(engine)
return anim
end
return {'gradient_animation': GradientAnimation, 'gradient_rainbow_linear': gradient_rainbow_linear, 'gradient_rainbow_radial': gradient_rainbow_radial, 'gradient_two_color_linear': gradient_two_color_linear}
return {'gradient_animation': GradientAnimation,
'gradient_rainbow_linear': gradient_rainbow_linear,
'gradient_rainbow_radial': gradient_rainbow_radial,
'gradient_two_color_linear': gradient_two_color_linear}

View File

@ -1,25 +1,25 @@
# PalettePattern animation effect for Berry Animation Framework
# PaletteGradient animation effect for Berry Animation Framework
#
# This animation applies colors from a color provider to specific patterns or regions.
# It allows for more complex visual effects by combining palette colors with patterns.
#
# This version supports both RichPaletteAnimation and ColorProvider instances as color sources,
# allowing for more flexible usage of color providers.
# This animation creates gradient patterns with palette colors.
# It supports shifting gradients, spatial periods, and phase shifts.
import "./core/param_encoder" as encode_constraints
#@ solidify:PalettePatternAnimation,weak
class PalettePatternAnimation : animation.animation
# Gradient pattern animation - creates shifting gradient patterns
#@ solidify:PaletteGradientAnimation,weak
class PaletteGradientAnimation : animation.animation
var value_buffer # Buffer to store values for each pixel (bytes object)
# Static definitions of parameters with constraints
static var PARAMS = animation.enc_params({
# Palette pattern-specific parameters
# Gradient-specific parameters
"color_source": {"default": nil, "type": "instance"},
"pattern_func": {"default": nil, "type": "function"}
"shift_period": {"min": 0, "default": 0}, # Time for one complete shift cycle in ms (0 = static)
"spatial_period": {"min": 0, "default": 0}, # Spatial period in pixels (0 = full strip)
"phase_shift": {"min": 0, "max": 255, "default": 0} # Phase shift in 0-255 range
})
# Initialize a new PalettePattern animation
# Initialize a new gradient pattern animation
#
# @param engine: AnimationEngine - Required animation engine reference
def init(engine)
@ -29,6 +29,9 @@ class PalettePatternAnimation : animation.animation
# Initialize non-parameter instance variables only
self.value_buffer = bytes()
# Set default name
self.name = "palette_gradient"
# Initialize value buffer with default frame width
self._initialize_value_buffer()
end
@ -46,24 +49,47 @@ class PalettePatternAnimation : animation.animation
end
end
# Update the value buffer based on the current time
#
# @param time_ms: int - Current time in milliseconds
# Update the value buffer to generate gradient pattern
def _update_value_buffer(time_ms, strip_length)
var pattern_func = self.pattern_func
if pattern_func == nil
return
# Cache parameter values for performance
var shift_period = self.shift_period
var spatial_period = self.spatial_period
var phase_shift = self.phase_shift
# Determine effective spatial period (0 means full strip)
var effective_spatial_period = spatial_period > 0 ? spatial_period : strip_length
# Calculate the temporal shift position (how much the pattern has moved over time)
var temporal_offset = 0
if shift_period > 0
temporal_offset = tasmota.scale_uint(time_ms % shift_period, 0, shift_period, 0, effective_spatial_period)
end
# Calculate the phase shift offset in pixels
var phase_offset = tasmota.scale_uint(phase_shift, 0, 255, 0, effective_spatial_period)
# Calculate values for each pixel
var i = 0
# Calculate position within the spatial period, including temporal and phase offsets
var spatial_pos = (temporal_offset + phase_offset) % effective_spatial_period
# Calculate the increment per pixel, in 1/1024 of pixels
# We calculate 1024*255/effective_spatial_period
# But for rounding we actually calculate
# ((1024 * 255 * 2) + 1) / (2 * effective_spatial_period)
# Note: (1024 * 255 * 2) + 1 = 522241
var incr_1024 = (522241 / effective_spatial_period) >> 1
# 'spatial_1024' is our accumulator in 1/1024th of pixels, 2^10
var spatial_1024 = spatial_pos * incr_1024
var buffer = self.value_buffer._buffer() # 'buffer' is of type 'comptr'
# var effective_spatial_period_1 = effective_spatial_period - 1
# # Calculate the increment in 1/256 of values
# var increment = tasmota.scale_uint(effective_spatial_period)
while i < strip_length
var pattern_value = pattern_func(i, time_ms, self)
# Pattern function should return values in 0-255 range, clamp to byte range
var byte_value = int(pattern_value)
if byte_value < 0 byte_value = 0 end
if byte_value > 255 byte_value = 255 end
self.value_buffer[i] = byte_value
buffer[i] = spatial_1024 >> 10
spatial_1024 += incr_1024 # we don't really care about overflow since we clamp modula 255 anyways
i += 1
end
end
@ -163,8 +189,8 @@ class PalettePatternAnimation : animation.animation
# Handle parameter changes
def on_param_changed(name, value)
super(self).on_param_changed(name, value)
if name == "pattern_func" || name == "color_source"
# Reinitialize value buffer when pattern or color source changes
if name == "color_source"
# Reinitialize value buffer when color source changes
self._initialize_value_buffer()
end
end
@ -176,125 +202,9 @@ class PalettePatternAnimation : animation.animation
end
end
# Wave pattern animation - creates sine wave patterns
#@ solidify:PaletteWaveAnimation,weak
class PaletteWaveAnimation : PalettePatternAnimation
# Static definitions of parameters with constraints
static var PARAMS = animation.enc_params({
# Wave-specific parameters only
"wave_period": {"min": 1, "default": 5000},
"wave_length": {"min": 1, "default": 10}
})
# Initialize a new wave pattern animation
#
# @param engine: AnimationEngine - Required animation engine reference
def init(engine)
# Call parent constructor
super(self).init(engine)
# Set default name
self.name = "palette_wave"
end
# Override _update_value_buffer to generate wave pattern directly
def _update_value_buffer(time_ms, strip_length)
# Cache parameter values for performance
var wave_period = self.wave_period
var wave_length = self.wave_length
# Calculate the wave position using scale_uint for better precision
# var position = tasmota.scale_uint(time_ms % wave_period, 0, wave_period, 0, 1000) / 1000.0
# var offset = int(position * wave_length)
var offset = tasmota.scale_uint(time_ms % wave_period, 0, wave_period, 0, wave_length)
# Calculate values for each pixel
var i = 0
while i < strip_length
# Calculate the wave value (0-255) using scale_uint
var pos_in_wave = (i + offset) % wave_length
var angle = tasmota.scale_uint(pos_in_wave, 0, wave_length, 0, 32767) # 0 to 2π in fixed-point
var sine_value = tasmota.sine_int(angle) # -4096 to 4096
# Map sine value from -4096..4096 to 0..255
var byte_value = tasmota.scale_int(sine_value, -4096, 4096, 0, 255)
self.value_buffer[i] = byte_value
i += 1
end
end
end
# Gradient pattern animation - creates shifting gradient patterns
#@ solidify:PaletteGradientAnimation,weak
class PaletteGradientAnimation : PalettePatternAnimation
# Static definitions of parameters with constraints
static var PARAMS = animation.enc_params({
# Gradient-specific parameters only
"shift_period": {"min": 0, "default": 0}, # Time for one complete shift cycle in ms (0 = static)
"spatial_period": {"min": 0, "default": 0}, # Spatial period in pixels (0 = full strip)
"phase_shift": {"min": 0, "max": 100, "default": 0} # Phase shift as percentage (0-100)
})
# Initialize a new gradient pattern animation
#
# @param engine: AnimationEngine - Required animation engine reference
def init(engine)
# Call parent constructor
super(self).init(engine)
# Set default name
self.name = "palette_gradient"
end
# Override _update_value_buffer to generate gradient pattern directly
def _update_value_buffer(time_ms, strip_length)
# Cache parameter values for performance
var shift_period = self.shift_period
var spatial_period = self.spatial_period
var phase_shift = self.phase_shift
# Determine effective spatial period (0 means full strip)
var effective_spatial_period = spatial_period > 0 ? spatial_period : strip_length
# Calculate the temporal shift position (how much the pattern has moved over time)
var temporal_offset = 0
if shift_period > 0
temporal_offset = tasmota.scale_uint(time_ms % shift_period, 0, shift_period, 0, effective_spatial_period)
end
# Calculate the phase shift offset in pixels
var phase_offset = tasmota.scale_uint(phase_shift, 0, 100, 0, effective_spatial_period)
# Calculate values for each pixel
var i = 0
# Calculate position within the spatial period, including temporal and phase offsets
var spatial_pos = (temporal_offset + phase_offset) % effective_spatial_period
# Calculate the increment per pixel, in 1/1024 of pixels
# We calculate 1024*255/effective_spatial_period
# But for rounding we actually calculate
# ((1024 * 255 * 2) + 1) / (2 * effective_spatial_period)
# Note: (1024 * 255 * 2) + 1 = 522241
var incr_1024 = (522241 / effective_spatial_period) >> 1
# 'spatial_1024' is our accumulator in 1/1024th of pixels, 2^10
var spatial_1024 = spatial_pos * incr_1024
var buffer = self.value_buffer._buffer() # 'buffer' is of type 'comptr'
# var effective_spatial_period_1 = effective_spatial_period - 1
# # Calculate the increment in 1/256 of values
# var increment = tasmota.scale_uint(effective_spatial_period)
while i < strip_length
buffer[i] = spatial_1024 >> 10
spatial_1024 += incr_1024 # we don't really care about overflow since we clamp modula 255 anyways
i += 1
end
end
end
# Value meter pattern animation - creates meter/bar patterns based on a value function
#@ solidify:PaletteMeterAnimation,weak
class PaletteMeterAnimation : PalettePatternAnimation
class PaletteMeterAnimation : PaletteGradientAnimation
# Static definitions of parameters with constraints
static var PARAMS = animation.enc_params({
# Meter-specific parameters only
@ -320,11 +230,14 @@ class PaletteMeterAnimation : PalettePatternAnimation
return
end
# Cache engine reference to avoid dereferencing
var engine = self.engine
# Get the current value
var current_value = value_func(time_ms, self)
var current_value = value_func(engine, time_ms, self)
# Calculate the meter position using scale_uint for better precision
var meter_position = tasmota.scale_uint(current_value, 0, 100, 0, strip_length)
var meter_position = tasmota.scale_uint(current_value, 0, 255, 0, strip_length)
# Calculate values for each pixel
var i = 0
@ -337,8 +250,6 @@ class PaletteMeterAnimation : PalettePatternAnimation
end
return {
'palette_pattern_animation': PalettePatternAnimation,
'palette_wave_animation': PaletteWaveAnimation,
'palette_gradient_animation': PaletteGradientAnimation,
'palette_meter_animation': PaletteMeterAnimation
}

View File

@ -587,17 +587,17 @@ extern "C" {
// Handle negative indices (Python-style)
if (start_pos < 0) { start_pos += width; }
if (end_pos < 0) { end_pos += width; }
if (end_pos < 0) { end_pos += width + 1; }
// Clamp to valid range
if (start_pos < 0) { start_pos = 0; }
if (end_pos < 0) { end_pos = 0; }
if (start_pos >= width) { be_return_nil(vm); }
if (end_pos >= width) { end_pos = width - 1; }
if (end_pos < start_pos) { be_return_nil(vm); }
if (end_pos >= width) { end_pos = width; }
if (end_pos <= start_pos) { be_return_nil(vm); }
// Fill the region with the color
for (int32_t i = start_pos; i <= end_pos; i++) {
for (int32_t i = start_pos; i < end_pos; i++) {
pixels_buf[i] = color;
}

View File

@ -5,6 +5,9 @@
# child management and rendering to the root animation.
class AnimationEngine
# Minimum milliseconds between ticks
static var TICK_MS = 50
# Core properties
var strip # LED strip object
var strip_length # Strip length (cached for performance)
@ -17,6 +20,7 @@ class AnimationEngine
var last_update # Last update time in milliseconds
var time_ms # Current time in milliseconds (updated each frame)
var fast_loop_closure # Stored closure for fast_loop registration
var tick_ms # Minimum milliseconds between ticks (runtime configurable)
# Performance optimization
var render_needed # Whether a render pass is needed
@ -77,6 +81,7 @@ class AnimationEngine
self.last_update = 0
self.time_ms = 0
self.fast_loop_closure = nil
self.tick_ms = self.TICK_MS # Initialize from static default
self.render_needed = false
# Initialize CPU metrics
@ -187,25 +192,25 @@ class AnimationEngine
return false
end
# Start timing this tick
self.ts_start = tasmota.millis()
if current_time == nil
current_time = self.ts_start
current_time = tasmota.millis()
end
# Throttle updates based on tick_ms setting
var delta_time = current_time - self.last_update
if delta_time < self.tick_ms
return true
end
# Start timing this tick (use tasmota.millis() for consistent profiling)
self.ts_start = tasmota.millis()
# Check if strip length changed since last time
self.check_strip_length()
# Update engine time
self.time_ms = current_time
# Throttle updates to ~5ms intervals
var delta_time = current_time - self.last_update
if delta_time < 5
return true
end
self.last_update = current_time
# Check if strip can accept updates

View File

@ -27,29 +27,20 @@ class Token
static var statement_keywords = [
"strip", "set", "color", "palette", "animation",
"sequence", "function", "zone", "on", "run", "template", "param", "import", "berry"
"sequence", "function", "on", "run", "template", "param", "import", "berry"
]
static var keywords = [
# Configuration keywords
"strip", "set", "import", "berry",
"strip", "set", "import", "berry", "extern",
# Definition keywords
"color", "palette", "animation", "sequence", "function", "zone", "template", "param", "type",
"color", "palette", "animation", "sequence", "function", "template", "param", "type",
# Control flow keywords
"play", "for", "with", "repeat", "times", "forever", "if", "else", "elif",
"choose", "random", "on", "run", "wait", "goto", "interrupt", "resume",
"while", "from", "to", "return", "reset", "restart",
# Modifier keywords (only actual DSL syntax keywords)
"at", "ease", "sync", "every", "stagger", "across", "pixels",
# Core built-in functions (minimal set for essential DSL operations)
"rgb", "hsv",
# Spatial keywords
"all", "even", "odd", "center", "edges", "left", "right", "top", "bottom",
"while", "from", "to", "return", "reset", "restart", "every",
# Boolean and special values
"true", "false", "nil", "transparent",
@ -59,7 +50,7 @@ class Token
"brightness_change", "timer", "time", "sound_peak", "network_message",
# Time and measurement keywords
"ms", "s", "m", "h", "bpm"
"ms", "s", "m", "h"
]
static var color_names = [

View File

@ -508,6 +508,8 @@ class SimpleDSLTranspiler
self.process_event_handler()
elif tok.value == "berry"
self.process_berry_code_block()
elif tok.value == "extern"
self.process_external_function()
else
self.error(f"Unknown keyword '{tok.value}'.")
self.skip_statement()
@ -1755,6 +1757,13 @@ class SimpleDSLTranspiler
return self.ExpressionResult.literal(entry.get_reference(), 11 #-animation_dsl._symbol_entry.TYPE_COLOR-#)
end
# Special handling for user functions used without parentheses
if entry.is_user_function()
# User function used without parentheses - call it with engine parameter
var result = f"animation.get_user_function('{name}')(engine)"
return self.ExpressionResult.function_call(result)
end
# Regular identifier - check if it's a variable reference
var ref = self.symbol_table.get_reference(name)
var return_type = self._determine_symbol_return_type(entry) # compute the return type based on entry
@ -2693,6 +2702,45 @@ class SimpleDSLTranspiler
self.add("# End berry code block")
end
# Process external function declaration: extern function function_name
def process_external_function()
self.next() # skip 'extern'
# Expect 'function' keyword
var tok = self.current()
if tok == nil || tok.type != 0 #-animation_dsl.Token.KEYWORD-# || tok.value != "function"
self.error("Expected 'function' keyword after 'extern'. Use: extern function function_name")
self.skip_statement()
return
end
self.next() # skip 'function'
# Expect an identifier for the function name
tok = self.current()
if tok == nil || tok.type != 1 #-animation_dsl.Token.IDENTIFIER-#
self.error("Expected function name after 'extern function'. Use: extern function function_name")
self.skip_statement()
return
end
var func_name = tok.value
self.next() # consume identifier token
var inline_comment = self.collect_inline_comment()
# Validate function name
self.validate_user_name(func_name, "extern function")
# Register the function as a user function in the symbol table
# This allows it to be used in computed parameters and function calls
self.symbol_table.register_user_function(func_name)
# Generate runtime registration call so the function is available at execution time
self.add(f"# External function declaration: {func_name}{inline_comment}")
self.add(f"animation.register_user_function(\"{func_name}\", {func_name})")
end
# Generate default strip initialization using Tasmota configuration
def generate_default_strip_initialization()
if self.strip_initialized

View File

@ -34,11 +34,11 @@ class OscillatorValueProvider : animation.value_provider
# Parameter definitions for the oscillator
static var PARAMS = animation.enc_params({
"min_value": {"default": 0},
"max_value": {"default": 100},
"max_value": {"default": 255},
"duration": {"min": 1, "default": 1000},
"form": {"enum": [1, 2, 3, 4, 5, 6, 7, 8, 9], "default": 1},
"phase": {"min": 0, "max": 100, "default": 0},
"duty_cycle": {"min": 0, "max": 100, "default": 50}
"phase": {"min": 0, "max": 255, "default": 0},
"duty_cycle": {"min": 0, "max": 255, "default": 127}
})
# Initialize a new OscillatorValueProvider
@ -92,7 +92,7 @@ class OscillatorValueProvider : animation.value_provider
past = 0
end
var duration_ms_mid = tasmota.scale_uint(duty_cycle, 0, 100, 0, duration)
var duration_ms_mid = tasmota.scale_uint(duty_cycle, 0, 255, 0, duration)
# Handle cycle wrapping
if past >= duration
@ -105,7 +105,7 @@ class OscillatorValueProvider : animation.value_provider
# Apply phase shift
if phase > 0
past_with_phase += tasmota.scale_uint(phase, 0, 100, 0, duration)
past_with_phase += tasmota.scale_uint(phase, 0, 255, 0, duration)
if past_with_phase >= duration
past_with_phase -= duration
end

View File

@ -209,6 +209,9 @@ class MockDynamicStrip
i += 1
end
end
def push_pixels_buffer_argb()
end
def show()
self.show_calls += 1
@ -287,9 +290,12 @@ assert_test(new_show_calls >= old_show_calls, "Strip should be updated after len
# Test 10d: Multiple length changes
print("\n--- Test 10d: Multiple length changes ---")
var lengths_to_test = [10, 50, 5, 30]
var base_tick_time = int(tasmota.millis()) + 5000 # Start well after previous tests
var tick_offset = 0
for new_length : lengths_to_test
dynamic_strip.set_length(new_length)
dynamic_engine.on_tick(tasmota.millis())
dynamic_engine.on_tick(base_tick_time + tick_offset)
tick_offset += 100 # Space ticks 100ms apart to avoid throttling
assert_equals(dynamic_engine.strip_length, new_length, f"Engine should adapt to length {new_length}")
assert_equals(dynamic_engine.frame_buffer.width, new_length, f"Frame buffer should adapt to length {new_length}")
assert_equals(dynamic_engine.temp_buffer.width, new_length, f"Temp buffer should adapt to length {new_length}")
@ -315,7 +321,8 @@ assert_equals(dynamic_engine.size(), 2, "Should have 2 animations")
# Change length and verify all animations continue working
dynamic_strip.set_length(40)
old_show_calls = dynamic_strip.show_calls
dynamic_engine.on_tick(tasmota.millis())
# Use a time that's guaranteed to be past the throttle window
dynamic_engine.on_tick(int(tasmota.millis()) + 10000)
assert_equals(dynamic_engine.strip_length, 40, "Engine should handle length change with multiple animations")
new_show_calls = dynamic_strip.show_calls
@ -325,20 +332,21 @@ assert_equals(dynamic_engine.size(), 2, "Should still have 2 animations after le
# Test 10f: Invalid length handling
print("\n--- Test 10f: Invalid length handling ---")
var current_width = dynamic_engine.strip_length
var invalid_test_time = int(tasmota.millis()) + 15000
# Test zero length (should be ignored)
dynamic_strip.set_length(0)
dynamic_engine.on_tick(tasmota.millis())
dynamic_engine.on_tick(invalid_test_time)
assert_equals(dynamic_engine.strip_length, current_width, "Should ignore zero length")
# Test negative length (should be ignored)
dynamic_strip.set_length(-5)
dynamic_engine.on_tick(tasmota.millis())
dynamic_engine.on_tick(invalid_test_time + 100)
assert_equals(dynamic_engine.strip_length, current_width, "Should ignore negative length")
# Restore valid length
dynamic_strip.set_length(20)
dynamic_engine.on_tick(tasmota.millis())
dynamic_engine.on_tick(invalid_test_time + 200)
assert_equals(dynamic_engine.strip_length, 20, "Should accept valid length after invalid ones")
# Test 10g: Performance impact of length checking
@ -366,6 +374,159 @@ assert_test(changing_time < 200, f"20 ticks with length changes should be reason
dynamic_engine.stop()
# Test 11: Tick Interval Configuration
print("\n--- Test 11: Tick Interval Configuration ---")
# Test 11a: Static default value
print("\n--- Test 11a: Static default value ---")
assert_equals(animation.create_engine.TICK_MS, 50, "Static TICK_MS should default to 50ms")
# Test 11b: Instance initialization from static default
print("\n--- Test 11b: Instance initialization from static default ---")
var tick_strip = global.Leds(10)
var tick_engine = animation.create_engine(tick_strip)
assert_equals(tick_engine.tick_ms, 50, "Instance tick_ms should initialize to static default (50ms)")
# Test 11c: Runtime modification
print("\n--- Test 11c: Runtime modification ---")
tick_engine.tick_ms = 100
assert_equals(tick_engine.tick_ms, 100, "Should be able to change tick_ms at runtime to 100ms")
tick_engine.tick_ms = 25
assert_equals(tick_engine.tick_ms, 25, "Should be able to change tick_ms at runtime to 25ms")
tick_engine.tick_ms = 5
assert_equals(tick_engine.tick_ms, 5, "Should be able to change tick_ms at runtime to 5ms")
# Test 11d: Throttling behavior with different tick_ms values
print("\n--- Test 11d: Throttling behavior with different tick_ms values ---")
# Create a mock strip to track show() calls
class ThrottleTestStrip
var _length
var show_calls
var last_show_time
def init(length)
self._length = length
self.show_calls = 0
self.last_show_time = 0
end
def length()
return self._length
end
def set_pixel_color(index, color)
end
def clear()
end
def push_pixels_buffer_argb(buffer)
end
def show()
self.show_calls += 1
self.last_show_time = tasmota.millis()
end
def can_show()
return true
end
end
var throttle_strip = ThrottleTestStrip(10)
var throttle_engine = animation.create_engine(throttle_strip)
# Add a simple animation
var throttle_anim = animation.solid(throttle_engine)
throttle_anim.color = 0xFFFF0000
throttle_engine.add(throttle_anim)
throttle_engine.run()
# Test with 50ms throttle (default)
print("\n--- Testing with 50ms throttle ---")
throttle_engine.tick_ms = 50
throttle_strip.show_calls = 0
var base_time = int(tasmota.millis()) + 10000 # Start well after any previous ticks
# Simulate rapid ticks within throttle window (should be throttled)
throttle_engine.on_tick(base_time)
var initial_calls = throttle_strip.show_calls
throttle_engine.on_tick(base_time + 10) # +10ms - should be throttled
throttle_engine.on_tick(base_time + 20) # +20ms - should be throttled
throttle_engine.on_tick(base_time + 40) # +40ms - should be throttled
var throttled_calls = throttle_strip.show_calls
assert_test(throttled_calls <= initial_calls + 1, f"Ticks within 50ms window should be throttled (got {throttled_calls - initial_calls} additional calls)")
# Tick after throttle window (should render)
throttle_engine.on_tick(base_time + 60) # +60ms - should render
var after_throttle_calls = throttle_strip.show_calls
# Debug: print the call counts
# print(f"DEBUG: initial={initial_calls}, throttled={throttled_calls}, after={after_throttle_calls}")
assert_test(after_throttle_calls > throttled_calls, f"Tick after throttle window should render (initial={initial_calls}, throttled={throttled_calls}, after={after_throttle_calls})")
# Test with 100ms throttle
print("\n--- Testing with 100ms throttle ---")
throttle_engine.tick_ms = 100
throttle_strip.show_calls = 0
base_time = int(tasmota.millis()) + 20000 # Start well after previous test
throttle_engine.on_tick(base_time)
var initial_calls_100 = throttle_strip.show_calls
throttle_engine.on_tick(base_time + 50) # +50ms - should be throttled
throttle_engine.on_tick(base_time + 80) # +80ms - should be throttled
throttled_calls = throttle_strip.show_calls
assert_test(throttled_calls <= initial_calls_100 + 1, f"Ticks within 100ms window should be throttled (got {throttled_calls - initial_calls_100} additional calls)")
throttle_engine.on_tick(base_time + 110) # +110ms - should render
after_throttle_calls = throttle_strip.show_calls
assert_test(after_throttle_calls > throttled_calls, "Tick after 100ms throttle window should render")
# Test with 10ms throttle (faster updates)
print("\n--- Testing with 10ms throttle ---")
throttle_engine.tick_ms = 10
throttle_strip.show_calls = 0
base_time = int(tasmota.millis()) + 30000 # Start well after previous test
throttle_engine.on_tick(base_time)
var initial_calls_10 = throttle_strip.show_calls
throttle_engine.on_tick(base_time + 5) # +5ms - should be throttled
var fast_throttled = throttle_strip.show_calls
assert_test(fast_throttled <= initial_calls_10 + 1, f"Ticks within 10ms window should be throttled (got {fast_throttled - initial_calls_10} additional calls)")
throttle_engine.on_tick(base_time + 15) # +15ms - should render
var fast_after = throttle_strip.show_calls
assert_test(fast_after > fast_throttled, "Tick after 10ms throttle window should render")
# Test 11e: Independent engine instances
print("\n--- Test 11e: Independent engine instances ---")
var strip_a = global.Leds(10)
var strip_b = global.Leds(10)
var engine_a = animation.create_engine(strip_a)
var engine_b = animation.create_engine(strip_b)
# Set different tick_ms values
engine_a.tick_ms = 25
engine_b.tick_ms = 75
assert_equals(engine_a.tick_ms, 25, "Engine A should have tick_ms of 25ms")
assert_equals(engine_b.tick_ms, 75, "Engine B should have tick_ms of 75ms")
assert_test(engine_a.tick_ms != engine_b.tick_ms, "Different engine instances should have independent tick_ms values")
# Test 11f: Tick interval doesn't affect static default
print("\n--- Test 11f: Tick interval doesn't affect static default ---")
var test_engine = animation.create_engine(global.Leds(10))
test_engine.tick_ms = 200
assert_equals(animation.create_engine.TICK_MS, 50, "Changing instance tick_ms should not affect static TICK_MS")
# New engine should still use static default
var new_engine = animation.create_engine(global.Leds(10))
assert_equals(new_engine.tick_ms, 50, "New engine should initialize with static default, not modified instance value")
throttle_engine.stop()
# Cleanup
engine.stop()

View File

@ -274,7 +274,6 @@ def test_closure_math_methods()
# Test 1: min/max functions
provider.closure = def(self, name, time_ms)
print(f">> {name=} {animation._math=}")
if name == "min_test"
return animation._math.min(5, 3, 8, 1, 9) # Should return 1
elif name == "max_test"

View File

@ -77,6 +77,7 @@ print("\n--- Test 4: Timestamps Set During Ticks ---")
# Create a fresh engine for timestamp testing with an animation
var ts_strip = global.Leds(20)
var ts_engine = animation.create_engine(ts_strip)
ts_engine.tick_ms = 5 # Set low tick interval for testing
# Add an animation so rendering happens
var ts_anim = animation.solid(ts_engine)
@ -85,7 +86,7 @@ ts_engine.add(ts_anim)
ts_engine.run()
# Run a single tick
var current_time = tasmota.millis()
var current_time = int(tasmota.millis())
ts_engine.on_tick(current_time)
# Check that timestamps were set
@ -121,10 +122,11 @@ print("\n--- Test 5: Phase Metrics Accumulation ---")
# Create engine and run multiple ticks
var phase_strip = global.Leds(15)
var phase_engine = animation.create_engine(phase_strip)
phase_engine.tick_ms = 5 # Set low tick interval for testing
phase_engine.run()
# Run 10 ticks
var phase_time = 0
var phase_time = int(tasmota.millis())
for i : 0..9
phase_engine.on_tick(phase_time)
phase_time += 5
@ -148,9 +150,10 @@ print("\n--- Test 6: Timestamp-Based Duration Calculation ---")
# Create engine and run a tick
var dur_strip = global.Leds(10)
var dur_engine = animation.create_engine(dur_strip)
dur_engine.tick_ms = 5 # Set low tick interval for testing
dur_engine.run()
var dur_time = tasmota.millis()
var dur_time = int(tasmota.millis())
dur_engine.on_tick(dur_time)
# Verify durations can be computed from timestamps
@ -177,6 +180,7 @@ print("\n--- Test 7: CPU Metrics During Ticks ---")
# Create a fresh engine for tick testing
var tick_strip = global.Leds(20)
var tick_engine = animation.create_engine(tick_strip)
tick_engine.tick_ms = 5 # Set low tick interval for testing
# Add a simple animation
var test_anim = animation.solid(tick_engine)
@ -185,7 +189,7 @@ tick_engine.add(test_anim)
tick_engine.run()
# Simulate several ticks
var current_time = tasmota.millis()
var current_time = int(tasmota.millis())
for i : 0..9
tick_engine.on_tick(current_time + i * 10)
end
@ -200,12 +204,13 @@ print("\n--- Test 8: Metrics Reset After Stats Period ---")
# Create engine and simulate ticks over stats period
var reset_strip = global.Leds(15)
var reset_engine = animation.create_engine(reset_strip)
reset_engine.tick_ms = 5 # Set low tick interval for testing
reset_engine.run()
# Simulate ticks for just under 5 seconds
var start_time = 0
var start_time = int(tasmota.millis())
var current_time = start_time
while current_time < 4900
while current_time < start_time + 4900
reset_engine.on_tick(current_time)
current_time += 5
end
@ -217,7 +222,7 @@ assert_greater_than(tick_count_before, 0, "Should have ticks before stats period
var last_stats_before = reset_engine.last_stats_time
# Simulate more ticks to cross the 5 second threshold
while current_time < 5100
while current_time < start_time + 5100
reset_engine.on_tick(current_time)
current_time += 5
end
@ -235,10 +240,11 @@ print("\n--- Test 9: Metrics Consistency Across Ticks ---")
var consistency_strip = global.Leds(25)
var consistency_engine = animation.create_engine(consistency_strip)
consistency_engine.tick_ms = 5 # Set low tick interval for testing
consistency_engine.run()
# Run multiple ticks and verify metrics consistency
var cons_time = 0
var cons_time = int(tasmota.millis())
for i : 0..19
consistency_engine.on_tick(cons_time)
cons_time += 5
@ -258,6 +264,7 @@ print("\n--- Test 10: Min/Max Tracking for All Metrics ---")
var minmax_strip = global.Leds(10)
var minmax_engine = animation.create_engine(minmax_strip)
minmax_engine.tick_ms = 5 # Set low tick interval for testing
# Add an animation so rendering happens
var mm_anim = animation.solid(minmax_engine)
@ -266,7 +273,7 @@ minmax_engine.add(mm_anim)
minmax_engine.run()
# Run several ticks
var mm_time = 0
var mm_time = int(tasmota.millis())
for i : 0..9
minmax_engine.on_tick(mm_time)
mm_time += 5
@ -287,10 +294,11 @@ print("\n--- Test 11: Streaming Statistics Accuracy ---")
var stats_strip = global.Leds(15)
var stats_engine = animation.create_engine(stats_strip)
stats_engine.tick_ms = 5 # Set low tick interval for testing
stats_engine.run()
# Run exactly 10 ticks
var stats_time = 0
var stats_time = int(tasmota.millis())
for i : 0..9
stats_engine.on_tick(stats_time)
stats_time += 5
@ -306,10 +314,11 @@ print("\n--- Test 12: Phase Metrics Cleared After Stats ---")
var clear_strip = global.Leds(20)
var clear_engine = animation.create_engine(clear_strip)
clear_engine.tick_ms = 5 # Set low tick interval for testing
clear_engine.run()
# Run some ticks to accumulate phase metrics
var clear_time = 0
var clear_time = int(tasmota.millis())
for i : 0..9
clear_engine.on_tick(clear_time)
clear_time += 5
@ -319,7 +328,8 @@ end
assert_greater_than(clear_engine.phase1_time_sum, -1, "Phase metrics should accumulate")
# Simulate ticks to cross stats period
while clear_time < 5100
var clear_start = clear_time
while clear_time < clear_start + 5100
clear_engine.on_tick(clear_time)
clear_time += 5
end
@ -336,15 +346,17 @@ print("\n--- Test 13: Multiple Engines Independence ---")
var strip1 = global.Leds(10)
var engine1 = animation.create_engine(strip1)
engine1.tick_ms = 5 # Set low tick interval for testing
engine1.run()
var strip2 = global.Leds(20)
var engine2 = animation.create_engine(strip2)
engine2.tick_ms = 5 # Set low tick interval for testing
engine2.run()
# Run ticks on both engines
var e1_time = 0
var e2_time = 0
var e1_time = int(tasmota.millis())
var e2_time = int(tasmota.millis())
for i : 0..4
engine1.on_tick(e1_time)
@ -385,10 +397,12 @@ print("\n--- Test 15: Performance of Metrics Collection ---")
var perf_strip = global.Leds(30)
var perf_engine = animation.create_engine(perf_strip)
perf_engine.tick_ms = 5 # Set low tick interval for testing
perf_engine.run()
# Measure overhead of metrics collection with timestamps
var perf_start = tasmota.millis()
var perf_start = int(tasmota.millis())
var perf_time = perf_start
for i : 0..99
perf_engine.on_tick(perf_start + i * 5)
end

View File

@ -1,5 +1,5 @@
# DSL Berry Code Blocks Test Suite
# Tests for berry code block functionality in SimpleDSLTranspiler
# DSL Berry Code Blocks and External Functions Test Suite
# Tests for berry code block functionality and external function declarations in SimpleDSLTranspiler
#
# Command to run test is:
# ./berry -s -g -m lib/libesp32/berry_animation/src -e "import tasmota" lib/libesp32/berry_animation/src/tests/dsl_berry_code_blocks_test.be
@ -275,9 +275,284 @@ def test_multiline_complex_syntax()
return true
end
# Test external function declaration - basic syntax
def test_external_function_basic()
print("Testing basic external function declaration...")
var dsl_source = 'berry """\n' +
'def test_func()\n' +
' return 100\n' +
'end\n' +
'"""\n' +
'extern function test_func\n' +
'animation test = solid(color=red)\n' +
'test.opacity = test_func\n' +
'run test'
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code")
assert(string.find(berry_code, "def test_func()") >= 0, "Should include function definition")
assert(string.find(berry_code, "# External function declaration: test_func") >= 0, "Should have external declaration comment")
assert(string.find(berry_code, "animation.get_user_function('test_func')(engine)") >= 0, "Should generate correct function call")
print("✓ Basic external function declaration test passed")
return true
end
# Test external function with parentheses
def test_external_function_with_parentheses()
print("Testing external function with parentheses...")
var dsl_source = 'berry """\n' +
'def paren_func()\n' +
' return 150\n' +
'end\n' +
'"""\n' +
'extern function paren_func\n' +
'animation test = solid(color=blue)\n' +
'test.opacity = paren_func()\n' +
'run test'
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code")
assert(string.find(berry_code, "animation.get_user_function('paren_func')(engine)") >= 0, "Should generate correct function call with parentheses")
print("✓ External function with parentheses test passed")
return true
end
# Test multiple external functions
def test_multiple_external_functions()
print("Testing multiple external functions...")
var dsl_source = 'berry """\n' +
'def func1()\n' +
' return 100\n' +
'end\n' +
'def func2()\n' +
' return 200\n' +
'end\n' +
'"""\n' +
'extern function func1\n' +
'extern function func2\n' +
'animation a1 = solid(color=red)\n' +
'a1.opacity = func1\n' +
'animation a2 = solid(color=blue)\n' +
'a2.opacity = func2\n' +
'run a1'
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code")
assert(string.find(berry_code, "animation.get_user_function('func1')(engine)") >= 0, "Should generate call for func1")
assert(string.find(berry_code, "animation.get_user_function('func2')(engine)") >= 0, "Should generate call for func2")
print("✓ Multiple external functions test passed")
return true
end
# Test external function in arithmetic expressions
def test_external_function_in_arithmetic()
print("Testing external function in arithmetic expressions...")
var dsl_source = 'berry """\n' +
'def math_func()\n' +
' return 50\n' +
'end\n' +
'"""\n' +
'extern function math_func\n' +
'animation test = solid(color=green)\n' +
'test.opacity = max(100, math_func + 50)\n' +
'run test'
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code")
assert(string.find(berry_code, "animation.get_user_function('math_func')(engine)") >= 0, "Should generate function call in arithmetic")
assert(string.find(berry_code, "animation._math.max(") >= 0, "Should include math function")
print("✓ External function in arithmetic expressions test passed")
return true
end
# Test external function with complex berry code
def test_external_function_complex()
print("Testing external function with complex berry code...")
var dsl_source = 'berry """\n' +
'import math\n' +
'def rand_meter(time_ms, self)\n' +
' var r = math.rand() % 101\n' +
' return r\n' +
'end\n' +
'def breathing_effect(base_value, amplitude)\n' +
' var time_factor = (tasmota.millis() / 1000) % 4\n' +
' var breath = math.sin(time_factor * math.pi / 2)\n' +
' return int(base_value + breath * amplitude)\n' +
'end\n' +
'"""\n' +
'extern function rand_meter\n' +
'extern function breathing_effect\n' +
'palette rainbow = [0xFF0000, 0x00FF00, 0x0000FF]\n' +
'animation meter = palette_meter_animation(value_func = rand_meter)\n' +
'animation breath = solid(color=blue)\n' +
'breath.opacity = breathing_effect\n' +
'run meter'
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code")
assert(string.find(berry_code, "def rand_meter(time_ms, self)") >= 0, "Should include complex function definition")
assert(string.find(berry_code, "def breathing_effect(base_value, amplitude)") >= 0, "Should include second function")
assert(string.find(berry_code, "animation.get_user_function('rand_meter')(engine)") >= 0, "Should call rand_meter")
assert(string.find(berry_code, "animation.get_user_function('breathing_effect')(engine)") >= 0, "Should call breathing_effect")
print("✓ External function with complex berry code test passed")
return true
end
# Test error handling - missing 'function' keyword
def test_external_error_missing_function_keyword()
print("Testing error handling for missing 'function' keyword...")
var dsl_source = 'berry """\n' +
'def test_func()\n' +
' return 100\n' +
'end\n' +
'"""\n' +
'extern test_func\n' +
'animation test = solid(color=red)\n' +
'run test'
try
var berry_code = animation_dsl.compile(dsl_source)
assert(false, "Should raise compilation error for missing 'function' keyword")
except "dsl_compilation_error" as e, msg
assert(string.find(msg, "Expected 'function' keyword after 'extern'") >= 0, "Should have helpful error message")
end
print("✓ Error handling (missing 'function' keyword) test passed")
return true
end
# Test error handling - missing function name
def test_external_error_missing_function_name()
print("Testing error handling for missing function name...")
var dsl_source = 'berry """\n' +
'def test_func()\n' +
' return 100\n' +
'end\n' +
'"""\n' +
'extern function\n' +
'animation test = solid(color=red)\n' +
'run test'
try
var berry_code = animation_dsl.compile(dsl_source)
assert(false, "Should raise compilation error for missing function name")
except "dsl_compilation_error" as e, msg
assert(string.find(msg, "Expected function name after 'extern function'") >= 0, "Should have helpful error message")
end
print("✓ Error handling (missing function name) test passed")
return true
end
# Test external function with reserved name validation
def test_external_function_reserved_name_validation()
print("Testing external function with reserved name validation...")
# Test with a name that conflicts with an existing definition
var dsl_source = 'color my_color = 0xFF0000\n' +
'berry """\n' +
'def my_color()\n' +
' return 100\n' +
'end\n' +
'"""\n' +
'extern function my_color\n' +
'animation test = solid(color=blue)\n' +
'run test'
try
var berry_code = animation_dsl.compile(dsl_source)
assert(false, "Should raise compilation error for already defined name")
except "dsl_compilation_error" as e, msg
# Check for redefinition error
var has_error = string.find(msg, "already defined") >= 0 ||
string.find(msg, "redefine") >= 0 ||
string.find(msg, "my_color") >= 0
if !has_error
print(f"Unexpected error message: {msg}")
assert(false, f"Should reject already defined names, got: {msg}")
end
end
print("✓ External function reserved name validation test passed")
return true
end
# Test external function in sequences
def test_external_function_in_sequences()
print("Testing external function in sequences...")
var dsl_source = 'berry """\n' +
'def seq_func()\n' +
' return 180\n' +
'end\n' +
'"""\n' +
'extern function seq_func\n' +
'animation test = solid(color=purple)\n' +
'sequence demo {\n' +
' test.opacity = seq_func\n' +
' play test for 2s\n' +
'}\n' +
'run demo'
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should generate Berry code")
assert(string.find(berry_code, "animation.get_user_function('seq_func')(engine)") >= 0, "Should call external function in sequence")
print("✓ External function in sequences test passed")
return true
end
# Test external function compilation and execution
def test_external_function_execution()
print("Testing external function compilation and execution...")
var dsl_source = 'berry """\n' +
'def exec_test_func()\n' +
' print("External function executed successfully")\n' +
' return 128\n' +
'end\n' +
'"""\n' +
'extern function exec_test_func\n' +
'animation test = solid(color=cyan)\n' +
'test.opacity = exec_test_func\n' +
'run test'
# Test compilation
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should compile successfully")
# Test that generated code compiles
try
compile(berry_code)
print("✓ External function execution test passed")
return true
except .. as e, m
print("✗ Generated code compilation failed:", e, m)
return false
end
end
# Run all tests
def run_all_berry_block_tests()
print("=== DSL Berry Code Blocks Test Suite ===")
print("=== DSL Berry Code Blocks and External Functions Test Suite ===")
print("")
var tests = [
@ -290,7 +565,17 @@ def run_all_berry_block_tests()
test_error_missing_string,
test_error_invalid_token,
test_berry_block_execution,
test_multiline_complex_syntax
test_multiline_complex_syntax,
test_external_function_basic,
test_external_function_with_parentheses,
test_multiple_external_functions,
test_external_function_in_arithmetic,
test_external_function_complex,
test_external_error_missing_function_keyword,
test_external_error_missing_function_name,
test_external_function_reserved_name_validation,
test_external_function_in_sequences,
test_external_function_execution
]
var passed = 0
@ -307,14 +592,14 @@ def run_all_berry_block_tests()
print("")
end
print("=== Berry Code Blocks Test Results ===")
print("=== Berry Code Blocks and External Functions Test Results ===")
print(f"Passed: {passed}/{total}")
if passed == total
print("All berry code block tests passed! ✓")
print("All berry code block and external function tests passed! ✓")
return true
else
print("Some berry code block tests failed! ✗")
print("Some berry code block or external function tests failed! ✗")
raise "test_failed"
end
end

View File

@ -337,7 +337,6 @@ def test_complex_dsl()
"# Color Definitions\n" +
"color red = 0xFF0000\n" +
"color orange = rgb(255, 128, 0)\n" +
"color yellow = hsv(60, 100, 100)\n" +
"\n" +
"# Animation Definitions\n" +
"animation fire_gradient = gradient(color=red)\n" +

View File

@ -75,6 +75,9 @@ def test_on_tick_performance()
var strip = global.Leds(10)
var engine = animation.create_engine(strip)
# Set tick_ms to 5 for testing (default is 50ms)
engine.tick_ms = 5
# Add a test animation
var anim = TestAnimation(engine)
anim.priority = 1
@ -89,7 +92,7 @@ def test_on_tick_performance()
tasmota.set_millis(initial_time)
engine.last_update = initial_time
# Call on_tick with less than 5ms elapsed
# Call on_tick with less than 5ms elapsed (should be throttled)
tasmota.set_millis(initial_time + 3)
var result = engine.on_tick()
@ -97,7 +100,7 @@ def test_on_tick_performance()
assert(result == true)
assert(anim.render_called == false)
# Call on_tick with more than 5ms elapsed
# Call on_tick with more than 5ms elapsed (should render)
tasmota.set_millis(initial_time + 10)
result = engine.on_tick()

View File

@ -156,7 +156,7 @@ def test_ease_with_phase()
provider.min_value = 0
provider.max_value = 100
provider.duration = 1000
provider.phase = 25 # 25% phase shift
provider.phase = 64 # 25% phase shift (64 out of 255 is ~25%)
provider.start(0) # Start at time 0
# With 25% phase shift, the curve should be shifted forward

View File

@ -40,16 +40,16 @@ def test_oscillator_basic()
assert(osc.duration == 1000, "Duration should be 1000ms")
assert(osc.form == animation.SAWTOOTH, "Form should be SAWTOOTH")
assert(osc.phase == 0, "Phase should default to 0")
assert(osc.duty_cycle == 50, "Duty cycle should default to 50")
assert(osc.duty_cycle == 127, "Duty cycle should default to 127")
# Test parameter modification
osc.phase = 25
osc.duty_cycle = 75
osc.phase = 64
osc.duty_cycle = 191
osc.min_value = 10
osc.max_value = 90
assert(osc.phase == 25, "Phase should be set to 25")
assert(osc.duty_cycle == 75, "Duty cycle should be set to 75")
assert(osc.phase == 64, "Phase should be set to 64")
assert(osc.duty_cycle == 191, "Duty cycle should be set to 191")
assert(osc.min_value == 10, "Starting value should be set to 10")
assert(osc.max_value == 90, "End value should be set to 90")
@ -147,8 +147,8 @@ def test_square_waveform()
assert(value_51 == 100, f"Value at 51% should be 100, got {value_51}")
assert(value_75 == 100, f"Value at 75% should be 100, got {value_75}")
# Test custom duty cycle (25%)
osc.duty_cycle = 25
# Test custom duty cycle (25% = 64 out of 255)
osc.duty_cycle = 64
var value_20 = osc.produce_value("test", start_time + 200) # t=200ms (20% - first quarter)
var value_30 = osc.produce_value("test", start_time + 300) # t=300ms (30% - second quarter)
@ -258,8 +258,8 @@ def test_phase_shift()
osc.phase = 0
var value_no_phase = osc.produce_value("test", start_time)
# Test with 25% phase shift (should be like starting at 25% of cycle)
osc.phase = 25
# Test with 25% phase shift (64 out of 255 is ~25%)
osc.phase = 64
var value_with_phase = osc.produce_value("test", start_time)
# Values should be different due to phase shift
@ -330,16 +330,16 @@ def test_static_constructors()
square1.min_value = 0
square1.max_value = 1
square1.duration = 500
square1.duty_cycle = 30
square1.duty_cycle = 76
assert(square1.form == animation.SQUARE, "square() should use SQUARE")
assert(square1.duty_cycle == 30, "square() should set duty cycle to 30")
assert(square1.duty_cycle == 76, "square() should set duty cycle to 76")
# Test square() with default duty cycle
var square2 = animation.square(mock_engine)
square2.min_value = 0
square2.max_value = 1
square2.duration = 500
assert(square2.duty_cycle == 50, "square() should default duty cycle to 50")
assert(square2.duty_cycle == 127, "square() should default duty cycle to 127")
print("✓ Static constructor functions test passed")
end
@ -400,7 +400,7 @@ def test_edge_cases()
# Test with default parameters
var osc1 = animation.oscillator_value(mock_engine)
assert(osc1.min_value == 0, "Default min_value should be 0")
assert(osc1.max_value == 100, "Default max_value should be 100")
assert(osc1.max_value == 255, "Default max_value should be 255")
assert(osc1.duration == 1000, "Default duration should be 1000")
assert(osc1.form == animation.SAWTOOTH, "Default form should be SAWTOOTH")
@ -420,14 +420,14 @@ def test_edge_cases()
# Test valid bounds
osc3.phase = 0
osc3.duty_cycle = 50
osc3.duty_cycle = 127
assert(osc3.phase == 0, "Phase 0 should be valid")
assert(osc3.duty_cycle == 50, "Duty cycle 50 should be valid")
assert(osc3.duty_cycle == 127, "Duty cycle 127 should be valid")
osc3.phase = 100
osc3.duty_cycle = 100
assert(osc3.phase == 100, "Phase 100 should be valid")
assert(osc3.duty_cycle == 100, "Duty cycle 100 should be valid")
osc3.phase = 255
osc3.duty_cycle = 255
assert(osc3.phase == 255, "Phase 255 should be valid")
assert(osc3.duty_cycle == 255, "Duty cycle 255 should be valid")
print("✓ Edge cases test passed")
end

View File

@ -29,10 +29,6 @@ var frame = animation.frame_buffer(10, 1)
# For simple testing, we'll use direct color values
# More complex color providers can be tested separately
# Test 1: Basic PalettePatternAnimation with custom pattern function
print("Test 1: Basic PalettePatternAnimation with custom pattern function")
var pattern_anim = animation.palette_pattern_animation(mock_engine)
# Create a simple mock color source that has get_color_for_value method
class MockColorSource
def get_color_for_value(value, time_ms)
@ -42,67 +38,7 @@ class MockColorSource
end
var mock_color_source = MockColorSource()
pattern_anim.color_source = mock_color_source
pattern_anim.priority = 10
pattern_anim.duration = 0
pattern_anim.loop = false
pattern_anim.opacity = 255
pattern_anim.name = "pattern_test"
# Create a simple pattern function that alternates between 0 and 255
def simple_pattern(pixel_index, time_ms, animation)
return pixel_index % 2 == 0 ? 255 : 0
end
pattern_anim.pattern_func = simple_pattern
assert(pattern_anim != nil, "Failed to create pattern animation")
# Start the animation
pattern_anim.start()
pattern_anim.update() # force first tick
assert(pattern_anim.is_running, "Animation should be running")
# Update and render
pattern_anim.update(mock_engine.time_ms)
frame.clear()
var result = pattern_anim.render(frame, mock_engine.time_ms)
assert(result, "Render should return true")
# Test 2: PaletteWaveAnimation
print("Test 2: PaletteWaveAnimation")
var wave_anim = animation.palette_wave_animation(mock_engine)
wave_anim.color_source = mock_color_source
wave_anim.wave_period = 2000 # 2 second wave period
wave_anim.wave_length = 5 # Wave length of 5 pixels
wave_anim.priority = 10
wave_anim.duration = 0
wave_anim.loop = false
wave_anim.opacity = 255
wave_anim.name = "wave_test"
assert(wave_anim != nil, "Failed to create wave animation")
assert(wave_anim.wave_period == 2000, "Wave period should be 2000")
assert(wave_anim.wave_length == 5, "Wave length should be 5")
# Start the animation
wave_anim.start()
wave_anim.update() # force first tick
assert(wave_anim.is_running, "Animation should be running")
# Update and render
wave_anim.update(mock_engine.time_ms)
frame.clear()
result = wave_anim.render(frame, mock_engine.time_ms)
assert(result, "Render should return true")
# Test parameter changes
wave_anim.wave_period = 1000
assert(wave_anim.wave_period == 1000, "Wave period should be updated to 1000")
wave_anim.wave_length = 8
assert(wave_anim.wave_length == 8, "Wave length should be updated to 8")
# Test 3: PaletteGradientAnimation
# Test 1: PaletteGradientAnimation
print("Test 3: PaletteGradientAnimation")
var gradient_anim = animation.palette_gradient_animation(mock_engine)
gradient_anim.color_source = mock_color_source
@ -135,20 +71,20 @@ assert(gradient_anim.shift_period == 1500, "Shift period should be updated to 15
gradient_anim.spatial_period = 5
assert(gradient_anim.spatial_period == 5, "Spatial period should be updated to 5")
gradient_anim.phase_shift = 25
assert(gradient_anim.phase_shift == 25, "Phase shift should be updated to 25")
gradient_anim.phase_shift = 64
assert(gradient_anim.phase_shift == 64, "Phase shift should be updated to 64")
# Test static gradient (shift_period = 0)
gradient_anim.shift_period = 0
assert(gradient_anim.shift_period == 0, "Shift period should be updated to 0 (static)")
# Test 4: PaletteMeterAnimation
print("Test 4: PaletteMeterAnimation")
# Test 2: PaletteMeterAnimation
print("Test 2: PaletteMeterAnimation")
var meter_anim = animation.palette_meter_animation(mock_engine)
meter_anim.color_source = mock_color_source
# Create a value function that returns 50% (half the strip)
def meter_value_func(time_ms, animation)
def meter_value_func(engine, time_ms, animation)
return 50 # 50% of the strip (this is still 0-100 for meter logic)
end
meter_anim.value_func = meter_value_func
@ -173,7 +109,7 @@ result = meter_anim.render(frame, mock_engine.time_ms)
assert(result, "Render should return true")
# Test changing value function
def new_meter_value_func(time_ms, animation)
def new_meter_value_func(engine, time_ms, animation)
return 75 # 75% of the strip (this is still 0-100 for meter logic)
end
meter_anim.value_func = new_meter_value_func
@ -183,12 +119,12 @@ frame.clear()
result = meter_anim.render(frame, mock_engine.time_ms)
assert(result, "Render should return true")
# Test 5: Changing color sources dynamically
print("Test 5: Changing color sources dynamically")
var dynamic_anim = animation.palette_wave_animation(mock_engine)
# Test 3: Changing color sources dynamically
print("Test 3: Changing color sources dynamically")
var dynamic_anim = animation.palette_gradient_animation(mock_engine)
dynamic_anim.color_source = mock_color_source
dynamic_anim.wave_period = 1000
dynamic_anim.wave_length = 3
dynamic_anim.shift_period = 1000
dynamic_anim.spatial_period = 3
# Start the animation
dynamic_anim.start()
@ -217,34 +153,22 @@ frame.clear()
result = dynamic_anim.render(frame, mock_engine.time_ms)
assert(result, "Render should return true")
# Test 6: Parameter validation
print("Test 6: Parameter validation")
var validation_anim = animation.palette_wave_animation(mock_engine)
# Test 4: Parameter validation
print("Test 4: Parameter validation")
var validation_anim = animation.palette_gradient_animation(mock_engine)
# Test valid parameter values
validation_anim.wave_period = 500
assert(validation_anim.wave_period == 500, "Valid wave period should be accepted")
validation_anim.shift_period = 500
assert(validation_anim.shift_period == 500, "Valid shift period should be accepted")
validation_anim.wave_length = 1
assert(validation_anim.wave_length == 1, "Valid wave length should be accepted")
validation_anim.spatial_period = 1
assert(validation_anim.spatial_period == 1, "Valid spatial period should be accepted")
# Test invalid parameter values (should be constrained by min values)
try
validation_anim.wave_period = 0 # Below minimum
assert(false, "Should not accept wave_period below minimum")
except .. as e
# Expected to fail validation
end
validation_anim.phase_shift = 128
assert(validation_anim.phase_shift == 128, "Valid phase shift should be accepted")
try
validation_anim.wave_length = 0 # Below minimum
assert(false, "Should not accept wave_length below minimum")
except .. as e
# Expected to fail validation
end
# Test 7: Animation with different color mapping
print("Test 7: Animation with different color mapping")
# Test 5: Animation with different color mapping
print("Test 5: Animation with different color mapping")
class MockRainbowColorSource
def get_color_for_value(value, time_ms)
# Simple rainbow mapping based on value (expecting 0-255 range)
@ -274,19 +198,22 @@ frame.clear()
result = rich_anim.render(frame, mock_engine.time_ms)
assert(result, "Render should return true")
# Test 8: Animation timing and synchronization
print("Test 8: Animation timing and synchronization")
# Test 6: Animation timing and synchronization
print("Test 6: Animation timing and synchronization")
var sync_time = mock_engine.time_ms + 1000
# Create multiple animations
var anim1 = animation.palette_wave_animation(mock_engine)
var anim1 = animation.palette_gradient_animation(mock_engine)
anim1.color_source = mock_color_source
anim1.wave_period = 1000
anim1.wave_length = 4
anim1.shift_period = 1000
anim1.spatial_period = 4
var anim2 = animation.palette_gradient_animation(mock_engine)
var anim2 = animation.palette_meter_animation(mock_engine)
anim2.color_source = mock_color_source2
anim2.shift_period = 1500
def meter_func(engine, time_ms, animation)
return 128
end
anim2.value_func = meter_func
# Start both animations at the same time
anim1.start(sync_time)
@ -297,11 +224,11 @@ anim2.update(sync_time) # force first tick
assert(anim1.start_time == sync_time, "Animation 1 should have correct start time")
assert(anim2.start_time == sync_time, "Animation 2 should have correct start time")
# Test 9: Animation without color source (should handle gracefully)
print("Test 9: Animation without color source")
var no_color_anim = animation.palette_wave_animation(mock_engine)
no_color_anim.wave_period = 1000
no_color_anim.wave_length = 3
# Test 7: Animation without color source (should handle gracefully)
print("Test 7: Animation without color source")
var no_color_anim = animation.palette_gradient_animation(mock_engine)
no_color_anim.shift_period = 1000
no_color_anim.spatial_period = 3
# Note: no color_source set
no_color_anim.start()
@ -310,9 +237,9 @@ frame.clear()
result = no_color_anim.render(frame, mock_engine.time_ms)
assert(!result, "Render should return false when no color source is set")
# Test 10: String representation
print("Test 10: String representation")
var str_anim = animation.palette_wave_animation(mock_engine)
# Test 8: String representation
print("Test 8: String representation")
var str_anim = animation.palette_gradient_animation(mock_engine)
var str_repr = str_anim.tostring()
print(f"String representation: {str_repr}")
assert(str_repr != nil, "String representation should not be nil")

View File

@ -0,0 +1,8 @@
.vscode/**
.vscode-test/**
.gitignore
vsc-extension-quickstart.md
**/node_modules/**
**/.eslintrc.json
**/*.map
**/*.ts

View File

@ -0,0 +1,87 @@
# Change Log
All notable changes to the Animation DSL extension will be documented in this file.
## [1.2.0] - 2025-01-24
### Added
- Support for `if` keyword for conditional execution in sequences
- Conditional execution allows boolean-based gating (runs 0 or 1 times)
### Changed
- Updated keyword patterns to include `if` statement
## [1.1.0] - 2025-01-09
### Added
- Support for `import` statements for Berry modules
- Support for `template` and `param` keywords for template definitions
- Support for `type` annotations in template parameters (`param name type color`)
- Support for `set` keyword for variable assignments
- Support for `reset` and `restart` keywords for value provider/animation control
- Support for `log` keyword for debug logging
- Support for `as` keyword in import and parameter declarations
- Mathematical functions: `abs`, `max`, `min`, `round`, `sqrt`, `scale`, `sin`, `cos`
- User function syntax: `user.function_name()` with proper highlighting
- Additional animation functions: `pulsating_animation`
- Additional value providers: `triangle`, `cosine_osc`, `sawtooth`, `color_cycle`, `strip_length`
- Additional easing types: `triangle`, `sine`, `sawtooth`, `elastic`, `bounce`
- Support for hexadecimal colors with `0x` prefix (e.g., `0xFF0000`, `0x80FF0000`)
- Additional animation properties: `opacity`, `priority`, `pos`, `beacon_size`, `slew_size`, `direction`, `tail_length`, `speed`, `period`, `cycle_period`, `min_value`, `max_value`, `duration`, `next`
### Changed
- Updated keyword patterns to match current DSL syntax based on actual examples
- Improved indentation rules to support `template` blocks with proper `{}`
- Enhanced color recognition to support both `#RRGGBB` and `0xRRGGBB` formats
- Expanded animation function list to include all currently implemented functions
- Updated oscillator functions to match actual DSL implementation
### Fixed
- Corrected animation function names to match actual DSL implementation
- Updated oscillator function patterns to include all available value providers
- Fixed keyword list to remove deprecated/unused keywords and add missing ones
## [1.0.0] - 2024-01-30
### Added
- Initial release of Animation DSL syntax highlighting
- Complete syntax highlighting for all DSL constructs:
- Keywords (strip, color, palette, animation, sequence, etc.)
- Animation functions (solid, rich_palette_animation, beacon_animation, etc.)
- Oscillator functions (ramp, linear, smooth, square)
- Colors (hex colors and 30+ named colors)
- Time literals (ms, s, m, h)
- Percentages (50%, 100%)
- Comments (# line comments)
- Semantic token scopes that work with any VSCode theme
- Language configuration with:
- Auto-closing pairs for brackets and quotes
- Comment toggling support
- Smart indentation for sequences and loops
- Bracket matching
- File association for .anim files
- Comprehensive documentation and examples
### Features
- **Syntax Highlighting**: Full coverage of Animation DSL grammar
- **Theme Compatibility**: Works with any VSCode theme (dark, light, high contrast)
- **Language Features**: Auto-closing, indentation, bracket matching
- **Documentation**: Complete README with examples and usage guide
### Supported DSL Features
- Strip configuration (`strip length 60`)
- Color definitions (`color red = #FF0000`)
- Palette definitions with VRGB format
- Animation definitions (8 animation functions)
- Value providers (4 oscillator functions)
- Property assignments (`animation.priority = 10`)
- Sequences with play, wait, and repeat
- Comments with preservation
- All currently implemented DSL constructs
### Technical Details
- Based on TextMate grammar for syntax highlighting
- JSON-based snippet system with parameter placeholders
- Custom theme with semantic color coding
- Language configuration for VSCode integration
- Supports all Animation DSL file extensions (.anim)

View File

@ -0,0 +1,139 @@
# Animation DSL for VSCode
This extension provides syntax highlighting, snippets, and language support for the Berry Animation Framework DSL (.anim files).
## Features
### Syntax Highlighting
- **Keywords**: `strip`, `color`, `palette`, `animation`, `sequence`, `template`, `import`, `set`, `play`, `run`, `if`, `repeat`, `reset`, `restart`, `log`, etc.
- **Animation Functions**: `solid`, `pulsating_animation`, `rich_palette_animation`, `beacon_animation`, `comet_animation`, etc.
- **Value Providers**: `triangle`, `cosine_osc`, `sawtooth`, `color_cycle`, `strip_length`, `ramp`, `linear`, `smooth`, `square`
- **Mathematical Functions**: `abs`, `max`, `min`, `round`, `sqrt`, `scale`, `sin`, `cos`
- **User Functions**: `user.function_name()` syntax with proper highlighting
- **Colors**: Hex colors (`#FF0000`, `0xFF0000`, `0x80FF0000`) and named colors (red, blue, etc.)
- **Time Literals**: 2s, 500ms, 1m, 2h
- **Percentages**: 50%, 100%
- **Comments**: Line comments starting with #
- **Template Syntax**: Template definitions with parameter type annotations
### Language Features
- **Auto-closing**: Brackets, quotes, and braces
- **Comment toggling**: Ctrl+/ for line comments
- **Bracket matching**: Matching brackets and braces
- **Indentation**: Smart indentation for sequences and loops
- **Theme Compatibility**: Works with any VSCode theme (dark, light, high contrast)
## File Association
This extension automatically activates for files with the `.anim` extension.
## Example Usage
```dsl
# Fire Effect Animation with Templates
import user_functions
# Define fire palette
palette fire_colors = [
(0, 0x000000) # Black
(64, 0x800000) # Dark red
(128, 0xFF0000) # Red
(192, 0xFF8000) # Orange
(255, 0xFFFF00) # Yellow
]
# Template for reusable fire effect
template fire_effect {
param base_palette type palette
param intensity type number
param duration
animation campfire = rich_palette_animation(
palette=base_palette
cycle_period=duration
)
# Use computed values and user functions
campfire.priority = max(5, intensity / 10)
campfire.opacity = user.breathing_effect()
run campfire
}
# Use the template
fire_effect(fire_colors, 200, 3s)
# Sequence with dynamic property changes
sequence fire_demo {
play campfire for 5s
campfire.opacity = abs(strip_length() * 4)
play campfire for 3s
reset campfire
play campfire for 2s
}
run fire_demo
```
## Theme Compatibility
The extension uses semantic token scopes that work with any VSCode theme:
- **Keywords**: Uses standard keyword colors from your chosen theme
- **Functions**: Uses function colors from your chosen theme
- **Constants**: Uses constant colors from your chosen theme (colors, numbers, etc.)
- **Comments**: Uses comment colors from your chosen theme
- **Strings**: Uses string colors from your chosen theme
This ensures the syntax highlighting looks great whether you prefer dark themes, light themes, or high contrast themes.
## Installation
### From VSIX (Recommended)
1. Download the `.vsix` file
2. Open VSCode
3. Go to Extensions (Ctrl+Shift+X)
4. Click the "..." menu and select "Install from VSIX..."
5. Select the downloaded `.vsix` file
### Manual Installation
1. Copy the extension folder to your VSCode extensions directory:
- **Windows**: `%USERPROFILE%\.vscode\extensions\`
- **macOS**: `~/.vscode/extensions/`
- **Linux**: `~/.vscode/extensions/`
2. Restart VSCode
## Development
### Building the Extension
```bash
npm install -g @vscode/vsce
cd vscode-animation-dsl
vsce package
```
This creates a `.vsix` file that can be installed in VSCode.
### Testing
1. Open the extension folder in VSCode
2. Press F5 to launch a new Extension Development Host
3. Open a `.anim` file to test syntax highlighting
## Contributing
Contributions are welcome! Please feel free to submit issues or pull requests.
### Adding New Features
- **Keywords**: Add to `syntaxes/animation-dsl.tmLanguage.json`
- **Token Scopes**: Modify semantic scopes in the grammar file
## License
This extension is part of the Berry Animation Framework project and follows the same license terms.
## Related
- [Berry Animation Framework](https://github.com/tasmota/berry-animation-framework)
- [Tasmota](https://tasmota.github.io/docs/)
- [Berry Language](https://github.com/berry-lang/berry)

View File

@ -0,0 +1,81 @@
{
"comments":
{
"lineComment": "#"
},
"brackets":
[
[
"{",
"}"
],
[
"[",
"]"
],
[
"(",
")"
]
],
"autoClosingPairs":
[
[
"{",
"}"
],
[
"[",
"]"
],
[
"(",
")"
],
[
"\"",
"\""
],
[
"'",
"'"
]
],
"surroundingPairs":
[
[
"{",
"}"
],
[
"[",
"]"
],
[
"(",
")"
],
[
"\"",
"\""
],
[
"'",
"'"
]
],
"folding":
{
"markers":
{
"start": "^\\s*#\\s*region\\b",
"end": "^\\s*#\\s*endregion\\b"
}
},
"wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)",
"indentationRules":
{
"increaseIndentPattern": "^\\s*(sequence|repeat|template)\\s+.*\\{\\s*$",
"decreaseIndentPattern": "^\\s*}\\s*$"
}
}

View File

@ -0,0 +1,49 @@
{
"name": "animation-dsl",
"displayName": "Animation DSL",
"description": "Syntax highlighting for Berry Animation Framework DSL (.anim files)",
"version": "1.2.1",
"publisher": "tasmota",
"engines": {
"vscode": "^1.74.0"
},
"categories": [
"Programming Languages"
],
"keywords": [
"animation",
"led",
"tasmota",
"berry"
],
"repository": {
"type": "git",
"url": "https://github.com/arendst/Tasmota"
},
"contributes": {
"languages": [
{
"id": "animation-dsl",
"aliases": [
"Animation DSL",
"animation-dsl",
"anim"
],
"extensions": [
".anim"
],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "animation-dsl",
"scopeName": "source.animation-dsl",
"path": "./syntaxes/animation-dsl.tmLanguage.json"
}
]
},
"devDependencies": {
"@vscode/vsce": "^2.15.0"
}
}

View File

@ -0,0 +1,297 @@
{
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"name": "Animation DSL",
"scopeName": "source.animation-dsl",
"patterns": [
{
"include": "#comments"
},
{
"include": "#keywords"
},
{
"include": "#strings"
},
{
"include": "#numbers"
},
{
"include": "#colors"
},
{
"include": "#time-literals"
},
{
"include": "#percentages"
},
{
"include": "#animation-functions"
},
{
"include": "#oscillator-functions"
},
{
"include": "#named-colors"
},
{
"include": "#easing-types"
},
{
"include": "#mathematical-functions"
},
{
"include": "#user-functions"
},
{
"include": "#operators"
},
{
"include": "#identifiers"
}
],
"repository": {
"comments": {
"patterns": [
{
"name": "comment.line.number-sign.animation-dsl",
"begin": "#",
"end": "$",
"captures": {
"0": {
"name": "punctuation.definition.comment.animation-dsl"
}
}
}
]
},
"keywords": {
"patterns": [
{
"name": "keyword.control.animation-dsl",
"match": "\\b(strip|set|import|berry|extern|color|palette|animation|sequence|function|template|param|type|play|for|with|repeat|times|forever|if|else|elif|choose|random|on|run|wait|goto|interrupt|resume|while|from|to|return|reset|restart|every)\\b"
},
{
"name": "keyword.other.animation-dsl",
"match": "\\b(opacity|priority|pos|beacon_size|slew_size|direction|tail_length|speed|period|cycle_period|min_value|max_value|duration|next)\\b"
},
{
"name": "constant.language.boolean.animation-dsl",
"match": "\\b(true|false|nil|transparent)\\b"
},
{
"name": "keyword.other.event.animation-dsl",
"match": "\\b(startup|shutdown|button_press|button_hold|motion_detected|brightness_change|timer|time|sound_peak|network_message)\\b"
}
]
},
"strings": {
"patterns": [
{
"name": "string.quoted.double.animation-dsl",
"begin": "\"",
"end": "\"",
"patterns": [
{
"name": "constant.character.escape.animation-dsl",
"match": "\\\\."
}
]
},
{
"name": "string.quoted.single.animation-dsl",
"begin": "'",
"end": "'",
"patterns": [
{
"name": "constant.character.escape.animation-dsl",
"match": "\\\\."
}
]
}
]
},
"numbers": {
"patterns": [
{
"name": "constant.numeric.float.animation-dsl",
"match": "\\b\\d+\\.\\d+\\b"
},
{
"name": "constant.numeric.integer.animation-dsl",
"match": "\\b\\d+\\b"
}
]
},
"colors": {
"patterns": [
{
"name": "constant.other.color.hex.animation-dsl",
"match": "0x[0-9A-Fa-f]{6}\\b",
"captures": {
"0": {
"name": "constant.other.color.hex.rgb.animation-dsl"
}
}
},
{
"name": "constant.other.color.hex.animation-dsl",
"match": "0x[0-9A-Fa-f]{8}\\b",
"captures": {
"0": {
"name": "constant.other.color.hex.argb.animation-dsl"
}
}
},
{
"name": "constant.other.color.hex.animation-dsl",
"match": "#[0-9A-Fa-f]{6}\\b",
"captures": {
"0": {
"name": "constant.other.color.hex.rgb.animation-dsl"
}
}
},
{
"name": "constant.other.color.hex.animation-dsl",
"match": "#[0-9A-Fa-f]{8}\\b",
"captures": {
"0": {
"name": "constant.other.color.hex.argb.animation-dsl"
}
}
}
]
},
"time-literals": {
"patterns": [
{
"name": "constant.numeric.time.animation-dsl",
"match": "\\b\\d+(?:\\.\\d+)?\\s*(ms|s|m|h)\\b",
"captures": {
"1": {
"name": "keyword.other.unit.time.animation-dsl"
}
}
}
]
},
"percentages": {
"patterns": [
{
"name": "constant.numeric.percentage.animation-dsl",
"match": "\\b\\d+(?:\\.\\d+)?%\\b",
"captures": {
"0": {
"name": "constant.numeric.percentage.animation-dsl"
}
}
}
]
},
"animation-functions": {
"patterns": [
{
"name": "entity.name.function.animation.animation-dsl",
"match": "\\b(solid|pulsating_animation|beacon_animation|comet_animation|rich_palette_animation|twinkle_animation|breathe_animation|fire_animation|crenel_position_animation)\\b"
}
]
},
"oscillator-functions": {
"patterns": [
{
"name": "entity.name.function.oscillator.animation-dsl",
"match": "\\b(triangle|cosine_osc|sawtooth|ramp|linear|smooth|square|sine|color_cycle|strip_length)\\b"
}
]
},
"named-colors": {
"patterns": [
{
"name": "constant.other.color.named.primary.animation-dsl",
"match": "\\b(red|green|blue|white|black|yellow|orange|purple|pink|cyan|magenta)\\b"
},
{
"name": "constant.other.color.named.extended.animation-dsl",
"match": "\\b(gray|grey|silver|gold|brown|lime|navy|olive|maroon|teal|aqua|fuchsia|indigo|violet|crimson|coral|salmon|khaki|plum|orchid|turquoise|tan|beige|ivory|snow|transparent)\\b"
}
]
},
"easing-types": {
"patterns": [
{
"name": "constant.other.easing.animation-dsl",
"match": "\\b(linear|triangle|smooth|sine|ease_in|ease_out|ramp|sawtooth|square|elastic|bounce)\\b"
}
]
},
"mathematical-functions": {
"patterns": [
{
"name": "entity.name.function.math.animation-dsl",
"match": "\\b(abs|max|min|round|sqrt|scale|sin|cos)\\b"
}
]
},
"user-functions": {
"patterns": [
{
"name": "entity.name.function.user.animation-dsl",
"match": "\\buser\\.[a-zA-Z_][a-zA-Z0-9_]*\\b",
"captures": {
"0": {
"name": "entity.name.function.user.animation-dsl"
}
}
}
]
},
"operators": {
"patterns": [
{
"name": "keyword.operator.assignment.animation-dsl",
"match": "="
},
{
"name": "keyword.operator.arithmetic.animation-dsl",
"match": "[+\\-*/%^]"
},
{
"name": "keyword.operator.comparison.animation-dsl",
"match": "(==|!=|<=|>=|<|>)"
},
{
"name": "keyword.operator.logical.animation-dsl",
"match": "(&&|\\|\\||!)"
},
{
"name": "punctuation.separator.comma.animation-dsl",
"match": ","
},
{
"name": "punctuation.separator.colon.animation-dsl",
"match": ":"
},
{
"name": "punctuation.separator.dot.animation-dsl",
"match": "\\."
},
{
"name": "punctuation.section.brackets.begin.animation-dsl",
"match": "[\\[\\(\\{]"
},
{
"name": "punctuation.section.brackets.end.animation-dsl",
"match": "[\\]\\)\\}]"
}
]
},
"identifiers": {
"patterns": [
{
"name": "variable.other.animation-dsl",
"match": "\\b[a-zA-Z_][a-zA-Z0-9_]*\\b"
}
]
}
}
}