Berry animation template system and performance improvements (#24086)

This commit is contained in:
s-hadinger 2025-11-01 12:48:40 +01:00 committed by GitHub
parent 5025715237
commit b5ac09d0df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
85 changed files with 20045 additions and 19990 deletions

View File

@ -182,11 +182,12 @@ SUCCESS
## Symbol Table
| Symbol | Type | Builtin | Dangerous | Takes Args |
|----------------|----------|---------|-----------|------------|
| `cylon_effect` | template | | | |
| `red` | color | ✓ | | |
| `transparent` | color | ✓ | | |
| Symbol | Type | Builtin | Dangerous | Takes Args |
|---------------|-----------------------|---------|-----------|------------|
| `cylon_red` | animation | | | |
| `cylon` | animation_constructor | | | ✓ |
| `red` | color | ✓ | | |
| `transparent` | color | ✓ | | |
### Compilation Output
@ -286,10 +287,11 @@ SUCCESS
## Symbol Table
| Symbol | Type | Builtin | Dangerous | Takes Args |
|----------------------|----------|---------|-----------|------------|
| `rainbow_with_white` | palette | | | |
| `shutter_bidir` | template | | | |
| Symbol | Type | Builtin | Dangerous | Takes Args |
|----------------------|-----------------------|---------|-----------|------------|
| `main` | animation | | | |
| `rainbow_with_white` | palette | | | |
| `shutter_bidir` | animation_constructor | | | ✓ |
### Compilation Output
@ -303,17 +305,18 @@ SUCCESS
## Symbol Table
| Symbol | Type | Builtin | Dangerous | Takes Args |
|----------------------|----------|---------|-----------|------------|
| `blue` | color | ✓ | | |
| `green` | color | ✓ | | |
| `indigo` | color | ✓ | | |
| `orange` | color | ✓ | | |
| `rainbow_with_white` | palette | | | |
| `red` | color | ✓ | | |
| `shutter_central` | template | | | |
| `white` | color | ✓ | | |
| `yellow` | color | ✓ | | |
| Symbol | Type | Builtin | Dangerous | Takes Args |
|----------------------|-----------------------|---------|-----------|------------|
| `blue` | color | ✓ | | |
| `green` | color | ✓ | | |
| `indigo` | color | ✓ | | |
| `main` | animation | | | |
| `orange` | color | ✓ | | |
| `rainbow_with_white` | palette | | | |
| `red` | color | ✓ | | |
| `shutter_central` | animation_constructor | | | ✓ |
| `white` | color | ✓ | | |
| `yellow` | color | ✓ | | |
### Compilation Output
@ -327,17 +330,18 @@ SUCCESS
## Symbol Table
| Symbol | Type | Builtin | Dangerous | Takes Args |
|----------------------|----------|---------|-----------|------------|
| `blue` | color | ✓ | | |
| `green` | color | ✓ | | |
| `indigo` | color | ✓ | | |
| `orange` | color | ✓ | | |
| `rainbow_with_white` | palette | | | |
| `red` | color | ✓ | | |
| `shutter_lr` | template | | | |
| `white` | color | ✓ | | |
| `yellow` | color | ✓ | | |
| Symbol | Type | Builtin | Dangerous | Takes Args |
|----------------------|-----------------------|---------|-----------|------------|
| `blue` | color | ✓ | | |
| `green` | color | ✓ | | |
| `indigo` | color | ✓ | | |
| `main` | animation | | | |
| `orange` | color | ✓ | | |
| `rainbow_with_white` | palette | | | |
| `red` | color | ✓ | | |
| `shutter_lr` | animation_constructor | | | ✓ |
| `white` | color | ✓ | | |
| `yellow` | color | ✓ | | |
### Compilation Output
@ -1000,9 +1004,9 @@ SUCCESS
## Symbol Table
| Symbol | Type | Builtin | Dangerous | Takes Args |
|----------------|----------|---------|-----------|------------|
| `cylon_effect` | template | | | |
| Symbol | Type | Builtin | Dangerous | Takes Args |
|----------------|-----------------------|---------|-----------|------------|
| `cylon_effect` | animation_constructor | | | ✓ |
### Compilation Output
@ -1016,11 +1020,12 @@ SUCCESS
## Symbol Table
| Symbol | Type | Builtin | Dangerous | Takes Args |
|-----------------|----------|---------|-----------|------------|
| `fire_palette` | palette | | | |
| `ocean_palette` | palette | | | |
| `rainbow_pulse` | template | | | |
| Symbol | Type | Builtin | Dangerous | Takes Args |
|-----------------|-----------------------|---------|-----------|------------|
| `fire_palette` | palette | | | |
| `main` | animation | | | |
| `ocean_palette` | palette | | | |
| `rainbow_pulse` | animation_constructor | | | ✓ |
### Compilation Output
@ -1055,17 +1060,18 @@ SUCCESS
## Symbol Table
| Symbol | Type | Builtin | Dangerous | Takes Args |
|----------------------|----------|---------|-----------|------------|
| `blue` | color | ✓ | | |
| `green` | color | ✓ | | |
| `indigo` | color | ✓ | | |
| `orange` | color | ✓ | | |
| `rainbow_with_white` | palette | | | |
| `red` | color | ✓ | | |
| `shutter_bidir` | template | | | |
| `white` | color | ✓ | | |
| `yellow` | color | ✓ | | |
| Symbol | Type | Builtin | Dangerous | Takes Args |
|----------------------|-----------------------|---------|-----------|------------|
| `blue` | color | ✓ | | |
| `green` | color | ✓ | | |
| `indigo` | color | ✓ | | |
| `main` | animation | | | |
| `orange` | color | ✓ | | |
| `rainbow_with_white` | palette | | | |
| `red` | color | ✓ | | |
| `shutter_bidir` | animation_constructor | | | ✓ |
| `white` | color | ✓ | | |
| `yellow` | color | ✓ | | |
### Compilation Output
@ -1079,17 +1085,18 @@ SUCCESS
## Symbol Table
| Symbol | Type | Builtin | Dangerous | Takes Args |
|----------------------|----------|---------|-----------|------------|
| `blue` | color | ✓ | | |
| `green` | color | ✓ | | |
| `indigo` | color | ✓ | | |
| `orange` | color | ✓ | | |
| `rainbow_with_white` | palette | | | |
| `red` | color | ✓ | | |
| `shutter_central` | template | | | |
| `white` | color | ✓ | | |
| `yellow` | color | ✓ | | |
| Symbol | Type | Builtin | Dangerous | Takes Args |
|----------------------|-----------------------|---------|-----------|------------|
| `blue` | color | ✓ | | |
| `green` | color | ✓ | | |
| `indigo` | color | ✓ | | |
| `main` | animation | | | |
| `orange` | color | ✓ | | |
| `rainbow_with_white` | palette | | | |
| `red` | color | ✓ | | |
| `shutter_central` | animation_constructor | | | ✓ |
| `white` | color | ✓ | | |
| `yellow` | color | ✓ | | |
### Compilation Output
@ -1142,10 +1149,11 @@ SUCCESS
## Symbol Table
| Symbol | Type | Builtin | Dangerous | Takes Args |
|----------------|----------|---------|-----------|------------|
| `pulse_effect` | template | | | |
| `red` | color | ✓ | | |
| Symbol | Type | Builtin | Dangerous | Takes Args |
|----------------|-----------------------|---------|-----------|------------|
| `main` | animation | | | |
| `pulse_effect` | animation_constructor | | | ✓ |
| `red` | color | ✓ | | |
### Compilation Output
@ -1159,10 +1167,11 @@ SUCCESS
## Symbol Table
| Symbol | Type | Builtin | Dangerous | Takes Args |
|----------------|----------|---------|-----------|------------|
| `pulse_effect` | template | | | |
| `red` | color | ✓ | | |
| Symbol | Type | Builtin | Dangerous | Takes Args |
|----------------|-----------------------|---------|-----------|------------|
| `main` | animation | | | |
| `pulse_effect` | animation_constructor | | | ✓ |
| `red` | color | ✓ | | |
### Compilation Output

View File

@ -8,31 +8,44 @@ import animation
# Cylon Red Eye
# Automatically adapts to the length of the strip
# Template function: cylon_effect
def cylon_effect_template(engine, eye_color_, back_color_, duration_)
var strip_len_ = animation.strip_length(engine)
var eye_animation_ = animation.beacon_animation(engine)
eye_animation_.color = eye_color_
eye_animation_.back_color = back_color_
eye_animation_.pos = (def (engine)
var provider = animation.cosine_osc(engine)
provider.min_value = (-1)
provider.max_value = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - 2 end)
provider.duration = duration_
return provider
end)(engine)
eye_animation_.beacon_size = 3 # small 3 pixels eye
eye_animation_.slew_size = 2 # with 2 pixel shading around
eye_animation_.priority = 5
engine.add(eye_animation_)
end
# Template animation class: cylon
class cylon_animation : animation.engine_proxy
static var PARAMS = animation.enc_params({
"eye_color": {"type": "color"},
"back_color": {"type": "color"},
"period": {"type": "time"}
})
animation.register_user_function('cylon_effect', cylon_effect_template)
# Template setup method - overrides EngineProxy placeholder
def setup_template()
var engine = self # using 'self' as a proxy to engine object (instead of 'self.engine')
var strip_len_ = animation.strip_length(engine)
var eye_animation_ = animation.beacon_animation(engine)
eye_animation_.color = animation.create_closure_value(engine, def (engine) return self.eye_color end)
eye_animation_.back_color = animation.create_closure_value(engine, def (engine) return self.back_color end)
eye_animation_.pos = (def (engine)
var provider = animation.cosine_osc(engine)
provider.min_value = (-1)
provider.max_value = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - 2 end)
provider.duration = animation.create_closure_value(engine, def (engine) return self.period end)
return provider
end)(engine)
eye_animation_.beacon_size = 3 # small 3 pixels eye
eye_animation_.slew_size = 2 # with 2 pixel shading around
eye_animation_.priority = 5
self.add(eye_animation_)
end
end
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
cylon_effect_template(engine, 0xFFFF0000, 0x00000000, 3000)
var cylon_red_ = cylon_animation(engine)
cylon_red_.eye_color = 0xFFFF0000
cylon_red_.back_color = 0x00000000
cylon_red_.period = 3000
engine.add(cylon_red_)
engine.run()
@ -40,17 +53,17 @@ engine.run()
# Cylon Red Eye
# Automatically adapts to the length of the strip
template cylon_effect {
template animation cylon {
param eye_color type color
param back_color type color
param duration
param period type time
set strip_len = strip_length()
animation eye_animation = beacon_animation(
color = eye_color
back_color = back_color
pos = cosine_osc(min_value = -1, max_value = strip_len - 2, duration = duration)
pos = cosine_osc(min_value = -1, max_value = strip_len - 2, duration = period)
beacon_size = 3 # small 3 pixels eye
slew_size = 2 # with 2 pixel shading around
priority = 5
@ -59,6 +72,7 @@ template cylon_effect {
run eye_animation
}
cylon_effect(red, transparent, 3s)
animation cylon_red = cylon(eye_color = red, back_color = transparent, period = 3s)
run cylon_red
-#

View File

@ -9,56 +9,64 @@ import animation
# Demo Shutter Rainbow Bidir
#
# Shutter from left to right iterating in all colors, then right to left
# Template function: shutter_bidir
def shutter_bidir_template(engine, colors_, duration_)
var strip_len_ = animation.strip_length(engine)
var shutter_size_ = (def (engine)
var provider = animation.sawtooth(engine)
provider.min_value = 0
provider.max_value = strip_len_
provider.duration = duration_
return provider
end)(engine)
var col1_ = animation.color_cycle(engine)
col1_.palette = colors_
col1_.cycle_period = 0
var col2_ = animation.color_cycle(engine)
col2_.palette = colors_
col2_.cycle_period = 0
col2_.next = 1
# shutter moving from left to right
var shutter_lr_animation_ = animation.beacon_animation(engine)
shutter_lr_animation_.color = col2_
shutter_lr_animation_.back_color = col1_
shutter_lr_animation_.pos = 0
shutter_lr_animation_.beacon_size = shutter_size_
shutter_lr_animation_.slew_size = 0
shutter_lr_animation_.priority = 5
# shutter moving from right to left
var shutter_rl_animation_ = animation.beacon_animation(engine)
shutter_rl_animation_.color = col1_
shutter_rl_animation_.back_color = col2_
shutter_rl_animation_.pos = 0
shutter_rl_animation_.beacon_size = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - animation.resolve(shutter_size_) end)
shutter_rl_animation_.slew_size = 0
shutter_rl_animation_.priority = 5
var shutter_seq_ = animation.sequence_manager(engine, -1)
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_lr_animation_, animation.resolve(duration_))
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
)
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_rl_animation_, animation.resolve(duration_))
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
)
engine.add(shutter_seq_)
end
# Template animation class: shutter_bidir
class shutter_bidir_animation : animation.engine_proxy
static var PARAMS = animation.enc_params({
"colors": {"type": "palette"},
"period": {"type": "time"}
})
animation.register_user_function('shutter_bidir', shutter_bidir_template)
# Template setup method - overrides EngineProxy placeholder
def setup_template()
var engine = self # using 'self' as a proxy to engine object (instead of 'self.engine')
var strip_len_ = animation.strip_length(engine)
var shutter_size_ = (def (engine)
var provider = animation.sawtooth(engine)
provider.min_value = 0
provider.max_value = strip_len_
provider.duration = animation.create_closure_value(engine, def (engine) return self.period end)
return provider
end)(engine)
var col1_ = animation.color_cycle(engine)
col1_.palette = animation.create_closure_value(engine, def (engine) return self.colors end)
col1_.cycle_period = 0
var col2_ = animation.color_cycle(engine)
col2_.palette = animation.create_closure_value(engine, def (engine) return self.colors end)
col2_.cycle_period = 0
col2_.next = 1
# shutter moving from left to right
var shutter_lr_animation_ = animation.beacon_animation(engine)
shutter_lr_animation_.color = col2_
shutter_lr_animation_.back_color = col1_
shutter_lr_animation_.pos = 0
shutter_lr_animation_.beacon_size = shutter_size_
shutter_lr_animation_.slew_size = 0
shutter_lr_animation_.priority = 5
# shutter moving from right to left
var shutter_rl_animation_ = animation.beacon_animation(engine)
shutter_rl_animation_.color = col1_
shutter_rl_animation_.back_color = col2_
shutter_rl_animation_.pos = 0
shutter_rl_animation_.beacon_size = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - animation.resolve(shutter_size_) end)
shutter_rl_animation_.slew_size = 0
shutter_rl_animation_.priority = 5
var shutter_seq_ = animation.sequence_manager(engine, -1)
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_lr_animation_, def (engine) return self.period end)
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
)
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_rl_animation_, def (engine) return self.period end)
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
)
self.add(shutter_seq_)
end
end
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
@ -73,7 +81,10 @@ var rainbow_with_white_ = bytes(
"FF8000FF" # Violet
"FFCCCCCC" # White
)
shutter_bidir_template(engine, rainbow_with_white_, 1500)
var main_ = shutter_bidir_animation(engine)
main_.colors = rainbow_with_white_
main_.period = 1500
engine.add(main_)
engine.run()
@ -82,12 +93,12 @@ engine.run()
#
# Shutter from left to right iterating in all colors, then right to left
template shutter_bidir {
template animation shutter_bidir {
param colors type palette
param duration
param period type time
set strip_len = strip_length()
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = duration)
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
@ -116,13 +127,13 @@ template shutter_bidir {
sequence shutter_seq repeat forever {
repeat col1.palette_size times {
restart shutter_size
play shutter_lr_animation for duration
play shutter_lr_animation for period
col1.next = 1
col2.next = 1
}
repeat col1.palette_size times {
restart shutter_size
play shutter_rl_animation for duration
play shutter_rl_animation for period
col1.next = 1
col2.next = 1
}
@ -142,6 +153,6 @@ palette rainbow_with_white = [
0xCCCCCC # White
]
shutter_bidir(rainbow_with_white, 1.5s)
animation main = shutter_bidir(colors=rainbow_with_white, period=1.5s)
run main
-#

View File

@ -9,57 +9,65 @@ import animation
# Demo Shutter Rainbow
#
# Shutter from center to both left and right
# Template function: shutter_central
def shutter_central_template(engine, colors_, duration_)
var strip_len_ = animation.strip_length(engine)
var strip_len2_ = animation.create_closure_value(engine, def (engine) return (animation.resolve(strip_len_) + 1) / 2 end)
var shutter_size_ = (def (engine)
var provider = animation.sawtooth(engine)
provider.min_value = 0
provider.max_value = strip_len_
provider.duration = duration_
return provider
end)(engine)
var col1_ = animation.color_cycle(engine)
col1_.palette = colors_
col1_.cycle_period = 0
var col2_ = animation.color_cycle(engine)
col2_.palette = colors_
col2_.cycle_period = 0
col2_.next = 1
# shutter moving in to out
var shutter_inout_animation_ = animation.beacon_animation(engine)
shutter_inout_animation_.color = col2_
shutter_inout_animation_.back_color = col1_
shutter_inout_animation_.pos = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len2_) - (animation.resolve(shutter_size_) + 1) / 2 end)
shutter_inout_animation_.beacon_size = shutter_size_
shutter_inout_animation_.slew_size = 0
shutter_inout_animation_.priority = 5
# shutter moving out to in
var shutter_outin_animation_ = animation.beacon_animation(engine)
shutter_outin_animation_.color = col1_
shutter_outin_animation_.back_color = col2_
shutter_outin_animation_.pos = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len2_) - (animation.resolve(strip_len_) - animation.resolve(shutter_size_) + 1) / 2 end)
shutter_outin_animation_.beacon_size = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - animation.resolve(shutter_size_) end)
shutter_outin_animation_.slew_size = 0
shutter_outin_animation_.priority = 5
var shutter_seq_ = animation.sequence_manager(engine, -1)
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_inout_animation_, animation.resolve(duration_))
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
)
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_outin_animation_, animation.resolve(duration_))
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
)
engine.add(shutter_seq_)
end
# Template animation class: shutter_central
class shutter_central_animation : animation.engine_proxy
static var PARAMS = animation.enc_params({
"colors": {"type": "palette"},
"period": {"type": "time"}
})
animation.register_user_function('shutter_central', shutter_central_template)
# Template setup method - overrides EngineProxy placeholder
def setup_template()
var engine = self # using 'self' as a proxy to engine object (instead of 'self.engine')
var strip_len_ = animation.strip_length(engine)
var strip_len2_ = animation.create_closure_value(engine, def (engine) return (animation.resolve(strip_len_) + 1) / 2 end)
var shutter_size_ = (def (engine)
var provider = animation.sawtooth(engine)
provider.min_value = 0
provider.max_value = strip_len_
provider.duration = animation.create_closure_value(engine, def (engine) return self.period end)
return provider
end)(engine)
var col1_ = animation.color_cycle(engine)
col1_.palette = animation.create_closure_value(engine, def (engine) return self.colors end)
col1_.cycle_period = 0
var col2_ = animation.color_cycle(engine)
col2_.palette = animation.create_closure_value(engine, def (engine) return self.colors end)
col2_.cycle_period = 0
col2_.next = 1
# shutter moving in to out
var shutter_inout_animation_ = animation.beacon_animation(engine)
shutter_inout_animation_.color = col2_
shutter_inout_animation_.back_color = col1_
shutter_inout_animation_.pos = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len2_) - (animation.resolve(shutter_size_) + 1) / 2 end)
shutter_inout_animation_.beacon_size = shutter_size_
shutter_inout_animation_.slew_size = 0
shutter_inout_animation_.priority = 5
# shutter moving out to in
var shutter_outin_animation_ = animation.beacon_animation(engine)
shutter_outin_animation_.color = col1_
shutter_outin_animation_.back_color = col2_
shutter_outin_animation_.pos = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len2_) - (animation.resolve(strip_len_) - animation.resolve(shutter_size_) + 1) / 2 end)
shutter_outin_animation_.beacon_size = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - animation.resolve(shutter_size_) end)
shutter_outin_animation_.slew_size = 0
shutter_outin_animation_.priority = 5
var shutter_seq_ = animation.sequence_manager(engine, -1)
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_inout_animation_, def (engine) return self.period end)
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
)
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_outin_animation_, def (engine) return self.period end)
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
)
self.add(shutter_seq_)
end
end
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
@ -73,7 +81,10 @@ var rainbow_with_white_ = bytes(
"FF4B0082"
"FFFFFFFF"
)
shutter_central_template(engine, rainbow_with_white_, 1500)
var main_ = shutter_central_animation(engine)
main_.colors = rainbow_with_white_
main_.period = 1500
engine.add(main_)
engine.run()
@ -82,65 +93,65 @@ engine.run()
#
# Shutter from center to both left and right
template shutter_central {
param colors type palette
param duration
set strip_len = strip_length()
set strip_len2 = (strip_len + 1) / 2
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = duration)
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
col2.next = 1
# shutter moving in to out
animation shutter_inout_animation = beacon_animation(
color = col2
back_color = col1
pos = strip_len2 - (shutter_size + 1) / 2
beacon_size = shutter_size
slew_size = 0
priority = 5
)
# shutter moving out to in
animation shutter_outin_animation = beacon_animation(
color = col1
back_color = col2
pos = strip_len2 - (strip_len - shutter_size + 1) / 2
beacon_size = strip_len - shutter_size
slew_size = 0
priority = 5
)
template animation shutter_central {
param colors type palette
param period type time
sequence shutter_seq repeat forever {
repeat col1.palette_size times {
restart shutter_size
play shutter_inout_animation for duration
col1.next = 1
col2.next = 1
}
repeat col1.palette_size times {
restart shutter_size
play shutter_outin_animation for duration
col1.next = 1
col2.next = 1
}
set strip_len = strip_length()
set strip_len2 = (strip_len + 1) / 2
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
col2.next = 1
# shutter moving in to out
animation shutter_inout_animation = beacon_animation(
color = col2
back_color = col1
pos = strip_len2 - (shutter_size + 1) / 2
beacon_size = shutter_size
slew_size = 0
priority = 5
)
# shutter moving out to in
animation shutter_outin_animation = beacon_animation(
color = col1
back_color = col2
pos = strip_len2 - (strip_len - shutter_size + 1) / 2
beacon_size = strip_len - shutter_size
slew_size = 0
priority = 5
)
sequence shutter_seq repeat forever {
repeat col1.palette_size times {
restart shutter_size
play shutter_inout_animation for period
col1.next = 1
col2.next = 1
}
repeat col1.palette_size times {
restart shutter_size
play shutter_outin_animation for period
col1.next = 1
col2.next = 1
}
run shutter_seq
}
palette rainbow_with_white = [ red
orange
yellow
green, # comma left on-purpose to test transpiler
blue # need for a lighter blue
indigo
white
]
shutter_central(rainbow_with_white, 1.5s)
run shutter_seq
}
palette rainbow_with_white = [ red
orange
yellow
green, # comma left on-purpose to test transpiler
blue # need for a lighter blue
indigo
white
]
animation main = shutter_central(colors=rainbow_with_white, period=1.5s)
run main
-#

View File

@ -9,40 +9,48 @@ import animation
# Demo Shutter Rainbow
#
# Shutter from left to right iterating in all colors, then right to left
# Template function: shutter_lr
def shutter_lr_template(engine, colors_, duration_)
var strip_len_ = animation.strip_length(engine)
var shutter_size_ = (def (engine)
var provider = animation.sawtooth(engine)
provider.min_value = 0
provider.max_value = strip_len_
provider.duration = duration_
return provider
end)(engine)
var col1_ = animation.color_cycle(engine)
col1_.palette = colors_
col1_.cycle_period = 0
var col2_ = animation.color_cycle(engine)
col2_.palette = colors_
col2_.cycle_period = 0
col2_.next = 1
# shutter moving from left to right
var shutter_lr_animation_ = animation.beacon_animation(engine)
shutter_lr_animation_.color = col2_
shutter_lr_animation_.back_color = col1_
shutter_lr_animation_.pos = 0
shutter_lr_animation_.beacon_size = shutter_size_
shutter_lr_animation_.slew_size = 0
shutter_lr_animation_.priority = 5
var shutter_seq_ = animation.sequence_manager(engine, -1)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_lr_animation_, animation.resolve(duration_))
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
engine.add(shutter_seq_)
end
# Template animation class: shutter_lr
class shutter_lr_animation : animation.engine_proxy
static var PARAMS = animation.enc_params({
"colors": {"type": "palette"},
"period": {}
})
animation.register_user_function('shutter_lr', shutter_lr_template)
# Template setup method - overrides EngineProxy placeholder
def setup_template()
var engine = self # using 'self' as a proxy to engine object (instead of 'self.engine')
var strip_len_ = animation.strip_length(engine)
var shutter_size_ = (def (engine)
var provider = animation.sawtooth(engine)
provider.min_value = 0
provider.max_value = strip_len_
provider.duration = animation.create_closure_value(engine, def (engine) return self.period end)
return provider
end)(engine)
var col1_ = animation.color_cycle(engine)
col1_.palette = animation.create_closure_value(engine, def (engine) return self.colors end)
col1_.cycle_period = 0
var col2_ = animation.color_cycle(engine)
col2_.palette = animation.create_closure_value(engine, def (engine) return self.colors end)
col2_.cycle_period = 0
col2_.next = 1
# shutter moving from left to right
var shutter_lr_animation_ = animation.beacon_animation(engine)
shutter_lr_animation_.color = col2_
shutter_lr_animation_.back_color = col1_
shutter_lr_animation_.pos = 0
shutter_lr_animation_.beacon_size = shutter_size_
shutter_lr_animation_.slew_size = 0
shutter_lr_animation_.priority = 5
var shutter_seq_ = animation.sequence_manager(engine, -1)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_lr_animation_, def (engine) return self.period end)
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
self.add(shutter_seq_)
end
end
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
@ -56,7 +64,10 @@ var rainbow_with_white_ = bytes(
"FF4B0082"
"FFFFFFFF"
)
shutter_lr_template(engine, rainbow_with_white_, 1500)
var main_ = shutter_lr_animation(engine)
main_.colors = rainbow_with_white_
main_.period = 1500
engine.add(main_)
engine.run()
@ -65,46 +76,46 @@ engine.run()
#
# Shutter from left to right iterating in all colors, then right to left
template shutter_lr {
param colors type palette
param duration
set strip_len = strip_length()
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = duration)
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
template animation shutter_lr {
param colors type palette
param period
set strip_len = strip_length()
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
col2.next = 1
# shutter moving from left to right
animation shutter_lr_animation = beacon_animation(
color = col2
back_color = col1
pos = 0
beacon_size = shutter_size
slew_size = 0
priority = 5
)
sequence shutter_seq repeat forever {
restart shutter_size
play shutter_lr_animation for period
col1.next = 1
col2.next = 1
# shutter moving from left to right
animation shutter_lr_animation = beacon_animation(
color = col2
back_color = col1
pos = 0
beacon_size = shutter_size
slew_size = 0
priority = 5
)
sequence shutter_seq repeat forever {
restart shutter_size
play shutter_lr_animation for duration
col1.next = 1
col2.next = 1
}
run shutter_seq
}
palette rainbow_with_white = [ red
orange
yellow
green, # comma left on-purpose to test transpiler
blue # need for a lighter blue
indigo
white
]
shutter_lr(rainbow_with_white, 1.5s)
run shutter_seq
}
palette rainbow_with_white = [ red
orange
yellow
green, # comma left on-purpose to test transpiler
blue # need for a lighter blue
indigo
white
]
animation main = shutter_lr(colors = rainbow_with_white, period = 1.5s)
run main
-#

View File

@ -8,26 +8,35 @@ import animation
# Cylon Red Eye
# Automatically adapts to the length of the strip
# Template function: cylon_effect
def cylon_effect_template(engine, eye_color_, duration_, back_color_)
var strip_len_ = animation.strip_length(engine)
var eye_animation_ = animation.beacon_animation(engine)
eye_animation_.color = eye_color_
eye_animation_.back_color = back_color_
eye_animation_.pos = (def (engine)
var provider = animation.cosine_osc(engine)
provider.min_value = (-1)
provider.max_value = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - 2 end)
provider.duration = duration_
return provider
end)(engine)
eye_animation_.beacon_size = 3 # small 3 pixels eye
eye_animation_.slew_size = 2 # with 2 pixel shading around
eye_animation_.priority = 5
engine.add(eye_animation_)
end
# Template animation class: cylon_effect
class cylon_effect_animation : animation.engine_proxy
static var PARAMS = animation.enc_params({
"eye_color": {"type": "color"},
"period": {"type": "time"},
"back_color": {"type": "color"}
})
animation.register_user_function('cylon_effect', cylon_effect_template)
# Template setup method - overrides EngineProxy placeholder
def setup_template()
var engine = self # using 'self' as a proxy to engine object (instead of 'self.engine')
var strip_len_ = animation.strip_length(engine)
var eye_animation_ = animation.beacon_animation(engine)
eye_animation_.color = animation.create_closure_value(engine, def (engine) return self.eye_color end)
eye_animation_.back_color = animation.create_closure_value(engine, def (engine) return self.back_color end)
eye_animation_.pos = (def (engine)
var provider = animation.cosine_osc(engine)
provider.min_value = (-1)
provider.max_value = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - 2 end)
provider.duration = animation.create_closure_value(engine, def (engine) return self.period end)
return provider
end)(engine)
eye_animation_.beacon_size = 3 # small 3 pixels eye
eye_animation_.slew_size = 2 # with 2 pixel shading around
eye_animation_.priority = 5
self.add(eye_animation_)
end
end
@ -35,9 +44,9 @@ animation.register_user_function('cylon_effect', cylon_effect_template)
# Cylon Red Eye
# Automatically adapts to the length of the strip
template cylon_effect {
template animation cylon_effect {
param eye_color type color
param duration type time
param period type time
param back_color type color
set strip_len = strip_length()
@ -45,7 +54,7 @@ template cylon_effect {
animation eye_animation = beacon_animation(
color = eye_color
back_color = back_color
pos = cosine_osc(min_value = -1, max_value = strip_len - 2, duration = duration)
pos = cosine_osc(min_value = -1, max_value = strip_len - 2, duration = period)
beacon_size = 3 # small 3 pixels eye
slew_size = 2 # with 2 pixel shading around
priority = 5

View File

@ -7,27 +7,37 @@
import animation
# Complex template test
# Template function: rainbow_pulse
def rainbow_pulse_template(engine, pal1_, pal2_, duration_, back_color_)
var cycle_color_ = animation.color_cycle(engine)
cycle_color_.palette = pal1_
cycle_color_.cycle_period = duration_
# Create pulsing animation
var pulse_ = animation.pulsating_animation(engine)
pulse_.color = cycle_color_
pulse_.period = duration_
# Create background
var background_ = animation.solid(engine)
background_.color = back_color_
background_.priority = 1
# Set pulse priority higher
pulse_.priority = 10
# Run both animations
engine.add(background_)
engine.add(pulse_)
end
# Template animation class: rainbow_pulse
class rainbow_pulse_animation : animation.engine_proxy
static var PARAMS = animation.enc_params({
"pal1": {"type": "palette"},
"pal2": {"type": "palette"},
"period": {"type": "time"},
"back_color": {"type": "color"}
})
animation.register_user_function('rainbow_pulse', rainbow_pulse_template)
# Template setup method - overrides EngineProxy placeholder
def setup_template()
var engine = self # using 'self' as a proxy to engine object (instead of 'self.engine')
var cycle_color_ = animation.color_cycle(engine)
cycle_color_.palette = animation.create_closure_value(engine, def (engine) return self.pal1 end)
cycle_color_.cycle_period = animation.create_closure_value(engine, def (engine) return self.period end)
# Create pulsing animation
var pulse_ = animation.pulsating_animation(engine)
pulse_.color = cycle_color_
pulse_.period = animation.create_closure_value(engine, def (engine) return self.period end)
# Create background
var background_ = animation.solid(engine)
background_.color = animation.create_closure_value(engine, def (engine) return self.back_color end)
background_.priority = 1
# Set pulse priority higher
pulse_.priority = 10
# Run both animations
self.add(background_)
self.add(pulse_)
end
end
# Create palettes
# Auto-generated strip initialization (using Tasmota configuration)
@ -36,7 +46,12 @@ var engine = animation.init_strip()
var fire_palette_ = bytes("00000000" "80FF0000" "FFFFFF00")
var ocean_palette_ = bytes("00000080" "800080FF" "FF00FFFF")
# Use the template
rainbow_pulse_template(engine, fire_palette_, ocean_palette_, 3000, 0xFF001100)
var main_ = rainbow_pulse_animation(engine)
main_.pal1 = fire_palette_
main_.pal2 = ocean_palette_
main_.perriod = 3000
main_.back_color = 0xFF001100
engine.add(main_)
engine.run()
# Compilation warnings:
@ -46,19 +61,19 @@ engine.run()
#- Original DSL source:
# Complex template test
template rainbow_pulse {
template animation rainbow_pulse {
param pal1 type palette
param pal2 type palette
param duration
param period type time
param back_color type color
# Create color cycle using first palette
color cycle_color = color_cycle(palette=pal1, cycle_period=duration)
color cycle_color = color_cycle(palette=pal1, cycle_period=period)
# Create pulsing animation
animation pulse = pulsating_animation(
color=cycle_color
period=duration
period=period
)
# Create background
@ -87,5 +102,7 @@ palette ocean_palette = [
]
# Use the template
rainbow_pulse(fire_palette, ocean_palette, 3s, 0x001100)
animation main = rainbow_pulse(pal1 = fire_palette, pal2 = ocean_palette, perriod = 3s, back_color = 0x001100)
run main
-#

View File

@ -9,58 +9,66 @@ import animation
# Demo Shutter Rainbow Bidir
#
# Shutter from left to right iterating in all colors, then right to left
# Template function: shutter_bidir
def shutter_bidir_template(engine, colors_, duration_)
var strip_len_ = animation.strip_length(engine)
var shutter_size_ = (def (engine)
var provider = animation.sawtooth(engine)
provider.min_value = 0
provider.max_value = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) + 0 end)
provider.duration = duration_
return provider
end)(engine)
var col1_ = animation.color_cycle(engine)
col1_.palette = colors_
col1_.cycle_period = 0
var col2_ = animation.color_cycle(engine)
col2_.palette = colors_
col2_.cycle_period = 0
col2_.next = 1
# shutter moving from left to right
var shutter_lr_animation_ = animation.beacon_animation(engine)
shutter_lr_animation_.color = col2_
shutter_lr_animation_.back_color = col1_
shutter_lr_animation_.pos = 0
shutter_lr_animation_.beacon_size = animation.create_closure_value(engine, def (engine) return animation.resolve(shutter_size_) + 0 end)
shutter_lr_animation_.slew_size = 0
shutter_lr_animation_.priority = 5
# shutter moving from right to left
var shutter_rl_animation_ = animation.beacon_animation(engine)
shutter_rl_animation_.color = col1_
shutter_rl_animation_.back_color = col2_
shutter_rl_animation_.pos = 0
shutter_rl_animation_.beacon_size = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - animation.resolve(shutter_size_) end)
shutter_rl_animation_.slew_size = 0 + 0
shutter_rl_animation_.priority = 5
var shutter_seq_ = animation.sequence_manager(engine, -1)
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
.push_closure_step(def (engine) log(f"begin 1", 3) end)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_lr_animation_, animation.resolve(duration_))
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
)
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
.push_closure_step(def (engine) log(f"begin 2", 3) end)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_rl_animation_, animation.resolve(duration_))
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
)
engine.add(shutter_seq_)
end
# Template animation class: shutter_bidir
class shutter_bidir_animation : animation.engine_proxy
static var PARAMS = animation.enc_params({
"colors": {"type": "palette"},
"period": {"type": "time"}
})
animation.register_user_function('shutter_bidir', shutter_bidir_template)
# Template setup method - overrides EngineProxy placeholder
def setup_template()
var engine = self # using 'self' as a proxy to engine object (instead of 'self.engine')
var strip_len_ = animation.strip_length(engine)
var shutter_size_ = (def (engine)
var provider = animation.sawtooth(engine)
provider.min_value = 0
provider.max_value = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) + 0 end)
provider.duration = animation.create_closure_value(engine, def (engine) return self.period end)
return provider
end)(engine)
var col1_ = animation.color_cycle(engine)
col1_.palette = animation.create_closure_value(engine, def (engine) return self.colors end)
col1_.cycle_period = 0
var col2_ = animation.color_cycle(engine)
col2_.palette = animation.create_closure_value(engine, def (engine) return self.colors end)
col2_.cycle_period = 0
col2_.next = 1
# shutter moving from left to right
var shutter_lr_animation_ = animation.beacon_animation(engine)
shutter_lr_animation_.color = col2_
shutter_lr_animation_.back_color = col1_
shutter_lr_animation_.pos = 0
shutter_lr_animation_.beacon_size = animation.create_closure_value(engine, def (engine) return animation.resolve(shutter_size_) + 0 end)
shutter_lr_animation_.slew_size = 0
shutter_lr_animation_.priority = 5
# shutter moving from right to left
var shutter_rl_animation_ = animation.beacon_animation(engine)
shutter_rl_animation_.color = col1_
shutter_rl_animation_.back_color = col2_
shutter_rl_animation_.pos = 0
shutter_rl_animation_.beacon_size = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - animation.resolve(shutter_size_) end)
shutter_rl_animation_.slew_size = 0 + 0
shutter_rl_animation_.priority = 5
var shutter_seq_ = animation.sequence_manager(engine, -1)
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
.push_closure_step(def (engine) log(f"begin 1", 3) end)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_lr_animation_, def (engine) return self.period end)
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
)
.push_repeat_subsequence(animation.sequence_manager(engine, def (engine) return col1_.palette_size end)
.push_closure_step(def (engine) log(f"begin 2", 3) end)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_rl_animation_, def (engine) return self.period end)
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
)
self.add(shutter_seq_)
end
end
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
@ -74,7 +82,10 @@ var rainbow_with_white_ = bytes(
"FF4B0082"
"FFFFFFFF"
)
shutter_bidir_template(engine, rainbow_with_white_, 1500)
var main_ = shutter_bidir_animation(engine)
main_.colors = rainbow_with_white_
main_.period = 1500
engine.add(main_)
engine.run()
@ -83,12 +94,12 @@ engine.run()
#
# Shutter from left to right iterating in all colors, then right to left
template shutter_bidir {
template animation shutter_bidir {
param colors type palette
param duration
param period type time
set strip_len = strip_length()
set shutter_size = sawtooth(min_value = 0, max_value = strip_len + 0, duration = duration)
set shutter_size = sawtooth(min_value = 0, max_value = strip_len + 0, duration = period)
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
@ -118,14 +129,14 @@ template shutter_bidir {
repeat col1.palette_size times {
log("begin 1")
restart shutter_size
play shutter_lr_animation for duration
play shutter_lr_animation for period
col1.next = 1
col2.next = 1
}
repeat col1.palette_size times {
log("begin 2")
restart shutter_size
play shutter_rl_animation for duration
play shutter_rl_animation for period
col1.next = 1
col2.next = 1
}
@ -143,6 +154,6 @@ palette rainbow_with_white = [ red
white
]
shutter_bidir(rainbow_with_white, 1.5s)
animation main = shutter_bidir(colors = rainbow_with_white, period = 1.5s)
run main
-#

View File

@ -9,40 +9,48 @@ import animation
# Demo Shutter Rainbow
#
# Shutter from center to both left and right
# Template function: shutter_central
def shutter_central_template(engine, colors_, duration_)
var strip_len_ = animation.strip_length(engine)
var shutter_size_ = (def (engine)
var provider = animation.sawtooth(engine)
provider.min_value = 0
provider.max_value = strip_len_
provider.duration = duration_
return provider
end)(engine)
var col1_ = animation.color_cycle(engine)
col1_.palette = colors_
col1_.cycle_period = 0
var col2_ = animation.color_cycle(engine)
col2_.palette = colors_
col2_.cycle_period = 0
col2_.next = 1
# shutter moving from left to right
var shutter_central_animation_ = animation.beacon_animation(engine)
shutter_central_animation_.color = col2_
shutter_central_animation_.back_color = col1_
shutter_central_animation_.pos = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - animation.resolve(shutter_size_) / 2 end)
shutter_central_animation_.beacon_size = shutter_size_
shutter_central_animation_.slew_size = 0
shutter_central_animation_.priority = 5
var shutter_seq_ = animation.sequence_manager(engine, -1)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_central_animation_, animation.resolve(duration_))
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
engine.add(shutter_seq_)
end
# Template animation class: shutter_central
class shutter_central_animation : animation.engine_proxy
static var PARAMS = animation.enc_params({
"colors": {"type": "palette"},
"period": {"type": "time"}
})
animation.register_user_function('shutter_central', shutter_central_template)
# Template setup method - overrides EngineProxy placeholder
def setup_template()
var engine = self # using 'self' as a proxy to engine object (instead of 'self.engine')
var strip_len_ = animation.strip_length(engine)
var shutter_size_ = (def (engine)
var provider = animation.sawtooth(engine)
provider.min_value = 0
provider.max_value = strip_len_
provider.duration = animation.create_closure_value(engine, def (engine) return self.period end)
return provider
end)(engine)
var col1_ = animation.color_cycle(engine)
col1_.palette = animation.create_closure_value(engine, def (engine) return self.colors end)
col1_.cycle_period = 0
var col2_ = animation.color_cycle(engine)
col2_.palette = animation.create_closure_value(engine, def (engine) return self.colors end)
col2_.cycle_period = 0
col2_.next = 1
# shutter moving from left to right
var shutter_central_animation_ = animation.beacon_animation(engine)
shutter_central_animation_.color = col2_
shutter_central_animation_.back_color = col1_
shutter_central_animation_.pos = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) - animation.resolve(shutter_size_) / 2 end)
shutter_central_animation_.beacon_size = shutter_size_
shutter_central_animation_.slew_size = 0
shutter_central_animation_.priority = 5
var shutter_seq_ = animation.sequence_manager(engine, -1)
.push_closure_step(def (engine) shutter_size_.start(engine.time_ms) end)
.push_play_step(shutter_central_animation_, def (engine) return self.period end)
.push_closure_step(def (engine) col1_.next = 1 end)
.push_closure_step(def (engine) col2_.next = 1 end)
self.add(shutter_seq_)
end
end
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
@ -56,7 +64,10 @@ var rainbow_with_white_ = bytes(
"FF4B0082"
"FFFFFFFF"
)
shutter_central_template(engine, rainbow_with_white_, 1500)
var main_ = shutter_central_animation(engine)
main_.colors = rainbow_with_white_
main_.period = 1500
engine.add(main_)
engine.run()
@ -65,12 +76,12 @@ engine.run()
#
# Shutter from center to both left and right
template shutter_central {
template animation shutter_central {
param colors type palette
param duration
param period type time
set strip_len = strip_length()
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = duration)
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
@ -88,7 +99,7 @@ template shutter_central {
sequence shutter_seq repeat forever {
restart shutter_size
play shutter_central_animation for duration
play shutter_central_animation for period
col1.next = 1
col2.next = 1
}
@ -105,6 +116,6 @@ palette rainbow_with_white = [ red
white
]
shutter_central(rainbow_with_white, 1.5s)
animation main = shutter_central(colors = rainbow_with_white, period = 1.5s)
run main
-#

View File

@ -8,22 +8,35 @@ import animation
# Test template functionality
# Define a simple template
# Template function: pulse_effect
def pulse_effect_template(engine, base_color_, duration_, brightness_)
var pulse_ = animation.pulsating_animation(engine)
pulse_.color = base_color_
pulse_.period = duration_
pulse_.opacity = brightness_
engine.add(pulse_)
end
# Template animation class: pulse_effect
class pulse_effect_animation : animation.engine_proxy
static var PARAMS = animation.enc_params({
"base_color": {"type": "color"},
"period": {"type": "time"},
"brightness": {"type": "percentage"}
})
animation.register_user_function('pulse_effect', pulse_effect_template)
# Template setup method - overrides EngineProxy placeholder
def setup_template()
var engine = self # using 'self' as a proxy to engine object (instead of 'self.engine')
var pulse_ = animation.pulsating_animation(engine)
pulse_.color = animation.create_closure_value(engine, def (engine) return self.base_color end)
pulse_.period = animation.create_closure_value(engine, def (engine) return self.period end)
pulse_.opacity = animation.create_closure_value(engine, def (engine) return self.brightness end)
self.add(pulse_)
end
end
# Use the template - templates add animations directly to engine and run them
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
pulse_effect_template(engine, 0xFFFF0000, 2000, 204)
var main_ = pulse_effect_animation(engine)
main_.base_color = 0xFFFF0000
main_.period = 2000
main_.brightness = 204
engine.add(main_)
engine.run()
@ -31,14 +44,14 @@ engine.run()
# Test template functionality
# Define a simple template
template pulse_effect {
template animation pulse_effect {
param base_color type color
param duration
param brightness
param period type time
param brightness type percentage
animation pulse = pulsating_animation(
color=base_color
period=duration
period=period
)
pulse.opacity = brightness
@ -46,5 +59,6 @@ template pulse_effect {
}
# Use the template - templates add animations directly to engine and run them
pulse_effect(red, 2s, 80%)
animation main = pulse_effect(base_color = red, period = 2s, brightness = 80%)
run main
-#

View File

@ -8,22 +8,35 @@ import animation
# Test template functionality
# Define a simple template
# Template function: pulse_effect
def pulse_effect_template(engine, base_color_, duration_, brightness_)
var pulse_ = animation.pulsating_animation(engine)
pulse_.color = base_color_
pulse_.period = duration_
pulse_.opacity = brightness_
engine.add(pulse_)
end
# Template animation class: pulse_effect
class pulse_effect_animation : animation.engine_proxy
static var PARAMS = animation.enc_params({
"base_color": {"type": "color"},
"period": {"type": "time"},
"brightness": {"type": "percentage"}
})
animation.register_user_function('pulse_effect', pulse_effect_template)
# Template setup method - overrides EngineProxy placeholder
def setup_template()
var engine = self # using 'self' as a proxy to engine object (instead of 'self.engine')
var pulse_ = animation.pulsating_animation(engine)
pulse_.color = animation.create_closure_value(engine, def (engine) return self.base_color end)
pulse_.period = animation.create_closure_value(engine, def (engine) return self.period end)
pulse_.opacity = animation.create_closure_value(engine, def (engine) return self.brightness end)
self.add(pulse_)
end
end
# Use the template - templates add animations directly to engine and run them
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
pulse_effect_template(engine, 0xFFFF0000, 2000, 204)
var main_ = pulse_effect_animation(engine)
main_.base_color = 0xFFFF0000
main_.period = 2000
main_.brightness = 204
engine.add(main_)
engine.run()
@ -31,14 +44,14 @@ engine.run()
# Test template functionality
# Define a simple template
template pulse_effect {
template animation pulse_effect {
param base_color type color
param duration
param brightness
param period type time
param brightness type percentage
animation pulse = pulsating_animation(
color=base_color
period=duration
period=period
)
pulse.opacity = brightness
@ -46,5 +59,6 @@ template pulse_effect {
}
# Use the template - templates add animations directly to engine and run them
pulse_effect(red, 2s, 80%)
animation main = pulse_effect(base_color = red, period = 2s, brightness = 80%)
run main
-#

View File

@ -1,17 +1,17 @@
# Cylon Red Eye
# Automatically adapts to the length of the strip
template cylon_effect {
template animation cylon {
param eye_color type color
param back_color type color
param duration
param period type time
set strip_len = strip_length()
animation eye_animation = beacon_animation(
color = eye_color
back_color = back_color
pos = cosine_osc(min_value = -1, max_value = strip_len - 2, duration = duration)
pos = cosine_osc(min_value = -1, max_value = strip_len - 2, duration = period)
beacon_size = 3 # small 3 pixels eye
slew_size = 2 # with 2 pixel shading around
priority = 5
@ -20,4 +20,5 @@ template cylon_effect {
run eye_animation
}
cylon_effect(red, transparent, 3s)
animation cylon_red = cylon(eye_color = red, back_color = transparent, period = 3s)
run cylon_red

View File

@ -2,12 +2,12 @@
#
# Shutter from left to right iterating in all colors, then right to left
template shutter_bidir {
template animation shutter_bidir {
param colors type palette
param duration
param period type time
set strip_len = strip_length()
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = duration)
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
@ -36,13 +36,13 @@ template shutter_bidir {
sequence shutter_seq repeat forever {
repeat col1.palette_size times {
restart shutter_size
play shutter_lr_animation for duration
play shutter_lr_animation for period
col1.next = 1
col2.next = 1
}
repeat col1.palette_size times {
restart shutter_size
play shutter_rl_animation for duration
play shutter_rl_animation for period
col1.next = 1
col2.next = 1
}
@ -62,4 +62,5 @@ palette rainbow_with_white = [
0xCCCCCC # White
]
shutter_bidir(rainbow_with_white, 1.5s)
animation main = shutter_bidir(colors=rainbow_with_white, period=1.5s)
run main

View File

@ -2,64 +2,64 @@
#
# Shutter from center to both left and right
template shutter_central {
param colors type palette
param duration
set strip_len = strip_length()
set strip_len2 = (strip_len + 1) / 2
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = duration)
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
col2.next = 1
# shutter moving in to out
animation shutter_inout_animation = beacon_animation(
color = col2
back_color = col1
pos = strip_len2 - (shutter_size + 1) / 2
beacon_size = shutter_size
slew_size = 0
priority = 5
)
# shutter moving out to in
animation shutter_outin_animation = beacon_animation(
color = col1
back_color = col2
pos = strip_len2 - (strip_len - shutter_size + 1) / 2
beacon_size = strip_len - shutter_size
slew_size = 0
priority = 5
)
template animation shutter_central {
param colors type palette
param period type time
sequence shutter_seq repeat forever {
repeat col1.palette_size times {
restart shutter_size
play shutter_inout_animation for duration
col1.next = 1
col2.next = 1
}
repeat col1.palette_size times {
restart shutter_size
play shutter_outin_animation for duration
col1.next = 1
col2.next = 1
}
set strip_len = strip_length()
set strip_len2 = (strip_len + 1) / 2
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
col2.next = 1
# shutter moving in to out
animation shutter_inout_animation = beacon_animation(
color = col2
back_color = col1
pos = strip_len2 - (shutter_size + 1) / 2
beacon_size = shutter_size
slew_size = 0
priority = 5
)
# shutter moving out to in
animation shutter_outin_animation = beacon_animation(
color = col1
back_color = col2
pos = strip_len2 - (strip_len - shutter_size + 1) / 2
beacon_size = strip_len - shutter_size
slew_size = 0
priority = 5
)
sequence shutter_seq repeat forever {
repeat col1.palette_size times {
restart shutter_size
play shutter_inout_animation for period
col1.next = 1
col2.next = 1
}
repeat col1.palette_size times {
restart shutter_size
play shutter_outin_animation for period
col1.next = 1
col2.next = 1
}
run shutter_seq
}
palette rainbow_with_white = [ red
orange
yellow
green, # comma left on-purpose to test transpiler
blue # need for a lighter blue
indigo
white
]
shutter_central(rainbow_with_white, 1.5s)
run shutter_seq
}
palette rainbow_with_white = [ red
orange
yellow
green, # comma left on-purpose to test transpiler
blue # need for a lighter blue
indigo
white
]
animation main = shutter_central(colors=rainbow_with_white, period=1.5s)
run main

View File

@ -2,45 +2,45 @@
#
# Shutter from left to right iterating in all colors, then right to left
template shutter_lr {
param colors type palette
param duration
set strip_len = strip_length()
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = duration)
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
template animation shutter_lr {
param colors type palette
param period
set strip_len = strip_length()
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
col2.next = 1
# shutter moving from left to right
animation shutter_lr_animation = beacon_animation(
color = col2
back_color = col1
pos = 0
beacon_size = shutter_size
slew_size = 0
priority = 5
)
sequence shutter_seq repeat forever {
restart shutter_size
play shutter_lr_animation for period
col1.next = 1
col2.next = 1
# shutter moving from left to right
animation shutter_lr_animation = beacon_animation(
color = col2
back_color = col1
pos = 0
beacon_size = shutter_size
slew_size = 0
priority = 5
)
sequence shutter_seq repeat forever {
restart shutter_size
play shutter_lr_animation for duration
col1.next = 1
col2.next = 1
}
run shutter_seq
}
palette rainbow_with_white = [ red
orange
yellow
green, # comma left on-purpose to test transpiler
blue # need for a lighter blue
indigo
white
]
shutter_lr(rainbow_with_white, 1.5s)
run shutter_seq
}
palette rainbow_with_white = [ red
orange
yellow
green, # comma left on-purpose to test transpiler
blue # need for a lighter blue
indigo
white
]
animation main = shutter_lr(colors = rainbow_with_white, period = 1.5s)
run main

View File

@ -1,9 +1,9 @@
# Cylon Red Eye
# Automatically adapts to the length of the strip
template cylon_effect {
template animation cylon_effect {
param eye_color type color
param duration type time
param period type time
param back_color type color
set strip_len = strip_length()
@ -11,7 +11,7 @@ template cylon_effect {
animation eye_animation = beacon_animation(
color = eye_color
back_color = back_color
pos = cosine_osc(min_value = -1, max_value = strip_len - 2, duration = duration)
pos = cosine_osc(min_value = -1, max_value = strip_len - 2, duration = period)
beacon_size = 3 # small 3 pixels eye
slew_size = 2 # with 2 pixel shading around
priority = 5

View File

@ -1,18 +1,18 @@
# Complex template test
template rainbow_pulse {
template animation rainbow_pulse {
param pal1 type palette
param pal2 type palette
param duration
param period type time
param back_color type color
# Create color cycle using first palette
color cycle_color = color_cycle(palette=pal1, cycle_period=duration)
color cycle_color = color_cycle(palette=pal1, cycle_period=period)
# Create pulsing animation
animation pulse = pulsating_animation(
color=cycle_color
period=duration
period=period
)
# Create background
@ -41,4 +41,5 @@ palette ocean_palette = [
]
# Use the template
rainbow_pulse(fire_palette, ocean_palette, 3s, 0x001100)
animation main = rainbow_pulse(pal1 = fire_palette, pal2 = ocean_palette, perriod = 3s, back_color = 0x001100)
run main

View File

@ -2,12 +2,12 @@
#
# Shutter from left to right iterating in all colors, then right to left
template shutter_bidir {
template animation shutter_bidir {
param colors type palette
param duration
param period type time
set strip_len = strip_length()
set shutter_size = sawtooth(min_value = 0, max_value = strip_len + 0, duration = duration)
set shutter_size = sawtooth(min_value = 0, max_value = strip_len + 0, duration = period)
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
@ -37,14 +37,14 @@ template shutter_bidir {
repeat col1.palette_size times {
log("begin 1")
restart shutter_size
play shutter_lr_animation for duration
play shutter_lr_animation for period
col1.next = 1
col2.next = 1
}
repeat col1.palette_size times {
log("begin 2")
restart shutter_size
play shutter_rl_animation for duration
play shutter_rl_animation for period
col1.next = 1
col2.next = 1
}
@ -62,4 +62,5 @@ palette rainbow_with_white = [ red
white
]
shutter_bidir(rainbow_with_white, 1.5s)
animation main = shutter_bidir(colors = rainbow_with_white, period = 1.5s)
run main

View File

@ -2,12 +2,12 @@
#
# Shutter from center to both left and right
template shutter_central {
template animation shutter_central {
param colors type palette
param duration
param period type time
set strip_len = strip_length()
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = duration)
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)
color col1 = color_cycle(palette=colors, cycle_period=0)
color col2 = color_cycle(palette=colors, cycle_period=0)
@ -25,7 +25,7 @@ template shutter_central {
sequence shutter_seq repeat forever {
restart shutter_size
play shutter_central_animation for duration
play shutter_central_animation for period
col1.next = 1
col2.next = 1
}
@ -42,4 +42,5 @@ palette rainbow_with_white = [ red
white
]
shutter_central(rainbow_with_white, 1.5s)
animation main = shutter_central(colors = rainbow_with_white, period = 1.5s)
run main

View File

@ -1,14 +1,14 @@
# Test template functionality
# Define a simple template
template pulse_effect {
template animation pulse_effect {
param base_color type color
param duration
param brightness
param period type time
param brightness type percentage
animation pulse = pulsating_animation(
color=base_color
period=duration
period=period
)
pulse.opacity = brightness
@ -16,4 +16,5 @@ template pulse_effect {
}
# Use the template - templates add animations directly to engine and run them
pulse_effect(red, 2s, 80%)
animation main = pulse_effect(base_color = red, period = 2s, brightness = 80%)
run main

View File

@ -1,14 +1,14 @@
# Test template functionality
# Define a simple template
template pulse_effect {
template animation pulse_effect {
param base_color type color
param duration
param brightness
param period type time
param brightness type percentage
animation pulse = pulsating_animation(
color=base_color
period=duration
period=period
)
pulse.opacity = brightness
@ -16,4 +16,5 @@ template pulse_effect {
}
# Use the template - templates add animations directly to engine and run them
pulse_effect(red, 2s, 80%)
animation main = pulse_effect(base_color = red, period = 2s, brightness = 80%)
run main

View File

@ -14,24 +14,23 @@ This document provides a comprehensive reference for all classes in the Berry An
## Class Hierarchy
```
ParameterizedObject
├── Playable (base interface for animations and sequences)
│ ├── Animation (unified base class for all visual elements)
│ │ ├── EngineProxy (combines rendering and orchestration)
│ │ │ └── (user-defined template animations)
│ │ ├── SolidAnimation (solid color fill)
│ │ ├── BeaconAnimation (pulse at specific position)
│ │ ├── CrenelPositionAnimation (crenel/square wave pattern)
│ │ ├── BreatheAnimation (breathing effect)
│ │ ├── PalettePatternAnimation (base for palette-based animations)
│ │ ├── CometAnimation (moving comet with tail)
│ │ ├── FireAnimation (realistic fire effect)
│ │ ├── TwinkleAnimation (twinkling stars effect)
│ │ ├── GradientAnimation (color gradients)
│ │ ├── NoiseAnimation (Perlin noise patterns)
│ │ ├── WaveAnimation (wave motion effects)
│ │ └── RichPaletteAnimation (smooth palette transitions)
│ └── SequenceManager (orchestrates animation sequences)
ParameterizedObject (base class with parameter management and playable interface)
├── Animation (unified base class for all visual elements)
│ ├── EngineProxy (combines rendering and orchestration)
│ │ └── (user-defined template animations)
│ ├── SolidAnimation (solid color fill)
│ ├── BeaconAnimation (pulse at specific position)
│ ├── CrenelPositionAnimation (crenel/square wave pattern)
│ ├── BreatheAnimation (breathing effect)
│ ├── PalettePatternAnimation (base for palette-based animations)
│ ├── CometAnimation (moving comet with tail)
│ ├── FireAnimation (realistic fire effect)
│ ├── TwinkleAnimation (twinkling stars effect)
│ ├── GradientAnimation (color gradients)
│ ├── NoiseAnimation (Perlin noise patterns)
│ ├── WaveAnimation (wave motion effects)
│ └── RichPaletteAnimation (smooth palette transitions)
├── SequenceManager (orchestrates animation sequences)
└── ValueProvider (dynamic value generation)
├── StaticValueProvider (wraps static values)
├── StripLengthProvider (provides LED strip length)
@ -50,11 +49,22 @@ ParameterizedObject
### ParameterizedObject
Base class for all parameterized objects in the framework.
Base class for all parameterized objects in the framework. Provides parameter management with validation, storage, and retrieval, as well as the playable interface for lifecycle management (start/stop/update).
This unified base class enables:
- Consistent parameter handling across all framework objects
- Unified engine management (animations and sequences treated uniformly)
- Hybrid objects that combine rendering and orchestration
- Consistent lifecycle management (start/stop/update)
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| *(none)* | - | - | - | Base class has no parameters |
| `is_running` | bool | false | - | Whether the object is active |
**Key Methods**:
- `start(time_ms)` - Start the object at a specific time
- `stop()` - Stop the object
- `update(time_ms)` - Update object state based on current time
**Factory**: N/A (base class)
@ -93,8 +103,8 @@ A specialized animation class that combines rendering and orchestration capabili
- Used as base class for template animations
**Child Management**:
- `add(playable)` - Adds a child animation or sequence
- `remove_child(playable)` - Removes a child
- `add(obj)` - Adds a child animation or sequence
- `remove(obj)` - Removes a child
- Children are automatically started/stopped with parent
- Children are rendered in priority order (higher priority on top)
@ -358,9 +368,11 @@ Cycles through a palette of colors with brutal switching. Inherits from `ColorPr
|-----------|------|---------|-------------|-------------|
| `palette` | bytes | default palette | - | Palette bytes in AARRGGBB format |
| `cycle_period` | int | 5000 | min: 0 | Cycle time in ms (0 = manual only) |
| `next` | int | 0 | - | Write 1 to move to next color manually, or any number to go forward or backwars by `n` colors |
| `next` | int | 0 | - | Write 1 to move to next color manually, or any number to go forward or backwards by `n` colors |
| `palette_size` | int | 3 | read-only | Number of colors in the palette (automatically updated when palette changes) |
**Note**: The `get_color_for_value()` method accepts values in the 0-255 range for value-based color mapping.
**Modes**: Auto-cycle (`cycle_period > 0`) or Manual-only (`cycle_period = 0`)
#### Usage Examples
@ -395,8 +407,6 @@ Generates colors from predefined palettes with smooth transitions and profession
| `cycle_period` | int | 5000 | min: 0 | Cycle time in ms (0 = value-based only) |
| `transition_type` | int | animation.LINEAR | enum: [animation.LINEAR, animation.SINE] | LINEAR=constant speed, SINE=smooth ease-in/ease-out |
| `brightness` | int | 255 | 0-255 | Overall brightness scaling |
| `range_min` | int | 0 | - | Minimum value for value-based mapping |
| `range_max` | int | 100 | - | Maximum value for value-based mapping |
#### Available Predefined Palettes
@ -902,8 +912,6 @@ Creates smooth color transitions using rich palette data with direct parameter a
| `cycle_period` | int | 5000 | min: 0 | Cycle time in ms (0 = value-based only) |
| `transition_type` | int | animation.LINEAR | enum: [animation.LINEAR, animation.SINE] | LINEAR=constant speed, SINE=smooth ease-in/ease-out |
| `brightness` | int | 255 | 0-255 | Overall brightness scaling |
| `range_min` | int | 0 | - | Minimum value for value-based mapping |
| `range_max` | int | 100 | - | Maximum value for value-based mapping |
| *(inherits all Animation parameters)* | | | | |
**Special Features**:

View File

@ -981,143 +981,15 @@ sequence clean_transitions {
}
```
## Templates
## Template Animations
Templates provide a powerful way to create reusable, parameterized animation patterns. They allow you to define animation blueprints that can be instantiated with different parameters, promoting code reuse and maintainability.
Template animations provide a powerful way to create reusable, parameterized animation classes. They allow you to define animation blueprints that can be instantiated multiple times with different parameters, promoting code reuse and maintainability.
**Template-Only Files**: DSL files containing only template definitions transpile to pure Berry functions without engine initialization or execution code. This allows templates to be used as reusable function libraries.
**Template-Only Files**: DSL files containing only template animation definitions transpile to pure Berry classes without engine initialization or execution code. This allows template animations to be used as reusable animation libraries.
### Template Definition
### Template Animation Definition
Templates are defined using the `template` keyword followed by a parameter block and body:
```berry
template template_name {
param parameter1 type color
param parameter2
param parameter3 type number
# Template body with DSL statements
animation my_anim = some_animation(color=parameter1, period=parameter2)
my_anim.opacity = parameter3
run my_anim
}
```
### Template Parameters
Template parameters are declared using the `param` keyword with optional type annotations:
```berry
template pulse_effect {
param base_color type color # Parameter with type annotation
param duration # Parameter without type annotation
param brightness type number # Another typed parameter
# Use parameters in template body
animation pulse = pulsating_animation(
color=base_color
period=duration
)
pulse.opacity = brightness
run pulse
}
```
**Parameter Types:**
- `color` - Color values (hex, named colors, color providers)
- `palette` - Palette definitions
- `number` - Numeric values (integers, percentages, time values)
- `animation` - Animation instances
- Type annotations are optional but improve readability
### Template Body
The template body can contain any valid DSL statements:
**Supported Statements:**
- Color definitions
- Palette definitions
- Animation definitions
- Property assignments
- Run statements
- Variable assignments (set statements)
```berry
template rainbow_pulse {
param pal1 as palette
param pal2 as palette
param duration
param back_color as color
# Create dynamic color cycling
color cycle_color = color_cycle(
palette=pal1
cycle_period=duration
)
# Create animations
animation pulse = pulsating_animation(
color=cycle_color
period=duration
)
animation background = solid(color=back_color)
# Set properties
background.priority = 1
pulse.priority = 10
# Run both animations
run background
run pulse
}
```
### Template Usage
Templates are called like functions with positional arguments:
```berry
# Define the template
template blink_red {
param speed
animation blink = pulsating_animation(
color=red
period=speed
)
run blink
}
# Use the template
blink_red(1s) # Call with 1 second period
blink_red(500ms) # Call with 500ms period
```
**Complex Template Usage:**
```berry
# Create palettes for the template
palette fire_palette = [
(0, black)
(128, red)
(255, yellow)
]
palette ocean_palette = [
(0, navy)
(128, cyan)
(255, white)
]
# Use the complex template
rainbow_pulse(fire_palette, ocean_palette, 3s, black)
```
### Template Animation
Template animations create reusable animation classes that extend `engine_proxy`, allowing complex animations with parameters to be instantiated multiple times:
Template animations are defined using the `template animation` keywords followed by a parameter block and body:
```berry
template animation shutter_effect {
@ -1220,39 +1092,12 @@ my_fade.opacity = 200 # Set the implicit opacity parameter
- They are accessed as `self.<param>` within the template body
- All implicit parameters come from the `Animation` and `ParameterizedObject` base classes
**Key Differences from Regular Templates:**
- Generates classes instead of functions
- Parameters accessed as `self.<param>` instead of `<param>_`
- Uses `self.add()` instead of `engine.add()`
**Key Features:**
- Generates reusable animation classes extending `engine_proxy`
- Parameters accessed as `self.<param>` within the template body
- Uses `self.add()` to add child animations
- Can be instantiated multiple times with different parameters
### Regular Template Behavior
**Code Generation:**
Regular templates generate Berry functions that are registered as user functions:
```berry
# Template definition generates:
def pulse_effect_template(engine, base_color_, duration_, brightness_)
var pulse_ = animation.pulsating_animation(engine)
pulse_.color = base_color_
pulse_.period = duration_
pulse_.opacity = brightness_
engine.add(pulse_)
end
animation.register_user_function('pulse_effect', pulse_effect_template)
```
**Parameter Handling:**
- Parameters get `_` suffix in generated code to avoid naming conflicts
- Templates receive `engine` as the first parameter automatically
- Template calls are converted to function calls with `engine` as first argument
**Execution Model:**
- Templates don't return values - they add animations directly to the engine
- Multiple `run` statements in templates add multiple animations
- Templates can be called multiple times to create multiple instances
- Supports parameter constraints (type, min, max, default, nillable)
### Template Parameter Validation
@ -1613,11 +1458,11 @@ config_stmt = variable_assignment ;
variable_assignment = "set" identifier "=" expression ;
(* Definitions *)
definition = color_def | palette_def | animation_def | template_def ;
definition = color_def | palette_def | animation_def | template_animation_def ;
color_def = "color" identifier "=" color_expression ;
palette_def = "palette" identifier "=" palette_array ;
animation_def = "animation" identifier "=" animation_expression ;
template_def = "template" identifier "{" template_body "}" ;
template_animation_def = "template" "animation" identifier "{" template_body "}" ;
(* Property Assignments *)
property_assignment = identifier "." identifier "=" expression ;
@ -1634,16 +1479,15 @@ if_stmt = "if" expression "{" sequence_body "}" ;
sequence_assignment = identifier "." identifier "=" expression ;
restart_stmt = "restart" identifier ;
(* Templates *)
template_def = "template" identifier "{" template_body "}" ;
(* Template Animations *)
template_animation_def = "template" "animation" identifier "{" template_body "}" ;
template_body = { template_statement } ;
template_statement = param_decl | color_def | palette_def | animation_def | property_assignment | execution_stmt ;
param_decl = "param" identifier [ "type" identifier ] ;
template_statement = param_decl | color_def | palette_def | animation_def | property_assignment | sequence_def | execution_stmt ;
param_decl = "param" identifier [ "type" identifier ] [ constraint_list ] ;
constraint_list = ( "min" number | "max" number | "default" expression | "nillable" boolean ) { constraint_list } ;
(* Execution *)
execution_stmt = "run" identifier | template_call ;
template_call = identifier "(" [ argument_list ] ")" ;
argument_list = expression { "," expression } ;
execution_stmt = "run" identifier ;
(* Expressions *)
expression = logical_or_expr ;
@ -1761,6 +1605,7 @@ This applies to:
- Reserved name validation
- Parameter validation at compile time
- 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)**
- **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

View File

@ -478,7 +478,7 @@ _validate_value_provider_factory_exists(func_name)
```
_validate_single_parameter(func_name, param_name, animation_instance)
├── Use introspection to check if parameter exists
├── Call instance._has_param(param_name) for validation
├── Call instance.has_param(param_name) for validation
├── Report detailed error messages with line numbers
├── Validate immediately as parameters are parsed
└── Graceful error handling to ensure transpiler robustness
@ -740,7 +740,7 @@ get_error_report()
### Animation Module Integration
- **Factory function discovery** via introspection with existence checking
- **Parameter validation** using instance methods and _has_param()
- **Parameter validation** using instance methods and has_param()
- **Symbol resolution** using module contents with fallback handling
- **Mathematical function detection** using dynamic introspection of ClosureValueProvider
- **Automatic strip initialization** when no explicit strip configuration

View File

@ -607,6 +607,9 @@ When the engine is running, it automatically logs performance statistics:
```
AnimEngine: ticks=1000/1000 missed=0 total=0.50ms(0-2) anim=0.30ms(0-1) hw=0.20ms(0-1) cpu=10.0%
Phase1(checks): mean=0.05ms(0-0)
Phase2(events): mean=0.05ms(0-0)
Phase3(anim): mean=0.20ms(0-1)
```
**Metrics Explained:**
@ -617,9 +620,26 @@ AnimEngine: ticks=1000/1000 missed=0 total=0.50ms(0-2) anim=0.30ms(0-1) hw=0.20m
- **hw**: Mean hardware output time with (min-max) range - just the LED strip update
- **cpu**: Overall CPU usage percentage over the 5-second period
**Custom Profiling API:**
**Phase Metrics (Optional):**
When intermediate measurement points are available, the engine also reports phase-based timing:
- **Phase1(checks)**: Initial checks (strip length, throttling, can_show)
- **Phase2(events)**: Event processing time
- **Phase3(anim)**: Animation update and render time (before hardware output)
For measuring specific code sections, use the profiling API:
**Timestamp-Based Profiling:**
The engine uses a timestamp-based profiling system that stores only timestamps (not durations) in instance variables:
- `ts_start` - Tick start timestamp
- `ts_1` - After initial checks (optional)
- `ts_2` - After event processing (optional)
- `ts_3` - After animation update/render (optional)
- `ts_hw` - After hardware output
- `ts_end` - Tick end timestamp
Durations are computed from these timestamps in `_record_tick_metrics()` with nil checks to ensure values are valid.
**Accessing Profiling Data:**
```berry
import animation
@ -627,97 +647,43 @@ import animation
var strip = Leds(30)
var engine = animation.create_engine(strip)
# Start measuring a code section
engine.profile_start("my_calculation")
# Your code to measure
var result = 0
var i = 0
while i < 1000
result += i
i += 1
end
# End measuring
engine.profile_end("my_calculation")
# Run the engine - stats will be printed every 5 seconds
# Add an animation
var anim = animation.solid(engine)
anim.color = 0xFFFF0000
engine.add(anim)
engine.run()
# Run for a while to collect metrics
# After 5 seconds, metrics are automatically logged
# Access current metrics programmatically
print("Tick count:", engine.tick_count)
print("Total time sum:", engine.tick_time_sum)
print("Animation time sum:", engine.anim_time_sum)
print("Hardware time sum:", engine.hw_time_sum)
# Access phase metrics if available
if engine.phase1_time_sum > 0
print("Phase 1 time sum:", engine.phase1_time_sum)
end
```
**Profiling Output:**
**Profiling Benefits:**
Custom profiling points appear in the stats output:
1. **Memory Efficient:**
- Only stores timestamps (6 instance variables)
- No duration storage or arrays
- Streaming statistics with no memory overhead
```
AnimEngine: ticks=1000/1000 missed=0 total=0.50ms(0-2) anim=0.30ms(0-1) hw=0.20ms(0-1) cpu=10.0%
Profile[my_calculation]: count=1000 mean=0.15ms min=0ms max=1ms
Profile[another_section]: count=500 mean=0.25ms min=0ms max=2ms
```
2. **Automatic Tracking:**
- No manual instrumentation needed
- Runs continuously in background
- Reports every 5 seconds
**Profiling Best Practices:**
1. **Use Descriptive Names:**
```berry
engine.profile_start("render_effects")
# ... rendering code ...
engine.profile_end("render_effects")
engine.profile_start("color_calculation")
# ... color processing ...
engine.profile_end("color_calculation")
```
2. **Profile Critical Sections:**
```berry
# Measure custom effect rendering
def my_custom_effect(frame, time_ms)
self.engine.profile_start("custom_effect")
# Your effect logic
var i = 0
while i < frame.width
var color = calculate_color(i, time_ms)
frame.set_pixel_color(i, color)
i += 1
end
self.engine.profile_end("custom_effect")
end
```
3. **Avoid Profiling in Tight Loops:**
```berry
# ❌ BAD - profiling overhead in loop
var i = 0
while i < 1000
engine.profile_start("loop_iteration")
# ... work ...
engine.profile_end("loop_iteration")
i += 1
end
# ✅ GOOD - profile entire loop
engine.profile_start("entire_loop")
var i = 0
while i < 1000
# ... work ...
i += 1
end
engine.profile_end("entire_loop")
```
4. **Multiple Profiling Points:**
```berry
# You can have multiple active profiling points
engine.profile_start("section_a")
# ... code A ...
engine.profile_end("section_a")
engine.profile_start("section_b")
# ... code B ...
engine.profile_end("section_b")
```
3. **Detailed Breakdown:**
- Separates animation calculation from hardware output
- Optional phase-based timing for deeper analysis
- Min/max/mean statistics for all metrics
**Interpreting Performance Metrics:**

View File

@ -65,7 +65,7 @@ register_to_animation(param_encoder)
import "core/math_functions" as math_functions
register_to_animation(math_functions)
# Base class for parameter management - shared by Animation and ValueProvider
# Base class for parameter management and playable behavior - shared by Animation and ValueProvider
import "core/parameterized_object" as parameterized_object
register_to_animation(parameterized_object)
@ -73,10 +73,6 @@ register_to_animation(parameterized_object)
import "core/frame_buffer" as frame_buffer
register_to_animation(frame_buffer)
# Playable base class - common interface for animations and sequences
import "core/playable_base" as playable_base
register_to_animation(playable_base)
# Base Animation class - unified foundation for all visual elements
import "core/animation_base" as animation_base
register_to_animation(animation_base)

View File

@ -25,7 +25,6 @@ class BeaconAnimation : animation.animation
# Parameter definitions following the new specification
static var PARAMS = animation.enc_params({
"color": {"default": 0xFFFFFFFF},
"back_color": {"default": 0xFF000000},
"pos": {"default": 0},
"beacon_size": {"min": 0, "default": 1},
@ -38,10 +37,6 @@ class BeaconAnimation : animation.animation
# @param time_ms: int - Optional current time in milliseconds (defaults to engine time)
# @return bool - True if frame was modified, false otherwise
def render(frame, time_ms)
if frame == nil
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
@ -54,7 +49,7 @@ class BeaconAnimation : animation.animation
var color = self.color
# Fill background if not transparent
if back_color != 0xFF000000
if (back_color != 0xFF000000) && ((back_color & 0xFF000000) != 0x00)
frame.fill_pixels(frame.pixels, back_color)
end
@ -71,11 +66,13 @@ class BeaconAnimation : animation.animation
end
# Draw the main beacon
var i = beacon_min
while i < beacon_max
frame.set_pixel_color(i, color)
i += 1
end
frame.fill_pixels(frame.pixels, color, beacon_min, beacon_max)
var i
# var i = beacon_min
# while i < beacon_max
# frame.set_pixel_color(i, color)
# i += 1
# end
# Draw slew regions if slew_size > 0
if slew_size > 0

View File

@ -41,7 +41,7 @@ class CometAnimation : animation.animation
super(self).on_param_changed(name, value)
if name == "direction"
# Reset position when direction changes
var strip_length = self.engine.get_strip_length()
var strip_length = self.engine.strip_length
if value > 0
self.head_position = 0 # Start at beginning for forward movement
else
@ -67,7 +67,7 @@ class CometAnimation : animation.animation
var current_speed = self.speed
var current_direction = self.direction
var current_wrap_around = self.wrap_around
var strip_length = self.engine.get_strip_length()
var strip_length = self.engine.strip_length
# Calculate elapsed time since animation started
var elapsed = time_ms - self.start_time
@ -129,7 +129,7 @@ class CometAnimation : animation.animation
var direction = self.direction
var wrap_around = self.wrap_around
var fade_factor = self.fade_factor
var strip_length = self.engine.get_strip_length()
var strip_length = self.engine.strip_length
# Extract color components from current color (ARGB format)
var head_a = (current_color >> 24) & 0xFF

View File

@ -41,7 +41,7 @@ class FireAnimation : animation.animation
# Initialize buffers based on current strip length
def _initialize_buffers()
var strip_length = self.engine.get_strip_length()
var strip_length = self.engine.strip_length
# Create new bytes() buffer for heat values (1 byte per pixel)
self.heat_map.clear()
@ -107,7 +107,7 @@ class FireAnimation : animation.animation
var intensity = self.intensity
var flicker_amount = self.flicker_amount
var color_param = self.color
var strip_length = self.engine.get_strip_length()
var strip_length = self.engine.strip_length
# Ensure buffers are correct size (bytes() uses .size() method)
if self.heat_map.size() != strip_length || self.current_colors.size() != strip_length * 4
@ -198,8 +198,6 @@ class FireAnimation : animation.animation
fire_provider.cycle_period = 0 # Use value-based color mapping, not time-based
fire_provider.transition_type = 1 # Use sine transition (smooth)
fire_provider.brightness = 255
fire_provider.range_min = 0
fire_provider.range_max = 255
resolved_color = fire_provider
end
@ -243,7 +241,7 @@ class FireAnimation : 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
# Render each pixel with its current color
var i = 0

View File

@ -28,7 +28,7 @@ class NoiseAnimation : animation.animation
super(self).init(engine)
# Initialize non-parameter instance variables only
var strip_length = self.engine.get_strip_length()
var strip_length = self.engine.strip_length
self.current_colors = []
self.current_colors.resize(strip_length)
self.time_offset = 0
@ -50,8 +50,6 @@ class NoiseAnimation : animation.animation
rainbow_provider.cycle_period = 5000
rainbow_provider.transition_type = 1
rainbow_provider.brightness = 255
rainbow_provider.range_min = 0
rainbow_provider.range_max = 255
self.color = rainbow_provider
end
end
@ -105,8 +103,6 @@ class NoiseAnimation : animation.animation
gradient_provider.cycle_period = 5000
gradient_provider.transition_type = 1
gradient_provider.brightness = 255
gradient_provider.range_min = 0
gradient_provider.range_max = 255
# Set the gradient provider instead of the integer
super(self).setmember(name, gradient_provider)
@ -124,7 +120,7 @@ class NoiseAnimation : animation.animation
end
# Update current_colors array size when strip length changes via engine
var new_strip_length = self.engine.get_strip_length()
var new_strip_length = self.engine.strip_length
if size(self.current_colors) != new_strip_length
self.current_colors.resize(new_strip_length)
var i = size(self.current_colors)
@ -209,7 +205,7 @@ class NoiseAnimation : animation.animation
# Calculate noise colors for all pixels
def _calculate_noise(time_ms)
var strip_length = self.engine.get_strip_length()
var strip_length = self.engine.strip_length
var current_color = self.color
var i = 0
@ -242,7 +238,7 @@ class NoiseAnimation : 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
if i < frame.width
@ -280,8 +276,6 @@ def noise_rainbow(engine)
rainbow_provider.cycle_period = 5000
rainbow_provider.transition_type = 1
rainbow_provider.brightness = 255
rainbow_provider.range_min = 0
rainbow_provider.range_max = 255
anim.color = rainbow_provider
anim.scale = 50
anim.speed = 30
@ -309,8 +303,6 @@ def noise_fractal(engine)
rainbow_provider.cycle_period = 5000
rainbow_provider.transition_type = 1
rainbow_provider.brightness = 255
rainbow_provider.range_min = 0
rainbow_provider.range_max = 255
anim.color = rainbow_provider
anim.scale = 30
anim.speed = 20

View File

@ -35,7 +35,7 @@ class PalettePatternAnimation : animation.animation
# Initialize the value buffer based on current strip length
def _initialize_value_buffer()
var strip_length = self.engine.get_strip_length()
var strip_length = self.engine.strip_length
self.value_buffer.resize(strip_length)
# Initialize with zeros
@ -49,19 +49,12 @@ class PalettePatternAnimation : animation.animation
# Update the value buffer based on the current time
#
# @param time_ms: int - Current time in milliseconds
def _update_value_buffer(time_ms)
def _update_value_buffer(time_ms, strip_length)
var pattern_func = self.pattern_func
if pattern_func == nil
return
end
var strip_length = self.engine.get_strip_length()
# Resize buffer if strip length changed
if size(self.value_buffer) != strip_length
self.value_buffer.resize(strip_length)
end
# Calculate values for each pixel
var i = 0
while i < strip_length
@ -91,8 +84,15 @@ class PalettePatternAnimation : animation.animation
# Calculate elapsed time since animation started
var elapsed = time_ms - self.start_time
var strip_length = self.engine.strip_length
# Resize buffer if strip length changed
if size(self.value_buffer) != strip_length
self.value_buffer.resize(strip_length)
end
# Update the value buffer
self._update_value_buffer(elapsed)
self._update_value_buffer(elapsed, strip_length)
return true
end
@ -103,10 +103,6 @@ class PalettePatternAnimation : animation.animation
# @param time_ms: int - Optional current time in milliseconds (defaults to engine time)
# @return bool - True if frame was modified, false otherwise
def render(frame, time_ms)
if !self.is_running || frame == nil
return false
end
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
@ -116,18 +112,13 @@ class PalettePatternAnimation : animation.animation
return false
end
# Check if color_source has the required method (more flexible than isinstance check)
if color_source.get_color_for_value == nil
return false
end
# Calculate elapsed time since animation started
var elapsed = time_ms - self.start_time
# Apply colors from the color source to each pixel based on its value
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
while (i < strip_length)
var byte_value = self.value_buffer[i]
# Use the color_source to get color for the byte value (0-255)
@ -151,8 +142,8 @@ class PalettePatternAnimation : animation.animation
# String representation of the animation
def tostring()
var strip_length = self.engine.get_strip_length()
return f"PalettePatternAnimation(strip_length={strip_length}, priority={self.priority}, running={self.is_running})"
var strip_length = self.engine.strip_length
return f"{classname(self)}(strip_length={strip_length}, priority={self.priority}, running={self.is_running})"
end
end
@ -178,20 +169,15 @@ class PaletteWaveAnimation : PalettePatternAnimation
end
# Override _update_value_buffer to generate wave pattern directly
def _update_value_buffer(time_ms)
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
var strip_length = self.engine.get_strip_length()
# Resize buffer if strip length changed
if size(self.value_buffer) != strip_length
self.value_buffer.resize(strip_length)
end
# 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 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
@ -232,17 +218,11 @@ class PaletteGradientAnimation : PalettePatternAnimation
end
# Override _update_value_buffer to generate gradient pattern directly
def _update_value_buffer(time_ms)
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
var strip_length = self.engine.get_strip_length()
# Resize buffer if strip length changed
if size(self.value_buffer) != strip_length
self.value_buffer.resize(strip_length)
end
# Determine effective spatial period (0 means full strip)
var effective_spatial_period = spatial_period > 0 ? spatial_period : strip_length
@ -250,8 +230,7 @@ class PaletteGradientAnimation : PalettePatternAnimation
# Calculate the temporal shift position (how much the pattern has moved over time)
var temporal_offset = 0
if shift_period > 0
var temporal_position = tasmota.scale_uint(time_ms % shift_period, 0, shift_period, 0, 1000) / 1000.0
temporal_offset = temporal_position * effective_spatial_period
temporal_offset = tasmota.scale_uint(time_ms % shift_period, 0, shift_period, 0, effective_spatial_period)
end
# Calculate the phase shift offset in pixels
@ -292,20 +271,13 @@ class PaletteMeterAnimation : PalettePatternAnimation
end
# Override _update_value_buffer to generate meter pattern directly
def _update_value_buffer(time_ms)
def _update_value_buffer(time_ms, strip_length)
# Cache parameter values for performance
var value_func = self.value_func
if value_func == nil
return
end
var strip_length = self.engine.get_strip_length()
# Resize buffer if strip length changed
if size(self.value_buffer) != strip_length
self.value_buffer.resize(strip_length)
end
# Get the current value
var current_value = value_func(time_ms, self)

View File

@ -20,11 +20,9 @@ class RichPaletteAnimation : animation.animation
"palette": {"type": "instance", "default": nil},
"cycle_period": {"min": 0, "default": 5000},
"transition_type": {"enum": [animation.LINEAR, animation.SINE], "default": animation.SINE},
"brightness": {"min": 0, "max": 255, "default": 255},
"range_min": {"default": 0},
"range_max": {"default": 255}
"brightness": {"min": 0, "max": 255, "default": 255}
})
# Initialize a new RichPaletteAnimation
#
# @param engine: AnimationEngine - Reference to the animation engine (required)
@ -50,7 +48,7 @@ class RichPaletteAnimation : animation.animation
super(self).on_param_changed(name, value)
# Forward rich palette parameters to internal color provider
if name == "palette" || name == "cycle_period" || name == "transition_type" ||
name == "brightness" || name == "range_min" || name == "range_max"
name == "brightness"
# Set parameter on internal color provider
self.color_provider.set_param(name, value)
else

View File

@ -46,7 +46,7 @@ class TwinkleAnimation : animation.animation
# Initialize arrays based on current strip length
def _initialize_arrays()
var strip_length = self.engine.get_strip_length()
var strip_length = self.engine.strip_length
# Resize arrays
self.twinkle_states.resize(strip_length)
@ -135,7 +135,7 @@ class TwinkleAnimation : animation.animation
var max_brightness = self.max_brightness
var color = self.color
var strip_length = self.engine.get_strip_length()
var strip_length = self.engine.strip_length
# Ensure arrays are properly sized
if size(self.twinkle_states) != strip_length || self.current_colors.size() != strip_length * 4
@ -206,7 +206,7 @@ class TwinkleAnimation : 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
# Ensure arrays are properly sized
if size(self.twinkle_states) != strip_length || self.current_colors.size() != strip_length * 4

View File

@ -120,7 +120,7 @@ class WaveAnimation : animation.animation
# Calculate wave colors for all pixels
def _calculate_wave(time_ms)
var strip_length = self.engine.get_strip_length()
var strip_length = self.engine.strip_length
var current_frequency = self.frequency
var current_phase = self.phase
var current_amplitude = self.amplitude
@ -205,7 +205,7 @@ class WaveAnimation : 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
if i < frame.width && i < self.current_colors.size()
@ -249,8 +249,6 @@ def wave_rainbow_sine(engine)
rainbow_provider.cycle_period = 5000
rainbow_provider.transition_type = 1 # sine transition
rainbow_provider.brightness = 255
rainbow_provider.range_min = 0
rainbow_provider.range_max = 255
anim.color = rainbow_provider
anim.wave_type = 0 # sine wave
anim.frequency = 32

View File

@ -43,7 +43,7 @@ class BounceAnimation : animation.animation
# Initialize frame buffers and arrays
def _initialize_buffers()
var current_strip_length = self.engine.get_strip_length()
var current_strip_length = self.engine.strip_length
self.bounce_center = current_strip_length * 256 / 2 # Center in 1/256th pixels
self.current_position = self.bounce_center
@ -141,7 +141,7 @@ class BounceAnimation : animation.animation
# Cache parameter values for performance
var current_gravity = self.gravity
var current_bounce_range = self.bounce_range
var current_strip_length = self.engine.get_strip_length()
var current_strip_length = self.engine.strip_length
var current_damping = self.damping
# Use integer arithmetic for physics (dt in milliseconds)
@ -198,7 +198,7 @@ class BounceAnimation : animation.animation
end
# Cache strip length for performance
var current_strip_length = self.engine.get_strip_length()
var current_strip_length = self.engine.strip_length
# Apply bounce transformation
var pixel_position = self.current_position / 256 # Convert to pixel units
@ -225,7 +225,7 @@ class BounceAnimation : animation.animation
return false
end
var current_strip_length = self.engine.get_strip_length()
var current_strip_length = self.engine.strip_length
var i = 0
while i < current_strip_length
if i < frame.width

View File

@ -42,7 +42,7 @@ class JitterAnimation : animation.animation
# Initialize buffers based on current strip length
def _initialize_buffers()
var current_strip_length = self.engine.get_strip_length()
var current_strip_length = self.engine.strip_length
self.jitter_offsets = []
self.jitter_offsets.resize(current_strip_length)
self.source_frame = animation.frame_buffer(current_strip_length)
@ -122,7 +122,7 @@ class JitterAnimation : animation.animation
# Update jitter offsets
def _update_jitter()
var current_strip_length = self.engine.get_strip_length()
var current_strip_length = self.engine.strip_length
var jitter_intensity = self.jitter_intensity
var max_offset = tasmota.scale_uint(jitter_intensity, 0, 255, 0, 10)
@ -136,7 +136,7 @@ class JitterAnimation : animation.animation
# Calculate jittered colors for all pixels
def _calculate_jitter()
var current_strip_length = self.engine.get_strip_length()
var current_strip_length = self.engine.strip_length
var source_animation = self.source_animation
var jitter_type = self.jitter_type
var position_range = self.position_range
@ -245,7 +245,7 @@ class JitterAnimation : animation.animation
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var current_strip_length = self.engine.get_strip_length()
var current_strip_length = self.engine.strip_length
var i = 0
while i < current_strip_length
if i < frame.width

View File

@ -53,7 +53,7 @@ class PlasmaAnimation : animation.animation
# Initialize colors array based on current strip length
def _initialize_colors()
var strip_length = self.engine.get_strip_length()
var strip_length = self.engine.strip_length
self.current_colors.resize(strip_length)
var i = 0
while i < strip_length
@ -74,8 +74,6 @@ class PlasmaAnimation : animation.animation
rainbow_provider.cycle_period = 5000
rainbow_provider.transition_type = 1
rainbow_provider.brightness = 255
rainbow_provider.range_min = 0
rainbow_provider.range_max = 255
self.color = rainbow_provider
end
@ -95,8 +93,6 @@ class PlasmaAnimation : animation.animation
rainbow_provider.cycle_period = 5000
rainbow_provider.transition_type = 1
rainbow_provider.brightness = 255
rainbow_provider.range_min = 0
rainbow_provider.range_max = 255
# Set the parameter directly to avoid recursion
self.set_param("color", rainbow_provider)
end
@ -127,7 +123,7 @@ class PlasmaAnimation : animation.animation
# Calculate plasma colors for all pixels
def _calculate_plasma(time_ms)
var strip_length = self.engine.get_strip_length()
var strip_length = self.engine.strip_length
# Ensure colors array is properly sized
if size(self.current_colors) != strip_length
@ -196,7 +192,7 @@ class PlasmaAnimation : 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
if i < frame.width

View File

@ -37,7 +37,7 @@ class ScaleAnimation : animation.animation
# Initialize frame buffers based on current strip length
def _initialize_buffers()
var current_strip_length = self.engine.get_strip_length()
var current_strip_length = self.engine.strip_length
self.source_frame = animation.frame_buffer(current_strip_length)
self.current_colors = []
self.current_colors.resize(current_strip_length)
@ -141,7 +141,7 @@ class ScaleAnimation : animation.animation
# Calculate scaled colors for all pixels
def _calculate_scale()
# Get current strip length from engine
var current_strip_length = self.engine.get_strip_length()
var current_strip_length = self.engine.strip_length
# Ensure buffers are properly sized
if size(self.current_colors) != current_strip_length
@ -243,7 +243,7 @@ class ScaleAnimation : animation.animation
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var current_strip_length = self.engine.get_strip_length()
var current_strip_length = self.engine.strip_length
var i = 0
while i < current_strip_length
if i < frame.width

View File

@ -32,7 +32,7 @@ class ShiftAnimation : animation.animation
# Initialize buffers based on current strip length
def _initialize_buffers()
var current_strip_length = self.engine.get_strip_length()
var current_strip_length = self.engine.strip_length
self.source_frame = animation.frame_buffer(current_strip_length)
self.current_colors = []
self.current_colors.resize(current_strip_length)
@ -65,7 +65,7 @@ class ShiftAnimation : animation.animation
var current_direction = self.direction
var current_wrap_around = self.wrap_around
var current_source_animation = self.source_animation
var current_strip_length = self.engine.get_strip_length()
var current_strip_length = self.engine.strip_length
# Update shift offset based on speed
if current_shift_speed > 0
@ -102,7 +102,7 @@ class ShiftAnimation : animation.animation
# Calculate shifted colors for all pixels
def _calculate_shift()
# Get current strip length and ensure buffers are correct size
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._initialize_buffers()
end
@ -160,7 +160,7 @@ class ShiftAnimation : animation.animation
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var current_strip_length = self.engine.get_strip_length()
var current_strip_length = self.engine.strip_length
var i = 0
while i < current_strip_length
if i < frame.width

View File

@ -61,7 +61,7 @@ class SparkleAnimation : animation.animation
# Initialize buffers based on current strip length
def _initialize_buffers()
var current_strip_length = self.engine.get_strip_length()
var current_strip_length = self.engine.strip_length
self.current_colors.resize(current_strip_length)
self.sparkle_states.resize(current_strip_length)
@ -113,7 +113,7 @@ class SparkleAnimation : animation.animation
# Update sparkle states and create new sparkles
def _update_sparkles(time_ms)
var current_strip_length = self.engine.get_strip_length()
var current_strip_length = self.engine.strip_length
# Cache parameter values for performance
var sparkle_duration = self.sparkle_duration
@ -206,7 +206,7 @@ class SparkleAnimation : animation.animation
# Auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
var current_strip_length = self.engine.get_strip_length()
var current_strip_length = self.engine.strip_length
var i = 0
while i < current_strip_length
if i < frame.width

View File

@ -7,11 +7,11 @@
# This is the unified base class for all visual elements in the framework.
# A Pattern is simply an Animation with infinite duration (duration = 0).
#
# Extends Playable to provide the common interface for lifecycle management.
# Extends ParameterizedObject to provide parameter management and playable interface.
import "./core/param_encoder" as encode_constraints
class Animation : animation.playable
class Animation : animation.parameterized_object
# Non-parameter instance variables only
var opacity_frame # Frame buffer for opacity animation rendering
@ -42,32 +42,32 @@ class Animation : animation.playable
# @param time_ms: int - Current time in milliseconds
# @return bool - True if animation is still running, false if completed
def update(time_ms)
# do nothing if not running
if (!self.is_running) return false end
# auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
# Access is_running via virtual member
var current_is_running = self.is_running
if !current_is_running
return false
end
var elapsed = time_ms - self.start_time
# Access parameters via virtual members
var current_duration = self.duration
var current_loop = self.loop
# Check if animation has completed its duration
if current_duration > 0 && elapsed >= current_duration
if current_loop
# Reset start time to create a looping effect
# We calculate the precise new start time to avoid drift
var loops_completed = elapsed / current_duration
self.start_time = self.start_time + (loops_completed * current_duration)
else
# Animation completed, make it inactive
# Set directly in values map to avoid triggering on_param_changed
self.values["is_running"] = false
return false
if current_duration > 0
var elapsed = time_ms - self.start_time
if elapsed >= current_duration
var current_loop = self.loop
if current_loop
# Reset start time to create a looping effect
# We calculate the precise new start time to avoid drift
var loops_completed = elapsed / current_duration
self.start_time = self.start_time + (loops_completed * current_duration)
else
# Animation completed, make it inactive
# Set directly in values map to avoid triggering on_param_changed
self.is_running = false
return false
end
end
end
@ -81,16 +81,7 @@ class Animation : animation.playable
# @param time_ms: int - Current time in milliseconds
# @return bool - True if frame was modified, false otherwise
def render(frame, time_ms)
# auto-fix time_ms and start_time
time_ms = self._fix_time_ms(time_ms)
# Access is_running via virtual member
var current_is_running = self.is_running
if !current_is_running || frame == nil
return false
end
# Update animation state
self.update(time_ms) # TODO IS UPDATE NOT ALREADY CALLED BY ENGINE?
if (!self.is_running) return false end
# Access parameters via virtual members (auto-resolves ValueProviders)
var current_color = self.color
@ -111,7 +102,15 @@ class Animation : animation.playable
# no need to auto-fix time_ms and start_time
# Handle opacity - can be number, frame buffer, or animation
var current_opacity = self.opacity
self._apply_opacity(frame, current_opacity, time_ms)
if (current_opacity == 255)
return # nothing to do
elif type(current_opacity) == 'int'
# Number mode: apply uniform opacity
frame.apply_opacity(frame.pixels, current_opacity)
else
# Opacity is a frame buffer
self._apply_opacity(frame, current_opacity, time_ms)
end
end
# Apply opacity to frame buffer - handles numbers and animations
@ -144,11 +143,7 @@ class Animation : animation.playable
# Use rendered frame buffer as opacity mask
frame.apply_opacity(frame.pixels, self.opacity_frame.pixels)
elif type(opacity) == 'int' && opacity < 255
# Number mode: apply uniform opacity
frame.apply_opacity(frame.pixels, opacity)
end
# If opacity is 255 (full opacity), do nothing
end
# Get a color for a specific pixel position and time

View File

@ -7,7 +7,7 @@
class AnimationEngine
# Core properties
var strip # LED strip object
var width # Strip width (cached for performance)
var strip_length # Strip length (cached for performance)
var root_animation # Root EngineProxy that holds all children
var frame_buffer # Main frame buffer
var temp_buffer # Temporary buffer for blending
@ -32,12 +32,28 @@ class AnimationEngine
var hw_time_sum # Sum of hardware output times
var hw_time_min # Minimum hardware output time
var hw_time_max # Maximum hardware output time
# Intermediate measurement point metrics
var phase1_time_sum # Sum of phase 1 times (ts_start to ts_1)
var phase1_time_min # Minimum phase 1 time
var phase1_time_max # Maximum phase 1 time
var phase2_time_sum # Sum of phase 2 times (ts_1 to ts_2)
var phase2_time_min # Minimum phase 2 time
var phase2_time_max # Maximum phase 2 time
var phase3_time_sum # Sum of phase 3 times (ts_2 to ts_3)
var phase3_time_min # Minimum phase 3 time
var phase3_time_max # Maximum phase 3 time
var last_stats_time # Last time stats were printed
var stats_period # Stats reporting period (5000ms)
# Custom profiling points
var profile_points # Map of profile point name -> {count, sum, min, max}
var profile_start_times # Map of profile point name -> start time
# Profiling timestamps (only store timestamps, compute durations in _record_tick_metrics)
var ts_start # Timestamp: tick start
var ts_1 # Timestamp: intermediate measure point 1 (optional)
var ts_2 # Timestamp: intermediate measure point 2 (optional)
var ts_3 # Timestamp: intermediate measure point 3 (optional)
var ts_hw # Timestamp: hardware output complete
var ts_end # Timestamp: tick end
# Initialize the animation engine for a specific LED strip
def init(strip)
@ -46,16 +62,16 @@ class AnimationEngine
end
self.strip = strip
self.width = strip.length()
self.strip_length = strip.length()
# Create frame buffers
self.frame_buffer = animation.frame_buffer(self.strip_length)
self.temp_buffer = animation.frame_buffer(self.strip_length)
# Create root EngineProxy to manage all children
self.root_animation = animation.engine_proxy(self)
self.root_animation.name = "root"
# Create frame buffers
self.frame_buffer = animation.frame_buffer(self.width)
self.temp_buffer = animation.frame_buffer(self.width)
# Initialize state
self.is_running = false
self.last_update = 0
@ -74,12 +90,28 @@ class AnimationEngine
self.hw_time_sum = 0
self.hw_time_min = 999999
self.hw_time_max = 0
# Initialize intermediate phase metrics
self.phase1_time_sum = 0
self.phase1_time_min = 999999
self.phase1_time_max = 0
self.phase2_time_sum = 0
self.phase2_time_min = 999999
self.phase2_time_max = 0
self.phase3_time_sum = 0
self.phase3_time_min = 999999
self.phase3_time_max = 0
self.last_stats_time = 0
self.stats_period = 5000
# Initialize custom profiling
self.profile_points = {}
self.profile_start_times = {}
# Initialize profiling timestamps
self.ts_start = nil
self.ts_1 = nil
self.ts_2 = nil
self.ts_3 = nil
self.ts_hw = nil
self.ts_end = nil
end
# Run the animation engine
@ -117,9 +149,9 @@ class AnimationEngine
return self
end
# Add a playable object (animation or sequence) to the root animation
# Add an animation or sequence to the root animation
#
# @param obj: Playable - The playable object to add
# @param obj: Animation|SequenceManager - The object to add
# @return bool - True if added, false if already exists
def add(obj)
var ret = self.root_animation.add(obj)
@ -129,9 +161,9 @@ class AnimationEngine
return ret
end
# Remove a playable object from the root animation
# Remove an animation or sequence from the root animation
#
# @param obj: Playable - The playable object to remove
# @param obj: Animation|SequenceManager - The object to remove
# @return bool - True if removed, false if not found
def remove(obj)
var ret = self.root_animation.remove(obj)
@ -156,10 +188,10 @@ class AnimationEngine
end
# Start timing this tick
var tick_start = tasmota.millis()
self.ts_start = tasmota.millis()
if current_time == nil
current_time = tick_start
current_time = self.ts_start
end
# Check if strip length changed since last time
@ -185,16 +217,11 @@ class AnimationEngine
self._process_events(current_time)
# Update and render root animation (which updates all children)
# Measure animation calculation time separately
var anim_start = tasmota.millis()
self._update_and_render(current_time)
var anim_end = tasmota.millis()
var anim_duration = anim_end - anim_start
# End timing and record metrics
var tick_end = tasmota.millis()
var tick_duration = tick_end - tick_start
self._record_tick_metrics(tick_duration, anim_duration, current_time)
self.ts_end = tasmota.millis()
self._record_tick_metrics(current_time)
return true
end
@ -204,41 +231,39 @@ class AnimationEngine
# Update root animation (which updates all children)
self.root_animation.update(time_ms)
self.ts_1 = tasmota.millis()
# Skip rendering if no children
if self.root_animation.is_empty()
if self.render_needed
self._clear_strip()
self.render_needed = false
end
return 0 # Return 0 for hardware time when no rendering
return
end
# Clear main buffer
self.frame_buffer.clear()
self.ts_2 = tasmota.millis()
# Render root animation (which renders all children with blending)
var rendered = self.root_animation.render(self.frame_buffer, time_ms)
if rendered
# Apply root animation's post-processing (opacity, etc.)
self.root_animation.post_render(self.frame_buffer, time_ms)
end
# Measure hardware output time separately
var hw_start = tasmota.millis()
self.ts_3 = tasmota.millis()
# Output to hardware and measure time
self._output_to_strip()
var hw_end = tasmota.millis()
var hw_duration = hw_end - hw_start
self.ts_hw = tasmota.millis()
self.render_needed = false
return hw_duration
end
# Output frame buffer to LED strip
def _output_to_strip()
var i = 0
while i < self.width
self.strip.set_pixel_color(i, self.frame_buffer.get_pixel_color(i))
var strip_length = self.strip_length
var strip = self.strip
var pixels = self.frame_buffer.pixels
while i < strip_length
strip.set_pixel_color(i, pixels.get(i * 4, 4))
i += 1
end
self.strip.show()
@ -260,41 +285,112 @@ class AnimationEngine
end
# Record tick metrics and print stats periodically
def _record_tick_metrics(tick_duration, anim_duration, current_time)
def _record_tick_metrics(current_time)
# Compute durations from timestamps (only if timestamps are not nil)
var tick_duration = nil
var anim_duration = nil
var hw_duration = nil
var phase1_duration = nil
var phase2_duration = nil
var phase3_duration = nil
# Total tick duration: from start to end
if self.ts_start != nil && self.ts_end != nil
tick_duration = self.ts_end - self.ts_start
end
# Animation duration: from ts_2 (after event processing) to ts_3 (before hardware)
if self.ts_2 != nil && self.ts_3 != nil
anim_duration = self.ts_3 - self.ts_2
end
# Hardware duration: from ts_3 (before hardware) to ts_hw (after hardware)
if self.ts_3 != nil && self.ts_hw != nil
hw_duration = self.ts_hw - self.ts_3
end
# Phase 1: from ts_start to ts_1 (initial checks)
if self.ts_start != nil && self.ts_1 != nil
phase1_duration = self.ts_1 - self.ts_start
end
# Phase 2: from ts_1 to ts_2 (event processing)
if self.ts_1 != nil && self.ts_2 != nil
phase2_duration = self.ts_2 - self.ts_1
end
# Phase 3: from ts_2 to ts_3 (animation update/render)
if self.ts_2 != nil && self.ts_3 != nil
phase3_duration = self.ts_3 - self.ts_2
end
# Initialize stats time on first tick
if self.last_stats_time == 0
self.last_stats_time = current_time
end
# Update streaming statistics (no array storage)
# Update streaming statistics (only if durations are valid)
self.tick_count += 1
self.tick_time_sum += tick_duration
# Update tick min/max
if tick_duration < self.tick_time_min
self.tick_time_min = tick_duration
end
if tick_duration > self.tick_time_max
self.tick_time_max = tick_duration
if tick_duration != nil
self.tick_time_sum += tick_duration
if tick_duration < self.tick_time_min
self.tick_time_min = tick_duration
end
if tick_duration > self.tick_time_max
self.tick_time_max = tick_duration
end
end
# Update animation calculation stats
self.anim_time_sum += anim_duration
if anim_duration < self.anim_time_min
self.anim_time_min = anim_duration
end
if anim_duration > self.anim_time_max
self.anim_time_max = anim_duration
if anim_duration != nil
self.anim_time_sum += anim_duration
if anim_duration < self.anim_time_min
self.anim_time_min = anim_duration
end
if anim_duration > self.anim_time_max
self.anim_time_max = anim_duration
end
end
# Hardware time is the difference between total and animation time
var hw_duration = tick_duration - anim_duration
self.hw_time_sum += hw_duration
if hw_duration < self.hw_time_min
self.hw_time_min = hw_duration
if hw_duration != nil
self.hw_time_sum += hw_duration
if hw_duration < self.hw_time_min
self.hw_time_min = hw_duration
end
if hw_duration > self.hw_time_max
self.hw_time_max = hw_duration
end
end
if hw_duration > self.hw_time_max
self.hw_time_max = hw_duration
# Update phase metrics
if phase1_duration != nil
self.phase1_time_sum += phase1_duration
if phase1_duration < self.phase1_time_min
self.phase1_time_min = phase1_duration
end
if phase1_duration > self.phase1_time_max
self.phase1_time_max = phase1_duration
end
end
if phase2_duration != nil
self.phase2_time_sum += phase2_duration
if phase2_duration < self.phase2_time_min
self.phase2_time_min = phase2_duration
end
if phase2_duration > self.phase2_time_max
self.phase2_time_max = phase2_duration
end
end
if phase3_duration != nil
self.phase3_time_sum += phase3_duration
if phase3_duration < self.phase3_time_min
self.phase3_time_min = phase3_duration
end
if phase3_duration > self.phase3_time_max
self.phase3_time_max = phase3_duration
end
end
# Check if it's time to print stats (every 5 seconds)
@ -313,6 +409,15 @@ class AnimationEngine
self.hw_time_sum = 0
self.hw_time_min = 999999
self.hw_time_max = 0
self.phase1_time_sum = 0
self.phase1_time_min = 999999
self.phase1_time_max = 0
self.phase2_time_sum = 0
self.phase2_time_min = 999999
self.phase2_time_max = 0
self.phase3_time_sum = 0
self.phase3_time_min = 999999
self.phase3_time_max = 0
self.last_stats_time = current_time
end
end
@ -339,77 +444,24 @@ class AnimationEngine
var stats_msg = f"AnimEngine: ticks={self.tick_count}/{int(expected_ticks)} missed={int(missed_ticks)} total={mean_time:.2f}ms({self.tick_time_min}-{self.tick_time_max}) anim={mean_anim:.2f}ms({self.anim_time_min}-{self.anim_time_max}) hw={mean_hw:.2f}ms({self.hw_time_min}-{self.hw_time_max}) cpu={cpu_percent:.1f}%"
tasmota.log(stats_msg, 3) # Log level 3 (DEBUG)
# Print custom profiling points if any
self._print_profile_points()
end
# Custom profiling API - start measuring a code section
#
# @param name: string - Name of the profiling point
#
# Usage:
# engine.profile_start("my_section")
# # ... code to measure ...
# engine.profile_end("my_section")
def profile_start(name)
self.profile_start_times[name] = tasmota.millis()
end
# Custom profiling API - end measuring a code section
#
# @param name: string - Name of the profiling point (must match profile_start)
def profile_end(name)
var start_time = self.profile_start_times.find(name)
if start_time == nil
return # No matching start
# Print intermediate phase metrics if available
if self.phase1_time_sum > 0
var mean_phase1 = self.phase1_time_sum / self.tick_count
var phase1_msg = f" Phase1(checks): mean={mean_phase1:.2f}ms({self.phase1_time_min}-{self.phase1_time_max})"
tasmota.log(phase1_msg, 3)
end
var end_time = tasmota.millis()
var duration = end_time - start_time
# Get or create stats for this profile point
var stats = self.profile_points.find(name)
if stats == nil
stats = {
'count': 0,
'sum': 0,
'min': 999999,
'max': 0
}
self.profile_points[name] = stats
if self.phase2_time_sum > 0
var mean_phase2 = self.phase2_time_sum / self.tick_count
var phase2_msg = f" Phase2(events): mean={mean_phase2:.2f}ms({self.phase2_time_min}-{self.phase2_time_max})"
tasmota.log(phase2_msg, 3)
end
# Update streaming statistics
stats['count'] += 1
stats['sum'] += duration
if duration < stats['min']
stats['min'] = duration
if self.phase3_time_sum > 0
var mean_phase3 = self.phase3_time_sum / self.tick_count
var phase3_msg = f" Phase3(anim): mean={mean_phase3:.2f}ms({self.phase3_time_min}-{self.phase3_time_max})"
tasmota.log(phase3_msg, 3)
end
if duration > stats['max']
stats['max'] = duration
end
# Clear start time
self.profile_start_times.remove(name)
end
# Print custom profiling points statistics
def _print_profile_points()
if size(self.profile_points) == 0
return
end
for name: self.profile_points.keys()
var stats = self.profile_points[name]
if stats['count'] > 0
var mean = stats['sum'] / stats['count']
var msg = f" Profile[{name}]: count={stats['count']} mean={mean:.2f}ms min={stats['min']}ms max={stats['max']}ms"
tasmota.log(msg, 3)
end
end
# Reset profile points for next period
self.profile_points = {}
end
# Interrupt current animations
@ -450,7 +502,7 @@ class AnimationEngine
end
def get_strip_length()
return self.width
return self.strip_length
end
def is_active()
@ -481,7 +533,7 @@ class AnimationEngine
# @return bool - True if strip lengtj was changed, false otherwise
def check_strip_length()
var current_length = self.strip.length()
if current_length != self.width
if current_length != self.strip_length
self._handle_strip_length_change(current_length)
return true # Length changed
end
@ -494,7 +546,7 @@ class AnimationEngine
return # Invalid length, ignore
end
self.width = new_length
self.strip_length = new_length
# Resize existing frame buffers instead of creating new ones
self.frame_buffer.resize(new_length)

View File

@ -13,8 +13,11 @@ import "./core/param_encoder" as encode_constraints
class EngineProxy : animation.animation
# Non-parameter instance variables
var animations # List of child playables (animations and sequences)
var sequences # List of child sequence managers
var animations # List of child animations
var sequences # List of child sequence managers
var value_providers # List of value providers that need update() calls
var strip_length # Proxy for strip_length from engine
var temp_buffer # proxy for the global 'engine.temp_buffer' used as a scratchad buffer during rendering, this object is maintained over time to avoid new objects creation
# Sequence iteration tracking (stack-based for nested sequences)
var iteration_stack # Stack of iteration numbers for nested sequences
@ -22,19 +25,17 @@ class EngineProxy : animation.animation
# Cached time for child access (updated during update())
var time_ms # Current time in milliseconds (cached from engine)
# Parameter definitions (extends Animation's PARAMS)
static var PARAMS = animation.enc_params({
# Inherited from Animation: name, is_running, priority, duration, loop, opacity, color
# EngineProxy has no additional parameters beyond Animation
})
def init(engine)
# Initialize parameter system with engine
super(self).init(engine)
# Keep a reference of 'engine.temp_buffer'
self.temp_buffer = self.engine.temp_buffer
# Initialize non-parameter instance variables
self.animations = []
self.sequences = []
self.value_providers = []
# Initialize iteration tracking stack
self.iteration_stack = []
@ -54,9 +55,9 @@ class EngineProxy : animation.animation
# Is empty
#
# @return true both animations and sequences are empty
# @return true if animations, sequences, and value_providers are all empty
def is_empty()
return (size(self.animations) == 0) && (size(self.sequences) == 0)
return (size(self.animations) == 0) && (size(self.sequences) == 0) && (size(self.value_providers) == 0)
end
# Number of animations
@ -77,19 +78,22 @@ class EngineProxy : animation.animation
return anims
end
# Add a child playable (animation or sequence)
# Add a child animation, sequence, or value provider
#
# @param child: Playable - The child to add
# @param obj: Animation|SequenceManager|ValueProvider - The child to add
# @return self for method chaining
def add(obj)
if isinstance(obj, animation.sequence_manager)
return self._add_sequence_manager(obj)
# Check if it's a ValueProvider (before Animation check, as some animations might also be providers)
elif isinstance(obj, animation.value_provider)
return self._add_value_provider(obj)
# Check if it's an Animation (or subclass)
elif isinstance(obj, animation.animation)
return self._add_animation(obj)
else
# Unknown type - provide helpful error message
raise "type_error", "only Animation or SequenceManager"
raise "type_error", "only Animation, SequenceManager, or ValueProvider"
end
end
@ -103,6 +107,21 @@ class EngineProxy : animation.animation
end
end
# Add a value provider
#
# @param provider: ValueProvider - The value provider instance to add
# @return true if successful, false if already in list
def _add_value_provider(provider)
if (self.value_providers.find(provider) == nil)
self.value_providers.push(provider)
# Note: We don't start the provider here - it's started by the animation that uses it
# We only register it so its update() method gets called in the update loop
return true
else
return false
end
end
# Add an animation with automatic priority sorting
#
# @param anim: animation - The animation instance to add (if not already listed)
@ -157,9 +176,9 @@ class EngineProxy : animation.animation
end
end
# Remove a child playable
# Remove a child animation
#
# @param child: Playable - The child to remove
# @param obj: Animation - The animation to remove
# @return true if actually removed
def _remove_animation(obj)
var idx = self.animations.find(obj)
@ -185,13 +204,30 @@ class EngineProxy : animation.animation
end
end
# Remove a value provider
#
# @param obj: ValueProvider instance
# @return true if actually removed
def _remove_value_provider(obj)
var idx = self.value_providers.find(obj)
if idx != nil
self.value_providers.remove(idx)
return true
else
return false
end
end
# Generic remove method that delegates to specific remove methods
# @param obj: Animation or SequenceManager - The object to remove
# @param obj: Animation, SequenceManager, or ValueProvider - The object to remove
# @return self for method chaining
def remove(obj)
# Check if it's a SequenceManager
if isinstance(obj, animation.sequence_manager)
return self._remove_sequence_manager(obj)
# Check if it's a ValueProvider (before Animation check)
elif isinstance(obj, animation.value_provider)
return self._remove_value_provider(obj)
# Check if it's an Animation (or subclass)
elif isinstance(obj, animation.animation)
return self._remove_animation(obj)
@ -200,7 +236,7 @@ class EngineProxy : animation.animation
end
end
# Start the hybrid animation and all its animations
# Start the hybrid animation and all its children
#
# @param time_ms: int - Start time in milliseconds
# @return self for method chaining
@ -208,14 +244,17 @@ class EngineProxy : animation.animation
# Call parent start
super(self).start(time_ms)
# Start all sequences
# Note: We don't start value_providers here - they are started by the animations that use them
# Value providers are only registered here so their update() method gets called
# Start all sequences FIRST (they may control animations)
var idx = 0
while idx < size(self.sequences)
self.sequences[idx].start(time_ms)
idx += 1
end
# Start all animations
# Start all animations SECOND (they use values from providers and sequences)
idx = 0
while idx < size(self.animations)
self.animations[idx].start(time_ms)
@ -225,23 +264,26 @@ class EngineProxy : animation.animation
return self
end
# Stop the hybrid animation and all its animations
# Stop the hybrid animation and all its children
#
# @return self for method chaining
def stop()
# Stop all sequences
# Stop all animations FIRST (they depend on sequences and value providers)
var idx = 0
while idx < size(self.animations)
self.animations[idx].stop()
idx += 1
end
# Stop all sequences SECOND (they may control animations)
idx = 0
while idx < size(self.sequences)
self.sequences[idx].stop()
idx += 1
end
# Stop all animations
idx = 0
while idx < size(self.animations)
self.animations[idx].stop()
idx += 1
end
# Note: We don't stop value_providers here - they are stopped by the animations that use them
# Value providers are only registered here so their update() method gets called
# Call parent stop
super(self).stop()
@ -249,24 +291,26 @@ class EngineProxy : animation.animation
return self
end
# Stop and clear the hybrid animation and all its animations
# Stop and clear the hybrid animation and all its children
#
# @return self for method chaining
def clear()
self.stop()
self.animations = []
self.sequences = []
self.value_providers = []
return self
end
# Update the hybrid animation and all its animations
# Update the hybrid animation and all its children
#
# @param time_ms: int - Current time in milliseconds
# @return bool - True if still running, false if completed
def update(time_ms)
# Cache time for child access
self.time_ms = time_ms
self.strip_length = self.engine.strip_length
# Update parent animation state
var still_running = super(self).update(time_ms)
@ -275,16 +319,28 @@ class EngineProxy : animation.animation
return false
end
# Update all child sequences
for seq : self.sequences
seq.update(time_ms)
# Update all value providers FIRST (they may produce values used by sequences and animations)
var idx = 0
var sz = size(self.value_providers)
while idx < sz
self.value_providers[idx].update(time_ms)
idx += 1
end
# Update all child animations (sequences are also in animations list)
for child : self.animations
if isinstance(child, animation.animation)
child.update(time_ms)
end
# Update all child sequences SECOND (they may control animations)
idx = 0
sz = size(self.sequences)
while idx < sz
self.sequences[idx].update(time_ms)
idx += 1
end
# Update all child animations LAST (they use values from providers and sequences)
idx = 0
sz = size(self.animations)
while idx < sz
var child = self.animations[idx].update(time_ms)
idx += 1
end
return true
@ -310,25 +366,32 @@ class EngineProxy : animation.animation
var modified = false
# Render own content (base Animation implementation)
modified = super(self).render(frame, time_ms)
# We don't call super method for optimization, skipping color computation
# modified = super(self).render(frame, time_ms)
# Render all child animations (but not sequences - they don't render)
for child : self.animations
if isinstance(child, animation.animation) && child.is_running
# Create temp buffer for child
var temp_frame = animation.frame_buffer(frame.width)
var child_rendered = child.render(temp_frame, time_ms)
var idx = 0
var sz = size(self.animations)
while idx < sz
var child = self.animations[idx]
if child.is_running
# Clear temporary buffer with transparent
self.temp_buffer.clear()
# Render child
var child_rendered = child.render(self.temp_buffer, time_ms)
if child_rendered
# Apply child's post-processing
child.post_render(temp_frame, time_ms)
child.post_render(self.temp_buffer, time_ms)
# Blend child into main frame
frame.blend_pixels(frame.pixels, temp_frame.pixels)
frame.blend_pixels(frame.pixels, self.temp_buffer.pixels)
modified = true
end
end
idx += 1
end
return modified
@ -338,7 +401,7 @@ class EngineProxy : animation.animation
# Get strip length from engine
def get_strip_length()
return (self.engine != nil) ? self.engine.get_strip_length() : 0
return self.engine.strip_length
end
# Sequence iteration tracking methods
@ -383,7 +446,7 @@ class EngineProxy : animation.animation
# String representation
def tostring()
return f"{classname(self)}({self.name}, animations={size(self.animations)}, sequences={size(self.sequences)}, running={self.is_running})"
return f"{classname(self)}({self.name}, animations={size(self.animations)}, sequences={size(self.sequences)}, value_providers={size(self.value_providers)}, running={self.is_running})"
end
end

View File

@ -93,7 +93,7 @@ class FrameBufferNtv
# pixels: destination bytes buffer
# color: the color to fill (ARGB format - 0xAARRGGBB)
# start_pos: start position (default: 0)
# end_pos: end position (default: -1 = last pixel)
# end_pos: end position excluded (default: -1 = last pixel)
static def fill_pixels(pixels, color, start_pos, end_pos)
# Default parameters
if (start_pos == nil) start_pos = 0 end
@ -104,18 +104,18 @@ class FrameBufferNtv
# Handle negative indices (Python-style)
if (start_pos < 0) start_pos += width end
if (end_pos < 0) end_pos += width end
if (end_pos < 0) end_pos += width + 1 end
# Clamp to valid range
if (start_pos < 0) start_pos = 0 end
if (end_pos < 0) end_pos = 0 end
if (start_pos >= width) return end
if (end_pos >= width) end_pos = width - 1 end
if (end_pos > width) end_pos = width end
if (end_pos < start_pos) return end
# Fill the region with the color
var i = start_pos
while i <= end_pos
while i < end_pos
pixels.set(i * 4, color, 4)
i += 1
end

View File

@ -1,9 +1,15 @@
# ParameterizedObject - Base class for parameter management
# ParameterizedObject - Base class for parameter management and playable behavior
#
# This class provides a common parameter management system that can be shared
# between Animation and ValueProvider classes. It handles parameter validation,
# storage, and retrieval with support for ValueProvider instances.
#
# It also provides the common interface for playable objects (animations and sequences)
# that can be started, stopped, and updated over time. This enables:
# - Unified engine management (single list instead of separate lists)
# - Hybrid objects that combine rendering and orchestration
# - Consistent lifecycle management (start/stop/update)
#
# Parameters are stored in a 'values' map and accessed via virtual instance variables
# through member() and setmember() methods. Subclasses should not declare instance
# variables for parameters, but use the PARAMS system only.
@ -14,12 +20,8 @@ class ParameterizedObject
var values # Map storing all parameter values
var engine # Reference to the animation engine
var start_time # Time when object started (ms) (int), value is set at first call to update() or render()
# Static parameter definitions - should be overridden by subclasses
static var PARAMS = animation.enc_params(
{"is_running": {"type": "bool", "default": false} # Whether the object is active
})
var is_running # Whether the object is active
# Initialize parameter system
#
# @param engine: AnimationEngine - Reference to the animation engine (required)
@ -30,6 +32,7 @@ class ParameterizedObject
self.engine = engine
self.values = {}
self.is_running = false
self._init_parameter_values()
end
@ -65,25 +68,8 @@ class ParameterizedObject
#
# @param name: string - Parameter name to check
# @return bool - True if parameter exists in any class in the hierarchy
def _has_param(name)
import introspect
# Walk up the class hierarchy to find the parameter
var current_class = classof(self)
while current_class != nil
# Check if this class has PARAMS
if introspect.contains(current_class, "PARAMS")
var class_params = current_class.PARAMS
if class_params.contains(name)
return true
end
end
# Move to parent class
current_class = super(current_class)
end
return false
def has_param(name)
return (self._get_param_def(name) != nil)
end
# Private method to get parameter definition from the class hierarchy
@ -118,12 +104,28 @@ class ParameterizedObject
# @return any - Resolved parameter value (ValueProvider resolved to actual value)
def member(name)
# Check if it's a parameter (either set in values or defined in PARAMS)
if self.values.contains(name) || self._has_param(name)
return self._resolve_parameter_value(name, self.engine.time_ms)
# Implement a fast-track if the value exists
if self.values.contains(name)
var value = self.values[name]
if type(value) != "instance"
return value
end
# Apply produce_value() if it' a ValueProvider
return self.resolve_value(value, name, self.engine.time_ms)
else
# Return default if available from class hierarchy
var encoded_constraints = self._get_param_def(name)
if encoded_constraints != nil
if self.constraint_mask(encoded_constraints, "default")
return self.constraint_find(encoded_constraints, "default")
else
return nil
end
else
raise "attribute_error", f"'{classname(self)}' object has no attribute '{name}'"
end
end
# Not a parameter, raise attribute error (consistent with setmember behavior)
raise "attribute_error", f"'{classname(self)}' object has no attribute '{name}'"
end
# Virtual member assignment - allows obj.param_name = value syntax
@ -133,7 +135,7 @@ class ParameterizedObject
# @param value: any - Value to set (can be static value or ValueProvider)
def setmember(name, value)
# Check if it's a parameter in the class hierarchy and set it with validation
if self._has_param(name)
if self.has_param(name)
self._set_parameter_value(name, value)
else
# Not a parameter, this will cause an error in normal Berry behavior
@ -291,7 +293,7 @@ class ParameterizedObject
# @return bool - True if parameter was set, false if validation failed
def set_param(name, value)
# Check if parameter exists in class hierarchy
if !self._has_param(name)
if !self.has_param(name)
return false
end
@ -355,8 +357,8 @@ class ParameterizedObject
# @param param_name: string - Name of the parameter
# @param time_ms: int - Current time in milliseconds
# @return any - The resolved value (static or from provider)
def get_param_value(param_name, time_ms)
return self._resolve_parameter_value(param_name, time_ms)
def get_param_value(param_name)
return self.member(param_name)
end
# Helper function to make sure both self.start_time and time_ms are valid
@ -386,39 +388,55 @@ class ParameterizedObject
# For value providers, start is typically not called because instances
# can be embedded in closures. So value providers must consider the first
# call to `produce_value()` as a start of their internal time reference.
# @param start_time: int - Optional start time in milliseconds
#
# Subclasses should override this to implement their start behavior.
#
# @param time_ms: int - Start time in milliseconds (optional, uses engine time if nil)
# @return self for method chaining
def start(time_ms)
# Use engine time if not provided
if time_ms == nil
time_ms = self.engine.time_ms
end
# if time_ms == nil
# raise "value_error", "engine.time_ms should not be 'nil'"
# end
if self.start_time != nil # reset time only if it was already started
# Set is_running to true
self.is_running = true
# Only reset start_time if it was already started (for value providers)
# Animations override this to always set start_time
if self.start_time != nil
self.start_time = time_ms
end
# Set is_running directly in values map to avoid infinite loop
self.values["is_running"] = true
return self
end
# Stop the object
# Subclasses should override this to implement their stop behavior
#
# @return self for method chaining
def stop()
# Set is_running to false
self.is_running = false
return self
end
# Update object state based on current time
# Subclasses must override this to implement their update logic
#
# @param time_ms: int - Current time in milliseconds
# @return bool - True if object is still running, false if completed
def update(time_ms)
# Default implementation just returns running state
return self.is_running
end
# Method called when a parameter is changed
# Subclasses should override this to handle parameter changes
#
# @param name: string - Parameter name
# @param value: any - New parameter value
def on_param_changed(name, value)
if name == "is_running"
if value == true
# Start the object (but avoid infinite loop by not setting is_running again)
# Call start method to handle start_time
self.start(nil)
elif value == false
# Stop the object - just set the internal state
# (is_running is already set to false by the parameter system)
end
end
end
# Equality operator for object identity comparison
@ -440,6 +458,11 @@ class ParameterizedObject
return true
end
# String representation
def tostring()
return f"{classname(self)}(running={self.is_running})"
end
# Inequality operator for object identity comparison
# This prevents the member() method from being called during != comparisons
#

View File

@ -1,73 +0,0 @@
# Playable Base Class - Common interface for animations and sequences
#
# A Playable is anything that can be started, stopped, and updated over time.
# This serves as the common base class for both Animation (visual rendering)
# and SequenceManager (orchestration), allowing the engine to treat them uniformly.
#
# This enables:
# - Unified engine management (single list instead of separate lists)
# - Hybrid objects that combine rendering and orchestration
# - Consistent lifecycle management (start/stop/update)
import "./core/param_encoder" as encode_constraints
class Playable : animation.parameterized_object
# Parameter definitions - minimal shared interface
static var PARAMS = animation.enc_params({
})
# Initialize a new playable
#
# @param engine: AnimationEngine - Reference to the animation engine (required)
def init(engine)
# Initialize parameter system with engine
super(self).init(engine)
end
# Start the playable at a specific time
# Subclasses should override this to implement their start behavior
#
# @param time_ms: int - Start time in milliseconds (optional, uses engine time if nil)
# @return self for method chaining
def start(time_ms)
# Use engine time if not provided
if time_ms == nil
time_ms = self.engine.time_ms
end
# Set is_running to true
self.values["is_running"] = true
# Always update start_time when start() is called (restart behavior)
self.start_time = time_ms
return self
end
# Stop the playable
# Subclasses should override this to implement their stop behavior
#
# @return self for method chaining
def stop()
# Set is_running to false
self.values["is_running"] = false
return self
end
# Update playable state based on current time
# Subclasses must override this to implement their update logic
#
# @param time_ms: int - Current time in milliseconds
# @return bool - True if playable is still running, false if completed
def update(time_ms)
# Default implementation just returns running state
return self.is_running
end
# String representation of the playable
def tostring()
return f"Playable(running={self.is_running})"
end
end
return {'playable': Playable}

View File

@ -2,12 +2,12 @@
# Handles async execution of animation sequences without blocking delays
# Supports sub-sequences and repeat logic through recursive composition
#
# Extends Playable to provide the common interface for lifecycle management,
# Extends ParameterizedObject to provide parameter management and playable interface,
# allowing sequences to be treated uniformly with animations by the engine.
import "./core/param_encoder" as encode_constraints
class SequenceManager : animation.playable
class SequenceManager : animation.parameterized_object
# Non-parameter instance variables
var active_sequence # Currently running sequence
var sequence_state # Current sequence execution state
@ -20,12 +20,6 @@ class SequenceManager : animation.playable
var current_iteration # Current iteration (0-based)
var is_repeat_sequence # Whether this is a repeat sub-sequence
# Parameter definitions (extends Playable's PARAMS)
static var PARAMS = animation.enc_params({
# Inherited from Playable: is_running
# SequenceManager has no additional parameters beyond Playable
})
def init(engine, repeat_count)
# Initialize parameter system with engine
super(self).init(engine)
@ -91,7 +85,7 @@ class SequenceManager : animation.playable
def start(time_ms)
# Stop any current sequence
if self.is_running
self.values["is_running"] = false
self.is_running = false
# Stop any sub-sequences
self.stop_all_subsequences()
end
@ -100,18 +94,16 @@ class SequenceManager : animation.playable
self.step_index = 0
self.step_start_time = time_ms
self.current_iteration = 0
self.values["is_running"] = true
self.is_running = true
# Initialize start_time if not already set
if self.start_time == nil
self.start_time = time_ms
end
# Always set start_time for restart behavior
self.start_time = time_ms
# FIXED: Check repeat count BEFORE starting execution
# If repeat_count is 0, don't execute at all
var resolved_repeat_count = self.get_resolved_repeat_count()
if resolved_repeat_count == 0
self.values["is_running"] = false
self.is_running = false
return self
end
@ -148,7 +140,7 @@ class SequenceManager : animation.playable
# Stop this sequence manager
def stop()
if self.is_running
self.values["is_running"] = false
self.is_running = false
# Pop iteration context from engine stack if this is a repeat sequence
if self.is_repeat_sequence
@ -433,7 +425,7 @@ class SequenceManager : animation.playable
end
else
# All iterations complete
self.values["is_running"] = false
self.is_running = false
# Pop iteration context from engine stack if this is a repeat sequence
if self.is_repeat_sequence

View File

@ -301,13 +301,19 @@ end
# Mock engine class for parameter validation during transpilation
class MockEngine
var time_ms
var strip_length
def init()
self.time_ms = 0
self.strip_length = 30 # Default strip length for validation
end
def get_strip_length()
return 30 # Default strip length for validation
return self.strip_length
end
def add(obj)
return true
end
end

View File

@ -476,12 +476,13 @@ class SimpleDSLTranspiler
self.skip_statement()
return
elif tok.value == "template"
# Check if this is "template animation" or just "template"
# Only "template animation" is supported
var next_tok = self.peek()
if next_tok != nil && next_tok.type == 0 #-animation_dsl.Token.KEYWORD-# && next_tok.value == "animation"
self.process_template_animation()
else
self.process_template()
self.error("Simple 'template' is not supported. Use 'template animation' instead to create reusable animation classes.")
self.skip_statement()
end
else
# For any other statement, ensure strip is initialized
@ -894,85 +895,6 @@ class SimpleDSLTranspiler
self.add(f"var {local_ref} = {value_result.expr}{inline_comment}")
end
# Process template definition: template name { param ... }
def process_template()
self.next() # skip 'template'
var name = self.expect_identifier()
# Validate that the template name is not reserved
if !self.validate_user_name(name, "template")
self.skip_statement()
return
end
self.expect_left_brace()
# First pass: collect all parameters with validation
var params = []
var param_types = {}
var param_names_seen = {} # Track duplicate parameter names
while !self.at_end() && !self.check_right_brace()
self.skip_whitespace_including_newlines()
if self.check_right_brace()
break
end
var tok = self.current()
if tok != nil && tok.type == 0 #-animation_dsl.Token.KEYWORD-# && tok.value == "param"
# Process parameter declaration
self.next() # skip 'param'
var param_name = self.expect_identifier()
# Validate parameter name (not a template animation)
if !self._validate_template_parameter_name(param_name, param_names_seen, false)
self.skip_statement()
return
end
# Check for optional type annotation
var param_type = nil
if self.current() != nil && self.current().type == 0 #-animation_dsl.Token.KEYWORD-# && self.current().value == "type"
self.next() # skip 'type'
param_type = self.expect_identifier()
# Validate type annotation
if !self._validate_template_parameter_type(param_type)
self.skip_statement()
return
end
end
# Add parameter to collections
params.push(param_name)
param_names_seen[param_name] = true
if param_type != nil
param_types[param_name] = param_type
end
# Skip optional newline after parameter
if self.current() != nil && self.current().type == 35 #-animation_dsl.Token.NEWLINE-#
self.next()
end
else
# Found non-param statement, break to collect body
break
end
end
# Generate Berry function for this template using direct pull-lexer approach
self.generate_template_function_direct(name, params, param_types)
# Add template to symbol table with parameter information
var template_info = {
"params": params,
"param_types": param_types
}
self.symbol_table.create_template(name, template_info)
end
# Process template animation definition: template animation name { param ... }
# Generates a class extending engine_proxy instead of a function
def process_template_animation()
@ -1189,52 +1111,6 @@ class SimpleDSLTranspiler
self.add(f"{self.get_indent()}.push_closure_step({closure_code}){inline_comment}")
end
# Generic method to process sequence assignment with configurable target array
def process_sequence_assignment_generic(indent, target_array)
var object_name = self.expect_identifier()
# Check if next token is a dot
if self.current() != nil && self.current().type == 33 #-animation_dsl.Token.DOT-#
self.next() # skip '.'
var property_name = self.expect_identifier()
# Validate parameter if we have this object in our symbol table
if self.symbol_table.contains(object_name)
var entry = self.symbol_table.get(object_name)
# Only validate parameters for actual instances, not sequence markers
if entry != nil && entry.instance != nil
var class_name = classname(entry.instance)
# Use the existing parameter validation logic
self._validate_single_parameter(class_name, property_name, entry.instance)
elif entry != nil && entry.type == 13 #-animation_dsl._symbol_entry.TYPE_SEQUENCE-#
# This is a sequence marker - sequences don't have properties
self.error(f"Sequences like '{object_name}' do not have properties. Property assignments are only valid for animations and color providers.")
return
end
end
self.expect_assign()
var value_result = self.process_value(self.CONTEXT_PROPERTY)
var inline_comment = self.collect_inline_comment()
# Generate assignment step with closure
# The closure receives the engine as parameter and performs the assignment
var object_ref = self.symbol_table.get_reference(object_name)
# Create closure that performs the assignment
var closure_code = f"def (engine) {object_ref}.{property_name} = {value_result.expr} end"
self.add(f"{indent}{target_array}.push(animation.create_assign_step({closure_code})){inline_comment}")
else
# Not a property assignment, this shouldn't happen since we checked for dot
self.error(f"Expected property assignment for '{object_name}' but found no dot")
self.skip_statement()
end
end
# Helper method to process play statement using fluent style
def process_play_statement_fluent()
self.next() # skip 'play'
@ -2828,73 +2704,7 @@ class SimpleDSLTranspiler
self.add("")
self.strip_initialized = true
end
# Generate Berry function for template definition using direct pull-lexer approach
def generate_template_function_direct(name, params, param_types)
import animation_dsl
import string
# Generate function signature with engine as first parameter
var param_list = "engine"
for param : params
param_list += f", {param}_"
end
self.add(f"# Template function: {name}")
self.add(f"def {name}_template({param_list})")
# Create a new transpiler that shares the same pull lexer
# It will consume tokens from the current position until the template ends
var template_transpiler = animation_dsl.SimpleDSLTranspiler(self.pull_lexer)
template_transpiler.symbol_table = animation_dsl._symbol_table() # Fresh symbol table for template
template_transpiler.strip_initialized = true # Templates assume engine exists
# Add parameters to template's symbol table with proper types
for param : params
var param_type = param_types.find(param)
if param_type != nil
# Create typed parameter based on type annotation
self._add_typed_parameter_to_symbol_table(template_transpiler.symbol_table, param, param_type)
else
# Default to variable type for untyped parameters
template_transpiler.symbol_table.create_variable(param)
end
end
# Transpile the template body - it will consume tokens until the closing brace
var template_body = template_transpiler.transpile_template_body()
if template_body != nil
# Add the transpiled body with proper indentation
var body_lines = string.split(template_body, "\n")
for line : body_lines
if size(line) > 0
self.add(f" {line}") # Add 2-space indentation
end
end
# Validate parameter usage in template body (post-transpilation check)
self._validate_template_parameter_usage(name, params, template_body)
else
# Error in template body transpilation
for error : template_transpiler.errors
self.error(f"Template '{name}' body error: {error}")
end
end
# Expect the closing brace (template_transpiler should have left us at this position)
self.expect_right_brace()
self.add("end")
self.add("")
# Register the template as a user function
self.add(f"animation.register_user_function('{name}', {name}_template)")
self.add("")
end
# Helper method to add inherited parameters from engine_proxy class hierarchy
# This dynamically discovers all parameters from engine_proxy and its superclasses
def _add_inherited_params_to_template(template_params_map)
@ -3092,9 +2902,9 @@ class SimpleDSLTranspiler
try
import introspect
# Validate parameter using the _has_param method
if animation_instance != nil && introspect.contains(animation_instance, "_has_param")
if !animation_instance._has_param(param_name)
# Validate parameter using the has_param method
if animation_instance != nil && introspect.contains(animation_instance, "has_param")
if !animation_instance.has_param(param_name)
var line = self.current() != nil ? self.current().line : 0
self.error(f"Animation '{func_name}' does not have parameter '{param_name}'. Check the animation documentation for valid parameters.")
end
@ -3319,10 +3129,10 @@ class SimpleDSLTranspiler
def _register_template_animation_constructor(name, params, param_types)
import animation_dsl
# Create a mock instance that has _has_param method for validation
# Create a mock instance that has has_param method for validation
var mock_instance = {
"_params": {},
"_has_param": def (param_name)
"has_param": def (param_name)
# Check if this parameter exists in the template's parameter list
for p : params
if p == param_name

View File

@ -159,7 +159,7 @@ class ColorCycleColorProvider : animation.color_provider
# Get a color based on a value (maps value to position in cycle)
# This method is kept for backward compatibility - brutal switching based on value
#
# @param value: int/float - Value to map to a color (0-100)
# @param value: int/float - Value to map to a color (0-255 range)
# @param time_ms: int - Current time in milliseconds (ignored for value-based color)
# @return int - Color in ARGB format (0xAARRGGBB)
def get_color_for_value(value, time_ms)
@ -173,15 +173,15 @@ class ColorCycleColorProvider : animation.color_provider
return self._get_color_at_index(0) # If only one color, just return it
end
# Clamp value to 0-100
# Clamp value to 0-255
if value < 0
value = 0
elif value > 100
value = 100
elif value > 255
value = 255
end
# Map value directly to color index (brutal switching using integer math)
var color_index = tasmota.scale_uint(value, 0, 100, 0, palette_size - 1)
var color_index = tasmota.scale_uint(value, 0, 255, 0, palette_size - 1)
# Clamp to valid range
if color_index >= palette_size

View File

@ -24,10 +24,10 @@ class ColorProvider : animation.value_provider
return 0xFFFFFFFF # Default white
end
# Get a color based on a value (0-100 by default)
# Get a color based on a value (0-255 range)
# This method is useful for mapping values to colors in different contexts
#
# @param value: int/float - Value to map to a color (typically 0-100)
# @param value: int/float - Value to map to a color (0-255 range)
# @param time_ms: int - Optional current time for time-based effects
# @return int - Color in ARGB format (0xAARRGGBB)
def get_color_for_value(value, time_ms)

View File

@ -66,7 +66,7 @@ class CompositeColorProvider : animation.color_provider
# Get a composite color based on a value
#
# @param value: int/float - Value to map to a color (0-100)
# @param value: int/float - Value to map to a color (0-255 range)
# @param time_ms: int - Current time in milliseconds
# @return int - Color in ARGB format (0xAARRGGBB)
def get_color_for_value(value, time_ms)

View File

@ -23,11 +23,6 @@ import "./core/param_encoder" as encode_constraints
#@ solidify:IterationNumberProvider,weak
class IterationNumberProvider : animation.value_provider
# Static parameter definitions (no parameters needed)
static var PARAMS = animation.enc_params({
})
# Produce the current iteration number from the animation engine
#
# @param name: string - Parameter name being requested (ignored)

View File

@ -3,6 +3,27 @@
# This color provider generates colors from a palette with smooth transitions.
# Reuses optimizations from Animate_palette class for maximum efficiency.
#
# PERFORMANCE OPTIMIZATION - LUT Cache:
# =====================================
# To avoid expensive palette interpolation on every pixel (binary search + RGB interpolation
# + brightness calculations), this provider uses a Lookup Table (LUT) cache:
#
# - LUT Structure: 129 entries covering values 0, 2, 4, 6, ..., 254, 255
# - Memory Usage: 516 bytes (129 entries × 4 bytes per ARGB color)
# - Resolution: 2-step resolution (ignoring LSB) plus special case for value 255
# - Mapping: lut_index = value >> 1 (divide by 2), except value 255 -> index 128
#
# Performance Impact:
# - Before: ~50-100 CPU cycles per lookup (search + interpolate + brightness)
# - After: ~10-15 CPU cycles per lookup (bit shift + bytes.get())
# - Speedup: ~5-10x faster per lookup
# - For 60-pixel gradient at 30 FPS: ~200x reduction in expensive operations
#
# LUT Invalidation:
# - Automatically rebuilt when palette, brightness, or transition_type changes
# - Lazy initialization: built on first use of get_color_for_value()
# - Transparent to users: no API changes required
#
# Follows the parameterized class specification:
# - Constructor takes only 'engine' parameter
# - All other parameters set via virtual member assignment after creation
@ -13,19 +34,20 @@ import "./core/param_encoder" as encode_constraints
class RichPaletteColorProvider : animation.color_provider
# Non-parameter instance variables only
var slots_arr # Constructed array of timestamp slots, based on cycle_period
var value_arr # Constructed array of value slots, based on range_min/range_max
var value_arr # Constructed array of value slots (always 0-255 range)
var slots # Number of slots in the palette
var current_color # Current interpolated color (calculated during update)
var light_state # light_state instance for proper color calculations
var color_lut # Color lookup table cache (129 entries: 0, 2, 4, ..., 254, 255)
var lut_dirty # Flag indicating LUT needs rebuilding
var _brightness # Cached value for `self.brightness` used during render()
# Parameter definitions
static var PARAMS = animation.enc_params({
"palette": {"type": "bytes", "default": nil}, # Palette bytes or predefined palette constant
"cycle_period": {"min": 0, "default": 5000}, # 5 seconds default, 0 = value-based only
"transition_type": {"enum": [animation.LINEAR, animation.SINE], "default": animation.LINEAR},
"brightness": {"min": 0, "max": 255, "default": 255},
"range_min": {"default": 0},
"range_max": {"default": 255}
"brightness": {"min": 0, "max": 255, "default": 255}
})
# Initialize a new RichPaletteColorProvider
@ -37,10 +59,15 @@ class RichPaletteColorProvider : animation.color_provider
# Initialize non-parameter instance variables
self.current_color = 0xFFFFFFFF
self.slots = 0
self.color_lut = nil
self.lut_dirty = true
# Create light_state instance for proper color calculations (reuse from Animate_palette)
import global
self.light_state = global.light_state(global.light_state.RGB)
# We need to register this value provider to receive 'update()'
engine.add(self)
end
# Handle parameter changes
@ -49,12 +76,17 @@ class RichPaletteColorProvider : animation.color_provider
# @param value: any - New value of the parameter
def on_param_changed(name, value)
super(self).on_param_changed(name, value)
if name == "range_min" || name == "range_max" || name == "cycle_period" || name == "palette"
if name == "cycle_period" || name == "palette"
if (self.slots_arr != nil) || (self.value_arr != nil)
# only if they were already computed
self._recompute_palette()
end
end
# Mark LUT as dirty when palette or transition_type changes
# Note: brightness changes do NOT invalidate LUT since brightness is applied after lookup
if name == "palette" || name == "transition_type"
self.lut_dirty = true
end
end
# Start/restart the animation cycle at a specific time
@ -100,13 +132,9 @@ class RichPaletteColorProvider : animation.color_provider
self.slots_arr = nil
end
# Compute value_arr based on 'range_min' and 'range_max'
var range_min = self.range_min
var range_max = self.range_max
if range_min >= range_max raise "value_error", "range_min must be lower than range_max" end
# Recompute palette with new range
# Compute value_arr for value-based mode (always 0-255 range)
if self._get_palette_bytes() != nil
self.value_arr = self._parse_palette(range_min, range_max)
self.value_arr = self._parse_palette(0, 255)
else
self.value_arr = nil
end
@ -210,6 +238,23 @@ class RichPaletteColorProvider : animation.color_provider
end
end
# Update object state based on current time
# Subclasses must override this to implement their update logic
#
# @param time_ms: int - Current time in milliseconds
# @return bool - True if object is still running, false if completed
def update(time_ms)
# Rebuild LUT if dirty
if self.lut_dirty || self.color_lut == nil
self._rebuild_color_lut()
end
# Cache the brightness to an instance variable for this tick
self._brightness = self.brightness
return self.is_running
end
# Produce a color value for any parameter name (optimized version from Animate_palette)
#
# @param name: string - Parameter name being requested (ignored)
@ -301,23 +346,70 @@ class RichPaletteColorProvider : animation.color_provider
return final_color
end
# Get color for a specific value (reused from Animate_palette.set_value)
# Rebuild the color lookup table (129 entries covering 0-255 range)
#
# @param value: int/float - Value to map to a color
# LUT Design:
# - Entries: 0, 2, 4, 6, ..., 254, 255 (129 entries = 516 bytes)
# - Covers full 0-255 range with 2-step resolution (ignoring LSB)
# - Final entry at index 128 stores color for value 255
# - Colors stored at MAXIMUM brightness (255) - actual brightness applied after lookup
#
# Why 2-step resolution?
# - Reduces memory from 1KB (256 entries) to 516 bytes (129 entries)
# - Visual quality: 2-step resolution is imperceptible in color gradients
# - Performance: Still provides ~5-10x speedup over full interpolation
#
# Why maximum brightness in LUT?
# - Allows brightness to change dynamically without invalidating LUT
# - Actual brightness scaling applied in get_color_for_value() after lookup
# - Critical for animations where brightness changes over time
#
# Storage format:
# - Uses bytes.set(offset, color, 4) for efficient 32-bit ARGB storage
# - Little-endian format (native Berry integer representation)
def _rebuild_color_lut()
# Ensure palette arrays are initialized
if self.value_arr == nil
self._recompute_palette()
end
# Allocate LUT if needed (129 entries * 4 bytes = 516 bytes)
if self.color_lut == nil
self.color_lut = bytes()
self.color_lut.resize(129 * 4)
end
# Pre-compute colors for values 0, 2, 4, ..., 254 at max brightness
var i = 0
while i < 128
var value = i * 2
var color = self._get_color_for_value_uncached(value, 0)
# Store color using efficient bytes.set()
self.color_lut.set(i * 4, color, 4)
i += 1
end
# Add final entry for value 255 at max brightness
var color_255 = self._get_color_for_value_uncached(255, 0)
self.color_lut.set(128 * 4, color_255, 4)
self.lut_dirty = false
end
# Get color for a specific value WITHOUT using cache (internal method)
# This is the original implementation moved to a separate method
#
# @param value: int/float - Value to map to a color (0-255 range)
# @param time_ms: int - Current time in milliseconds (ignored for value-based color)
# @return int - Color in ARGB format
def get_color_for_value(value, time_ms)
def _get_color_for_value_uncached(value, time_ms)
if (self.slots_arr == nil) && (self.value_arr == nil)
self._recompute_palette()
end
var palette_bytes = self._get_palette_bytes()
var range_min = self.range_min
var range_max = self.range_max
var brightness = self.brightness
if range_min == nil || range_max == nil return nil end
# Find slot (exact algorithm from Animate_palette.set_value)
var slots = self.slots
var idx = slots - 2
@ -336,15 +428,67 @@ class RichPaletteColorProvider : animation.color_provider
var g = self._interpolate(value, t0, t1, (bgrt0 >> 16) & 0xFF, (bgrt1 >> 16) & 0xFF)
var b = self._interpolate(value, t0, t1, (bgrt0 >> 24) & 0xFF, (bgrt1 >> 24) & 0xFF)
# Apply brightness scaling (from Animate_palette)
# Create final color in ARGB format
return (0xFF << 24) | (r << 16) | (g << 8) | b
end
# Get color for a specific value using LUT cache for performance
#
# This is the optimized version that uses the LUT cache instead of
# performing expensive palette interpolation on every call.
#
# Performance characteristics:
# - LUT lookup: ~10-15 CPU cycles (bit shift + bytes.get())
# - Original interpolation: ~50-100 CPU cycles (search + interpolate + brightness)
# - Speedup: ~5-10x faster
#
# LUT mapping:
# - Values 0-254: lut_index = value >> 1 (divide by 2, ignore LSB)
# - Value 255: lut_index = 128 (special case for exact 255)
#
# Brightness handling:
# - LUT stores colors at maximum brightness (255)
# - Actual brightness scaling applied here after lookup
# - This allows brightness to change dynamically without invalidating LUT
#
# @param value: int/float - Value to map to a color (0-255 range)
# @param time_ms: int - Current time in milliseconds (ignored for value-based color)
# @return int - Color in ARGB format
def get_color_for_value(value, time_ms)
# Clamp value to 0-255 range
# if value < 0 value = 0 end
# if value > 255 value = 255 end
# Map value to LUT index
# For values 0-254: index = value / 2 (integer division)
# For value 255: index = 128
var lut_index = value >> 1 # Divide by 2 using bit shift
if value >= 255
lut_index = 128
end
# Retrieve color from LUT using efficient bytes.get()
# This color is at maximum brightness (255)
var color = self.color_lut.get(lut_index * 4, 4)
# Apply brightness scaling if not at maximum
var brightness = self._brightness
if brightness != 255
# Extract RGB components
var r = (color >> 16) & 0xFF
var g = (color >> 8) & 0xFF
var b = color & 0xFF
# Scale each component by brightness
r = tasmota.scale_uint(r, 0, 255, 0, brightness)
g = tasmota.scale_uint(g, 0, 255, 0, brightness)
b = tasmota.scale_uint(b, 0, 255, 0, brightness)
# Reconstruct color with scaled brightness
color = (0xFF << 24) | (r << 16) | (g << 8) | b
end
# Create final color in ARGB format
return (0xFF << 24) | (r << 16) | (g << 8) | b
return color
end
# Generate CSS linear gradient (reused from Animate_palette.to_css_gradient)

View File

@ -38,11 +38,21 @@ class StaticValueProvider : animation.value_provider
end
def ==(other)
return self.value == int(other)
if type(other) == 'instance'
import introspect
return introspect.toptr(self) == introspect.toptr(other)
else
return self.value == int(other)
end
end
def !=(other)
return self.value != int(other)
if type(other) == 'instance'
import introspect
return introspect.toptr(self) != introspect.toptr(other)
else
return self.value != int(other)
end
end
# Produce the static value for any parameter name

View File

@ -19,12 +19,12 @@ class StripLengthProvider : animation.value_provider
# @param time_ms: int - Current time in milliseconds (ignored)
# @return int - The strip length in pixels
def produce_value(name, time_ms)
return (self.engine != nil) ? self.engine.get_strip_length() : 0
return self.engine.strip_length
end
# String representation of the provider
def tostring()
var strip_width = (self.engine != nil) ? self.engine.get_strip_length() : 'unknown'
var strip_width = (self.engine != nil) ? self.engine.strip_length : 'unknown'
return f"StripLengthProvider(length={strip_width})"
end
end

View File

@ -16,17 +16,6 @@ import "./core/param_encoder" as encode_constraints
#@ solidify:ValueProvider,weak
class ValueProvider : animation.parameterized_object
# Static parameter definitions - can be overridden by subclasses
static var PARAMS = animation.enc_params({
})
# Initialize the value provider
#
# @param engine: AnimationEngine - Reference to the animation engine (required)
def init(engine)
super(self).init(engine) # Initialize parameter system
end
# Produce a value for a specific parameter name and time
# This is the main method that subclasses should override
@ -46,6 +35,16 @@ class ValueProvider : animation.parameterized_object
def produce_value(name, time_ms)
return module("undefined") # Default behavior - return undefined
end
# Update object state based on current time
# Subclasses must override this to implement their update logic
#
# @param time_ms: int - Current time in milliseconds
# @return bool - True if object is still running, false if completed
def update(time_ms)
# Default implementation just returns running state
return self.is_running
end
end
# Add a method to check if an object is a value provider

File diff suppressed because it is too large Load Diff

View File

@ -33,7 +33,7 @@ var strip = global.Leds(20)
var engine = animation.create_engine(strip)
assert_not_nil(engine, "Engine should be created")
assert_equals(engine.width, 20, "Engine width should match strip length")
assert_equals(engine.strip_length, 20, "Engine strip_length should match strip length")
assert_equals(engine.is_active(), false, "Engine should start inactive")
assert_equals(engine.size(), 0, "Engine should start with no animations")
@ -165,11 +165,11 @@ end
print("\n--- Test 9: Engine API Consistency ---")
var engine2 = animation.create_engine(strip)
assert_not_nil(engine2, "Second engine should be created")
assert_equals(engine2.width, strip.length(), "Second engine width should match strip")
assert_equals(engine2.strip_length, strip.length(), "Second engine strip_length should match strip")
var engine3 = animation.create_engine(strip)
assert_not_nil(engine3, "Direct engine creation should work")
assert_equals(engine3.width, strip.length(), "Direct engine width should match strip")
assert_equals(engine3.strip_length, strip.length(), "Direct engine strip_length should match strip")
# Test 10: Dynamic Strip Length Detection
print("\n--- Test 10: Dynamic Strip Length Detection ---")
@ -224,7 +224,7 @@ var dynamic_strip = MockDynamicStrip(15)
var dynamic_engine = animation.create_engine(dynamic_strip)
# Test initial state
assert_equals(dynamic_engine.width, 15, "Engine should start with strip length 15")
assert_equals(dynamic_engine.strip_length, 15, "Engine should start with strip length 15")
assert_equals(dynamic_engine.frame_buffer.width, 15, "Frame buffer should match initial length")
assert_equals(dynamic_engine.temp_buffer.width, 15, "Temp buffer should match initial length")
@ -236,14 +236,14 @@ var original_temp_buffer = dynamic_engine.temp_buffer
print("\n--- Test 10a: No change detection ---")
var length_changed = dynamic_engine.check_strip_length()
assert_test(!length_changed, "Should detect no change when length is same")
assert_equals(dynamic_engine.width, 15, "Engine width should remain 15")
assert_equals(dynamic_engine.strip_length, 15, "Engine strip_length should remain 15")
# Test 10b: Manual length change detection
print("\n--- Test 10b: Manual length change detection ---")
dynamic_strip.set_length(25)
length_changed = dynamic_engine.check_strip_length()
assert_test(length_changed, "Should detect length change from 15 to 25")
assert_equals(dynamic_engine.width, 25, "Engine width should update to 25")
assert_equals(dynamic_engine.strip_length, 25, "Engine strip_length should update to 25")
assert_equals(dynamic_engine.frame_buffer.width, 25, "Frame buffer should resize to 25")
assert_equals(dynamic_engine.temp_buffer.width, 25, "Temp buffer should resize to 25")
@ -268,7 +268,7 @@ var tick_time = tasmota.millis()
for i : 0..2
dynamic_engine.on_tick(tick_time + i * 10)
end
assert_equals(dynamic_engine.width, 25, "Width should remain stable during normal ticks")
assert_equals(dynamic_engine.strip_length, 25, "Width should remain stable during normal ticks")
# Change strip length during runtime
dynamic_strip.set_length(35)
@ -276,7 +276,7 @@ var old_show_calls = dynamic_strip.show_calls
# Next tick should detect the change automatically
dynamic_engine.on_tick(tick_time + 50)
assert_equals(dynamic_engine.width, 35, "Engine should detect length change during on_tick()")
assert_equals(dynamic_engine.strip_length, 35, "Engine should detect length change during on_tick()")
assert_equals(dynamic_engine.frame_buffer.width, 35, "Frame buffer should resize during on_tick()")
assert_equals(dynamic_engine.temp_buffer.width, 35, "Temp buffer should resize during on_tick()")
@ -290,7 +290,7 @@ var lengths_to_test = [10, 50, 5, 30]
for new_length : lengths_to_test
dynamic_strip.set_length(new_length)
dynamic_engine.on_tick(tasmota.millis())
assert_equals(dynamic_engine.width, new_length, f"Engine should adapt to length {new_length}")
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}")
end
@ -317,29 +317,29 @@ dynamic_strip.set_length(40)
old_show_calls = dynamic_strip.show_calls
dynamic_engine.on_tick(tasmota.millis())
assert_equals(dynamic_engine.width, 40, "Engine should handle length change with multiple animations")
assert_equals(dynamic_engine.strip_length, 40, "Engine should handle length change with multiple animations")
new_show_calls = dynamic_strip.show_calls
assert_test(new_show_calls >= old_show_calls, "Rendering should continue with multiple animations (or at least not decrease)")
assert_equals(dynamic_engine.size(), 2, "Should still have 2 animations after length change")
# Test 10f: Invalid length handling
print("\n--- Test 10f: Invalid length handling ---")
var current_width = dynamic_engine.width
var current_width = dynamic_engine.strip_length
# Test zero length (should be ignored)
dynamic_strip.set_length(0)
dynamic_engine.on_tick(tasmota.millis())
assert_equals(dynamic_engine.width, current_width, "Should ignore zero length")
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())
assert_equals(dynamic_engine.width, current_width, "Should ignore negative length")
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())
assert_equals(dynamic_engine.width, 20, "Should accept valid length after invalid ones")
assert_equals(dynamic_engine.strip_length, 20, "Should accept valid length after invalid ones")
# Test 10g: Performance impact of length checking
print("\n--- Test 10g: Performance impact of length checking ---")

View File

@ -129,8 +129,8 @@ assert(param_anim.set_param("priority", -1) == false, "Value below min should be
assert(param_anim.get_param("unknown", "default") == "default", "Unknown parameter should return default")
assert(param_anim.get_param("priority", 0) == 75, "Known parameter should return current value")
# Test parameter definition using _has_param and _get_param_def
assert(param_anim._has_param("priority") == true, "Should have priority parameter")
# Test parameter definition using has_param and _get_param_def
assert(param_anim.has_param("priority") == true, "Should have priority parameter")
var param_def = param_anim._get_param_def("priority")
assert(param_def != nil, "Parameter definition should exist for static parameter")
# Use static methods to access encoded constraint data

View File

@ -83,12 +83,12 @@ def test_bytes_type_validation()
assert(success == false, "Method setting with invalid type should fail")
# Test 5: Parameter definition
assert(obj._has_param("data") == true, "data parameter should exist")
assert(obj.has_param("data") == true, "data parameter should exist")
var param_def = obj._get_param_def("data")
assert(obj.constraint_find(param_def, "type", nil) == "bytes", "Data parameter should have bytes type")
assert(obj.constraint_mask(param_def, "nillable") == 0x20, "Data parameter should be nillable")
assert(obj._has_param("required_data") == true, "required_data parameter should exist")
assert(obj.has_param("required_data") == true, "required_data parameter should exist")
var req_param_def = obj._get_param_def("required_data")
assert(obj.constraint_find(req_param_def, "type", nil) == "bytes", "Required data should have bytes type")
assert(obj.constraint_mask(req_param_def, "nillable") == 0x00, "Required data should not be nillable")

View File

@ -18,6 +18,10 @@ def test_closure_value_provider()
def tostring()
return ''
end
def add(obj)
return true
end
end
var engine = MockEngine()
@ -241,6 +245,11 @@ def test_closure_value_provider()
print("✓ Edge cases with zero, negative, and boundary values work")
print("All ClosureValueProvider tests passed!")
# Fake add() method for value provider auto-registration
def add(obj)
return true
end
end
# Test mathematical helper methods
@ -253,6 +262,10 @@ def test_closure_math_methods()
def init()
self.time_ms = 1000
end
def add(obj)
return true
end
end
var engine = MockEngine()

View File

@ -10,6 +10,11 @@ class MockEngine
def init()
self.time_ms = 1000
end
# Fake add() method for value provider auto-registration
def add(obj)
return true
end
end
def test_color_cycle_bytes_format()
@ -90,13 +95,13 @@ def test_color_cycle_bytes_format()
assert(next_color == custom_color2, f"Next should move to third color")
assert(provider.current_index == 2, f"Current index should be 2")
# Test 9: Test value-based color selection
# Test 9: Test value-based color selection (0-255 range)
var value_color_0 = provider.get_color_for_value(0, 0) # Should be first color
var value_color_50 = provider.get_color_for_value(50, 0) # Should be middle color
var value_color_100 = provider.get_color_for_value(100, 0) # Should be last color
var value_color_128 = provider.get_color_for_value(128, 0) # Should be middle color
var value_color_255 = provider.get_color_for_value(255, 0) # Should be last color
assert(value_color_0 == custom_color0, f"Value 0 should return first color")
assert(value_color_100 == custom_color3, f"Value 100 should return last color")
assert(value_color_255 == custom_color3, f"Value 255 should return last color")
# Test 10: Test edge cases
var invalid_color = provider._get_color_at_index(-1) # Invalid index

View File

@ -9,6 +9,11 @@ class MockEngine
def init()
self.time_ms = 1000
end
# Fake add() method for value provider auto-registration
def add(obj)
return true
end
end
def test_palette_size_parameter_access()
@ -168,7 +173,7 @@ def test_palette_size_parameter_metadata()
var provider = animation.color_cycle(engine)
# Test 1: Parameter should exist
assert(provider._has_param("palette_size") == true, "palette_size parameter should exist")
assert(provider.has_param("palette_size") == true, "palette_size parameter should exist")
var param_def = provider._get_param_def("palette_size")
assert(param_def != nil, "palette_size should have parameter definition")

View File

@ -50,82 +50,126 @@ assert_equals(engine.hw_time_sum, 0, "Hardware time sum should start at 0")
assert_equals(engine.last_stats_time, 0, "Last stats time should start at 0")
assert_equals(engine.stats_period, 5000, "Stats period should be 5000ms")
# Test 2: Profiling API Initialization
print("\n--- Test 2: Profiling API Initialization ---")
assert_not_nil(engine.profile_points, "Profile points map should exist")
assert_not_nil(engine.profile_start_times, "Profile start times map should exist")
assert_equals(size(engine.profile_points), 0, "Profile points should start empty")
assert_equals(size(engine.profile_start_times), 0, "Profile start times should start empty")
# Test 2: Profiling Timestamps Initialization
print("\n--- Test 2: Profiling Timestamps Initialization ---")
assert_test(engine.ts_start == nil, "ts_start should be nil initially")
assert_test(engine.ts_1 == nil, "ts_1 should be nil initially")
assert_test(engine.ts_2 == nil, "ts_2 should be nil initially")
assert_test(engine.ts_3 == nil, "ts_3 should be nil initially")
assert_test(engine.ts_hw == nil, "ts_hw should be nil initially")
assert_test(engine.ts_end == nil, "ts_end should be nil initially")
# Test 3: Basic Profiling API
print("\n--- Test 3: Basic Profiling API ---")
# Test 3: Phase Metrics Initialization
print("\n--- Test 3: Phase Metrics Initialization ---")
assert_equals(engine.phase1_time_sum, 0, "Phase 1 time sum should start at 0")
assert_equals(engine.phase1_time_min, 999999, "Phase 1 time min should start at max value")
assert_equals(engine.phase1_time_max, 0, "Phase 1 time max should start at 0")
assert_equals(engine.phase2_time_sum, 0, "Phase 2 time sum should start at 0")
assert_equals(engine.phase2_time_min, 999999, "Phase 2 time min should start at max value")
assert_equals(engine.phase2_time_max, 0, "Phase 2 time max should start at 0")
assert_equals(engine.phase3_time_sum, 0, "Phase 3 time sum should start at 0")
assert_equals(engine.phase3_time_min, 999999, "Phase 3 time min should start at max value")
assert_equals(engine.phase3_time_max, 0, "Phase 3 time max should start at 0")
# Test profile_start
engine.profile_start("test_section")
assert_equals(size(engine.profile_start_times), 1, "Should have 1 start time after profile_start")
assert_not_nil(engine.profile_start_times.find("test_section"), "Start time should be recorded")
# Test 4: Timestamps Set During Ticks
print("\n--- Test 4: Timestamps Set During Ticks ---")
# Test profile_end
engine.profile_end("test_section")
assert_equals(size(engine.profile_start_times), 0, "Start time should be cleared after profile_end")
assert_equals(size(engine.profile_points), 1, "Should have 1 profile point after profile_end")
# Create a fresh engine for timestamp testing with an animation
var ts_strip = global.Leds(20)
var ts_engine = animation.create_engine(ts_strip)
var stats = engine.profile_points.find("test_section")
assert_not_nil(stats, "Profile stats should exist")
assert_equals(stats['count'], 1, "Profile count should be 1")
assert_greater_than(stats['sum'], -1, "Profile sum should be non-negative")
assert_greater_than(stats['min'], -1, "Profile min should be non-negative")
assert_greater_than(stats['max'], -1, "Profile max should be non-negative")
# Add an animation so rendering happens
var ts_anim = animation.solid(ts_engine)
ts_anim.color = 0xFFFF0000
ts_engine.add(ts_anim)
ts_engine.run()
# Test 4: Multiple Profiling Points
print("\n--- Test 4: Multiple Profiling Points ---")
# Run a single tick
var current_time = tasmota.millis()
ts_engine.on_tick(current_time)
engine.profile_start("section_a")
# Simulate some work
var x = 0
while x < 10
x += 1
# Check that timestamps were set
assert_not_nil(ts_engine.ts_start, "ts_start should be set after tick")
assert_not_nil(ts_engine.ts_1, "ts_1 should be set after tick")
assert_not_nil(ts_engine.ts_2, "ts_2 should be set after tick")
assert_not_nil(ts_engine.ts_3, "ts_3 should be set after tick")
assert_not_nil(ts_engine.ts_hw, "ts_hw should be set after tick (with animation)")
assert_not_nil(ts_engine.ts_end, "ts_end should be set after tick")
# Check timestamp ordering (only if all timestamps are set)
if ts_engine.ts_start != nil && ts_engine.ts_1 != nil
assert_test(ts_engine.ts_start <= ts_engine.ts_1, "ts_start should be <= ts_1")
end
engine.profile_end("section_a")
engine.profile_start("section_b")
# Simulate different work
var y = 0
while y < 5
y += 1
if ts_engine.ts_1 != nil && ts_engine.ts_2 != nil
assert_test(ts_engine.ts_1 <= ts_engine.ts_2, "ts_1 should be <= ts_2")
end
if ts_engine.ts_2 != nil && ts_engine.ts_3 != nil
assert_test(ts_engine.ts_2 <= ts_engine.ts_3, "ts_2 should be <= ts_3")
end
if ts_engine.ts_3 != nil && ts_engine.ts_hw != nil
assert_test(ts_engine.ts_3 <= ts_engine.ts_hw, "ts_3 should be <= ts_hw")
end
if ts_engine.ts_hw != nil && ts_engine.ts_end != nil
assert_test(ts_engine.ts_hw <= ts_engine.ts_end, "ts_hw should be <= ts_end")
end
engine.profile_end("section_b")
assert_equals(size(engine.profile_points), 3, "Should have 3 profile points (test_section + section_a + section_b)")
assert_not_nil(engine.profile_points.find("section_a"), "section_a stats should exist")
assert_not_nil(engine.profile_points.find("section_b"), "section_b stats should exist")
ts_engine.stop()
# Test 5: Repeated Profiling
print("\n--- Test 5: Repeated Profiling ---")
# Test 5: Phase Metrics Accumulation
print("\n--- Test 5: Phase Metrics Accumulation ---")
# Profile the same section multiple times
# Create engine and run multiple ticks
var phase_strip = global.Leds(15)
var phase_engine = animation.create_engine(phase_strip)
phase_engine.run()
# Run 10 ticks
var phase_time = 0
for i : 0..9
engine.profile_start("repeated_section")
var z = 0
while z < i
z += 1
end
engine.profile_end("repeated_section")
phase_engine.on_tick(phase_time)
phase_time += 5
end
var repeated_stats = engine.profile_points.find("repeated_section")
assert_not_nil(repeated_stats, "Repeated section stats should exist")
assert_equals(repeated_stats['count'], 10, "Repeated section should have count of 10")
assert_greater_than(repeated_stats['sum'], -1, "Repeated section sum should be non-negative")
# Check that phase metrics accumulated
assert_greater_than(phase_engine.phase1_time_sum, -1, "Phase 1 time sum should be non-negative")
assert_greater_than(phase_engine.phase2_time_sum, -1, "Phase 2 time sum should be non-negative")
assert_greater_than(phase_engine.phase3_time_sum, -1, "Phase 3 time sum should be non-negative")
# Test 6: Profile End Without Start
print("\n--- Test 6: Profile End Without Start ---")
# Check min/max tracking
# assert_test(phase_engine.phase1_time_min <= phase_engine.phase1_time_max, "Phase 1 min should be <= max")
# assert_test(phase_engine.phase2_time_min <= phase_engine.phase2_time_max, "Phase 2 min should be <= max")
# assert_test(phase_engine.phase3_time_min <= phase_engine.phase3_time_max, "Phase 3 min should be <= max")
var points_before = size(engine.profile_points)
engine.profile_end("nonexistent_section")
var points_after = size(engine.profile_points)
phase_engine.stop()
assert_equals(points_after, points_before, "Profile end without start should not create new point")
# Test 6: Timestamp-Based Duration Calculation
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.run()
var dur_time = tasmota.millis()
dur_engine.on_tick(dur_time)
# Verify durations can be computed from timestamps
if dur_engine.ts_start != nil && dur_engine.ts_end != nil
var total_duration = dur_engine.ts_end - dur_engine.ts_start
assert_greater_than(total_duration, -1, "Total duration should be non-negative")
end
if dur_engine.ts_2 != nil && dur_engine.ts_3 != nil
var anim_duration = dur_engine.ts_3 - dur_engine.ts_2
assert_greater_than(anim_duration, -1, "Animation duration should be non-negative")
end
if dur_engine.ts_3 != nil && dur_engine.ts_hw != nil
var hw_duration = dur_engine.ts_hw - dur_engine.ts_3
assert_greater_than(hw_duration, -1, "Hardware duration should be non-negative")
end
dur_engine.stop()
# Test 7: CPU Metrics During Ticks
print("\n--- Test 7: CPU Metrics During Ticks ---")
@ -186,58 +230,57 @@ assert_test(reset_engine.last_stats_time > last_stats_before, "Stats should have
assert_less_than(reset_engine.tick_count, 50, "Tick count should be small after reset (< 50)")
assert_less_than(reset_engine.tick_time_sum, 100, "Tick time sum should be small after reset")
# Test 9: Profiling with Ticks
print("\n--- Test 9: Profiling with Ticks ---")
# Test 9: Metrics Consistency Across Ticks
print("\n--- Test 9: Metrics Consistency Across Ticks ---")
var profile_strip = global.Leds(25)
var profile_engine = animation.create_engine(profile_strip)
profile_engine.run()
var consistency_strip = global.Leds(25)
var consistency_engine = animation.create_engine(consistency_strip)
consistency_engine.run()
# Simulate ticks with profiling
var tick_time = 0
# Run multiple ticks and verify metrics consistency
var cons_time = 0
for i : 0..19
profile_engine.profile_start("tick_work")
# Simulate some work
var work = 0
while work < 5
work += 1
end
profile_engine.profile_end("tick_work")
profile_engine.on_tick(tick_time)
tick_time += 5
consistency_engine.on_tick(cons_time)
cons_time += 5
end
var tick_work_stats = profile_engine.profile_points.find("tick_work")
assert_not_nil(tick_work_stats, "Tick work profiling should exist")
assert_equals(tick_work_stats['count'], 20, "Should have 20 profiled sections")
# Verify tick count matches iterations
assert_equals(consistency_engine.tick_count, 20, "Tick count should match iterations")
# Test 10: Min/Max Tracking
print("\n--- Test 10: Min/Max Tracking ---")
# Verify all metrics are consistent
assert_test(consistency_engine.tick_time_sum >= consistency_engine.anim_time_sum, "Total time should be >= animation time")
assert_test(consistency_engine.tick_time_sum >= consistency_engine.hw_time_sum, "Total time should be >= hardware time")
consistency_engine.stop()
# Test 10: Min/Max Tracking for All Metrics
print("\n--- Test 10: Min/Max Tracking for All Metrics ---")
var minmax_strip = global.Leds(10)
var minmax_engine = animation.create_engine(minmax_strip)
# Profile sections with varying durations
for i : 0..4
minmax_engine.profile_start("varying_work")
# Variable amount of work
var work = 0
while work < i * 10
work += 1
end
minmax_engine.profile_end("varying_work")
# Add an animation so rendering happens
var mm_anim = animation.solid(minmax_engine)
mm_anim.color = 0xFF00FF00
minmax_engine.add(mm_anim)
minmax_engine.run()
# Run several ticks
var mm_time = 0
for i : 0..9
minmax_engine.on_tick(mm_time)
mm_time += 5
end
var varying_stats = minmax_engine.profile_points.find("varying_work")
assert_not_nil(varying_stats, "Varying work stats should exist")
assert_test(varying_stats['min'] <= varying_stats['max'], "Min should be <= max")
assert_test(varying_stats['min'] >= 0, "Min should be non-negative")
assert_test(varying_stats['max'] >= 0, "Max should be non-negative")
# Verify min/max relationships for all metrics
assert_test(minmax_engine.tick_time_min <= minmax_engine.tick_time_max, "Tick min should be <= max")
assert_test(minmax_engine.anim_time_min <= minmax_engine.anim_time_max, "Anim min should be <= max")
assert_test(minmax_engine.hw_time_min <= minmax_engine.hw_time_max, "HW min should be <= max")
assert_test(minmax_engine.phase1_time_min <= minmax_engine.phase1_time_max, "Phase1 min should be <= max")
assert_test(minmax_engine.phase2_time_min <= minmax_engine.phase2_time_max, "Phase2 min should be <= max")
assert_test(minmax_engine.phase3_time_min <= minmax_engine.phase3_time_max, "Phase3 min should be <= max")
minmax_engine.stop()
# Test 11: Streaming Statistics Accuracy
print("\n--- Test 11: Streaming Statistics Accuracy ---")
@ -258,71 +301,84 @@ assert_test(stats_engine.tick_time_sum >= 0, "Tick time sum should be non-negati
assert_test(stats_engine.anim_time_sum >= 0, "Animation time sum should be non-negative")
assert_test(stats_engine.hw_time_sum >= 0, "Hardware time sum should be non-negative")
# Test 12: Profile Points Cleared After Stats
print("\n--- Test 12: Profile Points Cleared After Stats ---")
# Test 12: Phase Metrics Cleared After Stats
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.run()
# Add some profile points
clear_engine.profile_start("temp_section")
clear_engine.profile_end("temp_section")
# Run some ticks to accumulate phase metrics
var clear_time = 0
for i : 0..9
clear_engine.on_tick(clear_time)
clear_time += 5
end
assert_equals(size(clear_engine.profile_points), 1, "Should have 1 profile point")
# Verify phase metrics accumulated
assert_greater_than(clear_engine.phase1_time_sum, -1, "Phase metrics should accumulate")
# Simulate ticks to cross stats period
var clear_time = 0
while clear_time < 5100
clear_engine.on_tick(clear_time)
clear_time += 5
end
# Profile points should be cleared after stats are printed
assert_equals(size(clear_engine.profile_points), 0, "Profile points should be cleared after stats period")
# Phase metrics should be reset after stats period
assert_less_than(clear_engine.phase1_time_sum, 50, "Phase1 sum should be small after reset")
assert_less_than(clear_engine.phase2_time_sum, 50, "Phase2 sum should be small after reset")
assert_less_than(clear_engine.phase3_time_sum, 50, "Phase3 sum should be small after reset")
clear_engine.stop()
# Test 13: Multiple Engines Independence
print("\n--- Test 13: Multiple Engines Independence ---")
var strip1 = global.Leds(10)
var engine1 = animation.create_engine(strip1)
engine1.run()
var strip2 = global.Leds(20)
var engine2 = animation.create_engine(strip2)
engine2.run()
# Profile in engine1
engine1.profile_start("engine1_work")
engine1.profile_end("engine1_work")
# Run ticks on both engines
var e1_time = 0
var e2_time = 0
# Profile in engine2
engine2.profile_start("engine2_work")
engine2.profile_end("engine2_work")
for i : 0..4
engine1.on_tick(e1_time)
e1_time += 5
end
assert_equals(size(engine1.profile_points), 1, "Engine1 should have 1 profile point")
assert_equals(size(engine2.profile_points), 1, "Engine2 should have 1 profile point")
assert_not_nil(engine1.profile_points.find("engine1_work"), "Engine1 should have engine1_work")
assert_not_nil(engine2.profile_points.find("engine2_work"), "Engine2 should have engine2_work")
assert_test(engine1.profile_points.find("engine2_work") == nil, "Engine1 should not have engine2_work")
assert_test(engine2.profile_points.find("engine1_work") == nil, "Engine2 should not have engine1_work")
for i : 0..9
engine2.on_tick(e2_time)
e2_time += 5
end
# Test 14: Nested Profiling (Same Name)
print("\n--- Test 14: Nested Profiling (Same Name) ---")
# Verify independent tick counts
assert_equals(engine1.tick_count, 5, "Engine1 should have 5 ticks")
assert_equals(engine2.tick_count, 10, "Engine2 should have 10 ticks")
var nested_strip = global.Leds(15)
var nested_engine = animation.create_engine(nested_strip)
# Verify independent timestamps (engines maintain their own state)
assert_test(engine1.ts_start != engine2.ts_start || engine1.tick_count != engine2.tick_count, "Engines should have independent state")
# Start outer profiling
nested_engine.profile_start("nested_section")
engine1.stop()
engine2.stop()
# Start inner profiling (overwrites start time)
nested_engine.profile_start("nested_section")
# Test 14: Timestamp Nil Safety
print("\n--- Test 14: Timestamp Nil Safety ---")
# End profiling (uses most recent start time)
nested_engine.profile_end("nested_section")
var nil_strip = global.Leds(15)
var nil_engine = animation.create_engine(nil_strip)
var nested_stats = nested_engine.profile_points.find("nested_section")
assert_not_nil(nested_stats, "Nested section stats should exist")
assert_equals(nested_stats['count'], 1, "Should have 1 count (inner timing)")
# Before any ticks, timestamps should be nil
assert_test(nil_engine.ts_start == nil, "ts_start should be nil before ticks")
assert_test(nil_engine.ts_end == nil, "ts_end should be nil before ticks")
# Metrics should handle nil timestamps gracefully
assert_equals(nil_engine.tick_count, 0, "Tick count should be 0 before ticks")
assert_equals(nil_engine.tick_time_sum, 0, "Tick time sum should be 0 before ticks")
# Test 15: Performance of Metrics Collection
print("\n--- Test 15: Performance of Metrics Collection ---")
@ -331,31 +387,21 @@ var perf_strip = global.Leds(30)
var perf_engine = animation.create_engine(perf_strip)
perf_engine.run()
# Measure overhead of metrics collection
# Measure overhead of metrics collection with timestamps
var perf_start = tasmota.millis()
for i : 0..99
perf_engine.on_tick(perf_start + i * 5)
end
var perf_duration = tasmota.millis() - perf_start
assert_less_than(perf_duration, 200, f"100 ticks with metrics should be fast (took {perf_duration}ms)")
assert_less_than(perf_duration, 200, f"100 ticks with timestamp metrics should be fast (took {perf_duration}ms)")
# Measure overhead of profiling
perf_start = tasmota.millis()
for i : 0..99
perf_engine.profile_start("perf_test")
perf_engine.profile_end("perf_test")
end
var profile_duration = tasmota.millis() - perf_start
assert_less_than(profile_duration, 100, f"100 profile calls should be fast (took {profile_duration}ms)")
perf_engine.stop()
# Cleanup
engine.stop()
tick_engine.stop()
reset_engine.stop()
profile_engine.stop()
clear_engine.stop()
# Test Results
print(f"\n=== Test Results ===")
@ -372,11 +418,13 @@ else
end
print("\n=== CPU Metrics Benefits ===")
print("CPU Metrics and Profiling features:")
print("CPU Metrics and Timestamp-Based Profiling features:")
print("- Automatic performance tracking every 5 seconds")
print("- Separate animation vs hardware timing")
print("- Custom profiling API for any code section")
print("- Timestamp-based profiling (no duration storage)")
print("- Intermediate measurement points (ts_1, ts_2, ts_3)")
print("- Streaming statistics (no array storage)")
print("- Memory-efficient for ESP32 embedded systems")
print("- Helps identify performance bottlenecks")
print("- Min/max/mean timing statistics")
print("- Phase-based timing breakdown")

View File

@ -1,421 +0,0 @@
# DSL Template Parameter Validation Test
# Tests that template parameters are properly validated during DSL transpilation
#
# This test suite covers:
# 1. Template parameter name validation (duplicates, reserved keywords, color names)
# 2. Template parameter type annotation validation
# 3. Template parameter usage validation (unused parameters)
# 4. Template call argument validation
# 5. Templates with no parameters (should be allowed)
# 6. Templates with proper parameters and type annotations
# 7. Edge cases and error message validation
import animation
import animation_dsl
import string
# Test class to verify template parameter validation
class DSLTemplateValidationTest
var test_results
def init()
self.test_results = []
end
# Helper method to run a test case
def run_test(test_name, test_func)
try
test_func()
self.test_results.push(f"✓ {test_name}")
return true
except .. as e, msg
self.test_results.push(f"✗ {test_name}: {msg}")
return false
end
end
# Test valid template with proper parameters
def test_valid_template_with_parameters()
var dsl_code =
"template pulse_effect {\n" +
" param base_color type color\n" +
" param duration type time\n" +
" param intensity type number\n" +
" \n" +
" animation pulse = pulsating_animation(color=base_color, period=duration)\n" +
" pulse.opacity = intensity\n" +
" run pulse\n" +
"}\n"
var berry_code = animation_dsl.compile_dsl(dsl_code)
if berry_code == nil
raise "compilation_error", "Valid template with parameters should compile successfully"
end
# Check that the generated code contains the expected template function
if string.find(berry_code, "def pulse_effect_template(engine, base_color_, duration_, intensity_)") == -1
raise "generation_error", "Generated code should contain template function with correct parameters"
end
# Check that template is registered as user function
if string.find(berry_code, "animation.register_user_function('pulse_effect', pulse_effect_template)") == -1
raise "generation_error", "Template should be registered as user function"
end
end
# Test template with no parameters (should be allowed)
def test_template_with_no_parameters()
var dsl_code =
"template simple_effect {\n" +
" animation test = solid(color=red)\n" +
" run test\n" +
"}\n"
var berry_code = animation_dsl.compile_dsl(dsl_code)
if berry_code == nil
raise "compilation_error", "Template with no parameters should compile successfully"
end
# Check that the generated code contains the expected template function with only engine parameter
if string.find(berry_code, "def simple_effect_template(engine)") == -1
raise "generation_error", "Generated code should contain template function with only engine parameter"
end
end
# Test duplicate parameter names
def test_duplicate_parameter_names()
var dsl_code =
"template bad_template {\n" +
" param my_color type color\n" +
" param my_color type number\n" +
" \n" +
" animation test = solid(color=red)\n" +
" run test\n" +
"}\n"
var compilation_failed = false
var error_message = ""
try
var berry_code = animation_dsl.compile_dsl(dsl_code)
if berry_code == nil
compilation_failed = true
end
except "dsl_compilation_error" as e, msg
compilation_failed = true
error_message = msg
end
if !compilation_failed
raise "validation_error", "Duplicate parameter names should cause compilation to fail"
end
# Check that the error message mentions duplicate parameter
if string.find(error_message, "Duplicate parameter name 'my_color'") == -1
raise "error_message_error", f"Error message should mention duplicate parameter, got: {error_message}"
end
end
# Test reserved keyword as parameter name
def test_reserved_keyword_parameter_name()
var dsl_code =
"template reserved_template {\n" +
" param animation type color\n" +
" \n" +
" animation test = solid(color=red)\n" +
" run test\n" +
"}\n"
var compilation_failed = false
var error_message = ""
try
var berry_code = animation_dsl.compile_dsl(dsl_code)
if berry_code == nil
compilation_failed = true
end
except "dsl_compilation_error" as e, msg
compilation_failed = true
error_message = msg
end
if !compilation_failed
raise "validation_error", "Reserved keyword as parameter name should cause compilation to fail"
end
# Check that the error message mentions reserved keyword conflict
if string.find(error_message, "Parameter name 'animation' conflicts with reserved keyword") == -1
raise "error_message_error", f"Error message should mention reserved keyword conflict, got: {error_message}"
end
end
# Test built-in color name as parameter name
def test_builtin_color_parameter_name()
var dsl_code =
"template color_template {\n" +
" param red type number\n" +
" \n" +
" animation test = solid(color=blue)\n" +
" run test\n" +
"}\n"
var compilation_failed = false
var error_message = ""
try
var berry_code = animation_dsl.compile_dsl(dsl_code)
if berry_code == nil
compilation_failed = true
end
except "dsl_compilation_error" as e, msg
compilation_failed = true
error_message = msg
end
if !compilation_failed
raise "validation_error", "Built-in color name as parameter should cause compilation to fail"
end
# Check that the error message mentions color name conflict
if string.find(error_message, "Parameter name 'red' conflicts with built-in color name") == -1
raise "error_message_error", f"Error message should mention color name conflict, got: {error_message}"
end
end
# Test invalid type annotation
def test_invalid_type_annotation()
var dsl_code =
"template type_template {\n" +
" param value type invalid_type\n" +
" \n" +
" animation test = solid(color=red)\n" +
" run test\n" +
"}\n"
var compilation_failed = false
var error_message = ""
try
var berry_code = animation_dsl.compile_dsl(dsl_code)
if berry_code == nil
compilation_failed = true
end
except "dsl_compilation_error" as e, msg
compilation_failed = true
error_message = msg
end
if !compilation_failed
raise "validation_error", "Invalid type annotation should cause compilation to fail"
end
# Check that the error message mentions invalid type and shows valid types
if string.find(error_message, "Invalid parameter type 'invalid_type'") == -1
raise "error_message_error", f"Error message should mention invalid type, got: {error_message}"
end
if string.find(error_message, "Valid types are:") == -1
raise "error_message_error", f"Error message should show valid types, got: {error_message}"
end
end
# Test all valid type annotations
def test_valid_type_annotations()
var dsl_code =
"template all_types_template {\n" +
" param my_color type color\n" +
" param my_number type number\n" +
" param my_time type time\n" +
" \n" +
" animation test = pulsating_animation(color=my_color, period=my_time)\n" +
" test.opacity = my_number\n" +
" run test\n" +
"}\n"
var berry_code = animation_dsl.compile_dsl(dsl_code)
if berry_code == nil
raise "compilation_error", "Template with all valid type annotations should compile successfully"
end
# Check that the main parameters are included in function signature
if string.find(berry_code, "def all_types_template_template(engine, my_color_, my_number_, my_time_)") == -1
raise "generation_error", "Generated function should include the used parameters"
end
end
# Test unused parameter warning
def test_unused_parameter_warning()
var dsl_code =
"template unused_template {\n" +
" param used_color type color\n" +
" param unused_param type number\n" +
" \n" +
" animation test = solid(color=used_color)\n" +
" run test\n" +
"}\n"
var berry_code = animation_dsl.compile_dsl(dsl_code)
if berry_code == nil
raise "compilation_error", "Template with unused parameter should compile successfully (warnings don't prevent compilation)"
end
# Check that the generated code contains the warning as a comment
if string.find(berry_code, "# Line") == -1 || string.find(berry_code, "unused_param") == -1
raise "warning_error", f"Generated code should contain warning about unused parameter as comment, got: {berry_code}"
end
# Check that the template function is still generated correctly
if string.find(berry_code, "def unused_template_template(engine, used_color_, unused_param_)") == -1
raise "generation_error", "Template function should be generated with all parameters even if some are unused"
end
end
# Test template with mixed typed and untyped parameters
def test_mixed_typed_untyped_parameters()
var dsl_code =
"template mixed_template {\n" +
" param typed_color type color\n" +
" param untyped_param\n" +
" param typed_number type number\n" +
" \n" +
" animation test = solid(color=typed_color)\n" +
" test.opacity = typed_number\n" +
" test.priority = untyped_param\n" +
" run test\n" +
"}\n"
var berry_code = animation_dsl.compile_dsl(dsl_code)
if berry_code == nil
raise "compilation_error", "Template with mixed typed/untyped parameters should compile successfully"
end
# Check that function signature includes all parameters
if string.find(berry_code, "def mixed_template_template(engine, typed_color_, untyped_param_, typed_number_)") == -1
raise "generation_error", "Generated function should include all parameters in correct order"
end
end
# Test template parameter validation with edge case names
def test_edge_case_parameter_names()
var dsl_code =
"template edge_template {\n" +
" param _valid_name type color\n" +
" param valid123 type number\n" +
" \n" +
" animation test = solid(color=_valid_name)\n" +
" test.opacity = valid123\n" +
" run test\n" +
"}\n"
var berry_code = animation_dsl.compile_dsl(dsl_code)
if berry_code == nil
raise "compilation_error", "Template with edge case parameter names should compile successfully"
end
# Check that function signature includes the used parameters
if string.find(berry_code, "def edge_template_template(engine, _valid_name_, valid123_)") == -1
raise "generation_error", "Generated function should handle edge case parameter names correctly"
end
end
# Test template with complex body using parameters
def test_complex_template_body()
var dsl_code =
"template complex_template {\n" +
" param base_color type color\n" +
" param speed type time\n" +
" param intensity type number\n" +
" \n" +
" color dynamic_color = color_cycle(palette=[base_color, white], cycle_period=speed)\n" +
" animation main = pulsating_animation(color=dynamic_color, period=speed)\n" +
" main.opacity = intensity\n" +
" main.priority = 10\n" +
" \n" +
" animation background = solid(color=black)\n" +
" background.priority = 1\n" +
" \n" +
" run background\n" +
" run main\n" +
"}\n"
var berry_code = animation_dsl.compile_dsl(dsl_code)
if berry_code == nil
raise "compilation_error", "Template with complex body should compile successfully"
end
# Check that all parameters are used in the generated code
if string.find(berry_code, "base_color_") == -1 ||
string.find(berry_code, "speed_") == -1 ||
string.find(berry_code, "intensity_") == -1
raise "generation_error", "All parameters should be used in generated template body"
end
# Check that template creates multiple animations
if string.find(berry_code, "engine.add(background_)") == -1 ||
string.find(berry_code, "engine.add(main_)") == -1
raise "generation_error", "Template should add all animations to engine"
end
end
# Run all tests
def run_all_tests()
print("Running DSL Template Parameter Validation Tests...")
var total_tests = 0
var passed_tests = 0
# Test cases
var tests = [
["Valid Template with Parameters", / -> self.test_valid_template_with_parameters()],
["Template with No Parameters", / -> self.test_template_with_no_parameters()],
["Duplicate Parameter Names", / -> self.test_duplicate_parameter_names()],
["Reserved Keyword Parameter Name", / -> self.test_reserved_keyword_parameter_name()],
["Built-in Color Parameter Name", / -> self.test_builtin_color_parameter_name()],
["Invalid Type Annotation", / -> self.test_invalid_type_annotation()],
["Valid Type Annotations", / -> self.test_valid_type_annotations()],
["Unused Parameter Warning", / -> self.test_unused_parameter_warning()],
["Mixed Typed/Untyped Parameters", / -> self.test_mixed_typed_untyped_parameters()],
["Edge Case Parameter Names", / -> self.test_edge_case_parameter_names()],
["Complex Template Body", / -> self.test_complex_template_body()]
]
for test : tests
total_tests += 1
if self.run_test(test[0], test[1])
passed_tests += 1
end
end
# Print results
print(f"\nTest Results:")
for result : self.test_results
print(f" {result}")
end
print(f"\nSummary: {passed_tests}/{total_tests} tests passed")
if passed_tests == total_tests
print("✓ All DSL template parameter validation tests passed!")
return true
else
print("✗ Some DSL template parameter validation tests failed!")
raise "test_failed"
end
end
end
# Run tests
var test_runner = DSLTemplateValidationTest()
test_runner.run_all_tests()
# Export for use in other test files
return {
"DSLTemplateValidationTest": DSLTemplateValidationTest
}

View File

@ -1130,141 +1130,6 @@ def test_invalid_sequence_commands()
return true
end
# Test template-only transpilation
def test_template_only_transpilation()
print("Testing template-only transpilation...")
# Test single template definition
var single_template_dsl = "template pulse_effect {\n" +
" param base_color type color\n" +
" param duration\n" +
" param brightness type number\n" +
" \n" +
" animation pulse = pulsating_animation(\n" +
" color=base_color\n" +
" period=duration\n" +
" )\n" +
" pulse.opacity = brightness\n" +
" run pulse\n" +
"}"
var single_code = animation_dsl.compile(single_template_dsl)
assert(single_code != nil, "Should compile single template")
# Should NOT contain engine initialization
assert(string.find(single_code, "var engine = animation.init_strip()") < 0, "Should NOT generate engine initialization for template-only file")
# Should NOT contain engine.run()
assert(string.find(single_code, "engine.run()") < 0, "Should NOT generate engine.run() for template-only file")
# Should contain template function definition
assert(string.find(single_code, "def pulse_effect_template(engine, base_color_, duration_, brightness_)") >= 0, "Should generate template function")
# Should contain function registration
assert(string.find(single_code, "animation.register_user_function('pulse_effect', pulse_effect_template)") >= 0, "Should register template function")
# Test multiple templates
var multiple_templates_dsl = "template pulse_effect {\n" +
" param base_color type color\n" +
" param duration\n" +
" \n" +
" animation pulse = pulsating_animation(\n" +
" color=base_color\n" +
" period=duration\n" +
" )\n" +
" run pulse\n" +
"}\n" +
"\n" +
"template blink_red {\n" +
" param speed\n" +
" \n" +
" animation blink = pulsating_animation(\n" +
" color=red\n" +
" period=speed\n" +
" )\n" +
" \n" +
" run blink\n" +
"}"
var multiple_code = animation_dsl.compile(multiple_templates_dsl)
assert(multiple_code != nil, "Should compile multiple templates")
# Should NOT contain engine initialization or run
assert(string.find(multiple_code, "var engine = animation.init_strip()") < 0, "Should NOT generate engine initialization for multiple templates")
assert(string.find(multiple_code, "engine.run()") < 0, "Should NOT generate engine.run() for multiple templates")
# Should contain both template functions
assert(string.find(multiple_code, "def pulse_effect_template(") >= 0, "Should generate first template function")
assert(string.find(multiple_code, "def blink_red_template(") >= 0, "Should generate second template function")
# Should contain both registrations
assert(string.find(multiple_code, "animation.register_user_function('pulse_effect'") >= 0, "Should register first template")
assert(string.find(multiple_code, "animation.register_user_function('blink_red'") >= 0, "Should register second template")
print("✓ Template-only transpilation test passed")
return true
end
# Test mixed template and DSL transpilation
def test_mixed_template_dsl_transpilation()
print("Testing mixed template and DSL transpilation...")
# Test template with regular DSL (should generate engine initialization and run)
var mixed_dsl = "template pulse_effect {\n" +
" param base_color type color\n" +
" param duration\n" +
" \n" +
" animation pulse = pulsating_animation(\n" +
" color=base_color\n" +
" period=duration\n" +
" )\n" +
" run pulse\n" +
"}\n" +
"\n" +
"color my_red = 0xFF0000\n" +
"animation test_anim = solid(color=my_red)\n" +
"run test_anim"
var mixed_code = animation_dsl.compile(mixed_dsl)
assert(mixed_code != nil, "Should compile mixed template and DSL")
# Should contain engine initialization because of non-template DSL
assert(string.find(mixed_code, "var engine = animation.init_strip()") >= 0, "Should generate engine initialization for mixed content")
# Should contain engine.run() because of run statement
assert(string.find(mixed_code, "engine.run()") >= 0, "Should generate engine.run() for mixed content")
# Should contain template function
assert(string.find(mixed_code, "def pulse_effect_template(") >= 0, "Should generate template function")
# Should contain regular DSL elements
assert(string.find(mixed_code, "var my_red_ = 0xFFFF0000") >= 0, "Should generate color definition")
assert(string.find(mixed_code, "var test_anim_ = animation.solid(engine)") >= 0, "Should generate animation definition")
# Test template with property assignment (should generate engine initialization)
var template_with_property_dsl = "template pulse_effect {\n" +
" param base_color type color\n" +
" \n" +
" animation pulse = pulsating_animation(color=base_color, period=2s)\n" +
" run pulse\n" +
"}\n" +
"\n" +
"animation test_anim = solid(color=red)\n" +
"test_anim.opacity = 128"
var property_code = animation_dsl.compile(template_with_property_dsl)
assert(property_code != nil, "Should compile template with property assignment")
# Should generate engine initialization because of property assignment
assert(string.find(property_code, "var engine = animation.init_strip()") >= 0, "Should generate engine initialization for property assignment")
# Should NOT generate engine.run() because no run statement
assert(string.find(property_code, "engine.run()") < 0, "Should NOT generate engine.run() without run statement")
print("✓ Mixed template and DSL transpilation test passed")
return true
end
# Run all tests
def run_dsl_transpiler_tests()
print("=== DSL Transpiler Test Suite ===")
@ -1292,9 +1157,7 @@ def run_dsl_transpiler_tests()
test_easing_keywords,
test_animation_type_checking,
test_color_type_checking,
test_invalid_sequence_commands,
test_template_only_transpilation,
test_mixed_template_dsl_transpilation
test_invalid_sequence_commands
]
var passed = 0

View File

@ -15,7 +15,7 @@ var engine = animation.create_engine(strip)
print("\n=== Test 1: Basic Creation ===")
var proxy = animation.engine_proxy(engine)
assert(proxy != nil, "Engine proxy should be created")
assert(isinstance(proxy, animation.playable), "Engine proxy should be a Playable")
assert(isinstance(proxy, animation.parameterized_object), "Engine proxy should be a ParameterizedObject")
assert(isinstance(proxy, animation.animation), "Engine proxy should be an Animation")
assert(proxy.is_running == false, "Engine proxy should not be running initially")
print("✓ Basic creation test passed")
@ -124,7 +124,7 @@ print("✓ Engine integration test passed")
# Test 11: Type checking
print("\n=== Test 11: Type Checking ===")
assert(isinstance(proxy, animation.playable), "Engine proxy is a Playable")
assert(isinstance(proxy, animation.parameterized_object), "Engine proxy is a ParameterizedObject")
assert(isinstance(proxy, animation.animation), "Engine proxy is an Animation")
assert(!isinstance(proxy, animation.sequence_manager), "Engine proxy is not a SequenceManager")
print("✓ Type checking test passed")

View File

@ -16,6 +16,11 @@ class MockEngine
def get_strip_length()
return 10 # Mock strip length
end
# Fake add() method for value provider auto-registration
def add(obj)
return true
end
end
var mock_engine = MockEngine()

View File

@ -116,8 +116,6 @@ fire_palette.palette = animation.PALETTE_FIRE
fire_palette.cycle_period = 5000
fire_palette.transition_type = 1 # Use sine transition (smooth)
fire_palette.brightness = 255
fire_palette.range_min = 0
fire_palette.range_max = 255
fire.color = fire_palette
print("Set back to fire palette")

View File

@ -12,6 +12,11 @@ class MockEngine
def init()
self.time_ms = 1000 # Fixed time for testing
end
# Fake add() method for value provider auto-registration
def add(obj)
return true
end
end
var mock_engine = MockEngine()

View File

@ -0,0 +1,141 @@
# Integration test for gradient animation with LUT optimization
#
# This test verifies that palette_gradient_animation works correctly
# with the LUT-optimized RichPaletteColorProvider
import animation
import animation_dsl
def log(msg)
print(msg)
end
# Create a test engine
var engine = animation.init_strip()
log("=== Gradient Animation LUT Integration Test ===")
log("")
# Create the exact scenario from the user's example
log("Test: Rainbow gradient with oscillating spatial period")
log("------------------------------------------------------")
# Define a palette of rainbow colors including white
var rainbow_with_white = bytes(
"00FC0000" # Red
"24FF8000" # Orange
"49FFFF00" # Yellow
"6E00FF00" # Green
"9200FFFF" # Cyan
"B70080FF" # Blue
"DB8000FF" # Violet
"FFCCCCCC" # White
)
# Create a rich palette color provider
var rainbow_rich_color = animation.rich_palette(engine)
rainbow_rich_color.palette = rainbow_with_white
rainbow_rich_color.cycle_period = 10000 # 10 seconds
rainbow_rich_color.transition_type = animation.SINE
# Get strip length
var strip_len = engine.get_strip_length()
log(f"Strip length: {strip_len} pixels")
# Create oscillator for spatial period
var period_osc = animation.sine_osc(engine)
period_osc.min_value = strip_len / 2
period_osc.max_value = (3 * strip_len) / 2
period_osc.duration = 5000 # 5 seconds
# Create gradient animation
var back_pattern = animation.palette_gradient_animation(engine)
back_pattern.color_source = rainbow_rich_color
back_pattern.spatial_period = strip_len # Start with full strip
back_pattern.shift_period = 0 # Static for testing
log(f"Animation created: {back_pattern}")
log(f"Color source: {rainbow_rich_color}")
log("")
# Test 1: Verify LUT is built
log("Test 1: LUT initialization")
log("---------------------------")
back_pattern.start(0)
back_pattern.update(0)
# Trigger LUT build by calling get_color_for_value
rainbow_rich_color.get_color_for_value(128, 0)
if rainbow_rich_color.color_lut != nil
log(f"✓ LUT initialized: {size(rainbow_rich_color.color_lut)} bytes")
log(f"✓ LUT dirty flag: {rainbow_rich_color.lut_dirty}")
else
log("✗ LUT not initialized!")
end
log("")
# Test 2: Verify color lookups work
log("Test 2: Color lookups")
log("---------------------")
log("Sample gradient colors (0-255 range):")
var sample_values = [0, 64, 128, 192, 255]
for value : sample_values
var color = rainbow_rich_color.get_color_for_value(value, 0)
var r = (color >> 16) & 0xFF
var g = (color >> 8) & 0xFF
var b = color & 0xFF
log(f" Value {value:3d}: RGB({r:3d}, {g:3d}, {b:3d})")
end
log("")
# Test 3: Performance test - color lookups
log("Test 3: Performance measurement")
log("-------------------------------")
var num_lookups = 10000
var start_time = tasmota.millis()
var lookup_idx = 0
while lookup_idx < num_lookups
var value = (lookup_idx * 17) % 256 # Pseudo-random values
rainbow_rich_color.get_color_for_value(value, 0)
lookup_idx += 1
end
var elapsed = tasmota.millis() - start_time
if elapsed > 0
log(f"Performed {num_lookups} lookups in {elapsed}ms")
log(f"Average: {elapsed * 1000.0 / num_lookups:.2f} microseconds per lookup")
else
log(f"Performed {num_lookups} lookups in < 1ms")
log("Performance: Too fast to measure accurately (< 0.1 microseconds per lookup)")
end
log("")
# Test 4: Verify LUT invalidation
log("Test 4: LUT invalidation")
log("------------------------")
rainbow_rich_color.lut_dirty = false
log(f"Initial lut_dirty: {rainbow_rich_color.lut_dirty}")
rainbow_rich_color.brightness = 200
log(f"After brightness change: lut_dirty = {rainbow_rich_color.lut_dirty}")
# Trigger LUT rebuild by calling get_color_for_value
rainbow_rich_color.get_color_for_value(128, 1000)
log(f"After color lookup: lut_dirty = {rainbow_rich_color.lut_dirty}")
log("")
# Test 5: Dynamic spatial period (using value provider)
log("Test 5: Dynamic spatial period")
log("------------------------------")
# Note: This would require resolving the value provider in the animation
# For now, just verify the setup works
log(f"Period oscillator: min={period_osc.min_value}, max={period_osc.max_value}")
log(f"Period at t=0: {period_osc.produce_value('value', 0)}")
log(f"Period at t=2500: {period_osc.produce_value('value', 2500)}")
log(f"Period at t=5000: {period_osc.produce_value('value', 5000)}")
log("")
log("=== All integration tests completed successfully ===")

View File

@ -12,6 +12,11 @@ class MockEngine
def init()
self.time_ms = 0
end
# Fake add() method for value provider auto-registration
def add(obj)
return true
end
end
var mock_engine = MockEngine()

View File

@ -9,13 +9,15 @@ import animation
# Create a mock engine for testing
class MockEngine
var time_ms
var strip_length
def init()
self.time_ms = 1000 # Fixed time for testing
self.strip_length = 10
end
def get_strip_length()
return 10 # Mock strip length
return self.strip_length # Mock strip length
end
end

View File

@ -14,6 +14,12 @@ class MockEngine
def init()
self.time_ms = 1000 # Fixed time for testing
end
# Fake add() method for value provider auto-registration
def add(obj)
# Do nothing - just prevent errors when value providers auto-register
return true
end
end
var mock_engine = MockEngine()
@ -210,14 +216,14 @@ def test_parameter_metadata()
var obj = TestClass(mock_engine)
# Test getting single parameter definition
assert(obj._has_param("range_param") == true, "range_param should exist")
assert(obj.has_param("range_param") == true, "range_param should exist")
var range_def = obj._get_param_def("range_param")
assert(range_def != nil, "Should get range parameter definition")
assert(obj.constraint_find(range_def, "min", nil) == 0, "Should have min constraint")
assert(obj.constraint_find(range_def, "max", nil) == 100, "Should have max constraint")
assert(obj.constraint_find(range_def, "default", nil) == 50, "Should have default value")
assert(obj._has_param("enum_param") == true, "enum_param should exist")
assert(obj.has_param("enum_param") == true, "enum_param should exist")
var enum_def = obj._get_param_def("enum_param")
assert(enum_def != nil, "Should get enum parameter definition")
assert(obj.constraint_mask(enum_def, "enum") == 0x10, "Should have enum constraint")
@ -361,16 +367,20 @@ def test_undefined_parameter_behavior()
obj.defined_param = 75
assert(obj.defined_param == 75, "Defined parameter assignment should still work")
# Test _has_param and _get_param_def for undefined parameter
# Test has_param and _get_param_def for undefined parameter
print(" Testing parameter definition for undefined parameter...")
assert(obj._has_param("undefined_param") == false, "_has_param for undefined parameter should return false")
assert(obj.has_param("undefined_param") == false, "has_param for undefined parameter should return false")
var undefined_def = obj._get_param_def("undefined_param")
assert(undefined_def == nil, "_get_param_def for undefined parameter should be nil")
# Test get_param_value for undefined parameter
print(" Testing get_param_value for undefined parameter...")
var undefined_param_value = obj.get_param_value("undefined_param", 1000)
assert(undefined_param_value == nil, "get_param_value for undefined parameter should return nil")
try
var undefined_param_value = obj.get_param_value("undefined_param", 1000)
assert(true, "get_param_value for undefined parameter should raise an exception")
except .. as e, m
# exception is ok
end
print("✓ Undefined parameter behavior test passed")
end

View File

@ -29,8 +29,6 @@ anim.palette = bytes("00FF0000" "80FFFF00" "FF0000FF") # Red to Yellow to Blue
anim.cycle_period = 3000
anim.transition_type = 1 # sine
anim.brightness = 200
anim.range_min = 0
anim.range_max = 100
# Set Animation base parameters
anim.priority = 15
@ -45,8 +43,6 @@ print(f"Palette: {bool(anim.palette)}")
print(f"Cycle period: {anim.cycle_period}")
print(f"Transition type: {anim.transition_type}")
print(f"Brightness: {anim.brightness}")
print(f"Range min: {anim.range_min}")
print(f"Range max: {anim.range_max}")
# Test Animation base parameters
print(f"Priority: {anim.priority}")

View File

@ -23,6 +23,11 @@ class MockEngine
def set_time(time)
self.time_ms = time
end
# Fake add() method for value provider auto-registration
def add(obj)
return true
end
end
var mock_engine = MockEngine()
@ -192,26 +197,23 @@ class RichPaletteAnimationTest
# Check basic properties
self.assert_equal(provider.cycle_period, 1000, "Cycle period is 1000ms")
# Test range setting and value-based colors
provider.range_min = 0
provider.range_max = 100
self.assert_equal(provider.range_min, 0, "Range min is 0")
self.assert_equal(provider.range_max, 100, "Range max is 100")
# Value-based colors now always use 0-255 range
# Test value-based color generation
# Test value-based color generation (now always 0-255 range)
provider.start()
provider.update()
print(f"{provider.slots_arr=} {provider.value_arr=}")
var color_0 = provider.get_color_for_value(0, 0)
var color_50 = provider.get_color_for_value(50, 0)
var color_100 = provider.get_color_for_value(100, 0)
var color_128 = provider.get_color_for_value(128, 0)
var color_255 = provider.get_color_for_value(255, 0)
self.assert_equal(color_0 != nil, true, "Color at value 0 is not nil")
self.assert_equal(color_50 != nil, true, "Color at value 50 is not nil")
self.assert_equal(color_100 != nil, true, "Color at value 100 is not nil")
self.assert_equal(color_128 != nil, true, "Color at value 128 is not nil")
self.assert_equal(color_255 != nil, true, "Color at value 255 is not nil")
# Colors should be different
self.assert_equal(color_0 != color_50, true, "Color at 0 differs from color at 50")
self.assert_equal(color_50 != color_100, true, "Color at 50 differs from color at 100")
self.assert_equal(color_0 != color_128, true, "Color at 0 differs from color at 128")
self.assert_equal(color_128 != color_255, true, "Color at 128 differs from color at 255")
end
def test_css_gradient()
@ -240,9 +242,8 @@ class RichPaletteAnimationTest
var provider = animation.rich_palette(mock_engine)
provider.palette = palette
provider.cycle_period = 0 # Value-based mode
provider.range_min = 0
provider.range_max = 255
provider.start()
provider.update()
# Check that cycle_period can be set to 0
self.assert_equal(provider.cycle_period, 0, "Cycle period can be set to 0")
@ -275,6 +276,7 @@ class RichPaletteAnimationTest
# Start the provider for time-based mode
provider.start(0)
provider.update(0)
# Now colors should change over time again
var time_color_0 = provider.produce_value("color", 0)
@ -351,37 +353,36 @@ class RichPaletteAnimationTest
provider.palette = palette
provider.cycle_period = 0 # Value-based mode
provider.transition_type = animation.SINE
provider.range_min = 0
provider.range_max = 100
provider.start()
provider.update()
# Get colors at different values
# Get colors at different values (now using 0-255 range)
var color_0 = provider.get_color_for_value(0, 0)
var color_25 = provider.get_color_for_value(25, 0)
var color_50 = provider.get_color_for_value(50, 0)
var color_75 = provider.get_color_for_value(75, 0)
var color_100 = provider.get_color_for_value(100, 0)
var color_64 = provider.get_color_for_value(64, 0)
var color_128 = provider.get_color_for_value(128, 0)
var color_192 = provider.get_color_for_value(192, 0)
var color_255 = provider.get_color_for_value(255, 0)
# Extract blue channel
var blue_0 = color_0 & 0xFF
var blue_25 = color_25 & 0xFF
var blue_50 = color_50 & 0xFF
var blue_75 = color_75 & 0xFF
var blue_100 = color_100 & 0xFF
var blue_64 = color_64 & 0xFF
var blue_128 = color_128 & 0xFF
var blue_192 = color_192 & 0xFF
var blue_255 = color_255 & 0xFF
# Test that we have a smooth S-curve
# Change from 0-25 should be smaller than 25-50 (ease-in)
var change_0_25 = blue_25 - blue_0
var change_25_50 = blue_50 - blue_25
self.assert_equal(change_0_25 < change_25_50, true, "Value-based SINE has ease-in")
# Change from 0-64 should be smaller than 64-128 (ease-in)
var change_0_64 = blue_64 - blue_0
var change_64_128 = blue_128 - blue_64
self.assert_equal(change_0_64 < change_64_128, true, "Value-based SINE has ease-in")
# Change from 50-75 should be larger than 75-100 (ease-out)
var change_50_75 = blue_75 - blue_50
var change_75_100 = blue_100 - blue_75
self.assert_equal(change_50_75 > change_75_100, true, "Value-based SINE has ease-out")
# Change from 128-192 should be larger than 192-255 (ease-out)
var change_128_192 = blue_192 - blue_128
var change_192_255 = blue_255 - blue_192
self.assert_equal(change_128_192 > change_192_255, true, "Value-based SINE has ease-out")
# Midpoint should be approximately 128
self.assert_approx_equal(blue_50, 128, "Value-based SINE midpoint is ~128")
self.assert_approx_equal(blue_128, 128, "Value-based SINE midpoint is ~128")
end
end

View File

@ -0,0 +1,169 @@
# Demonstration: RichPaletteColorProvider with dynamic brightness
#
# This demo shows how the LUT optimization now works correctly with
# animations that have time-varying brightness, such as breathing effects.
#
# Before the fix: LUT would include brightness, making it useless for dynamic brightness
# After the fix: LUT stores max brightness colors, actual brightness applied after lookup
import animation
import animation_dsl
def log(msg)
print(msg)
end
# Create a test engine with 30 LEDs
var engine = animation.init_strip(30)
log("=== Rich Palette Breathing Effect Demo ===")
log("")
log("This demo simulates a gradient animation with breathing brightness.")
log("The LUT cache remains valid throughout the brightness oscillation.")
log("")
# Create a rainbow palette
var rainbow_palette = bytes(
"00FF0000" # Red
"24FFA500" # Orange
"49FFFF00" # Yellow
"6E00FF00" # Green
"920000FF" # Blue
"B74B0082" # Indigo
"DBEE82EE" # Violet
"FFFF0000" # Red
)
# Create the color provider
var provider = animation.rich_palette(engine)
provider.palette = rainbow_palette
provider.cycle_period = 0 # Value-based mode for gradient
provider.brightness = 255
# Initialize the provider
provider.produce_value("color", 0)
log("Simulating breathing effect over 10 cycles...")
log("Each cycle: brightness oscillates from 50 to 255 and back")
log("")
# Simulate 10 breathing cycles
var cycles = 10
var steps_per_cycle = 20
var total_lut_rebuilds = 0
var cycle = 0
while cycle < cycles
log(f"Cycle {cycle + 1}/{cycles}:")
# Breathing up (50 -> 255)
var step = 0
while step < steps_per_cycle / 2
var brightness = tasmota.scale_uint(step, 0, steps_per_cycle / 2 - 1, 50, 255)
# Check if LUT is dirty before setting brightness
var was_dirty = provider.lut_dirty
provider.brightness = brightness
var is_dirty = provider.lut_dirty
if is_dirty && !was_dirty
total_lut_rebuilds += 1
end
# Render a gradient across all pixels
var pixel = 0
while pixel < 30
var value = tasmota.scale_uint(pixel, 0, 29, 0, 255)
var color = provider.get_color_for_value(value, 0)
pixel += 1
end
step += 1
end
# Breathing down (255 -> 50)
step = 0
while step < steps_per_cycle / 2
var brightness = tasmota.scale_uint(step, 0, steps_per_cycle / 2 - 1, 255, 50)
# Check if LUT is dirty before setting brightness
var was_dirty = provider.lut_dirty
provider.brightness = brightness
var is_dirty = provider.lut_dirty
if is_dirty && !was_dirty
total_lut_rebuilds += 1
end
# Render a gradient across all pixels
var pixel = 0
while pixel < 30
var value = tasmota.scale_uint(pixel, 0, 29, 0, 255)
var color = provider.get_color_for_value(value, 0)
pixel += 1
end
step += 1
end
log(f" Brightness range: 50-255-50, LUT rebuilds so far: {total_lut_rebuilds}")
cycle += 1
end
log("")
log("=== Results ===")
log(f"Total breathing cycles: {cycles}")
log(f"Total brightness changes: {cycles * steps_per_cycle}")
log(f"Total LUT rebuilds: {total_lut_rebuilds}")
log("")
if total_lut_rebuilds == 0
log("✓ SUCCESS: LUT was never rebuilt during brightness changes!")
log(" This confirms the fix is working correctly.")
else
log("✗ FAILURE: LUT was rebuilt {total_lut_rebuilds} times")
log(" The fix may not be working as expected.")
end
log("")
# Performance comparison
log("=== Performance Comparison ===")
log("")
# Test with dynamic brightness (current implementation)
var start_time = tasmota.millis()
var frames = 100
var frame = 0
while frame < frames
# Oscillating brightness
var brightness = tasmota.scale_uint(frame % 50, 0, 49, 50, 255)
provider.brightness = brightness
# Render gradient
var pixel = 0
while pixel < 30
var value = tasmota.scale_uint(pixel, 0, 29, 0, 255)
provider.get_color_for_value(value, 0)
pixel += 1
end
frame += 1
end
var elapsed = tasmota.millis() - start_time
log(f"Rendered {frames} frames with dynamic brightness:")
log(f" Total pixels: {frames * 30}")
log(f" Time: {elapsed}ms")
if elapsed > 0
log(f" Frame rate: {frames * 1000.0 / elapsed:.1f} FPS")
else
log(f" Frame rate: > 10000 FPS (too fast to measure)")
end
log("")
log("=== Demo Complete ===")
log("")
log("Key takeaways:")
log("1. LUT cache remains valid when brightness changes")
log("2. No performance penalty for dynamic brightness")
log("3. Breathing effects and other brightness animations work efficiently")

View File

@ -0,0 +1,179 @@
# Test for RichPaletteColorProvider dynamic brightness
#
# This test verifies that brightness can change over time without
# invalidating the LUT cache, which is critical for animations
# where brightness changes dynamically.
import animation
import animation_dsl
def log(msg)
print(msg)
end
# Create a test engine
var engine = animation.init_strip()
log("=== RichPaletteColorProvider Dynamic Brightness Test ===")
log("")
# Test 1: Verify brightness changes don't invalidate LUT
log("Test 1: Brightness changes should NOT invalidate LUT")
log("------------------------------------------------------")
# Create a simple RGB palette
var rgb_palette = bytes(
"00FF0000" # Value 0: Red
"80FFFF00" # Value 128: Yellow
"FFFFFF00" # Value 255: Yellow
)
var provider = animation.rich_palette(engine)
provider.palette = rgb_palette
provider.cycle_period = 0 # Static mode
# Initialize the provider and build LUT
provider.produce_value("color", 0)
var color_at_255 = provider.get_color_for_value(128, 0)
log(f"Initial color at value 128 with brightness 255:")
var r = (color_at_255 >> 16) & 0xFF
var g = (color_at_255 >> 8) & 0xFF
var b = color_at_255 & 0xFF
log(f" RGB({r:3d}, {g:3d}, {b:3d}) = 0x{color_at_255:08X}")
# Verify LUT is not dirty
log(f"LUT dirty after initial build: {provider.lut_dirty}")
log("")
# Change brightness multiple times and verify LUT stays valid
var brightness_values = [200, 150, 100, 50, 255]
for brightness : brightness_values
provider.brightness = brightness
log(f"Changed brightness to {brightness}")
log(f" LUT dirty: {provider.lut_dirty}")
var color = provider.get_color_for_value(128, 0)
r = (color >> 16) & 0xFF
g = (color >> 8) & 0xFF
b = color & 0xFF
log(f" Color at value 128: RGB({r:3d}, {g:3d}, {b:3d})")
# Verify brightness scaling is correct
# At value 128, we should get yellow (255, 255, 0) scaled by brightness
var expected_r = tasmota.scale_uint(255, 0, 255, 0, brightness)
var expected_g = tasmota.scale_uint(255, 0, 255, 0, brightness)
var expected_b = 0
if r == expected_r && g == expected_g && b == expected_b
log(f" ✓ Brightness scaling correct")
else
log(f" ✗ ERROR: Expected RGB({expected_r}, {expected_g}, {expected_b})")
end
log("")
end
log("")
# Test 2: Verify colors at different values with varying brightness
log("Test 2: Color accuracy at different brightness levels")
log("-------------------------------------------------------")
var test_values = [0, 64, 128, 192, 255]
var test_brightness = [255, 128, 64]
for brightness : test_brightness
provider.brightness = brightness
log(f"Brightness: {brightness}")
for value : test_values
var color = provider.get_color_for_value(value, 0)
r = (color >> 16) & 0xFF
g = (color >> 8) & 0xFF
b = color & 0xFF
log(f" Value {value:3d}: RGB({r:3d}, {g:3d}, {b:3d})")
end
log("")
end
log("")
# Test 3: Performance with dynamic brightness
log("Test 3: Performance with dynamic brightness changes")
log("----------------------------------------------------")
# Simulate an animation where brightness oscillates
var iterations = 1000
var start_time = tasmota.millis()
var i = 0
while i < iterations
# Simulate oscillating brightness (0-255)
var brightness = tasmota.scale_uint(i % 256, 0, 255, 0, 255)
provider.brightness = brightness
# Get colors for a gradient (simulate 10 pixels)
var pixel = 0
while pixel < 10
var value = tasmota.scale_uint(pixel, 0, 9, 0, 255)
provider.get_color_for_value(value, 0)
pixel += 1
end
i += 1
end
var elapsed = tasmota.millis() - start_time
var total_lookups = iterations * 10
log(f"Rendered {iterations} frames with dynamic brightness")
log(f"Total lookups: {total_lookups}")
log(f"Time: {elapsed}ms")
if elapsed > 0
log(f"Average: {elapsed * 1000.0 / total_lookups:.2f} microseconds per lookup")
log(f"Frame rate: {iterations * 1000.0 / elapsed:.1f} FPS")
else
log("Average: < 0.01 microseconds per lookup (too fast to measure)")
log("Frame rate: > 100000 FPS")
end
log("")
# Test 4: Verify LUT rebuild only happens when needed
log("Test 4: LUT rebuild verification")
log("---------------------------------")
# Create a fresh provider
var rebuild_provider = animation.rich_palette(engine)
rebuild_provider.palette = rgb_palette
rebuild_provider.cycle_period = 0
# Force initial build
rebuild_provider.get_color_for_value(128, 0)
log(f"After initial build: lut_dirty = {rebuild_provider.lut_dirty}")
# Change brightness - should NOT trigger rebuild
rebuild_provider.brightness = 100
log(f"After brightness change: lut_dirty = {rebuild_provider.lut_dirty}")
rebuild_provider.get_color_for_value(128, 0)
log(f"After lookup with new brightness: lut_dirty = {rebuild_provider.lut_dirty}")
# Change palette - SHOULD trigger rebuild
rebuild_provider.palette = bytes("00FF0000" "FFFFFF00")
log(f"After palette change: lut_dirty = {rebuild_provider.lut_dirty}")
rebuild_provider.get_color_for_value(128, 0)
log(f"After lookup with new palette: lut_dirty = {rebuild_provider.lut_dirty}")
# Change transition_type - SHOULD trigger rebuild
rebuild_provider.transition_type = animation.SINE
log(f"After transition_type change: lut_dirty = {rebuild_provider.lut_dirty}")
rebuild_provider.get_color_for_value(128, 0)
log(f"After lookup with new transition: lut_dirty = {rebuild_provider.lut_dirty}")
log("")
log("=== All tests completed successfully ===")
log("")
log("Summary:")
log("--------")
log("✓ Brightness changes do NOT invalidate LUT cache")
log("✓ Brightness scaling is applied correctly after LUT lookup")
log("✓ Performance remains optimal with dynamic brightness")
log("✓ LUT only rebuilds when palette or transition_type changes")

View File

@ -0,0 +1,193 @@
# Test for RichPaletteColorProvider LUT optimization
#
# This test verifies that the LUT cache produces correct colors
# and measures the performance improvement
import animation
import animation_dsl
def log(msg)
print(msg)
end
# Create a test engine
var engine = animation.init_strip()
log("=== RichPaletteColorProvider LUT Cache Test ===")
log("")
# Test 1: Verify LUT produces correct colors
log("Test 1: Color accuracy verification")
log("------------------------------------")
# Create a rainbow palette (format: VRGB where V=value, RGB=color)
var rainbow_palette = bytes(
"00FF0000" # Value 0: Red
"49FFFF00" # Value 73: Yellow
"92FF00FF" # Value 146: Magenta
"FFFF0000" # Value 255: Red
)
# Debug: Print palette bytes
log("Palette bytes:")
var i = 0
while i < size(rainbow_palette)
var v = rainbow_palette[i]
var r = rainbow_palette[i+1]
var g = rainbow_palette[i+2]
var b = rainbow_palette[i+3]
log(f" [{i/4}] V={v:3d} R={r:3d} G={g:3d} B={b:3d}")
i += 4
end
log("")
var provider = animation.rich_palette(engine)
provider.palette = rainbow_palette
provider.cycle_period = 0 # Static mode for testing
# Trigger initialization by calling produce_value once
# This will initialize value_arr and slots
provider.produce_value("color", 0)
# Debug: Check palette
log(f"Palette size: {size(provider.palette)} bytes")
log(f"Slots: {provider.slots}")
log("Range: 0 to 255 (fixed)")
# Force LUT rebuild
provider.lut_dirty = true
# Test key values
var test_values = [0, 2, 4, 50, 100, 150, 200, 254, 255]
log("Testing color values at key positions:")
for value : test_values
var color = provider.get_color_for_value(value, 0)
var r = (color >> 16) & 0xFF
var g = (color >> 8) & 0xFF
var b = color & 0xFF
log(f" Value {value:3d}: RGB({r:3d}, {g:3d}, {b:3d}) = 0x{color:08X}")
end
log("")
# Test 2: Verify LUT invalidation on parameter changes
log("Test 2: LUT invalidation on parameter changes")
log("----------------------------------------------")
provider.lut_dirty = false
log(f"Initial lut_dirty: {provider.lut_dirty}")
provider.brightness = 200
log(f"After brightness change: lut_dirty = {provider.lut_dirty}")
provider.lut_dirty = false
provider.transition_type = animation.SINE
log(f"After transition_type change: lut_dirty = {provider.lut_dirty}")
provider.lut_dirty = false
provider.palette = bytes("00FF0000" "FFFFFF00" "FF00FF00")
log(f"After palette change: lut_dirty = {provider.lut_dirty}")
log("")
# Test 3: Performance comparison
log("Test 3: Performance measurement")
log("-------------------------------")
# Create a fresh provider for performance testing
var perf_provider = animation.rich_palette(engine)
perf_provider.palette = rainbow_palette
perf_provider.cycle_period = 0
# Warm up the LUT
perf_provider.get_color_for_value(128, 0)
# Measure LUT-based lookups
var iterations = 10000
var start_time = tasmota.millis()
var i = 0
while i < iterations
var value = (i * 17) % 256 # Pseudo-random values
perf_provider.get_color_for_value(value, 0)
i += 1
end
var lut_time = tasmota.millis() - start_time
log(f"LUT-based: {iterations} lookups in {lut_time}ms")
log(f"Average: {lut_time * 1000 / iterations:.2f} microseconds per lookup")
log("")
# Test 4: Gradient animation scenario
log("Test 4: Gradient animation scenario (60 pixels)")
log("------------------------------------------------")
# Simulate gradient animation with 60 pixels
var strip_length = 60
var gradient_values = []
gradient_values.resize(strip_length)
# Pre-calculate gradient values
i = 0
while i < strip_length
gradient_values[i] = tasmota.scale_uint(i, 0, strip_length - 1, 0, 255)
i += 1
end
# Measure time to render one frame
start_time = tasmota.millis()
var frames = 100
var frame_idx = 0
while frame_idx < frames
i = 0
while i < strip_length
perf_provider.get_color_for_value(gradient_values[i], 0)
i += 1
end
frame_idx += 1
end
var total_time = tasmota.millis() - start_time
var lookups = frames * strip_length
log(f"Rendered {frames} frames ({lookups} lookups) in {total_time}ms")
if total_time > 0
log(f"Average: {total_time * 1000.0 / lookups:.2f} microseconds per lookup")
log(f"Frame rate: {frames * 1000.0 / total_time:.1f} FPS equivalent")
else
log("Average: < 0.01 microseconds per lookup (too fast to measure)")
log("Frame rate: > 100000 FPS (too fast to measure)")
end
log("")
# Test 5: Edge cases
log("Test 5: Edge case verification")
log("-------------------------------")
# Test boundary values
var edge_cases = [
[0, "Minimum value (0)"],
[1, "Odd value (1) - should use index 0"],
[2, "Even value (2) - exact LUT entry"],
[3, "Odd value (3) - should use index 1"],
[254, "Even value (254) - exact LUT entry"],
[255, "Maximum value (255) - special case"],
[-5, "Negative value (should clamp to 0)"],
[300, "Over-range value (should clamp to 255)"]
]
for case : edge_cases
var value = case[0]
var description = case[1]
var color = perf_provider.get_color_for_value(value, 0)
var r = (color >> 16) & 0xFF
var g = (color >> 8) & 0xFF
var b = color & 0xFF
log(f" {description}: RGB({r:3d}, {g:3d}, {b:3d})")
end
log("")
log("=== All tests completed successfully ===")

View File

@ -12,14 +12,14 @@ print("Testing StripLengthProvider...")
# Create a mock LED strip for testing
class MockStrip
var _length
var strip_length
def init(length)
self._length = length
self.strip_length = length
end
def length()
return self._length
return self.strip_length
end
def set_pixel_color(index, color)
@ -59,7 +59,7 @@ def test_basic_functionality()
assert(result == length, f"Expected {length}, got {result}")
# Test that parameter name doesn't matter
var result2 = provider.produce_value("width", 2000)
var result2 = provider.produce_value("strip_length", 2000)
assert(result2 == length, f"Expected {length}, got {result2}")
# Test that time doesn't matter
@ -133,7 +133,7 @@ def test_engine_consistency()
# Test that provider returns same value as engine properties
var provider_length = provider.produce_value("length", 0)
var engine_width = engine.width
var engine_width = engine.strip_length
var engine_strip_length = engine.get_strip_length()
assert(provider_length == engine_width, f"Provider length {provider_length} != engine width {engine_width}")

View File

@ -100,6 +100,7 @@ def run_all_tests()
# Value provider tests
"lib/libesp32/berry_animation/src/tests/core_value_provider_test.be",
"lib/libesp32/berry_animation/src/tests/value_provider_integration_test.be",
"lib/libesp32/berry_animation/src/tests/test_time_ms_requirement.be",
"lib/libesp32/berry_animation/src/tests/value_provider_test.be",
"lib/libesp32/berry_animation/src/tests/oscillator_value_provider_test.be",
@ -125,7 +126,6 @@ def run_all_tests()
"lib/libesp32/berry_animation/src/tests/palette_dsl_test.be",
"lib/libesp32/berry_animation/src/tests/dsl_parameter_validation_test.be",
"lib/libesp32/berry_animation/src/tests/dsl_value_provider_validation_test.be",
"lib/libesp32/berry_animation/src/tests/dsl_template_validation_test.be",
"lib/libesp32/berry_animation/src/tests/dsl_template_animation_test.be", # Tests template animation feature
"lib/libesp32/berry_animation/src/tests/dsl_undefined_identifier_test.be",
"lib/libesp32/berry_animation/src/tests/dsl_newline_syntax_test.be",

View File

@ -0,0 +1,167 @@
# Integration Test: Value Provider Auto-Registration
#
# This test demonstrates the practical use case where value providers
# automatically register with the engine and receive update() calls.
import animation
def test_auto_registration_with_animation()
print("=== Testing Value Provider Auto-Registration with Animation ===")
# Create engine
var strip = Leds(30)
var engine = animation.create_engine(strip)
# Create a value provider - it auto-registers with engine
var oscillator = animation.triangle(engine)
oscillator.min_value = 0
oscillator.max_value = 255
oscillator.duration = 2000
# Create an animation that uses the oscillator
var beacon = animation.beacon_animation(engine)
beacon.color = 0xFFFF0000
beacon.pos = oscillator
beacon.beacon_size = 3
# Start the animation (which starts the oscillator)
beacon.start(1000)
oscillator.start(1000)
# Start the engine
engine.run()
# Simulate a few update cycles
engine.on_tick(1000)
engine.on_tick(1100)
engine.on_tick(1200)
# The oscillator should have been updated by the engine
var value1 = oscillator.produce_value("pos", 1200)
assert(value1 != nil, "Oscillator should produce values")
print(f"✓ Oscillator producing values: {value1}")
# Stop the engine
engine.stop()
print("✓ Auto-registration integration test passed")
end
def test_multiple_providers_coordination()
print("\n=== Testing Multiple Value Providers Coordination ===")
# Create engine
var strip = Leds(30)
var engine = animation.create_engine(strip)
# Create multiple value providers - all auto-register
var position_osc = animation.triangle(engine)
position_osc.min_value = 0
position_osc.max_value = 29
position_osc.duration = 3000
var brightness_osc = animation.smooth(engine)
brightness_osc.min_value = 50
brightness_osc.max_value = 255
brightness_osc.duration = 2000
var color_cycle = animation.color_cycle(engine)
color_cycle.palette = bytes("FFFF0000" "FF00FF00" "FF0000FF")
color_cycle.cycle_period = 5000
# Create animation using all providers
var beacon = animation.beacon_animation(engine)
beacon.color = color_cycle
beacon.pos = position_osc
beacon.opacity = brightness_osc
beacon.beacon_size = 2
# Start everything
position_osc.start(1000)
brightness_osc.start(1000)
color_cycle.start(1000)
beacon.start(1000)
engine.run()
# Simulate updates
engine.on_tick(1000)
engine.on_tick(2000)
engine.on_tick(3000)
# All providers should be producing values
var pos_val = position_osc.produce_value("pos", 3000)
var bright_val = brightness_osc.produce_value("opacity", 3000)
var color_val = color_cycle.produce_value("color", 3000)
assert(pos_val != nil, "Position oscillator should produce values")
assert(bright_val != nil, "Brightness oscillator should produce values")
assert(color_val != nil, "Color cycle should produce values")
print(f"✓ All providers producing values: pos={pos_val}, brightness={bright_val}, color=0x{color_val:08X}")
engine.stop()
print("✓ Multiple providers coordination test passed")
end
def test_template_animation_scenario()
print("\n=== Testing Template Animation Scenario ===")
# Simulate what happens in a template animation
var strip = Leds(30)
var engine = animation.create_engine(strip)
# In a template animation, the engine_proxy is 'self'
# For this test, we'll use the root_animation as the proxy
var proxy = engine.root_animation
# Template creates value providers - they auto-register with engine
var sweep = animation.triangle(engine)
sweep.min_value = 0
sweep.max_value = 29
sweep.duration = 4000
# Template creates animations using the provider
var beacon1 = animation.beacon_animation(engine)
beacon1.color = 0xFFFF0000
beacon1.pos = sweep
beacon1.beacon_size = 2
var beacon2 = animation.beacon_animation(engine)
beacon2.color = 0xFF0000FF
beacon2.pos = sweep # Same provider used by multiple animations
beacon2.beacon_size = 1
# Start everything
sweep.start(1000)
beacon1.start(1000)
beacon2.start(1000)
engine.run()
# Simulate updates - the sweep provider is updated once per frame
# even though it's used by multiple animations
engine.on_tick(1000)
engine.on_tick(2000)
var sweep_val = sweep.produce_value("pos", 2000)
assert(sweep_val != nil, "Sweep should produce values")
print(f"✓ Shared provider producing values: {sweep_val}")
engine.stop()
print("✓ Template animation scenario test passed")
end
# Run all integration tests
print("=== Value Provider Integration Tests ===\n")
try
test_auto_registration_with_animation()
test_multiple_providers_coordination()
test_template_animation_scenario()
print("\n=== All Integration Tests Passed! ===")
except .. as e, msg
print(f"\nIntegration test failed: {e} - {msg}")
raise "test_failed"
end

View File

@ -159,6 +159,130 @@ def test_lifecycle_methods()
print("✓ Lifecycle methods test passed")
end
# Test value provider registration in EngineProxy
def test_value_provider_registration()
print("Testing ValueProvider registration in EngineProxy...")
# Create a mock LED strip
var strip = Leds(30)
var engine = animation.create_engine(strip)
# Get the root proxy (engine delegates to this)
var proxy = engine.root_animation
# Create a simple value provider (oscillator)
# It should auto-register with the engine (which delegates to root_animation)
var oscillator = animation.triangle(engine)
oscillator.min_value = 0
oscillator.max_value = 255
oscillator.duration = 2000
# Test: Start proxy (should NOT auto-start value provider)
var start_time = 1000
proxy.start(start_time)
assert(proxy.is_running == true, "Proxy should be running")
assert(oscillator.is_running == false, "Value provider should NOT be auto-started by proxy")
# Test: Manually start value provider and update proxy
oscillator.start(start_time)
assert(oscillator.is_running == true, "Value provider should be running after manual start")
var update_time = 2000
proxy.update(update_time)
# Value provider should have been updated
var value = oscillator.produce_value("test", update_time)
assert(value != nil, "Value provider should produce a value")
# Test: Stop proxy (should NOT auto-stop value provider)
proxy.stop()
assert(proxy.is_running == false, "Proxy should be stopped")
assert(oscillator.is_running == true, "Value provider should still be running after proxy stop")
# Manually stop the value provider
oscillator.stop()
assert(oscillator.is_running == false, "Value provider should be stopped after manual stop")
# Test: Clear proxy (should clear value providers)
proxy.clear()
assert(size(proxy.value_providers) == 0, "Proxy should have no value providers after clear")
# Test: Remove value provider
var oscillator2 = animation.triangle(engine)
proxy.add(oscillator2)
assert(size(proxy.value_providers) == 1, "Should have 1 provider after add")
var removed = proxy.remove(oscillator2)
assert(removed == true, "Value provider should be removed successfully")
assert(size(proxy.value_providers) == 0, "Proxy should have no value providers after remove")
print("✓ ValueProvider registration test passed")
end
# Test multiple value providers
def test_multiple_value_providers()
print("Testing multiple ValueProviders in EngineProxy...")
var strip = Leds(30)
var engine = animation.create_engine(strip)
var proxy = animation.engine_proxy(engine)
var osc1 = animation.triangle(engine)
var osc2 = animation.smooth(engine)
var osc3 = animation.sine_osc(engine)
proxy.add(osc1)
proxy.add(osc2)
proxy.add(osc3)
assert(size(proxy.value_providers) == 3, "Should have 3 value providers")
# Manually start all value providers (simulating what animations would do)
osc1.start(3000)
osc2.start(3000)
osc3.start(3000)
proxy.start(3000)
assert(osc1.is_running == true, "Oscillator 1 should be running")
assert(osc2.is_running == true, "Oscillator 2 should be running")
assert(osc3.is_running == true, "Oscillator 3 should be running")
proxy.update(4000)
# All should be updated (we can't directly verify, but no errors means success)
proxy.stop()
# Value providers should still be running (not auto-stopped by proxy)
assert(osc1.is_running == true, "Oscillator 1 should still be running")
assert(osc2.is_running == true, "Oscillator 2 should still be running")
assert(osc3.is_running == true, "Oscillator 3 should still be running")
# Manually stop them
osc1.stop()
osc2.stop()
osc3.stop()
print("✓ Multiple ValueProviders test passed")
end
# Test is_empty() includes value_providers
def test_is_empty_with_value_providers()
print("Testing is_empty() with ValueProviders...")
var strip = Leds(30)
var engine = animation.create_engine(strip)
var proxy = animation.engine_proxy(engine)
assert(proxy.is_empty() == true, "Proxy should be empty initially")
proxy.add(animation.triangle(engine))
assert(proxy.is_empty() == false, "Proxy should not be empty with value provider")
proxy.clear()
assert(proxy.is_empty() == true, "Proxy should be empty after clear")
print("✓ is_empty() with ValueProviders test passed")
end
# Run all tests
def run_value_provider_tests()
print("=== ValueProvider Base Class Tests ===")
@ -170,7 +294,12 @@ def run_value_provider_tests()
test_parameterized_object_integration()
test_lifecycle_methods()
print("=== All ValueProvider base class tests passed! ===")
print("\n=== ValueProvider Registration Tests ===")
test_value_provider_registration()
test_multiple_value_providers()
test_is_empty_with_value_providers()
print("\n=== All ValueProvider tests passed! ===")
return true
except .. as e, msg
print(f"Test failed: {e} - {msg}")