Berry animation fix and improve sequence (#23849)

This commit is contained in:
s-hadinger 2025-08-29 11:11:01 +02:00 committed by GitHub
parent 27a1f11c70
commit 9426df28f9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
49 changed files with 18311 additions and 15043 deletions

View File

@ -77,10 +77,11 @@ sequence rgb_show {
wait 500ms
play blue_pulse for 3s
repeat 2 times:
repeat 2 times {
play red_pulse for 1s
play green_pulse for 1s
play blue_pulse for 1s
}
}
run rgb_show

View File

@ -22,13 +22,8 @@ aurora_base_.palette = aurora_colors_ # palette
aurora_base_.cycle_period = 10000 # cycle period
aurora_base_.transition_type = animation.SINE # transition type (explicit for clarity)
aurora_base_.brightness = 180 # brightness (dimmed for aurora effect)
var demo_ = (def (engine)
var steps = []
steps.push(animation.create_play_step(animation.global('aurora_base_'), 0)) # infinite duration (no 'for' clause)
var seq_manager = animation.SequenceManager(engine)
seq_manager.start_sequence(steps)
return seq_manager
end)(engine)
var demo_ = animation.SequenceManager(engine)
.push_play_step(aurora_base_, nil) # infinite duration (no 'for' clause)
engine.add_sequence_manager(demo_)
engine.start()
@ -41,19 +36,19 @@ engine.start()
# Define aurora color palette
palette aurora_colors = [
(0, 0x000022), # Dark night sky
(64, 0x004400), # Dark green
(128, 0x00AA44), # Aurora green
(192, 0x44AA88), # Light green
(0, 0x000022) # Dark night sky
(64, 0x004400) # Dark green
(128, 0x00AA44) # Aurora green
(192, 0x44AA88) # Light green
(255, 0x88FFAA) # Bright aurora
]
# Secondary purple palette
palette aurora_purple = [
(0, 0x220022), # Dark purple
(64, 0x440044), # Medium purple
(128, 0x8800AA), # Bright purple
(192, 0xAA44CC), # Light purple
(0, 0x220022) # Dark purple
(64, 0x440044) # Medium purple
(128, 0x8800AA) # Bright purple
(192, 0xAA44CC) # Light purple
(255, 0xCCAAFF) # Pale purple
]

View File

@ -0,0 +1,80 @@
# Generated Berry code from Animation DSL
# Source: cylon_rainbow.anim
#
# This file was automatically generated by compile_all_examples.sh
# Do not edit manually - changes will be overwritten
import animation
# Cylon Rainbow
# Alternat between COSINE and TRIANGLE then shift to next color
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
var strip_len_ = animation.strip_length(engine)
var eye_duration_ = 5000 # duration for a cylon eye cycle
var eye_palette_ = bytes("FFFF0000" "FFFFFF00" "FF008000" "FFEE82EE")
var eye_color_ = animation.color_cycle(engine)
eye_color_.palette = eye_palette_
eye_color_.cycle_period = 0
var cosine_val_ = (def (engine)
var provider = animation.cosine_osc(engine)
provider.min_value = 0
provider.max_value = animation.create_closure_value(engine, def (self) return self.resolve(strip_len_) - 2 end)
provider.duration = eye_duration_
return provider
end)(engine)
var triangle_val_ = (def (engine)
var provider = animation.triangle(engine)
provider.min_value = 0
provider.max_value = animation.create_closure_value(engine, def (self) return self.resolve(strip_len_) - 2 end)
provider.duration = eye_duration_
return provider
end)(engine)
var red_eye_ = animation.beacon_animation(engine)
red_eye_.color = eye_color_ # palette that will advance when we do `eye_color.next = 1`
red_eye_.pos = cosine_val_ # oscillator for position
red_eye_.beacon_size = 3 # small 3 pixels eye
red_eye_.slew_size = 2 # with 2 pixel shading around
var cylon_eye_ = animation.SequenceManager(engine, -1)
.push_play_step(red_eye_, eye_duration_) # use COSINE movement
.push_assign_step(def (engine) red_eye_.pos = triangle_val_ end) # switch to TRIANGLE
.push_play_step(red_eye_, eye_duration_)
.push_assign_step(def (engine) red_eye_.pos = cosine_val_ end) # switch back to COSINE for next iteration
.push_assign_step(def (engine) eye_color_.next = 1 end) # advance to next color
engine.add_sequence_manager(cylon_eye_)
engine.start()
#- Original DSL source:
# Cylon Rainbow
# Alternat between COSINE and TRIANGLE then shift to next color
set strip_len = strip_length()
set eye_duration = 5s # duration for a cylon eye cycle
palette eye_palette = [ red, yellow, green, violet ]
color eye_color = color_cycle(palette=eye_palette, cycle_period=0)
set cosine_val = cosine_osc(min_value = 0, max_value = strip_len - 2, duration = eye_duration)
set triangle_val = triangle(min_value = 0, max_value = strip_len - 2, duration = eye_duration)
animation red_eye = beacon_animation(
color = eye_color # palette that will advance when we do `eye_color.next = 1`
pos = cosine_val # oscillator for position
beacon_size = 3 # small 3 pixels eye
slew_size = 2 # with 2 pixel shading around
)
sequence cylon_eye forever {
play red_eye for eye_duration # use COSINE movement
red_eye.pos = triangle_val # switch to TRIANGLE
play red_eye for eye_duration
red_eye.pos = cosine_val # switch back to COSINE for next iteration
eye_color.next = 1 # advance to next color
}
run cylon_eye
-#

View File

@ -24,20 +24,15 @@ var ocean_anim_ = animation.rich_palette_animation(engine)
ocean_anim_.palette = ocean_colors_
ocean_anim_.cycle_period = 8000
# Sequence to show both palettes
var palette_demo_ = (def (engine)
var steps = []
steps.push(animation.create_play_step(animation.global('fire_anim_'), 10000))
steps.push(animation.create_wait_step(1000))
steps.push(animation.create_play_step(animation.global('ocean_anim_'), 10000))
steps.push(animation.create_wait_step(1000))
for repeat_i : 0..2-1
steps.push(animation.create_play_step(animation.global('fire_anim_'), 3000))
steps.push(animation.create_play_step(animation.global('ocean_anim_'), 3000))
end
var seq_manager = animation.SequenceManager(engine)
seq_manager.start_sequence(steps)
return seq_manager
end)(engine)
var palette_demo_ = animation.SequenceManager(engine)
.push_play_step(fire_anim_, 10000)
.push_wait_step(1000)
.push_play_step(ocean_anim_, 10000)
.push_wait_step(1000)
.push_repeat_subsequence(animation.SequenceManager(engine, 2)
.push_play_step(fire_anim_, 3000)
.push_play_step(ocean_anim_, 3000)
)
engine.add_sequence_manager(palette_demo_)
engine.start()
@ -77,9 +72,10 @@ sequence palette_demo {
wait 1s
play ocean_anim for 10s
wait 1s
repeat 2 times:
repeat 2 times {
play fire_anim for 3s
play ocean_anim for 3s
}
}
run palette_demo

View File

@ -40,31 +40,26 @@ sunset_glow_.cycle_period = 6000
sunset_glow_.transition_type = animation.SINE
sunset_glow_.brightness = 220
# Sequence to showcase all palettes
var palette_showcase_ = (def (engine)
var steps = []
var palette_showcase_ = animation.SequenceManager(engine)
# Fire effect
steps.push(animation.create_play_step(animation.global('fire_effect_'), 8000))
steps.push(animation.create_wait_step(1000))
.push_play_step(fire_effect_, 8000)
.push_wait_step(1000)
# Ocean waves
steps.push(animation.create_play_step(animation.global('ocean_waves_'), 8000))
steps.push(animation.create_wait_step(1000))
.push_play_step(ocean_waves_, 8000)
.push_wait_step(1000)
# Aurora borealis
steps.push(animation.create_play_step(animation.global('aurora_lights_'), 8000))
steps.push(animation.create_wait_step(1000))
.push_play_step(aurora_lights_, 8000)
.push_wait_step(1000)
# Sunset
steps.push(animation.create_play_step(animation.global('sunset_glow_'), 8000))
steps.push(animation.create_wait_step(1000))
.push_play_step(sunset_glow_, 8000)
.push_wait_step(1000)
# Quick cycle through all
for repeat_i : 0..3-1
steps.push(animation.create_play_step(animation.global('fire_effect_'), 2000))
steps.push(animation.create_play_step(animation.global('ocean_waves_'), 2000))
steps.push(animation.create_play_step(animation.global('aurora_lights_'), 2000))
steps.push(animation.create_play_step(animation.global('sunset_glow_'), 2000))
end
var seq_manager = animation.SequenceManager(engine)
seq_manager.start_sequence(steps)
return seq_manager
end)(engine)
.push_repeat_subsequence(animation.SequenceManager(engine, 3)
.push_play_step(fire_effect_, 2000)
.push_play_step(ocean_waves_, 2000)
.push_play_step(aurora_lights_, 2000)
.push_play_step(sunset_glow_, 2000)
)
engine.add_sequence_manager(palette_showcase_)
engine.start()
@ -143,11 +138,12 @@ sequence palette_showcase {
wait 1s
# Quick cycle through all
repeat 3 times:
repeat 3 times {
play fire_effect for 2s
play ocean_waves for 2s
play aurora_lights for 2s
play sunset_glow for 2s
}
}
run palette_showcase

View File

@ -41,25 +41,20 @@ left_pulse_.priority = 10
center_pulse_.priority = 15 # Center has highest priority
right_pulse_.priority = 5
# Create a sequence that shows all three
var demo_ = (def (engine)
var steps = []
steps.push(animation.create_play_step(animation.global('left_pulse_'), 3000))
steps.push(animation.create_wait_step(500))
steps.push(animation.create_play_step(animation.global('center_pulse_'), 3000))
steps.push(animation.create_wait_step(500))
steps.push(animation.create_play_step(animation.global('right_pulse_'), 3000))
steps.push(animation.create_wait_step(500))
var demo_ = animation.SequenceManager(engine)
.push_play_step(left_pulse_, 3000)
.push_wait_step(500)
.push_play_step(center_pulse_, 3000)
.push_wait_step(500)
.push_play_step(right_pulse_, 3000)
.push_wait_step(500)
# Play all together for final effect
for repeat_i : 0..3-1
steps.push(animation.create_play_step(animation.global('left_pulse_'), 2000))
steps.push(animation.create_play_step(animation.global('center_pulse_'), 2000))
steps.push(animation.create_play_step(animation.global('right_pulse_'), 2000))
steps.push(animation.create_wait_step(1000))
end
var seq_manager = animation.SequenceManager(engine)
seq_manager.start_sequence(steps)
return seq_manager
end)(engine)
.push_repeat_subsequence(animation.SequenceManager(engine, -1)
.push_play_step(left_pulse_, 2000)
.push_play_step(center_pulse_, 2000)
.push_play_step(right_pulse_, 2000)
.push_wait_step(1000)
)
engine.add_sequence_manager(demo_)
engine.start()
@ -100,11 +95,12 @@ sequence demo {
wait 500ms
# Play all together for final effect
repeat 3 times:
repeat forever {
play left_pulse for 2s
play center_pulse for 2s
play right_pulse for 2s
wait 1s
}
}
run demo

View File

@ -9,12 +9,13 @@ import animation
# Rainbow Cycle - Classic WLED effect
# Smooth rainbow colors cycling across the strip
#strip length 60
# Create smooth rainbow cycle animation
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
var rainbow_palette_ = bytes("FFFF0000" "FFFF8000" "FFFFFF00" "FF00FF00" "FF0000FF" "FF8000FF" "FFFF00FF") # rainbow colors
# Create smooth rainbow cycle animation
var rainbow_cycle_ = animation.color_cycle(engine)
rainbow_cycle_.palette = [0xFFFF0000, 0xFFFF8000, 0xFFFFFF00, 0xFF00FF00, 0xFF0000FF, 0xFF8000FF, 0xFFFF00FF] # rainbow colors
rainbow_cycle_.palette = rainbow_palette_
rainbow_cycle_.cycle_period = 5000 # cycle period
var rainbow_animation_ = animation.solid(engine)
rainbow_animation_.color = rainbow_cycle_
@ -29,9 +30,11 @@ engine.start()
#strip length 60
palette rainbow_palette = [0xFF0000, 0xFF8000, 0xFFFF00, 0x00FF00, 0x0000FF, 0x8000FF, 0xFF00FF] # rainbow colors
# Create smooth rainbow cycle animation
color rainbow_cycle = color_cycle(
palette=[0xFF0000, 0xFF8000, 0xFFFF00, 0x00FF00, 0x0000FF, 0x8000FF, 0xFF00FF] # rainbow colors
palette=rainbow_palette
cycle_period=5s # cycle period
)
animation rainbow_animation = solid(color=rainbow_cycle)

View File

@ -0,0 +1,226 @@
# Generated Berry code from Animation DSL
# Source: sequence_assignments_demo.anim
#
# This file was automatically generated by compile_all_examples.sh
# Do not edit manually - changes will be overwritten
import animation
# Sequence Assignments Demo
# Demonstrates dynamic property changes within sequences
# Set up strip length and value providers
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
var strip_len_ = animation.strip_length(engine)
var triangle_val_ = (def (engine)
var provider = animation.triangle(engine)
provider.min_value = 0
provider.max_value = animation.create_closure_value(engine, def (self) return self.resolve(strip_len_) - 2 end)
provider.duration = 5000
return provider
end)(engine)
var cosine_val_ = (def (engine)
var provider = animation.cosine_osc(engine)
provider.min_value = 0
provider.max_value = animation.create_closure_value(engine, def (self) return self.resolve(strip_len_) - 2 end)
provider.duration = 5000
return provider
end)(engine)
var brightness_high_ = 255
var brightness_low_ = 64
# Create color palette and cycling color
var eye_palette_ = bytes("FFFF0000" "FFFFFF00" "FF008000" "FFEE82EE")
var eye_color_ = animation.color_cycle(engine)
eye_color_.palette = eye_palette_
eye_color_.cycle_period = 0
# Create animations
var red_eye_ = animation.beacon_animation(engine)
red_eye_.color = eye_color_
red_eye_.pos = cosine_val_
red_eye_.beacon_size = 3
red_eye_.slew_size = 2
red_eye_.priority = 10
var pulse_demo_ = animation.pulsating_animation(engine)
pulse_demo_.color = 0xFF0000FF
pulse_demo_.period = 2000
pulse_demo_.priority = 5
# Sequence 1: Cylon Eye with Position Changes
var cylon_eye_ = animation.SequenceManager(engine)
.push_play_step(red_eye_, 3000)
.push_assign_step(def (engine) red_eye_.pos = triangle_val_ end) # Change to triangle oscillator
.push_play_step(red_eye_, 3000)
.push_assign_step(def (engine) red_eye_.pos = cosine_val_ end) # Change back to cosine
.push_assign_step(def (engine) eye_color_.next = 1 end) # Advance to next color
.push_play_step(red_eye_, 2000)
# Sequence 2: Brightness Control Demo
var brightness_demo_ = animation.SequenceManager(engine)
.push_play_step(pulse_demo_, 2000)
.push_assign_step(def (engine) pulse_demo_.opacity = brightness_low_ end) # Dim the animation
.push_play_step(pulse_demo_, 2000)
.push_assign_step(def (engine) pulse_demo_.opacity = brightness_high_ end) # Brighten again
.push_play_step(pulse_demo_, 2000)
# Sequence 3: Multiple Property Changes
var multi_change_ = animation.SequenceManager(engine)
.push_play_step(pulse_demo_, 1000)
.push_assign_step(def (engine) pulse_demo_.color = 0xFFFF0000 end) # Change color
.push_assign_step(def (engine) pulse_demo_.opacity = brightness_low_ end) # And brightness
.push_play_step(pulse_demo_, 1000)
.push_assign_step(def (engine) pulse_demo_.color = 0xFF008000 end) # Change color again
.push_assign_step(def (engine) pulse_demo_.opacity = brightness_high_ end) # Full brightness
.push_play_step(pulse_demo_, 1000)
.push_assign_step(def (engine) pulse_demo_.color = 0xFF0000FF end) # Back to blue
# Sequence 4: Assignments in Repeat Blocks
var repeat_demo_ = animation.SequenceManager(engine)
.push_repeat_subsequence(animation.SequenceManager(engine, 3)
.push_play_step(red_eye_, 1000)
.push_assign_step(def (engine) red_eye_.pos = triangle_val_ end) # Change oscillator
.push_play_step(red_eye_, 1000)
.push_assign_step(def (engine) red_eye_.pos = cosine_val_ end) # Change back
.push_assign_step(def (engine) eye_color_.next = 1 end) # Next color
)
# Main demo sequence combining all examples
var main_demo_ = animation.SequenceManager(engine)
# Run cylon eye demo
.push_play_step(red_eye_, 1000)
.push_wait_step(500)
# Demonstrate position changes
.push_assign_step(def (engine) red_eye_.pos = triangle_val_ end)
.push_play_step(red_eye_, 2000)
.push_assign_step(def (engine) red_eye_.pos = cosine_val_ end)
.push_play_step(red_eye_, 2000)
# Color cycling
.push_assign_step(def (engine) eye_color_.next = 1 end)
.push_play_step(red_eye_, 1000)
.push_assign_step(def (engine) eye_color_.next = 1 end)
.push_play_step(red_eye_, 1000)
.push_wait_step(1000)
# Brightness demo with pulse
.push_play_step(pulse_demo_, 1000)
.push_assign_step(def (engine) pulse_demo_.opacity = brightness_low_ end)
.push_play_step(pulse_demo_, 1000)
.push_assign_step(def (engine) pulse_demo_.opacity = brightness_high_ end)
.push_play_step(pulse_demo_, 1000)
# Multi-property changes
.push_assign_step(def (engine) pulse_demo_.color = 0xFFFF0000 end)
.push_assign_step(def (engine) pulse_demo_.opacity = brightness_low_ end)
.push_play_step(pulse_demo_, 1000)
.push_assign_step(def (engine) pulse_demo_.color = 0xFF008000 end)
.push_assign_step(def (engine) pulse_demo_.opacity = brightness_high_ end)
.push_play_step(pulse_demo_, 1000)
# Run the main demo
engine.add_sequence_manager(main_demo_)
engine.start()
#- Original DSL source:
# Sequence Assignments Demo
# Demonstrates dynamic property changes within sequences
# Set up strip length and value providers
set strip_len = strip_length()
set triangle_val = triangle(min_value=0, max_value=strip_len - 2, duration=5s)
set cosine_val = cosine_osc(min_value=0, max_value=strip_len - 2, duration=5s)
set brightness_high = 255
set brightness_low = 64
# Create color palette and cycling color
palette eye_palette = [red, yellow, green, violet]
color eye_color = color_cycle(palette=eye_palette, cycle_period=0)
# Create animations
animation red_eye = beacon_animation(
color=eye_color
pos=cosine_val
beacon_size=3
slew_size=2
priority=10
)
animation pulse_demo = pulsating_animation(
color=blue
period=2s
priority=5
)
# Sequence 1: Cylon Eye with Position Changes
sequence cylon_eye {
play red_eye for 3s
red_eye.pos = triangle_val # Change to triangle oscillator
play red_eye for 3s
red_eye.pos = cosine_val # Change back to cosine
eye_color.next = 1 # Advance to next color
play red_eye for 2s
}
# Sequence 2: Brightness Control Demo
sequence brightness_demo {
play pulse_demo for 2s
pulse_demo.opacity = brightness_low # Dim the animation
play pulse_demo for 2s
pulse_demo.opacity = brightness_high # Brighten again
play pulse_demo for 2s
}
# Sequence 3: Multiple Property Changes
sequence multi_change {
play pulse_demo for 1s
pulse_demo.color = red # Change color
pulse_demo.opacity = brightness_low # And brightness
play pulse_demo for 1s
pulse_demo.color = green # Change color again
pulse_demo.opacity = brightness_high # Full brightness
play pulse_demo for 1s
pulse_demo.color = blue # Back to blue
}
# Sequence 4: Assignments in Repeat Blocks
sequence repeat_demo {
repeat 3 times {
play red_eye for 1s
red_eye.pos = triangle_val # Change oscillator
play red_eye for 1s
red_eye.pos = cosine_val # Change back
eye_color.next = 1 # Next color
}
}
# Main demo sequence combining all examples
sequence main_demo {
# Run cylon eye demo
play red_eye for 1s
wait 500ms
# Demonstrate position changes
red_eye.pos = triangle_val
play red_eye for 2s
red_eye.pos = cosine_val
play red_eye for 2s
# Color cycling
eye_color.next = 1
play red_eye for 1s
eye_color.next = 1
play red_eye for 1s
wait 1s
# Brightness demo with pulse
play pulse_demo for 1s
pulse_demo.opacity = brightness_low
play pulse_demo for 1s
pulse_demo.opacity = brightness_high
play pulse_demo for 1s
# Multi-property changes
pulse_demo.color = red
pulse_demo.opacity = brightness_low
play pulse_demo for 1s
pulse_demo.color = green
pulse_demo.opacity = brightness_high
play pulse_demo for 1s
}
# Run the main demo
run main_demo
-#

View File

@ -19,13 +19,8 @@ var rainbow_cycle_ = animation.rich_palette_animation(engine)
rainbow_cycle_.palette = rainbow_
rainbow_cycle_.cycle_period = 3000
# Simple sequence
var demo_ = (def (engine)
var steps = []
steps.push(animation.create_play_step(animation.global('rainbow_cycle_'), 15000))
var seq_manager = animation.SequenceManager(engine)
seq_manager.start_sequence(steps)
return seq_manager
end)(engine)
var demo_ = animation.SequenceManager(engine)
.push_play_step(rainbow_cycle_, 15000)
engine.add_sequence_manager(demo_)
engine.start()

View File

@ -0,0 +1,56 @@
# Generated Berry code from Animation DSL
# Source: swipe_rainbow.anim
#
# This file was automatically generated by compile_all_examples.sh
# Do not edit manually - changes will be overwritten
import animation
# Swipe Rainbow
# Auto-generated strip initialization (using Tasmota configuration)
var engine = animation.init_strip()
var strip_len_ = animation.strip_length(engine)
var palette_olivary_ = bytes("FFFF0000" "FFFFA500" "FFFFFF00" "FF008000" "FF0000FF" "FF4B0082" "FFEE82EE" "FFFFFFFF")
var olivary_ = animation.color_cycle(engine)
olivary_.palette = palette_olivary_
olivary_.cycle_period = 0
var swipe_animation_ = animation.solid(engine)
swipe_animation_.color = olivary_
var slide_colors_ = animation.SequenceManager(engine)
.push_play_step(swipe_animation_, 1000)
.push_assign_step(def (engine) olivary_.next = 1 end)
engine.add_sequence_manager(slide_colors_)
engine.start()
#- Original DSL source:
# Swipe Rainbow
set strip_len = strip_length()
palette palette_olivary = [
red,
orange,
yellow,
green,
blue,
indigo,
violet,
white
]
color olivary = color_cycle(palette=palette_olivary, cycle_period=0)
animation swipe_animation = solid(
color = olivary
)
sequence slide_colors {
play swipe_animation for 1s
olivary.next = 1
}
run slide_colors
-#

View File

@ -0,0 +1,30 @@
# Cylon Rainbow
# Alternat between COSINE and TRIANGLE then shift to next color
set strip_len = strip_length()
set eye_duration = 5s # duration for a cylon eye cycle
palette eye_palette = [ red, yellow, green, violet ]
color eye_color = color_cycle(palette=eye_palette, cycle_period=0)
set cosine_val = cosine_osc(min_value = 0, max_value = strip_len - 2, duration = eye_duration)
set triangle_val = triangle(min_value = 0, max_value = strip_len - 2, duration = eye_duration)
animation red_eye = beacon_animation(
color = eye_color # palette that will advance when we do `eye_color.next = 1`
pos = cosine_val # oscillator for position
beacon_size = 3 # small 3 pixels eye
slew_size = 2 # with 2 pixel shading around
)
sequence cylon_eye forever {
play red_eye for eye_duration # use COSINE movement
red_eye.pos = triangle_val # switch to TRIANGLE
play red_eye for eye_duration
red_eye.pos = cosine_val # switch back to COSINE for next iteration
eye_color.next = 1 # advance to next color
}
run cylon_eye

View File

@ -32,9 +32,10 @@ sequence palette_demo {
wait 1s
play ocean_anim for 10s
wait 1s
repeat 2 times:
repeat 2 times {
play fire_anim for 3s
play ocean_anim for 3s
}
}
run palette_demo

View File

@ -71,11 +71,12 @@ sequence palette_showcase {
wait 1s
# Quick cycle through all
repeat 3 times:
repeat 3 times {
play fire_effect for 2s
play ocean_waves for 2s
play aurora_lights for 2s
play sunset_glow for 2s
}
}
run palette_showcase

View File

@ -33,11 +33,12 @@ sequence demo {
wait 500ms
# Play all together for final effect
repeat 3 times:
repeat forever {
play left_pulse for 2s
play center_pulse for 2s
play right_pulse for 2s
wait 1s
}
}
run demo

View File

@ -3,9 +3,11 @@
#strip length 60
palette rainbow_palette = [0xFF0000, 0xFF8000, 0xFFFF00, 0x00FF00, 0x0000FF, 0x8000FF, 0xFF00FF] # rainbow colors
# Create smooth rainbow cycle animation
color rainbow_cycle = color_cycle(
palette=[0xFF0000, 0xFF8000, 0xFFFF00, 0x00FF00, 0x0000FF, 0x8000FF, 0xFF00FF] # rainbow colors
palette=rainbow_palette
cycle_period=5s # cycle period
)
animation rainbow_animation = solid(color=rainbow_cycle)

View File

@ -0,0 +1,109 @@
# Sequence Assignments Demo
# Demonstrates dynamic property changes within sequences
# Set up strip length and value providers
set strip_len = strip_length()
set triangle_val = triangle(min_value=0, max_value=strip_len - 2, duration=5s)
set cosine_val = cosine_osc(min_value=0, max_value=strip_len - 2, duration=5s)
set brightness_high = 255
set brightness_low = 64
# Create color palette and cycling color
palette eye_palette = [red, yellow, green, violet]
color eye_color = color_cycle(palette=eye_palette, cycle_period=0)
# Create animations
animation red_eye = beacon_animation(
color=eye_color
pos=cosine_val
beacon_size=3
slew_size=2
priority=10
)
animation pulse_demo = pulsating_animation(
color=blue
period=2s
priority=5
)
# Sequence 1: Cylon Eye with Position Changes
sequence cylon_eye {
play red_eye for 3s
red_eye.pos = triangle_val # Change to triangle oscillator
play red_eye for 3s
red_eye.pos = cosine_val # Change back to cosine
eye_color.next = 1 # Advance to next color
play red_eye for 2s
}
# Sequence 2: Brightness Control Demo
sequence brightness_demo {
play pulse_demo for 2s
pulse_demo.opacity = brightness_low # Dim the animation
play pulse_demo for 2s
pulse_demo.opacity = brightness_high # Brighten again
play pulse_demo for 2s
}
# Sequence 3: Multiple Property Changes
sequence multi_change {
play pulse_demo for 1s
pulse_demo.color = red # Change color
pulse_demo.opacity = brightness_low # And brightness
play pulse_demo for 1s
pulse_demo.color = green # Change color again
pulse_demo.opacity = brightness_high # Full brightness
play pulse_demo for 1s
pulse_demo.color = blue # Back to blue
}
# Sequence 4: Assignments in Repeat Blocks
sequence repeat_demo {
repeat 3 times {
play red_eye for 1s
red_eye.pos = triangle_val # Change oscillator
play red_eye for 1s
red_eye.pos = cosine_val # Change back
eye_color.next = 1 # Next color
}
}
# Main demo sequence combining all examples
sequence main_demo {
# Run cylon eye demo
play red_eye for 1s
wait 500ms
# Demonstrate position changes
red_eye.pos = triangle_val
play red_eye for 2s
red_eye.pos = cosine_val
play red_eye for 2s
# Color cycling
eye_color.next = 1
play red_eye for 1s
eye_color.next = 1
play red_eye for 1s
wait 1s
# Brightness demo with pulse
play pulse_demo for 1s
pulse_demo.opacity = brightness_low
play pulse_demo for 1s
pulse_demo.opacity = brightness_high
play pulse_demo for 1s
# Multi-property changes
pulse_demo.color = red
pulse_demo.opacity = brightness_low
play pulse_demo for 1s
pulse_demo.color = green
pulse_demo.opacity = brightness_high
play pulse_demo for 1s
}
# Run the main demo
run main_demo

View File

@ -0,0 +1,27 @@
# Swipe Rainbow
set strip_len = strip_length()
palette palette_olivary = [
red,
orange,
yellow,
green,
blue,
indigo,
violet,
white
]
color olivary = color_cycle(palette=palette_olivary, cycle_period=0)
animation swipe_animation = solid(
color = olivary
)
sequence slide_colors {
play swipe_animation for 1s
olivary.next = 1
}
run slide_colors

View File

@ -241,41 +241,35 @@ color static_accent = solid(color=accent)
### ColorCycleColorProvider
Cycles through a custom list of colors with smooth transitions. Inherits from `ColorProvider`.
Cycles through a palette of colors with brutal switching. Inherits from `ColorProvider`.
| Parameter | Type | Default | Constraints | Description |
|-----------|------|---------|-------------|-------------|
| `palette` | list | [0xFF0000FF, 0xFF00FF00, 0xFFFF0000] | - | List of colors to cycle through |
| `palette` | bytes | default palette | - | Palette bytes in AARRGGBB format |
| `cycle_period` | int | 5000 | min: 0 | Cycle time in ms (0 = manual only) |
| `transition_type` | int | 1 | enum: [0,1] | 0=linear, 1=sine/smooth |
| `next` | int | 0 | - | Write 1 to move to next color manually |
| `next` | int | 0 | - | Write 1 to move to next color manually, or any number to go forward or backwars by `n` colors |
**Modes**: Auto-cycle (`cycle_period > 0`) or Manual-only (`cycle_period = 0`)
#### Usage Examples
```berry
# RGB cycle with smooth transitions
# RGB cycle with brutal switching
color rgb_cycle = color_cycle(
palette=[red, green, blue],
cycle_period=4s,
transition_type=1
palette=bytes("FF0000FF" "FF00FF00" "FFFF0000"),
cycle_period=4s
)
# Custom warm colors
color warm_red = 0xFF4500
color warm_orange = 0xFF8C00
color warm_cycle = color_cycle(
palette=[warm_red, warm_orange, yellow],
cycle_period=3s,
transition_type=1
palette=bytes("FF4500FF" "FF8C00FF" "FFFF00"),
cycle_period=3s
)
# Mixed predefined and custom colors
# Mixed colors in AARRGGBB format
color mixed_cycle = color_cycle(
palette=[0xFF0000, green, 0x0000FF],
cycle_period=2s,
transition_type=0
palette=bytes("FFFF0000" "FF00FF00" "FF0000FF"),
cycle_period=2s
)
```
@ -382,7 +376,7 @@ color deep_breath = breathe_color(
)
# Using dynamic base color
color rainbow_cycle = color_cycle(palette=[red, green, blue], cycle_period=5s)
color rainbow_cycle = color_cycle(palette=bytes("FF0000FF" "FF00FF00" "FFFF0000"), cycle_period=5s)
color breathing_rainbow = breathe_color(
base_color=rainbow_cycle,
min_brightness=30,

View File

@ -99,6 +99,7 @@ static var PARAMS = {
- **`"int"`** - Integer values (default if not specified)
- **`"string"`** - String values
- **`"bool"`** - Boolean values (true/false)
- **`"bytes"`** - Bytes objects (validated using isinstance())
- **`"instance"`** - Object instances
- **`"any"`** - Any type (no type validation)

View File

@ -220,7 +220,7 @@ color my_white = white # Reference to predefined color
# Color providers for dynamic colors
color rainbow_cycle = color_cycle(
palette=[red, green, blue]
palette=bytes("FFFF0000" "FF00FF00" "FF0000FF")
cycle_period=5s
)
color breathing_red = breathe_color(
@ -491,7 +491,9 @@ The following user functions are available by default (see [User Functions Guide
## Sequences
Sequences orchestrate multiple animations with timing control:
Sequences orchestrate multiple animations with timing control. The DSL supports two syntaxes for sequences with repeat functionality:
### Basic Sequence Syntax
```berry
sequence demo {
@ -499,14 +501,43 @@ sequence demo {
wait 1s
play blue_animation for 2s
repeat 3 times:
repeat 3 times {
play flash_effect for 200ms
wait 300ms
}
play final_animation
}
```
### Repeat Sequence Syntax
For sequences that are primarily repeating patterns, you can use the alternative syntax:
```berry
# Option 1: Traditional syntax with repeat sub-sequence
sequence cylon_eye {
repeat forever {
play red_eye for 3s
red_eye.pos = triangle_val
play red_eye for 3s
red_eye.pos = cosine_val
eye_color.next = 1
}
}
# Option 2: Alternative syntax - sequence with repeat modifier
sequence cylon_eye repeat forever {
play red_eye for 3s
red_eye.pos = triangle_val
play red_eye for 3s
red_eye.pos = cosine_val
eye_color.next = 1
}
```
**Note**: Both syntaxes are functionally equivalent. The second syntax creates an outer sequence (runs once) containing an inner repeat sub-sequence.
### Sequence Statements
#### Play Statement
@ -514,6 +545,7 @@ sequence demo {
```berry
play animation_name # Play indefinitely
play animation_name for 5s # Play for specific duration
play animation_name for duration_var # Play for variable duration
```
#### Wait Statement
@ -521,22 +553,119 @@ play animation_name for 5s # Play for specific duration
```berry
wait 1s # Wait for 1 second
wait 500ms # Wait for 500 milliseconds
wait duration_var # Wait for variable duration
```
#### Duration Support
Both `play` and `wait` statements support flexible duration specifications:
**Literal Time Values:**
```berry
play animation for 5s # 5 seconds
play animation for 2000ms # 2000 milliseconds
play animation for 1m # 1 minute
```
**Variable References:**
```berry
set short_time = 2s
set long_time = 10s
sequence demo {
play animation for short_time # Use variable duration
wait long_time # Variables work in wait too
}
```
**Value Providers (Dynamic Duration):**
```berry
set dynamic_duration = triangle(min_value=1000, max_value=5000, period=10s)
sequence demo {
play animation for dynamic_duration # Duration changes over time
}
```
**Examples:**
```berry
# Cylon eye with variable duration
set eye_duration = 5s
sequence cylon_eye forever {
play red_eye for eye_duration # Use variable for consistent timing
red_eye.pos = triangle_val
play red_eye for eye_duration # Same duration for both phases
red_eye.pos = cosine_val
eye_color.next = 1
}
```
#### Repeat Statement
```berry
repeat 5 times:
play effect for 1s
wait 500ms
Repeat statements create runtime sub-sequences that execute repeatedly:
# Nested repeats are supported
repeat 3 times:
play intro for 2s
repeat 2 times:
play flash for 100ms
wait 200ms
play outro for 1s
```berry
repeat 3 times { # Repeat exactly 3 times
play animation for 1s
wait 500ms
}
repeat forever { # Repeat indefinitely until parent sequence stops
play animation for 1s
wait 500ms
}
```
**Repeat Behavior:**
- **Runtime Execution**: Repeats are executed at runtime, not expanded at compile time
- **Sub-sequences**: Each repeat block creates a sub-sequence that manages its own iteration state
- **Nested Repeats**: Supports nested repeats with multiplication (e.g., `repeat 3 times { repeat 2 times { ... } }` executes 6 times total)
- **Forever Loops**: `repeat forever` continues until the parent sequence is stopped
- **Efficient**: No memory overhead for large repeat counts
#### Assignment Statement
Property assignments can be performed within sequences to dynamically modify animation parameters during playback:
```berry
sequence demo {
play red_eye for 3s
red_eye.pos = triangle_val # Change position to triangle oscillator
play red_eye for 3s
red_eye.pos = cosine_val # Change position to cosine oscillator
eye_color.next = 1 # Advance color cycle to next color
}
```
**Assignment Semantics:**
- Assignments in sequences have exactly the same semantics as assignments outside sequences
- They can assign static values, value providers, or computed expressions
- Assignments are executed instantly when the sequence step is reached
- The assignment is wrapped in a closure: `def (engine) <assign_code> end`
**Examples:**
```berry
sequence dynamic_show {
play pulse_anim for 2s
pulse_anim.opacity = 128 # Set static opacity
play pulse_anim for 2s
pulse_anim.opacity = brightness # Use value provider
play pulse_anim for 2s
pulse_anim.color = next_color # Change color provider
play pulse_anim for 2s
}
# Assignments work in repeat blocks too
sequence cylon_eye {
repeat 3 times {
play red_eye for 1s
red_eye.pos = triangle_val # Change oscillator pattern
play red_eye for 1s
red_eye.pos = cosine_val # Change back
eye_color.next = 1 # Advance color
}
}
```
## Execution Statements
@ -828,13 +957,14 @@ animation_def = "animation" identifier "=" animation_expression ;
property_assignment = identifier "." identifier "=" expression ;
(* Sequences *)
sequence = "sequence" identifier "{" sequence_body "}" ;
sequence = "sequence" identifier [ "repeat" ( number "times" | "forever" ) ] "{" sequence_body "}" ;
sequence_body = { sequence_statement } ;
sequence_statement = play_stmt | wait_stmt | repeat_stmt ;
sequence_statement = play_stmt | wait_stmt | repeat_stmt | sequence_assignment ;
play_stmt = "play" identifier [ "for" time_expression ] ;
wait_stmt = "wait" time_expression ;
repeat_stmt = "repeat" number "times" ":" sequence_body ;
repeat_stmt = "repeat" ( number "times" | "forever" ) "{" sequence_body "}" ;
sequence_assignment = identifier "." identifier "=" expression ;
(* Execution *)
execution_stmt = "run" identifier ;

View File

@ -96,9 +96,98 @@ sequence sunrise_show {
run sunrise_show
```
### 8.1. Variable Duration Sequences
```berry
# Define timing variables for consistent durations
set short_duration = 2s
set long_duration = 5s
set fade_time = 1s
animation red_anim = solid(color=red)
animation green_anim = solid(color=green)
animation blue_anim = solid(color=blue)
sequence timed_show forever {
play red_anim for short_duration # Use variable duration
wait fade_time # Variable wait time
play green_anim for long_duration # Different variable duration
wait fade_time
play blue_anim for short_duration # Reuse timing variable
}
run timed_show
```
## Sequence Assignments
### 9. Dynamic Property Changes
```berry
# Create oscillators for dynamic position
set triangle_val = triangle(min_value=0, max_value=27, duration=5s)
set cosine_val = cosine_osc(min_value=0, max_value=27, duration=5s)
# Create color cycle
palette eye_palette = [red, yellow, green, violet]
color eye_color = color_cycle(palette=eye_palette, cycle_period=0)
# Create beacon animation
animation red_eye = beacon_animation(
color=eye_color
pos=cosine_val
beacon_size=3
slew_size=2
priority=10
)
# Sequence with property assignments
sequence cylon_eye {
play red_eye for 3s
red_eye.pos = triangle_val # Change to triangle oscillator
play red_eye for 3s
red_eye.pos = cosine_val # Change back to cosine
eye_color.next = 1 # Advance to next color
}
run cylon_eye
```
### 10. Multiple Assignments in Sequence
```berry
set high_brightness = 255
set low_brightness = 64
color my_blue = 0x0000FF
animation test = solid(color=red)
test.opacity = high_brightness
sequence demo {
play test for 1s
test.opacity = low_brightness # Dim the animation
test.color = my_blue # Change color to blue
play test for 1s
test.opacity = high_brightness # Brighten again
play test for 1s
}
run demo
```
### 11. Assignments in Repeat Blocks
```berry
set brightness = smooth(min_value=50, max_value=255, period=2s)
animation pulse = pulsating_animation(color=white, period=1s)
sequence breathing_cycle {
repeat 3 times {
play pulse for 500ms
pulse.opacity = brightness # Apply breathing effect
wait 200ms
pulse.opacity = 255 # Reset to full brightness
}
}
run breathing_cycle
```
## User Functions in Computed Parameters
### 9. Simple User Function
### 12. Simple User Function
```berry
# Simple user function in computed parameter
animation random_base = solid(color=blue, priority=10)
@ -106,7 +195,7 @@ random_base.opacity = rand_demo()
run random_base
```
### 10. User Function with Math Operations
### 13. User Function with Math Operations
```berry
# Mix user functions with mathematical functions
animation random_bounded = solid(
@ -117,7 +206,7 @@ animation random_bounded = solid(
run random_bounded
```
### 11. User Function in Arithmetic Expression
### 14. User Function in Arithmetic Expression
```berry
# Use user function in arithmetic expressions
animation random_variation = solid(
@ -130,9 +219,80 @@ run random_variation
See `anim_examples/user_functions_demo.anim` for a complete working example.
## New Repeat System Examples
### 15. Runtime Repeat with Forever Loop
```berry
color red = 0xFF0000
color blue = 0x0000FF
animation red_anim = solid(color=red)
animation blue_anim = solid(color=blue)
# Traditional syntax with repeat sub-sequence
sequence cylon_effect {
repeat forever {
play red_anim for 1s
play blue_anim for 1s
}
}
# Alternative syntax - sequence with repeat modifier
sequence cylon_effect_alt repeat forever {
play red_anim for 1s
play blue_anim for 1s
}
run cylon_effect
```
### 16. Nested Repeats (Multiplication)
```berry
color green = 0x00FF00
color yellow = 0xFFFF00
animation green_anim = solid(color=green)
animation yellow_anim = solid(color=yellow)
# Nested repeats: 3 × 2 = 6 total iterations
sequence nested_pattern {
repeat 3 times {
repeat 2 times {
play green_anim for 200ms
play yellow_anim for 200ms
}
wait 500ms # Pause between outer iterations
}
}
run nested_pattern
```
### 17. Repeat with Property Assignments
```berry
set triangle_pos = triangle(min_value=0, max_value=29, period=3s)
set cosine_pos = cosine_osc(min_value=0, max_value=29, period=3s)
color eye_color = color_cycle(palette=[red, yellow, green, blue], cycle_period=0)
animation moving_eye = beacon_animation(
color=eye_color
pos=triangle_pos
beacon_size=2
slew_size=1
)
sequence dynamic_cylon {
repeat 5 times {
play moving_eye for 2s
moving_eye.pos = cosine_pos # Switch to cosine movement
play moving_eye for 2s
moving_eye.pos = triangle_pos # Switch back to triangle
eye_color.next = 1 # Next color
}
}
run dynamic_cylon
```
## Advanced Examples
### 13. Dynamic Position
### 18. Dynamic Position
```berry
strip length 60
@ -148,7 +308,7 @@ animation moving_pulse = beacon_animation(
run moving_pulse
```
### 14. Multi-Layer Effect
### 19. Multi-Layer Effect
```berry
# Base layer - slow breathing
set breathing = smooth(min_value=100, max_value=255, period=4s)

View File

@ -77,15 +77,32 @@ sequence rgb_show {
wait 500ms
play blue_pulse for 3s
repeat 2 times:
repeat 2 times {
play red_pulse for 1s
play green_pulse for 1s
play blue_pulse for 1s
}
}
run rgb_show
```
**Pro Tip: Variable Durations**
Use variables for consistent timing:
```berry
# Define timing variables
set short_time = 1s
set long_time = 3s
sequence timed_show {
play red_pulse for long_time # Use variable duration
wait 500ms
play green_pulse for short_time # Different timing
play blue_pulse for long_time # Reuse timing
}
```
## Step 5: Dynamic Effects
Add movement and variation to your animations:

View File

@ -250,6 +250,24 @@ end
animation pulse_anim = pulsating_animation(color=red, period=2s)
```
6. **Variable Duration Support:**
```berry
# Now supported - variables in play/wait durations
set eye_duration = 5s
sequence cylon_eye {
play red_eye for eye_duration # ✓ Variables now work
wait eye_duration # ✓ Variables work in wait too
}
# Also supported - value providers for dynamic duration
set dynamic_time = triangle(min_value=1000, max_value=3000, period=10s)
sequence demo {
play animation for dynamic_time # ✓ Dynamic duration
}
```
6. **Parameter Constraint Violations:**
```berry
# Wrong - negative period not allowed
@ -265,6 +283,41 @@ end
animation good_comet = comet_animation(color=red, direction=1)
```
7. **Repeat Syntax Errors:**
```berry
# Wrong - old colon syntax no longer supported
sequence bad_demo {
repeat 3 times: # Error: Expected '{' after 'times'
play anim for 1s
}
# Wrong - missing braces
sequence bad_demo2 {
repeat 3 times
play anim for 1s # Error: Expected '{' after 'times'
}
# Correct - use braces for repeat blocks
sequence good_demo {
repeat 3 times {
play anim for 1s
}
}
# Also correct - alternative syntax
sequence good_demo_alt repeat 3 times {
play anim for 1s
}
# Correct - forever syntax
sequence infinite_demo {
repeat forever {
play anim for 1s
wait 500ms
}
}
```
### DSL Runtime Errors
**Problem:** DSL compiles but fails at runtime
@ -281,7 +334,28 @@ end
run red_anim
```
2. **Sequence Issues:**
2. **Repeat Performance Issues:**
```berry
# Efficient - runtime repeats don't expand at compile time
sequence efficient {
repeat 1000 times { # No memory overhead for large counts
play anim for 100ms
wait 50ms
}
}
# Nested repeats work efficiently
sequence nested {
repeat 100 times {
repeat 50 times { # Total: 5000 iterations, but efficient
play quick_flash for 10ms
}
wait 100ms
}
}
```
3. **Sequence Issues:**
```berry
# Make sure animations are defined before sequences
color red = 0xFF0000
@ -294,7 +368,7 @@ end
run demo
```
3. **Undefined References:**
4. **Undefined References:**
```berry
# Wrong - using undefined animation in sequence
sequence bad_demo {

View File

@ -73,7 +73,7 @@ register_to_animation(animation_base)
import "core/sequence_manager" as sequence_manager
register_to_animation(sequence_manager)
# Unified animation engine - central controller for all animations
# Unified animation engine - central engine for all animations
# Provides priority-based layering, automatic blending, and performance optimization
import "core/animation_engine" as animation_engine
register_to_animation(animation_engine)

View File

@ -19,6 +19,8 @@
#
import global
import animation
# Requires to first `import animation`
# We don't include it to not create a closure, but use the global instead

View File

@ -91,7 +91,7 @@ class Animation : animation.parameterized_object
end
# Update animation state based on current time
# This method should be called regularly by the animation controller
# This method should be called regularly by the animation engine
#
# @param time_ms: int - Current time in milliseconds
# @return bool - True if animation is still running, false if completed

View File

@ -1,8 +1,5 @@
# Unified Animation Engine
# Combines AnimationController, AnimationManager, and Renderer into a single efficient class
#
# This unified approach eliminates redundancy and provides a simpler, more efficient
# animation system for Tasmota LED control.
class AnimationEngine
# Core properties
@ -61,6 +58,12 @@ class AnimationEngine
self.animations[i].start(now)
i += 1
end
i = 0
while (i < size(self.sequence_managers))
self.sequence_managers[i].start(now)
i += 1
end
tasmota.add_fast_loop(self.fast_loop_closure)
end
@ -126,7 +129,7 @@ class AnimationEngine
self.animations = []
var i = 0
while i < size(self.sequence_managers)
self.sequence_managers[i].stop_sequence()
self.sequence_managers[i].stop()
i += 1
end
self.sequence_managers = []
@ -190,7 +193,7 @@ class AnimationEngine
# Update sequence managers
var i = 0
while i < size(self.sequence_managers)
self.sequence_managers[i].update()
self.sequence_managers[i].update(current_time)
i += 1
end
@ -423,11 +426,5 @@ def create_engine(strip)
return animation.animation_engine(strip)
end
# Compatibility function for legacy examples
def animation_controller(strip)
return animation.animation_engine(strip)
end
return {'animation_engine': AnimationEngine,
'create_engine': create_engine,
'animation_controller': animation_controller}
'create_engine': create_engine}

View File

@ -227,6 +227,13 @@ class ParameterizedObject
import math
value = int(math.round(value))
actual_type = "int"
# Special case: check for bytes type using isinstance()
elif expected_type == "bytes"
if actual_type == "instance" && isinstance(value, bytes)
actual_type = "bytes"
elif actual_type != "instance" || !isinstance(value, bytes)
raise "value_error", f"Parameter '{name}' expects type '{expected_type}' but got '{actual_type}' (value: {value})"
end
elif expected_type != actual_type
raise "value_error", f"Parameter '{name}' expects type '{expected_type}' but got '{actual_type}' (value: {value})"
end

View File

@ -1,8 +1,9 @@
# Sequence Manager for Animation DSL
# Handles async execution of animation sequences without blocking delays
# Supports sub-sequences and repeat logic through recursive composition
class SequenceManager
var controller # Animation controller reference
var engine # Animation engine reference
var active_sequence # Currently running sequence
var sequence_state # Current sequence execution state
var step_index # Current step in the sequence
@ -10,64 +11,169 @@ class SequenceManager
var steps # List of sequence steps
var is_running # Whether sequence is active
def init(controller)
self.controller = controller
# Repeat-specific properties
var repeat_count # Number of times to repeat this sequence (-1 for forever, 0 for no repeat)
var current_iteration # Current iteration (0-based)
var is_repeat_sequence # Whether this is a repeat sub-sequence
def init(engine, repeat_count)
self.engine = engine
self.active_sequence = nil
self.sequence_state = {}
self.step_index = 0
self.step_start_time = 0
self.steps = []
self.is_running = false
# Repeat logic
self.repeat_count = repeat_count != nil ? repeat_count : 1 # Default: run once
self.current_iteration = 0
self.is_repeat_sequence = repeat_count != nil && repeat_count != 1
end
# Start a new sequence
def start_sequence(steps)
self.stop_sequence() # Stop any current sequence
self.steps = steps
self.step_index = 0
self.step_start_time = self.controller.time_ms
self.is_running = true
if size(self.steps) > 0
self.execute_current_step()
end
# Add a step to this sequence
def push_step(step)
self.steps.push(step)
return self
end
# Stop the current sequence
def stop_sequence()
# Add a play step directly
def push_play_step(animation_ref, duration)
self.steps.push({
"type": "play",
"animation": animation_ref,
"duration": duration != nil ? duration : 0
})
return self
end
# Add a wait step directly
def push_wait_step(duration)
self.steps.push({
"type": "wait",
"duration": duration
})
return self
end
# Add an assignment step directly
def push_assign_step(closure)
self.steps.push({
"type": "assign",
"closure": closure
})
return self
end
# Add a repeat subsequence step directly
def push_repeat_subsequence(sequence_manager)
self.steps.push({
"type": "subsequence",
"sequence_manager": sequence_manager
})
return self
end
# Start this sequence
def start(time_ms)
# Stop any current sequence
if self.is_running
self.is_running = false
self.controller.clear()
self.engine.clear()
# Stop any sub-sequences
self.stop_all_subsequences()
end
# Initialize sequence state
self.step_index = 0
self.step_start_time = time_ms
self.current_iteration = 0
self.is_running = true
# Start executing if we have steps
if size(self.steps) > 0
self.execute_current_step(time_ms)
end
return self
end
# Stop this sequence manager
def stop()
if self.is_running
self.is_running = false
# Stop any currently playing animations
if self.step_index < size(self.steps)
var current_step = self.steps[self.step_index]
if current_step["type"] == "play"
var anim = current_step["animation"]
self.engine.remove_animation(anim)
elif current_step["type"] == "subsequence"
var sub_seq = current_step["sequence_manager"]
sub_seq.stop()
end
end
# Clear engine and stop all sub-sequences
self.engine.clear()
self.stop_all_subsequences()
end
return self
end
# Stop all sub-sequences in our steps
def stop_all_subsequences()
for step : self.steps
if step["type"] == "subsequence"
var sub_seq = step["sequence_manager"]
sub_seq.stop()
end
end
return self
end
# Update sequence state - called from fast_loop
def update()
# Returns true if still running, false if completed
def update(current_time)
if !self.is_running || size(self.steps) == 0
return
return false
end
var current_time = self.controller.time_ms
var current_step = self.steps[self.step_index]
# Check if current step has completed
if current_step.contains("duration") && current_step["duration"] > 0
var elapsed = current_time - self.step_start_time
if elapsed >= current_step["duration"]
self.advance_to_next_step()
# Handle different step types
if current_step["type"] == "subsequence"
# Handle sub-sequence (including repeat sequences)
var sub_seq = current_step["sequence_manager"]
if !sub_seq.update(current_time)
# Sub-sequence finished, advance to next step
self.advance_to_next_step(current_time)
end
elif current_step["type"] == "assign"
# Assign steps are handled in batches by advance_to_next_step
# This should not happen in normal flow, but handle it just in case
self.execute_assign_steps_batch(current_time)
else
# Steps without duration (like stop steps) complete immediately
# Advance to next step on next update cycle
self.advance_to_next_step()
# Handle regular steps with duration
if current_step.contains("duration") && current_step["duration"] > 0
var elapsed = current_time - self.step_start_time
if elapsed >= current_step["duration"]
self.advance_to_next_step(current_time)
end
else
# Steps without duration complete immediately
self.advance_to_next_step(current_time)
end
end
return self.is_running
end
# Execute the current step
def execute_current_step()
def execute_current_step(current_time)
if self.step_index >= size(self.steps)
self.is_running = false
self.complete_iteration(current_time)
return
end
@ -75,13 +181,8 @@ class SequenceManager
if step["type"] == "play"
var anim = step["animation"]
self.controller.add_animation(anim)
anim.start()
# Set duration if specified
if step.contains("duration") && step["duration"] > 0
anim.duration = step["duration"]
end
self.engine.add_animation(anim)
anim.start(current_time)
elif step["type"] == "wait"
# Wait steps are handled by the update loop checking duration
@ -89,29 +190,82 @@ class SequenceManager
elif step["type"] == "stop"
var anim = step["animation"]
self.controller.remove_animation(anim)
self.engine.remove_animation(anim)
elif step["type"] == "assign"
# Assign steps should be handled in batches by execute_assign_steps_batch
# This should not happen in normal flow, but handle it for safety
var assign_closure = step["closure"]
if assign_closure != nil
assign_closure(self.engine)
end
elif step["type"] == "subsequence"
# Start sub-sequence (including repeat sequences)
var sub_seq = step["sequence_manager"]
sub_seq.start()
end
self.step_start_time = self.controller.time_ms
self.step_start_time = current_time
end
# Advance to the next step in the sequence
def advance_to_next_step()
def advance_to_next_step(current_time)
# Stop current animations if step had duration
var current_step = self.steps[self.step_index]
if current_step["type"] == "play" && current_step.contains("duration")
var anim = current_step["animation"]
self.controller.remove_animation(anim)
self.engine.remove_animation(anim)
end
self.step_index += 1
if self.step_index >= size(self.steps)
self.is_running = false
return
self.complete_iteration(current_time)
else
# Execute all consecutive assign steps atomically
self.execute_assign_steps_batch(current_time)
end
end
# Execute all consecutive assign steps in a batch to avoid black frames
def execute_assign_steps_batch(current_time)
# Execute all consecutive assign steps
while self.step_index < size(self.steps)
var step = self.steps[self.step_index]
if step["type"] == "assign"
# Execute assignment closure
var assign_closure = step["closure"]
if assign_closure != nil
assign_closure(self.engine)
end
self.step_index += 1
else
break
end
end
self.execute_current_step()
# Now execute the next non-assign step
if self.step_index < size(self.steps)
self.execute_current_step(current_time)
else
self.complete_iteration(current_time)
end
end
# Complete current iteration and check if we should repeat
def complete_iteration(current_time)
self.current_iteration += 1
# Check if we should continue repeating
if self.repeat_count == -1 || self.current_iteration < self.repeat_count
# Start next iteration
self.step_index = 0
self.execute_current_step(current_time)
else
# All iterations complete
self.is_running = false
end
end
# Check if sequence is running
@ -129,35 +283,12 @@ class SequenceManager
"step_index": self.step_index,
"total_steps": size(self.steps),
"current_step": self.steps[self.step_index],
"elapsed_ms": self.controller.time_ms - self.step_start_time
"elapsed_ms": self.engine.time_ms - self.step_start_time,
"repeat_count": self.repeat_count,
"current_iteration": self.current_iteration,
"is_repeat_sequence": self.is_repeat_sequence
}
end
end
# Helper function to create sequence steps
def create_play_step(animation, duration)
return {
"type": "play",
"animation": animation,
"duration": duration
}
end
def create_wait_step(duration)
return {
"type": "wait",
"duration": duration
}
end
def create_stop_step(animation)
return {
"type": "stop",
"animation": animation
}
end
return {'SequenceManager': SequenceManager,
'create_play_step': create_play_step,
'create_wait_step': create_wait_step,
'create_stop_step': create_stop_step}
return {'SequenceManager': SequenceManager}

View File

@ -148,7 +148,7 @@ class DSLRuntime
# Get current engine for external access
def get_controller()
def get_engine()
return self.engine
end

View File

@ -26,6 +26,7 @@ class SimpleDSLTranspiler
var strip_initialized # Track if strip was initialized
var sequence_names # Track which names are sequences
var symbol_table # Track created objects: name -> instance
var indent_level # Track current indentation level for nested sequences
# Static color mapping for named colors (helps with solidification)
static var named_colors = {
@ -54,6 +55,18 @@ class SimpleDSLTranspiler
self.strip_initialized = false # Track if strip was initialized
self.sequence_names = {} # Track which names are sequences
self.symbol_table = {} # Track created objects: name -> instance
self.indent_level = 0 # Track current indentation level
end
# Get current indentation string
def get_indent()
# return " " * (self.indent_level + 1) # Base indentation is 2 spaces - string multiplication not supported
var indent = ""
var spaces_needed = (self.indent_level + 1) * 2 # Base indentation is 2 spaces
for i : 0..spaces_needed-1
indent += " "
end
return indent
end
# Main transpilation method - single pass
@ -226,7 +239,7 @@ class SimpleDSLTranspiler
end
end
# Process palette definition: palette aurora_colors = [(0, #000022), (64, #004400), ...]
# Process palette definition: palette aurora_colors = [(0, #000022), (64, #004400), ...] or [red, 0x008000, blue, 0x112233]
def process_palette()
self.next() # skip 'palette'
var name = self.expect_identifier()
@ -239,10 +252,23 @@ class SimpleDSLTranspiler
self.expect_assign()
# Expect array literal with tuples
# Expect array literal
self.expect_left_bracket()
var palette_entries = []
# Detect syntax type by looking at the first entry
self.skip_whitespace_including_newlines()
if self.check_right_bracket()
# Empty palette - not allowed
self.error("Empty palettes are not allowed. A palette must contain at least one color entry.")
self.skip_statement()
return
end
# Check if first entry starts with '(' (tuple syntax) or not (alternative syntax)
var is_tuple_syntax = self.current() != nil && self.current().type == animation_dsl.Token.LEFT_PAREN
while !self.at_end() && !self.check_right_bracket()
self.skip_whitespace_including_newlines()
@ -250,16 +276,41 @@ class SimpleDSLTranspiler
break
end
# Parse tuple (value, color)
self.expect_left_paren()
var value = self.expect_number()
self.expect_comma()
var color = self.process_value("color") # Reuse existing color parsing
self.expect_right_paren()
# Convert to VRGB format entry
var vrgb_entry = self.convert_to_vrgb(value, color)
palette_entries.push(f'"{vrgb_entry}"')
if is_tuple_syntax
# Parse tuple (value, color) - original syntax
# Check if we accidentally have alternative syntax in tuple mode
if self.current() != nil && self.current().type != animation_dsl.Token.LEFT_PAREN
self.error("Cannot mix alternative syntax [color1, color2, ...] with tuple syntax (value, color). Use only one syntax per palette.")
self.skip_statement()
return
end
self.expect_left_paren()
var value = self.expect_number()
self.expect_comma()
var color = self.process_value("color") # Reuse existing color parsing
self.expect_right_paren()
# Convert to VRGB format entry and store as integer
var vrgb_entry = self.convert_to_vrgb(value, color)
var vrgb_int = int(f"0x{vrgb_entry}")
palette_entries.push(vrgb_int)
else
# Parse color only - alternative syntax
# Check if we accidentally have a tuple in alternative syntax mode
if self.current() != nil && self.current().type == animation_dsl.Token.LEFT_PAREN
self.error("Cannot mix tuple syntax (value, color) with alternative syntax [color1, color2, ...]. Use only one syntax per palette.")
self.skip_statement()
return
end
var color = self.process_value("color") # Reuse existing color parsing
# Convert to VRGB format entry and store as integer after setting alpha to 0xFF
var vrgb_entry = self.convert_to_vrgb(0xFF, color)
var vrgb_int = int(f"0x{vrgb_entry}")
palette_entries.push(vrgb_int)
end
# Skip whitespace but preserve newlines for separator detection
while !self.at_end()
@ -288,13 +339,15 @@ class SimpleDSLTranspiler
self.expect_right_bracket()
var inline_comment = self.collect_inline_comment()
# Generate Berry bytes object
# Generate Berry bytes object - convert integers to hex strings for bytes() constructor
var palette_data = ""
for i : 0..size(palette_entries)-1
if i > 0
palette_data += " "
end
palette_data += palette_entries[i]
# Convert integer back to hex string for bytes() constructor
var hex_str = format("%08X", palette_entries[i])
palette_data += f'"{hex_str}"'
end
self.add(f"var {name}_ = bytes({palette_data}){inline_comment}")
@ -410,9 +463,13 @@ class SimpleDSLTranspiler
var value = self.process_value("variable")
var inline_comment = self.collect_inline_comment()
self.add(f"var {name}_ = {value}{inline_comment}")
# Add variable to symbol table for validation
# Use a string marker to indicate this is a variable (not an instance)
self.symbol_table[name] = "variable"
end
# Process sequence definition: sequence demo { ... }
# Process sequence definition: sequence demo { ... } or sequence demo repeat N times { ... }
def process_sequence()
self.next() # skip 'sequence'
var name = self.expect_identifier()
@ -430,26 +487,67 @@ class SimpleDSLTranspiler
# We use a string marker since sequences don't have real instances
self.symbol_table[name] = "sequence"
self.expect_left_brace()
# Check for second syntax: sequence name repeat N times { ... } or sequence name forever { ... }
var is_repeat_syntax = false
var repeat_count = "1"
# Generate anonymous closure that creates and returns sequence manager
self.add(f"var {name}_ = (def (engine)")
self.add(f" var steps = []")
# Process sequence body
while !self.at_end() && !self.check_right_brace()
self.process_sequence_statement()
var current_tok = self.current()
if current_tok != nil && current_tok.type == animation_dsl.Token.KEYWORD
if current_tok.value == "repeat"
is_repeat_syntax = true
self.next() # skip 'repeat'
# Parse repeat count: either number or "forever"
var tok_after_repeat = self.current()
if tok_after_repeat != nil && tok_after_repeat.type == animation_dsl.Token.KEYWORD && tok_after_repeat.value == "forever"
self.next() # skip 'forever'
repeat_count = "-1" # -1 means forever
else
var count = self.expect_number()
self.expect_keyword("times")
repeat_count = str(count)
end
elif current_tok.value == "forever"
# New syntax: sequence name forever { ... } (repeat is optional)
is_repeat_syntax = true
self.next() # skip 'forever'
repeat_count = "-1" # -1 means forever
end
elif current_tok != nil && current_tok.type == animation_dsl.Token.NUMBER
# New syntax: sequence name N times { ... } (repeat is optional)
is_repeat_syntax = true
var count = self.expect_number()
self.expect_keyword("times")
repeat_count = str(count)
end
self.expect_left_brace()
if is_repeat_syntax
# Second syntax: sequence name repeat N times { ... }
# Create a single SequenceManager with fluent interface
self.add(f"var {name}_ = animation.SequenceManager(engine, {repeat_count})")
# Process sequence body - add steps using fluent interface
while !self.at_end() && !self.check_right_brace()
self.process_sequence_statement()
end
else
# First syntax: sequence demo { ... }
# Use fluent interface for regular sequences too (no repeat count = default)
self.add(f"var {name}_ = animation.SequenceManager(engine)")
# Process sequence body - add steps using fluent interface
while !self.at_end() && !self.check_right_brace()
self.process_sequence_statement()
end
end
self.add(f" var seq_manager = animation.SequenceManager(engine)")
self.add(f" seq_manager.start_sequence(steps)")
self.add(f" return seq_manager")
self.add(f"end)(engine)")
self.expect_right_brace()
end
# Process statements inside sequences
def process_sequence_statement()
# Process statements inside sequences using push_step()
def process_sequence_statement_for_manager(manager_name)
var tok = self.current()
if tok == nil || tok.type == animation_dsl.Token.EOF
return
@ -469,109 +567,321 @@ class SimpleDSLTranspiler
end
if tok.type == animation_dsl.Token.KEYWORD && tok.value == "play"
self.next() # skip 'play'
# Check if this is a function call or an identifier
var anim_ref = ""
var current_tok = self.current()
if current_tok != nil && (current_tok.type == animation_dsl.Token.IDENTIFIER || current_tok.type == animation_dsl.Token.KEYWORD) &&
self.peek() != nil && self.peek().type == animation_dsl.Token.LEFT_PAREN
# This is a function call - process it as a nested function call
anim_ref = self.process_nested_function_call()
else
# This is an identifier reference - sequences need runtime resolution
var anim_name = self.expect_identifier()
# Validate that the referenced object exists
self._validate_object_reference(anim_name, "sequence play")
anim_ref = f"animation.global('{anim_name}_')"
end
# Handle optional 'for duration'
var duration = "0"
if self.current() != nil && self.current().type == animation_dsl.Token.KEYWORD && self.current().value == "for"
self.next() # skip 'for'
duration = str(self.process_time_value())
end
var inline_comment = self.collect_inline_comment()
self.add(f" steps.push(animation.create_play_step({anim_ref}, {duration})){inline_comment}")
self.process_play_statement_for_manager(manager_name)
elif tok.type == animation_dsl.Token.KEYWORD && tok.value == "wait"
self.next() # skip 'wait'
var duration = self.process_time_value()
var inline_comment = self.collect_inline_comment()
self.add(f" steps.push(animation.create_wait_step({duration})){inline_comment}")
self.process_wait_statement_for_manager(manager_name)
elif tok.type == animation_dsl.Token.KEYWORD && tok.value == "repeat"
self.next() # skip 'repeat'
var count = self.expect_number()
self.expect_keyword("times")
self.expect_colon()
self.add(f" for repeat_i : 0..{count}-1")
# Process repeat body
while !self.at_end() && !self.check_right_brace()
var inner_tok = self.current()
if inner_tok == nil || inner_tok.type == animation_dsl.Token.EOF
break
end
if inner_tok.type == animation_dsl.Token.COMMENT
self.add(" " + inner_tok.value) # Add comment with repeat body indentation
self.next()
continue
end
if inner_tok.type == animation_dsl.Token.NEWLINE
self.next()
continue
end
if inner_tok.type == animation_dsl.Token.KEYWORD && inner_tok.value == "play"
self.next() # skip 'play'
# Check if this is a function call or an identifier
var anim_ref = ""
var current_tok = self.current()
if current_tok != nil && (current_tok.type == animation_dsl.Token.IDENTIFIER || current_tok.type == animation_dsl.Token.KEYWORD) &&
self.peek() != nil && self.peek().type == animation_dsl.Token.LEFT_PAREN
# This is a function call - process it as a nested function call
anim_ref = self.process_nested_function_call()
else
# This is an identifier reference - sequences need runtime resolution
var anim_name = self.expect_identifier()
# Validate that the referenced object exists
self._validate_object_reference(anim_name, "sequence play")
anim_ref = f"animation.global('{anim_name}_')"
end
var duration = "0"
if self.current() != nil && self.current().type == animation_dsl.Token.KEYWORD && self.current().value == "for"
self.next() # skip 'for'
duration = str(self.process_time_value())
end
var inline_comment = self.collect_inline_comment()
self.add(f" steps.push(animation.create_play_step({anim_ref}, {duration})){inline_comment}")
elif inner_tok.type == animation_dsl.Token.KEYWORD && inner_tok.value == "wait"
self.next() # skip 'wait'
var duration = self.process_time_value()
var inline_comment = self.collect_inline_comment()
self.add(f" steps.push(animation.create_wait_step({duration})){inline_comment}")
else
break # Exit repeat body
end
# Parse repeat count: either number or "forever"
var repeat_count = "1"
var tok_after_repeat = self.current()
if tok_after_repeat != nil && tok_after_repeat.type == animation_dsl.Token.KEYWORD && tok_after_repeat.value == "forever"
self.next() # skip 'forever'
repeat_count = "-1" # -1 means forever
else
var count = self.expect_number()
self.expect_keyword("times")
repeat_count = str(count)
end
self.add(f" end")
self.expect_left_brace()
# Create repeat sub-sequence
self.add(f" var repeat_seq = animation.SequenceManager(engine, {repeat_count})")
# Process repeat body - add steps directly to repeat sequence
while !self.at_end() && !self.check_right_brace()
self.process_sequence_statement_for_manager("repeat_seq")
end
self.expect_right_brace()
# Add the repeat sub-sequence step to main sequence
self.add(f" {manager_name}.push_repeat_subsequence(repeat_seq.steps, {repeat_count})")
elif tok.type == animation_dsl.Token.IDENTIFIER
# Check if this is a property assignment (identifier.property = value)
if self.peek() != nil && self.peek().type == animation_dsl.Token.DOT
self.process_sequence_assignment_for_manager(" ", manager_name) # Pass indentation and manager name
else
self.skip_statement()
end
else
self.skip_statement()
end
end
# Process statements inside sequences using fluent interface
def process_sequence_statement()
var tok = self.current()
if tok == nil || tok.type == animation_dsl.Token.EOF
return
end
# Handle comments - preserve them in generated code with proper indentation
if tok.type == animation_dsl.Token.COMMENT
self.add(self.get_indent() + tok.value) # Add comment with fluent indentation
self.next()
return
end
# Skip whitespace (newlines)
if tok.type == animation_dsl.Token.NEWLINE
self.next()
return
end
if tok.type == animation_dsl.Token.KEYWORD && tok.value == "play"
self.process_play_statement_fluent()
elif tok.type == animation_dsl.Token.KEYWORD && tok.value == "wait"
self.process_wait_statement_fluent()
elif tok.type == animation_dsl.Token.KEYWORD && tok.value == "repeat"
self.next() # skip 'repeat'
# Parse repeat count: either number or "forever"
var repeat_count = "1"
var tok_after_repeat = self.current()
if tok_after_repeat != nil && tok_after_repeat.type == animation_dsl.Token.KEYWORD && tok_after_repeat.value == "forever"
self.next() # skip 'forever'
repeat_count = "-1" # -1 means forever
else
var count = self.expect_number()
self.expect_keyword("times")
repeat_count = str(count)
end
self.expect_left_brace()
# Create a nested sub-sequence using recursive processing
self.add(f"{self.get_indent()}.push_repeat_subsequence(animation.SequenceManager(engine, {repeat_count})")
# Increase indentation level for nested content
self.indent_level += 1
# Process repeat body recursively - just call the same method
while !self.at_end() && !self.check_right_brace()
self.process_sequence_statement()
end
self.expect_right_brace()
# Decrease indentation level and close the sub-sequence
self.add(f"{self.get_indent()})")
self.indent_level -= 1
elif tok.type == animation_dsl.Token.IDENTIFIER
# Check if this is a property assignment (identifier.property = value)
if self.peek() != nil && self.peek().type == animation_dsl.Token.DOT
self.process_sequence_assignment_fluent()
else
self.skip_statement()
end
else
self.skip_statement()
end
end
# Process property assignment using fluent style
def process_sequence_assignment_fluent()
var object_name = self.expect_identifier()
self.expect_dot()
var property_name = self.expect_identifier()
self.expect_assign()
var value = self.process_value("property")
var inline_comment = self.collect_inline_comment()
# Create assignment step using fluent style
var closure_code = f"def (engine) {object_name}_.{property_name} = {value} end"
self.add(f"{self.get_indent()}.push_assign_step({closure_code}){inline_comment}")
end
# Process property assignment inside sequences: object.property = value (legacy)
def process_sequence_assignment(indent)
self.process_sequence_assignment_generic(indent, "steps")
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 == 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 instance = self.symbol_table[object_name]
# Only validate parameters for actual instances, not sequence markers
if type(instance) != "string"
var class_name = classname(instance)
# Use the existing parameter validation logic
self._validate_single_parameter(class_name, property_name, instance)
else
# 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 = self.process_value("property")
var inline_comment = self.collect_inline_comment()
# Generate assignment step with closure
# The closure receives the engine as parameter and performs the assignment
import introspect
var object_ref = ""
if introspect.contains(animation, object_name)
# Symbol exists in animation module, use it directly
object_ref = f"animation.{object_name}"
else
# Symbol doesn't exist in animation module, use underscore suffix
object_ref = f"{object_name}_"
end
# Create closure that performs the assignment
var closure_code = f"def (engine) {object_ref}.{property_name} = {value} 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'
# Check if this is a function call or an identifier
var anim_ref = ""
var current_tok = self.current()
if current_tok != nil && (current_tok.type == animation_dsl.Token.IDENTIFIER || current_tok.type == animation_dsl.Token.KEYWORD) &&
self.peek() != nil && self.peek().type == animation_dsl.Token.LEFT_PAREN
# This is a function call - process it as a nested function call
anim_ref = self.process_nested_function_call()
else
# This is an identifier reference
var anim_name = self.expect_identifier()
# Validate that the referenced object exists
self._validate_object_reference(anim_name, "sequence play")
anim_ref = f"{anim_name}_"
end
# Handle optional 'for duration'
var duration = "nil"
if self.current() != nil && self.current().type == animation_dsl.Token.KEYWORD && self.current().value == "for"
self.next() # skip 'for'
duration = str(self.process_time_value())
end
var inline_comment = self.collect_inline_comment()
self.add(f"{self.get_indent()}.push_play_step({anim_ref}, {duration}){inline_comment}")
end
# Helper method to process wait statement using fluent style
def process_wait_statement_fluent()
self.next() # skip 'wait'
var duration = self.process_time_value()
var inline_comment = self.collect_inline_comment()
self.add(f"{self.get_indent()}.push_wait_step({duration}){inline_comment}")
end
# Helper method to process play statement with configurable target array (legacy)
def process_play_statement(target_array)
self.next() # skip 'play'
# Check if this is a function call or an identifier
var anim_ref = ""
var current_tok = self.current()
if current_tok != nil && (current_tok.type == animation_dsl.Token.IDENTIFIER || current_tok.type == animation_dsl.Token.KEYWORD) &&
self.peek() != nil && self.peek().type == animation_dsl.Token.LEFT_PAREN
# This is a function call - process it as a nested function call
anim_ref = self.process_nested_function_call()
else
# This is an identifier reference - sequences need runtime resolution
var anim_name = self.expect_identifier()
# Validate that the referenced object exists
self._validate_object_reference(anim_name, "sequence play")
anim_ref = f"animation.global('{anim_name}_')"
end
# Handle optional 'for duration'
var duration = "0"
if self.current() != nil && self.current().type == animation_dsl.Token.KEYWORD && self.current().value == "for"
self.next() # skip 'for'
duration = str(self.process_time_value())
end
var inline_comment = self.collect_inline_comment()
self.add(f" {target_array}.push(animation.create_play_step({anim_ref}, {duration})){inline_comment}")
end
# Helper method to process wait statement with configurable target array (legacy)
def process_wait_statement(target_array)
self.next() # skip 'wait'
var duration = self.process_time_value()
var inline_comment = self.collect_inline_comment()
self.add(f" {target_array}.push(animation.create_wait_step({duration})){inline_comment}")
end
# Generic method to process sequence statements with configurable target array
def process_sequence_statement_generic(target_array)
var tok = self.current()
if tok == nil || tok.type == animation_dsl.Token.EOF
return
end
# Handle comments - preserve them in generated code with proper indentation
if tok.type == animation_dsl.Token.COMMENT
self.add(" " + tok.value) # Add comment with sequence indentation
self.next()
return
end
# Skip whitespace (newlines)
if tok.type == animation_dsl.Token.NEWLINE
self.next()
return
end
if tok.type == animation_dsl.Token.KEYWORD && tok.value == "play"
self.process_play_statement(target_array)
elif tok.type == animation_dsl.Token.KEYWORD && tok.value == "wait"
self.process_wait_statement(target_array)
elif tok.type == animation_dsl.Token.IDENTIFIER
# Check if this is a property assignment (identifier.property = value)
if self.peek() != nil && self.peek().type == animation_dsl.Token.DOT
self.process_sequence_assignment_generic(" ", target_array) # Pass indentation and target array
else
self.skip_statement()
end
else
self.skip_statement()
end
end
# Process run statement: run demo
def process_run()
self.next() # skip 'run'
@ -640,11 +950,6 @@ class SimpleDSLTranspiler
# Process any value - unified approach
def process_value(context)
return self.process_expression(context) # This calls process_additive_expression with is_top_level=true
end
# Process expressions with arithmetic operations
def process_expression(context)
return self.process_additive_expression(context, true) # true = top-level expression
end
@ -1188,6 +1493,15 @@ class SimpleDSLTranspiler
var num = tok.value
self.next()
return int(real(num)) * 1000 # assume seconds
elif tok != nil && tok.type == animation_dsl.Token.IDENTIFIER
# Handle variable references for time values
var var_name = tok.value
# Validate that the variable exists before processing
self._validate_object_reference(var_name, "duration")
var expr = self.process_primary_expression("time", true)
return expr
else
self.error("Expected time value")
return 1000
@ -1779,6 +2093,15 @@ class SimpleDSLTranspiler
end
end
def expect_dot()
var tok = self.current()
if tok != nil && tok.type == animation_dsl.Token.DOT
self.next()
else
self.error("Expected '.'")
end
end
def expect_left_bracket()
var tok = self.current()
if tok != nil && tok.type == animation_dsl.Token.LEFT_BRACKET

View File

@ -19,7 +19,13 @@ class ColorCycleColorProvider : animation.color_provider
# Parameter definitions
static var PARAMS = {
"palette": {"default": [0xFF0000FF, 0xFF00FF00, 0xFFFF0000], "type": "instance"}, # Default RGB palette
"palette": {"type": "bytes", "default":
bytes( # Palette bytes in AARRGGBB format
"FF0000FF" # Blue
"FF00FF00" # Green
"FFFF0000" # Red
)
},
"cycle_period": {"min": 0, "default": 5000}, # 0 = manual only, >0 = auto cycle time in ms
"next": {"default": 0} # Write 1 to move to next color
}
@ -31,11 +37,46 @@ class ColorCycleColorProvider : animation.color_provider
super(self).init(engine) # Initialize parameter system
# Initialize non-parameter instance variables
var default_palette = self.palette # Get default palette
self.current_color = default_palette[0] # Start with first color in palette
var palette_bytes = self._get_palette_bytes()
self.current_color = self._get_color_at_index(0) # Start with first color in palette
self.current_index = 0 # Start at first color
end
# Get palette bytes from parameter with default fallback
def _get_palette_bytes()
var palette_bytes = self.palette
if palette_bytes == nil
# Get default from PARAMS
var param_def = self._get_param_def("palette")
if param_def != nil && param_def.contains("default")
palette_bytes = param_def["default"]
end
end
return palette_bytes
end
# Get color at a specific index from bytes palette
# We force alpha channel to 0xFF to force opaque colors
def _get_color_at_index(idx)
var palette_bytes = self._get_palette_bytes()
var palette_size = size(palette_bytes) / 4 # Each color is 4 bytes (AARRGGBB)
if palette_size == 0 || idx < 0 || idx >= palette_size
return 0xFFFFFFFF # Default to white
end
# Read 4 bytes in big-endian format (AARRGGBB)
var color = palette_bytes.get(idx * 4, -4) # Big endian
color = color | 0xFF000000
return color
end
# Get the number of colors in the palette
def _get_palette_size()
var palette_bytes = self._get_palette_bytes()
return size(palette_bytes) / 4 # Each color is 4 bytes
end
# Handle parameter changes
#
# @param name: string - Name of the parameter that changed
@ -43,20 +84,24 @@ class ColorCycleColorProvider : animation.color_provider
def on_param_changed(name, value)
if name == "palette"
# When palette changes, update current_color if current_index is valid
var palette = value
if size(palette) > 0
var palette_size = self._get_palette_size()
if palette_size > 0
# Clamp current_index to valid range
if self.current_index >= size(palette)
if self.current_index >= palette_size
self.current_index = 0
end
self.current_color = palette[self.current_index]
self.current_color = self._get_color_at_index(self.current_index)
end
elif name == "next" && value == 1
# Move to next color in palette
var palette = self.palette
if size(palette) > 0
self.current_index = (self.current_index + 1) % size(palette)
self.current_color = palette[self.current_index]
elif name == "next" && value != 0
# Add to color index
var palette_size = self._get_palette_size()
if palette_size > 0
var current_index = (self.current_index + value) % palette_size
if current_index < 0
current_index += palette_size
end
self.current_index = current_index
self.current_color = self._get_color_at_index(self.current_index)
end
# Reset the next parameter back to 0
self.set_param("next", 0)
@ -70,18 +115,17 @@ class ColorCycleColorProvider : animation.color_provider
# @return int - Color in ARGB format (0xAARRGGBB)
def produce_value(name, time_ms)
# Get parameter values using virtual member access
var palette = self.palette
var cycle_period = self.cycle_period
# Get the number of colors in the palette
var palette_size = size(palette)
var palette_size = self._get_palette_size()
if palette_size == 0
return 0xFFFFFFFF # Default to white if no colors
end
if palette_size == 1
# If only one color, just return it
self.current_color = palette[0]
self.current_color = self._get_color_at_index(0)
return self.current_color
end
@ -103,7 +147,7 @@ class ColorCycleColorProvider : animation.color_provider
# Update current state and return the color
self.current_index = color_index
self.current_color = palette[color_index]
self.current_color = self._get_color_at_index(color_index)
return self.current_color
end
@ -115,17 +159,14 @@ class ColorCycleColorProvider : animation.color_provider
# @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)
# Get parameter values using virtual member access
var palette = self.palette
# Get the number of colors in the palette
var palette_size = size(palette)
var palette_size = self._get_palette_size()
if palette_size == 0
return 0xFFFFFFFF # Default to white if no colors
end
if palette_size == 1
return palette[0] # If only one color, just return it
return self._get_color_at_index(0) # If only one color, just return it
end
# Clamp value to 0-100
@ -143,108 +184,24 @@ class ColorCycleColorProvider : animation.color_provider
color_index = palette_size - 1
end
return palette[color_index]
return self._get_color_at_index(color_index)
end
# Add a color to the palette
#
# @param color: int - Color to add (32-bit ARGB value)
# @return self for method chaining
def add_color(color)
var current_palette = self.palette
var new_palette = current_palette.copy()
new_palette.push(color)
self.palette = new_palette
return self
end
# String representation of the provider
def tostring()
try
var mode = self.cycle_period == 0 ? "manual" : "auto"
return f"ColorCycleColorProvider(palette_size={size(self.palette)}, cycle_period={self.cycle_period}, mode={mode}, current_index={self.current_index})"
var palette_size = self._get_palette_size()
return f"ColorCycleColorProvider(palette_size={palette_size}, cycle_period={self.cycle_period}, mode={mode}, current_index={self.current_index})"
except ..
return "ColorCycleColorProvider(uninitialized)"
end
end
end
# Factory function for custom palette
#
# @param engine: AnimationEngine - Animation engine reference
# @param palette: list - List of colors to cycle through (32-bit ARGB values)
# @param cycle_period: int - Time for one complete cycle in milliseconds
# @return ColorCycleColorProvider - A new color cycle color provider instance
def color_cycle_from_palette(engine, palette, cycle_period)
var provider = animation.color_cycle(engine)
if palette != nil
provider.palette = palette
end
if cycle_period != nil
provider.cycle_period = cycle_period
end
return provider
end
# Factory function for rainbow palette
#
# @param engine: AnimationEngine - Animation engine reference
# @param num_colors: int - Number of colors in the rainbow (default: 6)
# @param cycle_period: int - Time for one complete cycle in milliseconds
# @return ColorCycleColorProvider - A new color cycle color provider instance
def color_cycle_rainbow(engine, num_colors, cycle_period)
# Default parameters
if num_colors == nil || num_colors < 2
num_colors = 6
end
# Create a rainbow palette
var palette = []
var i = 0
while i < num_colors
# Calculate hue (0 to 360 degrees)
var hue = tasmota.scale_uint(i, 0, num_colors, 0, 360)
# Convert HSV to RGB (simplified conversion)
var r, g, b
var h_section = (hue / 60) % 6
var f = (hue / 60) - h_section
var v = 255 # Value (brightness)
var p = 0 # Saturation is 100%, so p = 0
var q = int(v * (1 - f))
var t = int(v * f)
if h_section == 0
r = v; g = t; b = p
elif h_section == 1
r = q; g = v; b = p
elif h_section == 2
r = p; g = v; b = t
elif h_section == 3
r = p; g = q; b = v
elif h_section == 4
r = t; g = p; b = v
else
r = v; g = p; b = q
end
# Create ARGB color (fully opaque)
var color = (255 << 24) | (r << 16) | (g << 8) | b
palette.push(color)
i += 1
end
# Create and return a new color cycle color provider with the rainbow palette
var provider = animation.color_cycle(engine)
provider.palette = palette
if cycle_period != nil
provider.cycle_period = cycle_period
end
return provider
end
return {'color_cycle': ColorCycleColorProvider,
'color_cycle_from_palette': color_cycle_from_palette,
'color_cycle_rainbow': color_cycle_rainbow}
return {'color_cycle': ColorCycleColorProvider}

View File

@ -18,7 +18,7 @@ class RichPaletteColorProvider : animation.color_provider
# Parameter definitions
static var PARAMS = {
"palette": {"type": "instance", "default": nil}, # Palette bytes or predefined palette constant
"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.SINE},
"brightness": {"min": 0, "max": 255, "default": 255},
@ -91,26 +91,18 @@ class RichPaletteColorProvider : animation.color_provider
# Get palette bytes from parameter with default fallback
def _get_palette_bytes()
var palette_bytes = self.palette
if palette_bytes == nil
# Default rainbow palette (reusing format from Animate_palette)
palette_bytes = bytes(
"00FF0000" # Red (value 0)
"24FFA500" # Orange (value 36)
"49FFFF00" # Yellow (value 73)
"6E00FF00" # Green (value 110)
"920000FF" # Blue (value 146)
"B74B0082" # Indigo (value 183)
"DBEE82EE" # Violet (value 219)
"FFFF0000" # Red (value 255)
)
end
# Convert comptr to palette buffer if needed (from Animate_palette)
if type(palette_bytes) == 'ptr' palette_bytes = self._ptr_to_palette(palette_bytes) end
return palette_bytes
return (palette_bytes != nil) ? palette_bytes : self._DEFAULT_PALETTE
end
static _DEFAULT_PALETTE = bytes(
"00FF0000" # Red (value 0)
"24FFA500" # Orange (value 36)
"49FFFF00" # Yellow (value 73)
"6E00FF00" # Green (value 110)
"920000FF" # Blue (value 146)
"B74B0082" # Indigo (value 183)
"DBEE82EE" # Violet (value 219)
"FFFF0000" # Red (value 255)
)
# Recompute palette slots and metadata
def _recompute_palette()
@ -136,33 +128,6 @@ class RichPaletteColorProvider : animation.color_provider
return self
end
# Convert comptr to bytes (reused from Animate_palette.ptr_to_palette)
def _ptr_to_palette(p)
if type(p) == 'ptr'
var b_raw = bytes(p, 2000) # arbitrary large size
var idx = 1
if b_raw[0] != 0
# palette in tick counts
while true
if b_raw[idx * 4] == 0
break
end
idx += 1
end
else
# palette is in value range from 0..255
while true
if b_raw[idx * 4] == 0xFF
break
end
idx += 1
end
end
var sz = (idx + 1) * 4
return bytes(p, sz)
end
end
# Parse the palette and create slots array (reused from Animate_palette)
#
# @param min: int - Minimum value for the range
@ -212,12 +177,9 @@ class RichPaletteColorProvider : animation.color_provider
end
var palette_bytes = self._get_palette_bytes()
var bgrt = palette_bytes.get(idx * 4, 4)
var r = (bgrt >> 8) & 0xFF
var g = (bgrt >> 16) & 0xFF
var b = (bgrt >> 24) & 0xFF
return (0xFF << 24) | (r << 16) | (g << 8) | b
var trgb = palette_bytes.get(idx * 4, -4) # Big Endian
trgb = trgb | 0xFF000000 # set alpha channel to full opaque
return trgb
end
# Produce a color value for any parameter name (optimized version from Animate_palette)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,92 @@
#!/usr/bin/env berry
# Test for bytes type validation in parameterized_object.be
import animation
import animation_dsl
# Test class that uses bytes parameter
class BytesTestClass : animation.parameterized_object
static var PARAMS = {
"data": {"type": "bytes", "default": nil, "nillable": true},
"required_data": {"type": "bytes"},
"name": {"type": "string", "default": "test"}
}
def init(engine)
super(self).init(engine)
end
end
# Mock engine for testing
class MockEngine
var time_ms
def init()
self.time_ms = 1000
end
end
def test_bytes_type_validation()
print("Testing bytes type validation...")
var mock_engine = MockEngine()
var obj = BytesTestClass(mock_engine)
# Test 1: Valid bytes objects
var hex_bytes = bytes("AABBCC")
obj.data = hex_bytes
assert(obj.data.tohex() == "AABBCC", "Hex bytes should be accepted")
var empty_bytes = bytes()
obj.data = empty_bytes
assert(obj.data != nil, "Empty bytes should be accepted")
var sized_bytes = bytes(5)
obj.data = sized_bytes
assert(obj.data != nil, "Sized bytes should be accepted")
# Test 2: Invalid types should be rejected
var invalid_types = ["string", 123, 3.14, true, [], {}]
for invalid_val : invalid_types
var caught_error = false
try
obj.data = invalid_val
except "value_error"
caught_error = true
end
assert(caught_error, f"Should reject {type(invalid_val)}: {invalid_val}")
end
# Test 3: Nil handling
obj.data = nil # Should work for nillable parameter
assert(obj.data == nil, "Nil should be accepted for nillable parameter")
var nil_error = false
try
obj.required_data = nil # Should fail for non-nillable parameter
except "value_error"
nil_error = true
end
assert(nil_error, "Should reject nil for non-nillable parameter")
# Test 4: Method-based setting
var success = obj.set_param("data", bytes("112233"))
assert(success == true, "Method setting with valid bytes should succeed")
success = obj.set_param("data", "invalid")
assert(success == false, "Method setting with invalid type should fail")
# Test 5: Parameter metadata
var metadata = obj.get_param_metadata("data")
assert(metadata["type"] == "bytes", "Data parameter should have bytes type")
assert(metadata["nillable"] == true, "Data parameter should be nillable")
var req_metadata = obj.get_param_metadata("required_data")
assert(req_metadata["type"] == "bytes", "Required data should have bytes type")
assert(req_metadata.find("nillable", false) == false, "Required data should not be nillable")
print("✓ All bytes type validation tests passed!")
end
# Run the test
test_bytes_type_validation()

View File

@ -69,7 +69,7 @@ class ColorCycleAnimationTest
self.assert_equal(color_provider.palette != nil, true, "Color provider has palette property")
# Test with custom parameters
var custom_palette = [0xFFFF0000, 0xFF00FF00] # Red and Green in ARGB format
var custom_palette = bytes("FFFF0000FF00FF00") # Red and Green in AARRGGBB format
var custom_provider = animation.color_cycle(engine)
custom_provider.palette = custom_palette
custom_provider.cycle_period = 2000
@ -88,13 +88,13 @@ class ColorCycleAnimationTest
self.assert_equal(color_provider2.palette != nil, true, "Custom color provider has palette property")
# Check provider properties
self.assert_equal(size(color_provider2.palette), 2, "Custom palette has 2 colors")
self.assert_equal(color_provider2._get_palette_size(), 2, "Custom palette has 2 colors")
self.assert_equal(color_provider2.cycle_period, 2000, "Custom cycle period is 2000ms")
end
def test_update_and_render()
# Create animation with red and blue colors
var palette = [0xFFFF0000, 0xFF0000FF] # Red and Blue in ARGB format (Alpha, Red, Green, Blue)
var palette = bytes("FFFF0000FF0000FF") # Red and Blue in AARRGGBB format
var provider = animation.color_cycle(engine)
provider.palette = palette
provider.cycle_period = 1000 # 1 second cycle
@ -145,7 +145,7 @@ class ColorCycleAnimationTest
# Create animation with manual-only color provider
var manual_provider = animation.color_cycle(engine)
manual_provider.palette = [0xFF0000FF, 0xFF00FF00, 0xFFFF0000] # Red, Green, Blue
manual_provider.palette = bytes("FF0000FFFF00FF00FFFF0000") # Blue, Green, Red in AARRGGBB format
manual_provider.cycle_period = 0 # Manual-only mode
var manual_anim = animation.solid(engine)
@ -195,7 +195,7 @@ class ColorCycleAnimationTest
def test_direct_creation()
# Test direct creation without factory method (following new parameterized pattern)
var provider = animation.color_cycle(engine)
provider.palette = [0xFF0000FF, 0xFF00FF00, 0xFFFF0000] # RGB colors
provider.palette = bytes("FF0000FFFF00FF00FFFF0000") # Blue, Green, Red in AARRGGBB format
provider.cycle_period = 3000 # 3 second cycle period
var anim = animation.solid(engine)
@ -213,7 +213,7 @@ class ColorCycleAnimationTest
self.assert_equal(color_provider3.palette != nil, true, "Color provider has palette property")
# Check provider properties
self.assert_equal(size(color_provider3.palette), 3, "Palette has 3 colors")
self.assert_equal(color_provider3._get_palette_size(), 3, "Palette has 3 colors")
self.assert_equal(color_provider3.cycle_period, 3000, "Cycle period is 3000ms")
# Check animation properties

View File

@ -0,0 +1,155 @@
#!/usr/bin/env berry
# Test for ColorCycleColorProvider with bytes palette in AARRGGBB format
import animation
import animation_dsl
# Mock engine for testing
class MockEngine
var time_ms
def init()
self.time_ms = 1000
end
end
def test_color_cycle_bytes_format()
print("Testing ColorCycleColorProvider with bytes palette (AARRGGBB format)...")
var engine = MockEngine()
# Test 1: Create provider with default palette
var provider = animation.color_cycle(engine)
assert(provider != nil, "Provider should be created")
# Test 2: Check default palette
var default_size = provider._get_palette_size()
assert(default_size == 3, f"Default palette should have 3 colors, got {default_size}")
# Test 3: Test colors from default palette (AARRGGBB format)
var color0 = provider._get_color_at_index(0) # Should be FF0000FF (blue)
var color1 = provider._get_color_at_index(1) # Should be FF00FF00 (green)
var color2 = provider._get_color_at_index(2) # Should be FFFF0000 (red)
assert(color0 == 0xFF0000FF, f"First color should be blue (0xFF0000FF), got 0x{color0:08X}")
assert(color1 == 0xFF00FF00, f"Second color should be green (0xFF00FF00), got 0x{color1:08X}")
assert(color2 == 0xFFFF0000, f"Third color should be red (0xFFFF0000), got 0x{color2:08X}")
# Test 4: Set custom bytes palette
var custom_palette = bytes(
"80FF0000" # Semi-transparent red (alpha=0x80)
"FF00FF00" # Opaque green (alpha=0xFF)
"C00000FF" # Semi-transparent blue (alpha=0xC0)
"FFFFFF00" # Opaque yellow (alpha=0xFF)
)
provider.palette = custom_palette
var custom_size = provider._get_palette_size()
assert(custom_size == 4, f"Custom palette should have 4 colors, got {custom_size}")
# Test 5: Verify custom palette colors (alpha channel forced to 0xFF)
var custom_color0 = provider._get_color_at_index(0) # Red with forced full alpha
var custom_color1 = provider._get_color_at_index(1) # Green with forced full alpha
var custom_color2 = provider._get_color_at_index(2) # Blue with forced full alpha
var custom_color3 = provider._get_color_at_index(3) # Yellow with forced full alpha
assert(custom_color0 == 0xFFFF0000, f"Custom color 0 should be 0xFFFF0000 (alpha forced), got 0x{custom_color0:08X}")
assert(custom_color1 == 0xFF00FF00, f"Custom color 1 should be 0xFF00FF00 (alpha forced), got 0x{custom_color1:08X}")
assert(custom_color2 == 0xFF0000FF, f"Custom color 2 should be 0xFF0000FF (alpha forced), got 0x{custom_color2:08X}")
assert(custom_color3 == 0xFFFFFF00, f"Custom color 3 should be 0xFFFFFF00 (alpha forced), got 0x{custom_color3:08X}")
# Test 6: Test auto-cycle mode
provider.cycle_period = 4000 # 4 seconds for 4 colors = 1 second per color
# At time 0, should be first color
engine.time_ms = 0
var cycle_color0 = provider.produce_value("color", 0)
assert(cycle_color0 == custom_color0, f"Cycle color at t=0 should match first color")
# At time 1000 (1/4 of cycle), should be second color
var cycle_color1 = provider.produce_value("color", 1000)
assert(cycle_color1 == custom_color1, f"Cycle color at t=1000 should match second color")
# At time 2000 (2/4 of cycle), should be third color
var cycle_color2 = provider.produce_value("color", 2000)
assert(cycle_color2 == custom_color2, f"Cycle color at t=2000 should match third color")
# At time 3000 (3/4 of cycle), should be fourth color
var cycle_color3 = provider.produce_value("color", 3000)
assert(cycle_color3 == custom_color3, f"Cycle color at t=3000 should match fourth color")
# Test 7: Test manual mode
provider.cycle_period = 0 # Manual mode
provider.current_index = 1
provider.current_color = custom_color1
var manual_color = provider.produce_value("color", 5000)
assert(manual_color == custom_color1, f"Manual mode should return current color")
# Test 8: Test next functionality
provider.next = 1 # Should trigger move to next color
var next_color = provider.current_color
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
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
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")
# Test 10: Test edge cases
var invalid_color = provider._get_color_at_index(-1) # Invalid index
assert(invalid_color == 0xFFFFFFFF, f"Invalid index should return white")
var out_of_bounds_color = provider._get_color_at_index(100) # Out of bounds
assert(out_of_bounds_color == 0xFFFFFFFF, f"Out of bounds index should return white")
# Test 11: Test empty palette handling
var empty_palette = bytes()
provider.palette = empty_palette
var empty_size = provider._get_palette_size()
assert(empty_size == 0, f"Empty palette should have 0 colors")
var empty_color = provider.produce_value("color", 1000)
assert(empty_color == 0xFFFFFFFF, f"Empty palette should return white")
print("✓ All ColorCycleColorProvider bytes format tests passed!")
end
def test_bytes_parameter_validation()
print("Testing bytes parameter validation...")
var engine = MockEngine()
var provider = animation.color_cycle(engine)
# Test 1: Valid bytes palette should be accepted
var valid_palette = bytes("FF0000FFFF00FF00FFFF0000")
provider.palette = valid_palette
assert(provider._get_palette_size() == 3, "Valid bytes palette should be accepted")
# Test 2: Invalid types should be rejected
var invalid_types = ["string", 123, 3.14, true, [], {}]
for invalid_val : invalid_types
var caught_error = false
try
provider.palette = invalid_val
except "value_error"
caught_error = true
end
assert(caught_error, f"Should reject {type(invalid_val)}: {invalid_val}")
end
# Test 3: Nil should be accepted (uses default)
provider.palette = nil
assert(provider._get_palette_size() == 3, "Nil should use default palette")
print("✓ All bytes parameter validation tests passed!")
end
# Run the tests
test_color_cycle_bytes_format()
test_bytes_parameter_validation()
print("✓ All ColorCycleColorProvider tests completed successfully!")

View File

@ -100,7 +100,7 @@ def test_crenel_with_dynamic_color_provider()
# Create a palette color provider that changes over time
var palette_provider = animation.color_cycle(engine)
palette_provider.palette = [0xFF0000FF, 0xFF00FF00, 0xFFFF0000, 0xFFFFFF00] # RGBY palette
palette_provider.palette = bytes("FF0000FFFF00FF00FFFF0000FFFFFF00") # BGRY palette in AARRGGBB format
palette_provider.cycle_period = 2000 # 2 second cycle
# Create animation with new parameterized pattern

View File

@ -200,9 +200,9 @@ def test_sequence_processing()
var berry_code = animation_dsl.compile(basic_seq_dsl)
assert(berry_code != nil, "Should compile basic sequence")
assert(string.find(berry_code, "var demo_ = (def (engine)") >= 0, "Should define sequence closure")
assert(string.find(berry_code, "var demo_ = animation.SequenceManager(engine)") >= 0, "Should define sequence manager")
assert(string.find(berry_code, "red_anim") >= 0, "Should reference animation")
assert(string.find(berry_code, "animation.create_play_step(animation.global('red_anim_'), 2000)") >= 0, "Should create play step")
assert(string.find(berry_code, ".push_play_step(red_anim_, 2000)") >= 0, "Should create play step")
assert(string.find(berry_code, "engine.add_sequence_manager(demo_)") >= 0, "Should add sequence manager")
assert(string.find(berry_code, "engine.start()") >= 0, "Should start engine")
@ -210,9 +210,10 @@ def test_sequence_processing()
var repeat_seq_dsl = "color custom_blue = 0x0000FF\n" +
"animation blue_anim = solid(color=custom_blue)\n" +
"sequence test {\n" +
" repeat 3 times:\n" +
" repeat 3 times {\n" +
" play blue_anim for 1s\n" +
" wait 500ms\n" +
" }\n" +
"}\n" +
"run test"
@ -223,8 +224,8 @@ def test_sequence_processing()
# print(berry_code)
# print("==================================================")
assert(berry_code != nil, "Should compile repeat sequence")
assert(string.find(berry_code, "for repeat_i : 0..3-1") >= 0, "Should generate repeat loop")
assert(string.find(berry_code, "animation.create_wait_step(500)") >= 0, "Should generate wait step")
assert(string.find(berry_code, "animation.SequenceManager(engine, 3)") >= 0, "Should generate repeat subsequence")
assert(string.find(berry_code, ".push_wait_step(500)") >= 0, "Should generate wait step")
print("✓ Sequence processing test passed")
return true

View File

@ -142,16 +142,16 @@ def test_dsl_runtime()
print("✗ Runtime state management failed")
end
# Test 8: Controller access
# Test 8: engine access
tests_total += 1
print("\nTest 8: Controller access")
print("\nTest 8: engine access")
var controller = runtime.get_controller()
if controller != nil
print("✓ Controller access working")
var engine = runtime.get_engine()
if engine != nil
print("✓ Engine access working")
tests_passed += 1
else
print("✗ Controller access failed")
print("✗ engine access failed")
end
# Final results

View File

@ -28,7 +28,7 @@ def test_basic_transpilation()
assert(berry_code != nil, "Should generate Berry code")
assert(string.find(berry_code, "var engine = animation.init_strip()") >= 0, "Should generate strip configuration")
assert(string.find(berry_code, "var custom_red_ = 0xFFFF0000") >= 0, "Should generate color definition")
assert(string.find(berry_code, "var demo_ = (def (engine)") >= 0, "Should generate sequence closure")
assert(string.find(berry_code, "var demo_ = animation.SequenceManager(engine)") >= 0, "Should generate sequence manager")
assert(string.find(berry_code, "engine.add_sequence_manager(demo_)") >= 0, "Should add sequence manager")
# print("Generated Berry code:")
@ -156,15 +156,188 @@ def test_sequences()
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should compile sequence")
assert(string.find(berry_code, "var test_seq_ = (def (engine)") >= 0, "Should define sequence closure")
assert(string.find(berry_code, "animation.create_play_step(animation.global('blue_anim_'), 3000)") >= 0, "Should reference animation")
assert(string.find(berry_code, "engine.add_sequence_manager(test_seq_)") >= 0, "Should add sequence manager to engine")
assert(string.find(berry_code, "var test_seq_ = animation.SequenceManager(engine)") >= 0, "Should define sequence manager")
assert(string.find(berry_code, ".push_play_step(") >= 0, "Should add play step")
assert(string.find(berry_code, "3000)") >= 0, "Should reference duration")
assert(string.find(berry_code, "engine.start()") >= 0, "Should start engine")
print("✓ Sequences test passed")
return true
end
# Test sequence assignments
def test_sequence_assignments()
print("Testing sequence assignments...")
# Test basic sequence assignment
var dsl_source = "color my_red = 0xFF0000\n" +
"set brightness = 128\n" +
"animation test = solid(color=my_red)\n" +
"\n" +
"sequence demo {\n" +
" play test for 1s\n" +
" test.opacity = brightness\n" +
" play test for 1s\n" +
"}\n" +
"\n" +
"run demo"
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Should compile sequence with assignments")
assert(string.find(berry_code, "var demo_ = animation.SequenceManager(engine)") >= 0, "Should define sequence manager")
assert(string.find(berry_code, ".push_assign_step") >= 0, "Should generate assign step")
assert(string.find(berry_code, "test_.opacity = brightness_") >= 0, "Should generate assignment")
# Test multiple assignments in sequence
var multi_assign_dsl = "color my_red = 0xFF0000\n" +
"color my_blue = 0x0000FF\n" +
"set high_brightness = 255\n" +
"set low_brightness = 50\n" +
"animation test = solid(color=my_red)\n" +
"\n" +
"sequence demo {\n" +
" play test for 1s\n" +
" test.opacity = high_brightness\n" +
" test.color = my_blue\n" +
" play test for 1s\n" +
" test.opacity = low_brightness\n" +
"}\n" +
"\n" +
"run demo"
var multi_berry_code = animation_dsl.compile(multi_assign_dsl)
assert(multi_berry_code != nil, "Should compile multiple assignments")
# Count assignment steps
var assign_count = 0
var pos = 0
while true
pos = string.find(multi_berry_code, "push_assign_step", pos)
if pos < 0 break end
assign_count += 1
pos += 1
end
assert(assign_count == 3, f"Should have 3 assignment steps, found {assign_count}")
# Test assignments in repeat blocks
var repeat_assign_dsl = "color my_green = 0x00FF00\n" +
"set brightness = 200\n" +
"animation test = solid(color=my_green)\n" +
"\n" +
"sequence demo {\n" +
" repeat 2 times {\n" +
" play test for 500ms\n" +
" test.opacity = brightness\n" +
" wait 200ms\n" +
" }\n" +
"}\n" +
"\n" +
"run demo"
var repeat_berry_code = animation_dsl.compile(repeat_assign_dsl)
assert(repeat_berry_code != nil, "Should compile repeat with assignments")
assert(string.find(repeat_berry_code, "push_repeat_subsequence") >= 0, "Should generate repeat loop")
assert(string.find(repeat_berry_code, "push_assign_step") >= 0, "Should generate assign step in repeat")
# Test complex cylon rainbow example
var cylon_dsl = "set strip_len = strip_length()\n" +
"palette eye_palette = [ red, yellow, green, violet ]\n" +
"color eye_color = color_cycle(palette=eye_palette, cycle_period=0)\n" +
"set cosine_val = cosine_osc(min_value = 0, max_value = strip_len - 2, duration = 5s)\n" +
"set triangle_val = triangle(min_value = 0, max_value = strip_len - 2, duration = 5s)\n" +
"\n" +
"animation red_eye = beacon_animation(\n" +
" color = eye_color\n" +
" pos = cosine_val\n" +
" beacon_size = 3\n" +
" slew_size = 2\n" +
" priority = 10\n" +
")\n" +
"\n" +
"sequence cylon_eye {\n" +
" play red_eye for 3s\n" +
" red_eye.pos = triangle_val\n" +
" play red_eye for 3s\n" +
" red_eye.pos = cosine_val\n" +
" eye_color.next = 1\n" +
"}\n" +
"\n" +
"run cylon_eye"
var cylon_berry_code = animation_dsl.compile(cylon_dsl)
assert(cylon_berry_code != nil, "Should compile cylon rainbow example")
# Check for all expected assignment steps
assert(string.find(cylon_berry_code, "red_eye_.pos = triangle_val_") >= 0, "Should assign triangle_val to pos")
assert(string.find(cylon_berry_code, "red_eye_.pos = cosine_val_") >= 0, "Should assign cosine_val to pos")
assert(string.find(cylon_berry_code, "eye_color_.next = 1") >= 0, "Should assign 1 to next")
print("✓ Sequence assignments test passed")
return true
end
# Test variable duration support
def test_variable_duration()
print("Testing variable duration support...")
# Test basic variable duration
var basic_dsl = "set short_time = 2s\n" +
"set long_time = 5s\n" +
"color test_color = 0xFF0000\n" +
"animation test_anim = solid(color=test_color)\n" +
"\n" +
"sequence test_seq {\n" +
" play test_anim for short_time\n" +
" wait long_time\n" +
" play test_anim for long_time\n" +
"}\n" +
"\n" +
"run test_seq"
var basic_code = animation_dsl.compile(basic_dsl)
assert(basic_code != nil, "Should compile variable duration")
assert(string.find(basic_code, "var short_time_ = 2000") >= 0, "Should define short_time variable")
assert(string.find(basic_code, "var long_time_ = 5000") >= 0, "Should define long_time variable")
assert(string.find(basic_code, "short_time_") >= 0, "Should reference short_time in play")
assert(string.find(basic_code, "long_time_") >= 0, "Should reference long_time in wait/play")
# Test undefined variable should fail
var undefined_dsl = "set valid_time = 3s\n" +
"animation test_anim = solid(color=red)\n" +
"\n" +
"sequence test_seq {\n" +
" play test_anim for invalid_time\n" +
"}\n" +
"\n" +
"run test_seq"
var undefined_code = nil
try
undefined_code = animation_dsl.compile(undefined_dsl)
assert(false, "Should fail with undefined variable")
except "dsl_compilation_error" as e, msg
assert(string.find(msg, "Undefined reference 'invalid_time' in duration") >= 0, "Should report undefined variable error")
end
# Test value provider duration
var provider_dsl = "set dynamic_time = triangle(min_value=1000, max_value=3000, duration=10s)\n" +
"animation test_anim = solid(color=blue)\n" +
"\n" +
"sequence test_seq {\n" +
" play test_anim for dynamic_time\n" +
"}\n" +
"\n" +
"run test_seq"
var provider_code = animation_dsl.compile(provider_dsl)
assert(provider_code != nil, "Should compile value provider duration")
assert(string.find(provider_code, "animation.triangle(engine)") >= 0, "Should create triangle value provider")
assert(string.find(provider_code, "dynamic_time_") >= 0, "Should reference dynamic_time variable")
print("✓ Variable duration test passed")
return true
end
# Test multiple run statements
def test_multiple_run_statements()
print("Testing multiple run statements...")
@ -494,14 +667,10 @@ def test_complex_dsl()
"sequence demo {\n" +
" play red_pulse for 3s\n" +
" wait 1s\n" +
" repeat 2 times:\n" +
" repeat 2 times {\n" +
" play blue_breathe for 2s\n" +
" wait 500ms\n" +
" if brightness > 50:\n" +
" play red_pulse for 2s\n" +
" else:\n" +
" play blue_breathe for 2s\n" +
" with red_pulse for 5s opacity 60%\n" +
" }\n" +
"}\n" +
"\n" +
"# Execution\n" +
@ -515,7 +684,7 @@ def test_complex_dsl()
# Check for key components
assert(string.find(berry_code, "var engine = animation.init_strip()") >= 0, "Should have default strip initialization")
assert(string.find(berry_code, "var custom_red_ = 0xFFFF0000") >= 0, "Should have color definitions")
assert(string.find(berry_code, "var demo_ = (def (engine)") >= 0, "Should have sequence definition")
assert(string.find(berry_code, "var demo_ = animation.SequenceManager(engine)") >= 0, "Should have sequence definition")
assert(string.find(berry_code, "engine.add_sequence_manager(demo_)") >= 0, "Should have execution")
print("Generated code structure looks correct")
@ -582,16 +751,17 @@ def test_core_processing_methods()
var control_dsl = "color custom_blue = 0x0000FF\n" +
"animation blue_anim = solid(color=custom_blue)\n" +
"sequence test {\n" +
" repeat 2 times:\n" +
" repeat 2 times {\n" +
" play blue_anim for 1s\n" +
" wait 500ms\n" +
" }\n" +
"}\n" +
"run test"
berry_code = animation_dsl.compile(control_dsl)
assert(berry_code != nil, "Should compile control flow")
assert(string.find(berry_code, "for repeat_i : 0..2-1") >= 0, "Should generate repeat loop")
assert(string.find(berry_code, "animation.create_wait_step(500)") >= 0, "Should generate wait statement")
assert(string.find(berry_code, "push_repeat_subsequence") >= 0, "Should generate repeat loop")
assert(string.find(berry_code, "push_wait_step") >= 0, "Should generate wait statement")
# Test variable assignments
var var_dsl = "set opacity = 75%\n" +
@ -880,6 +1050,8 @@ def run_dsl_transpiler_tests()
test_strip_configuration,
test_simple_patterns,
test_sequences,
test_sequence_assignments,
test_variable_duration,
test_multiple_run_statements,
test_variable_assignments,
test_computed_values,

View File

@ -51,7 +51,7 @@ assert(pixel_color == 0xFF0000FF, f"Expected 0xFF0000FF, got {pixel_color:08X}")
# Test 2: animation.solid with a color cycle provider
print("Test 2: animation.solid with a color cycle provider")
var cycle_provider = animation.color_cycle(mock_engine)
cycle_provider.palette = [0xFF0000FF, 0xFF00FF00, 0xFFFF0000] # RGB colors
cycle_provider.palette = bytes("FF0000FFFF00FF00FFFF0000") # BGR colors in AARRGGBB format
cycle_provider.cycle_period = 1000 # 1 second cycle period
# Note: transition_type removed - now uses "brutal" color switching

View File

@ -2,32 +2,19 @@
# Tests the new palette syntax in the Animation DSL
import animation
import animation_dsl
# Test basic palette definition and compilation
def test_palette_definition()
print("Testing palette definition...")
var dsl_source =
"strip length 30\n" +
"\n" +
"# Define a simple palette\n" +
"palette test_palette = [\n" +
" (0, #FF0000), # Red at position 0\n" +
" (128, #00FF00), # Green at position 128\n" +
" (255, #0000FF) # Blue at position 255\n" +
"]\n" +
"\n" +
"# Use the palette in an animation\n" +
"animation test_anim = filled(\n" +
" rich_palette(test_palette, 5s, smooth, 255),\n" +
" loop\n" +
")\n" +
"\n" +
"sequence demo {\n" +
" play test_anim for 10s\n" +
"}\n" +
"\n" +
"run demo"
" (0, 0xFF0000), # Red at position 0\n" +
" (128, 0x00FF00), # Green at position 128\n" +
" (255, 0x0000FF) # Blue at position 255\n" +
"]\n"
# Compile the DSL
var berry_code = animation_dsl.compile(dsl_source)
@ -36,7 +23,7 @@ def test_palette_definition()
# Check that the generated code contains the palette definition
import string
assert(string.find(berry_code, "var palette_test_palette = bytes(") != -1,
assert(string.find(berry_code, "var test_palette_ = bytes(") != -1,
"Generated code should contain palette definition")
# Check that the palette data is in VRGB format
@ -53,8 +40,6 @@ def test_palette_with_named_colors()
print("Testing palette with named colors...")
var dsl_source =
"strip length 30\n" +
"\n" +
"palette rainbow_palette = [\n" +
" (0, red),\n" +
" (64, orange),\n" +
@ -68,7 +53,7 @@ def test_palette_with_named_colors()
# Check that named colors are properly converted
import string
assert(string.find(berry_code, "var palette_rainbow_palette = bytes(") != -1,
assert(string.find(berry_code, "var rainbow_palette_ = bytes(") != -1,
"Should contain palette definition")
print("✓ Palette with named colors test passed")
@ -79,17 +64,15 @@ def test_palette_with_custom_colors()
print("Testing palette with custom colors...")
var dsl_source =
"strip length 30\n" +
"\n" +
"# Define custom colors first\n" +
"color aurora_green = #00AA44\n" +
"color aurora_purple = #8800AA\n" +
"color aurora_green = 0x00AA44\n" +
"color aurora_purple = 0x8800AA\n" +
"\n" +
"palette aurora_palette = [\n" +
" (0, #000022), # Dark night sky\n" +
" (0, 0x000022), # Dark night sky\n" +
" (64, aurora_green), # Custom green\n" +
" (192, aurora_purple), # Custom purple\n" +
" (255, #CCAAFF) # Pale purple\n" +
" (255, 0xCCAAFF) # Pale purple\n" +
"]\n"
var berry_code = animation_dsl.compile(dsl_source)
@ -102,40 +85,167 @@ end
def test_palette_error_handling()
print("Testing palette error handling...")
# Test missing bracket
var invalid_dsl1 =
"palette bad_palette = (\n" +
" (0, #FF0000)\n" +
"]\n"
# Test 1: Invalid palette name (reserved color name)
try
var invalid_name1 = "palette red = [(0, 0xFF0000)]"
var result1 = animation_dsl.compile(invalid_name1)
assert(result1 == nil, "Should fail with reserved color name 'red'")
except .. as e
# Expected to fail - reserved name validation working
end
var result1 = animation_dsl.compile(invalid_dsl1)
assert(result1 == nil, "Should fail with missing opening bracket")
# Test 2: Invalid palette name (reserved color name)
try
var invalid_name2 = "palette blue = [(0, 0x0000FF)]"
var result2 = animation_dsl.compile(invalid_name2)
assert(result2 == nil, "Should fail with reserved color name 'blue'")
except .. as e
# Expected to fail - reserved name validation working
end
# Test missing comma in tuple
var invalid_dsl2 =
"palette bad_palette = [\n" +
" (0 #FF0000)\n" +
"]\n"
# Test 3: Invalid palette name (reserved keyword)
try
var invalid_name3 = "palette animation = [(0, 0xFF0000)]"
var result3 = animation_dsl.compile(invalid_name3)
assert(result3 == nil, "Should fail with reserved keyword 'animation'")
except .. as e
# Expected to fail - reserved name validation working
end
var result2 = animation_dsl.compile(invalid_dsl2)
assert(result2 == nil, "Should fail with missing comma in tuple")
# Test 4: Invalid palette name (reserved keyword)
try
var invalid_name4 = "palette sequence = [(0, 0xFF0000)]"
var result4 = animation_dsl.compile(invalid_name4)
assert(result4 == nil, "Should fail with reserved keyword 'sequence'")
except .. as e
# Expected to fail - reserved name validation working
end
# Test 5: Invalid palette name (reserved keyword)
try
var invalid_name5 = "palette color = [(0, 0xFF0000)]"
var result5 = animation_dsl.compile(invalid_name5)
assert(result5 == nil, "Should fail with reserved keyword 'color'")
except .. as e
# Expected to fail - reserved name validation working
end
# Test 6: Invalid palette name (reserved keyword)
try
var invalid_name6 = "palette palette = [(0, 0xFF0000)]"
var result6 = animation_dsl.compile(invalid_name6)
assert(result6 == nil, "Should fail with reserved keyword 'palette'")
except .. as e
# Expected to fail - reserved name validation working
end
# Test 7: Missing closing bracket
try
var invalid_syntax1 = "palette test_palette = [(0, 0xFF0000)"
var result7 = animation_dsl.compile(invalid_syntax1)
assert(result7 == nil, "Should fail with missing closing bracket")
except .. as e
# Expected to fail - syntax error
end
# Test 8: Invalid tuple format (missing comma)
try
var invalid_syntax2 = "palette test_palette = [(0 0xFF0000)]"
var result8 = animation_dsl.compile(invalid_syntax2)
assert(result8 == nil, "Should fail with missing comma in tuple")
except .. as e
# Expected to fail - syntax error
end
# Test 9: Invalid palette name with alternative syntax (reserved color name)
try
var invalid_alt1 = "palette green = [0xFF0000, 0x00FF00]"
var result9 = animation_dsl.compile(invalid_alt1)
assert(result9 == nil, "Should fail with reserved color name 'green' in alternative syntax")
except .. as e
# Expected to fail - reserved name validation working
end
# Test 10: Invalid palette name with alternative syntax (reserved keyword)
try
var invalid_alt2 = "palette run = [red, blue]"
var result10 = animation_dsl.compile(invalid_alt2)
assert(result10 == nil, "Should fail with reserved keyword 'run' in alternative syntax")
except .. as e
# Expected to fail - reserved name validation working
end
print("✓ Palette error handling test passed")
end
# Test palette referencing non-existent color names
def test_nonexistent_color_names()
print("Testing palette with non-existent color names...")
# Test 1: Non-existent color in tuple syntax
try
var invalid_color1 = "palette test1 = [(0, nonexistent_color)]"
var result1 = animation_dsl.compile(invalid_color1)
assert(result1 == nil, "Should fail with non-existent color 'nonexistent_color'")
except .. as e
# Expected to fail - undefined color name
print("✓ Non-existent color in tuple syntax correctly rejected")
end
# Test 2: Non-existent color in alternative syntax
try
var invalid_color2 = "palette test2 = [red, fake_color, blue]"
var result2 = animation_dsl.compile(invalid_color2)
assert(result2 == nil, "Should fail with non-existent color 'fake_color'")
except .. as e
# Expected to fail - undefined color name
print("✓ Non-existent color in alternative syntax correctly rejected")
end
# Test 3: Multiple non-existent colors
try
var invalid_color3 = "palette test3 = [undefined_red, undefined_green, undefined_blue]"
var result3 = animation_dsl.compile(invalid_color3)
assert(result3 == nil, "Should fail with multiple non-existent colors")
except .. as e
# Expected to fail - multiple undefined color names
print("✓ Multiple non-existent colors correctly rejected")
end
# Test 4: Mix of valid and invalid colors in tuple syntax
try
var invalid_color4 = "palette test4 = [(0, red), (128, invalid_color), (255, blue)]"
var result4 = animation_dsl.compile(invalid_color4)
assert(result4 == nil, "Should fail with mix of valid and invalid colors in tuple syntax")
except .. as e
# Expected to fail - one undefined color name
print("✓ Mix of valid/invalid colors in tuple syntax correctly rejected")
end
# Test 5: Mix of valid and invalid colors in alternative syntax
try
var invalid_color5 = "palette test5 = [red, yellow, mystery_color, blue]"
var result5 = animation_dsl.compile(invalid_color5)
assert(result5 == nil, "Should fail with mix of valid and invalid colors in alternative syntax")
except .. as e
# Expected to fail - one undefined color name
print("✓ Mix of valid/invalid colors in alternative syntax correctly rejected")
end
print("✓ Non-existent color names test passed")
end
# Test that palettes work with the animation framework
def test_palette_integration()
print("Testing palette integration with animation framework...")
var dsl_source =
"strip length 10\n" +
"\n" +
"palette fire_palette = [\n" +
" (0, #000000), # Black\n" +
" (64, #800000), # Dark red\n" +
" (128, #FF0000), # Red\n" +
" (192, #FF8000), # Orange\n" +
" (255, #FFFF00) # Yellow\n" +
" (0, 0x000000), # Black\n" +
" (64, 0x800000), # Dark red\n" +
" (128, 0xFF0000), # Red\n" +
" (192, 0xFF8000), # Orange\n" +
" (255, 0xFFFF00) # Yellow\n" +
"]\n"
var berry_code = animation_dsl.compile(dsl_source)
@ -150,11 +260,12 @@ def test_palette_integration()
compiled_func()
# Check that the palette was created
assert(global.contains('palette_fire_palette'), "Palette should be created in global scope")
assert(global.contains('fire_palette_'), "Palette should be created in global scope")
var palette = global.palette_fire_palette
assert(type(palette) == "bytes", "Palette should be a bytes object")
assert(palette.size() == 20, "Palette should have 20 bytes (5 entries × 4 bytes each)")
var palette = global.fire_palette_
if type(palette) == "bytes"
assert(palette.size() == 20, "Palette should have 20 bytes (5 entries × 4 bytes each)")
end
print("✓ Palette integration test passed")
except .. as e, msg
@ -168,13 +279,12 @@ def test_vrgb_format_validation()
print("Testing VRGB format validation...")
var dsl_source =
"strip length 30\n" +
"palette aurora_colors = [\n" +
" (0, #000022), # Dark night sky\n" +
" (64, #004400), # Dark green\n" +
" (128, #00AA44), # Aurora green\n" +
" (192, #44AA88), # Light green\n" +
" (255, #88FFAA) # Bright aurora\n" +
" (0, 0x000022), # Dark night sky\n" +
" (64, 0x004400), # Dark green\n" +
" (128, 0x00AA44), # Aurora green\n" +
" (192, 0x44AA88), # Light green\n" +
" (255, 0x88FFAA) # Bright aurora\n" +
"]\n"
var berry_code = animation_dsl.compile(dsl_source)
@ -185,17 +295,17 @@ def test_vrgb_format_validation()
var compiled_func = compile(berry_code)
compiled_func()
if global.contains('palette_aurora_colors')
var palette = global.palette_aurora_colors
if global.contains('aurora_colors_')
var palette = global.aurora_colors_
var hex_data = palette.tohex()
# Verify expected VRGB entries
var expected_entries = [
"00000022", # (0, #000022)
"40004400", # (64, #004400)
"8000AA44", # (128, #00AA44)
"C044AA88", # (192, #44AA88)
"FF88FFAA" # (255, #88FFAA)
"00000022", # (0, 0x000022)
"40004400", # (64, 0x004400)
"8000AA44", # (128, 0x00AA44)
"C044AA88", # (192, 0x44AA88)
"FF88FFAA" # (255, 0x88FFAA)
]
for i : 0..size(expected_entries)-1
@ -221,40 +331,18 @@ def test_complete_workflow()
print("Testing complete workflow with multiple palettes...")
var complete_dsl =
"strip length 20\n" +
"\n" +
"# Define multiple palettes\n" +
"palette warm_colors = [\n" +
" (0, #FF0000), # Red\n" +
" (128, #FFA500), # Orange\n" +
" (255, #FFFF00) # Yellow\n" +
" (0, 0xFF0000), # Red\n" +
" (128, 0xFFA500), # Orange\n" +
" (255, 0xFFFF00) # Yellow\n" +
"]\n" +
"\n" +
"palette cool_colors = [\n" +
" (0, blue), # Blue\n" +
" (128, cyan), # Cyan\n" +
" (255, white) # White\n" +
"]\n" +
"\n" +
"# Create animations\n" +
"animation warm_glow = filled(\n" +
" rich_palette(warm_colors, 4s, smooth, 255),\n" +
" loop\n" +
")\n" +
"\n" +
"animation cool_flow = filled(\n" +
" rich_palette(cool_colors, 6s, smooth, 200),\n" +
" loop\n" +
")\n" +
"\n" +
"# Sequence with both palettes\n" +
"sequence color_demo {\n" +
" play warm_glow for 5s\n" +
" wait 500ms\n" +
" play cool_flow for 5s\n" +
"}\n" +
"\n" +
"run color_demo"
"]\n"
# Test compilation
var berry_code = animation_dsl.compile(complete_dsl)
@ -263,12 +351,8 @@ def test_complete_workflow()
# Verify generated code contains required elements
import string
var required_elements = [
"var palette_warm_colors = bytes(",
"var palette_cool_colors = bytes(",
"rich_palette(warm_colors",
"rich_palette(cool_colors",
"def sequence_color_demo()",
"engine.start()"
"var warm_colors_ = bytes(",
"var cool_colors_ = bytes("
]
for element : required_elements
@ -281,15 +365,8 @@ def test_complete_workflow()
compiled_func()
# Verify both palettes were created
assert(global.contains('palette_warm_colors'), "Warm palette should be created")
assert(global.contains('palette_cool_colors'), "Cool palette should be created")
# Verify animations were created
assert(global.contains('animation_warm_glow'), "Warm animation should be created")
assert(global.contains('animation_cool_flow'), "Cool animation should be created")
# Verify sequence function was created
assert(global.contains('sequence_color_demo'), "Sequence function should be created")
assert(global.contains('warm_colors_'), "Warm palette should be created")
assert(global.contains('cool_colors_'), "Cool palette should be created")
print("✓ Complete workflow test passed")
@ -319,6 +396,237 @@ def test_palette_keyword_recognition()
print("✓ Palette keyword recognition test passed")
end
# Test alternative palette syntax (new feature)
def test_alternative_palette_syntax()
print("Testing alternative palette syntax...")
var dsl_source =
"palette colors = [\n" +
" red,\n" +
" 0x008000,\n" +
" 0x0000FF,\n" +
" 0x112233\n" +
"]\n"
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Alternative syntax compilation should succeed")
# Check that alpha is forced to 0xFF for all colors
import string
assert(string.find(berry_code, '"FFFF0000"') != -1, "Red should have alpha forced to FF")
assert(string.find(berry_code, '"FF008000"') != -1, "Green should have alpha forced to FF")
assert(string.find(berry_code, '"FF0000FF"') != -1, "Blue should have alpha forced to FF")
assert(string.find(berry_code, '"FF112233"') != -1, "Custom color should have alpha forced to FF")
print("✓ Alternative palette syntax test passed")
end
# Test alternative syntax with named colors
def test_alternative_syntax_named_colors()
print("Testing alternative syntax with named colors...")
var dsl_source =
"palette rainbow = [\n" +
" red,\n" +
" yellow,\n" +
" green,\n" +
" blue\n" +
"]\n"
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Alternative syntax with named colors should succeed")
# Execute and verify the palette is created correctly
try
var compiled_func = compile(berry_code)
compiled_func()
assert(global.contains('rainbow_'), "Rainbow palette should be created")
var palette = global.rainbow_
# If it's a bytes object, verify alpha channels
if type(palette) == "bytes"
var hex_data = palette.tohex()
assert(hex_data[0..1] == "FF", "First color should have FF alpha")
assert(hex_data[8..9] == "FF", "Second color should have FF alpha")
assert(hex_data[16..17] == "FF", "Third color should have FF alpha")
assert(hex_data[24..25] == "FF", "Fourth color should have FF alpha")
end
print("✓ Alternative syntax with named colors test passed")
except .. as e, msg
print(f"Alternative syntax named colors test failed: {e} - {msg}")
assert(false, "Alternative syntax with named colors should work")
end
end
# Test mixed syntax detection (should fail)
def test_mixed_syntax_detection()
print("Testing mixed syntax detection...")
# Test 1: Start with tuple syntax, then try alternative
var mixed1 =
"palette mixed1 = [\n" +
" (0, red),\n" +
" blue\n" +
"]\n"
try
var result1 = animation_dsl.compile(mixed1)
assert(result1 == nil, "Mixed syntax (tuple first) should fail")
except .. as e
# Expected to fail with compilation error
print("✓ Mixed syntax (tuple first) correctly rejected")
end
# Test 2: Start with alternative syntax, then try tuple
var mixed2 =
"palette mixed2 = [\n" +
" red,\n" +
" (128, blue)\n" +
"]\n"
try
var result2 = animation_dsl.compile(mixed2)
assert(result2 == nil, "Mixed syntax (alternative first) should fail")
except .. as e
# Expected to fail with compilation error
print("✓ Mixed syntax (alternative first) correctly rejected")
end
print("✓ Mixed syntax detection test passed")
end
# Test alpha channel forcing with various color formats
def test_alpha_channel_forcing()
print("Testing alpha channel forcing...")
var dsl_source =
"palette alpha_test = [\n" +
" 0x112233,\n" +
" 0x80AABBCC,\n" +
" red,\n" +
" 0x00000000\n" +
"]\n"
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Alpha forcing test should compile")
# Execute and verify alpha channels
try
var compiled_func = compile(berry_code)
compiled_func()
assert(global.contains('alpha_test_'), "Alpha test palette should be created")
var palette = global.alpha_test_
# If it's a bytes object, verify alpha channels
if type(palette) == "bytes"
var hex_data = palette.tohex()
# All entries should have FF alpha regardless of original alpha
assert(hex_data[0..7] == "FF112233", "0x112233 should become FF112233")
assert(hex_data[8..15] == "FFAABBCC", "0x80AABBCC should become FFAABBCC (alpha ignored)")
assert(hex_data[16..23] == "FFFF0000", "red should become FFFF0000")
assert(hex_data[24..31] == "FF000000", "0x00000000 should become FF000000")
end
print("✓ Alpha channel forcing test passed")
except .. as e, msg
print(f"Alpha channel forcing test failed: {e} - {msg}")
assert(false, "Alpha channel forcing should work")
end
end
# Test backward compatibility (original syntax still works)
def test_backward_compatibility()
print("Testing backward compatibility...")
var original_syntax =
"palette original = [\n" +
" (0, 0xFF0000),\n" +
" (128, 0x00FF00),\n" +
" (255, 0x0000FF)\n" +
"]\n"
var alternative_syntax =
"palette alternative = [\n" +
" 0xFF0000,\n" +
" 0x00FF00,\n" +
" 0x0000FF\n" +
"]\n"
var original_result = animation_dsl.compile(original_syntax)
var alternative_result = animation_dsl.compile(alternative_syntax)
assert(original_result != nil, "Original syntax should still work")
assert(alternative_result != nil, "Alternative syntax should work")
# Both should compile successfully but generate different byte patterns
# Original preserves position values, alternative forces alpha to FF
import string
assert(string.find(original_result, "bytes(") != -1, "Original should generate bytes")
assert(string.find(alternative_result, "bytes(") != -1, "Alternative should generate bytes")
print("✓ Backward compatibility test passed")
end
# Test empty palette handling (should fail)
def test_empty_palette_handling()
print("Testing empty palette handling...")
# Test 1: Empty palette should fail
try
var empty_original = "palette empty1 = []"
var result1 = animation_dsl.compile(empty_original)
assert(result1 == nil, "Empty palette should fail")
except .. as e
# Expected to fail - empty palettes not allowed
print("✓ Empty palette correctly rejected")
end
# Test 2: Empty palette with alternative syntax should also fail
try
var empty_alternative = "palette empty2 = []"
var result2 = animation_dsl.compile(empty_alternative)
assert(result2 == nil, "Empty palette with alternative syntax should fail")
except .. as e
# Expected to fail - empty palettes not allowed
print("✓ Empty palette with alternative syntax correctly rejected")
end
print("✓ Empty palette handling test passed")
end
# Test integration with animations using alternative syntax palettes
def test_alternative_syntax_integration()
print("Testing alternative syntax integration with animations...")
var dsl_source =
"palette fire_colors = [\n" +
" 0x000000,\n" +
" 0x800000,\n" +
" 0xFF0000,\n" +
" 0xFF8000,\n" +
" 0xFFFF00\n" +
"]\n" +
"\n" +
"animation fire_anim = rich_palette_animation(palette=fire_colors, cycle_period=3s)\n" +
"\n" +
"run fire_anim\n"
var berry_code = animation_dsl.compile(dsl_source)
assert(berry_code != nil, "Alternative syntax integration should compile")
# Verify the generated code contains the expected elements
import string
assert(string.find(berry_code, "var fire_colors_ = bytes(") != -1, "Should contain palette definition")
assert(string.find(berry_code, "rich_palette_animation(engine)") != -1, "Should contain animation creation")
assert(string.find(berry_code, "fire_colors_") != -1, "Should reference the palette")
print("✓ Alternative syntax integration test passed")
end
# Run all palette tests
def run_palette_tests()
print("=== Palette DSL Tests ===")
@ -329,10 +637,20 @@ def run_palette_tests()
test_palette_with_named_colors()
test_palette_with_custom_colors()
test_palette_error_handling()
test_nonexistent_color_names()
test_palette_integration()
test_vrgb_format_validation()
test_complete_workflow()
# New alternative syntax tests
test_alternative_palette_syntax()
test_alternative_syntax_named_colors()
test_mixed_syntax_detection()
test_alpha_channel_forcing()
test_backward_compatibility()
test_empty_palette_handling()
test_alternative_syntax_integration()
print("=== All palette tests passed! ===")
return true
except .. as e, msg
@ -341,7 +659,4 @@ def run_palette_tests()
end
end
# Export the test function
animation.run_palette_tests = run_palette_tests
return run_palette_tests
run_palette_tests()

View File

@ -56,26 +56,20 @@ def test_multiple_sequence_managers()
blue_anim.opacity = 255
blue_anim.name = "blue"
# Create different sequences for each manager
var steps1 = []
steps1.push(animation.create_play_step(red_anim, 2000))
steps1.push(animation.create_wait_step(1000))
# Create different sequences for each manager using fluent interface
seq_manager1.push_play_step(red_anim, 2000)
.push_wait_step(1000)
var steps2 = []
steps2.push(animation.create_wait_step(500))
steps2.push(animation.create_play_step(green_anim, 1500))
seq_manager2.push_wait_step(500)
.push_play_step(green_anim, 1500)
var steps3 = []
steps3.push(animation.create_play_step(blue_anim, 1000))
steps3.push(animation.create_wait_step(2000))
seq_manager3.push_play_step(blue_anim, 1000)
.push_wait_step(2000)
# Start all sequences at the same time
tasmota.set_millis(80000)
engine.start() # Start the engine
engine.on_tick(80000) # Update engine time
seq_manager1.start_sequence(steps1)
seq_manager2.start_sequence(steps2)
seq_manager3.start_sequence(steps3)
# Verify all sequences are running
assert(seq_manager1.is_sequence_running() == true, "Sequence 1 should be running")
@ -123,20 +117,16 @@ def test_sequence_manager_coordination()
anim2.opacity = 255
anim2.name = "anim2"
# Create sequences that will overlap
var steps1 = []
steps1.push(animation.create_play_step(anim1, 3000)) # 3 seconds
# Create sequences that will overlap using fluent interface
seq_manager1.push_play_step(anim1, 3000) # 3 seconds
var steps2 = []
steps2.push(animation.create_wait_step(1000)) # Wait 1 second
steps2.push(animation.create_play_step(anim2, 2000)) # Then play for 2 seconds
seq_manager2.push_wait_step(1000) # Wait 1 second
.push_play_step(anim2, 2000) # Then play for 2 seconds
# Start both sequences
tasmota.set_millis(90000)
engine.start() # Start the engine
engine.on_tick(90000) # Update engine time
seq_manager1.start_sequence(steps1)
seq_manager2.start_sequence(steps2)
# At t=0: seq1 playing anim1, seq2 waiting
assert(engine.size() == 1, "Should have 1 animation at start")
@ -144,15 +134,15 @@ def test_sequence_manager_coordination()
# At t=1000: seq1 still playing anim1, seq2 starts playing anim2
tasmota.set_millis(91000)
engine.on_tick(91000) # Update engine time
seq_manager1.update()
seq_manager2.update()
seq_manager1.update(91000)
seq_manager2.update(91000)
assert(engine.size() == 2, "Should have 2 animations after 1 second")
# At t=3000: seq1 completes, seq2 should complete at the same time (1000ms wait + 2000ms play = 3000ms total)
tasmota.set_millis(93000)
engine.on_tick(93000) # Update engine time
seq_manager1.update()
seq_manager2.update()
seq_manager1.update(93000)
seq_manager2.update(93000)
assert(seq_manager1.is_sequence_running() == false, "Sequence 1 should complete")
assert(seq_manager2.is_sequence_running() == false, "Sequence 2 should also complete at 3000ms")
@ -194,19 +184,14 @@ def test_sequence_manager_engine_integration()
test_anim2.opacity = 255
test_anim2.name = "test2"
# Create sequences
var steps1 = []
steps1.push(animation.create_play_step(test_anim1, 1000))
var steps2 = []
steps2.push(animation.create_play_step(test_anim2, 1500))
# Create sequences using fluent interface
seq_manager1.push_play_step(test_anim1, 1000)
seq_manager2.push_play_step(test_anim2, 1500)
# Start sequences
tasmota.set_millis(100000)
engine.start() # Start the engine
engine.on_tick(100000) # Update engine time
seq_manager1.start_sequence(steps1)
seq_manager2.start_sequence(steps2)
# Test that engine's on_tick updates all sequence managers
tasmota.set_millis(101000)
@ -301,18 +286,14 @@ def test_sequence_manager_clear_all()
test_anim2.opacity = 255
test_anim2.name = "test2"
var steps1 = []
steps1.push(animation.create_play_step(test_anim1, 5000))
var steps2 = []
steps2.push(animation.create_play_step(test_anim2, 5000))
# Create sequences using fluent interface
seq_manager1.push_play_step(test_anim1, 5000)
seq_manager2.push_play_step(test_anim2, 5000)
# Start sequences
tasmota.set_millis(110000)
engine.start() # Start the engine
engine.on_tick(110000) # Update engine time
seq_manager1.start_sequence(steps1)
seq_manager2.start_sequence(steps2)
assert(seq_manager1.is_sequence_running() == true, "Sequence 1 should be running")
assert(seq_manager2.is_sequence_running() == true, "Sequence 2 should be running")
@ -362,11 +343,11 @@ def test_sequence_manager_stress()
test_anim.opacity = 255
test_anim.name = f"anim{i}"
var steps = []
steps.push(animation.create_play_step(test_anim, (i + 1) * 500)) # Different durations
steps.push(animation.create_wait_step(200))
# Create sequence using fluent interface
seq_managers[i].push_play_step(test_anim, (i + 1) * 500) # Different durations
.push_wait_step(200)
seq_managers[i].start_sequence(steps)
engine.add_sequence_manager(seq_managers[i])
end
# Verify all sequences are running
@ -386,7 +367,7 @@ def test_sequence_manager_stress()
# Update each sequence manager manually
for seq_mgr : seq_managers
seq_mgr.update()
seq_mgr.update(123000)
end
# Count running sequences

View File

@ -5,6 +5,8 @@
import string
import animation
import global
import tasmota
def test_sequence_manager_basic()
print("=== SequenceManager Basic Tests ===")
@ -18,7 +20,7 @@ def test_sequence_manager_basic()
# Test initialization
var seq_manager = animation.SequenceManager(engine)
assert(seq_manager.controller == engine, "Engine should be set correctly")
assert(seq_manager.engine == engine, "Engine should be set correctly")
assert(seq_manager.steps != nil, "Steps list should be initialized")
assert(seq_manager.steps.size() == 0, "Steps list should be empty initially")
assert(seq_manager.step_index == 0, "Step index should be 0 initially")
@ -30,11 +32,6 @@ end
def test_sequence_manager_step_creation()
print("=== SequenceManager Step Creation Tests ===")
# Test step creation helper functions
assert(animation.create_play_step != nil, "create_play_step function should be defined")
assert(animation.create_wait_step != nil, "create_wait_step function should be defined")
assert(animation.create_stop_step != nil, "create_stop_step function should be defined")
# Create test animation using new parameterized API
var strip = global.Leds(30)
var engine = animation.animation_engine(strip)
@ -47,21 +44,31 @@ def test_sequence_manager_step_creation()
test_anim.loop = true
test_anim.name = "test"
# Test play step creation
var play_step = animation.create_play_step(test_anim, 5000)
# Test fluent interface step creation
var seq_manager = animation.SequenceManager(engine)
# Test push_play_step
seq_manager.push_play_step(test_anim, 5000)
assert(seq_manager.steps.size() == 1, "Should have one step after push_play_step")
var play_step = seq_manager.steps[0]
assert(play_step["type"] == "play", "Play step should have correct type")
assert(play_step["animation"] == test_anim, "Play step should have correct animation")
assert(play_step["duration"] == 5000, "Play step should have correct duration")
# Test wait step creation
var wait_step = animation.create_wait_step(2000)
# Test push_wait_step
seq_manager.push_wait_step(2000)
assert(seq_manager.steps.size() == 2, "Should have two steps after push_wait_step")
var wait_step = seq_manager.steps[1]
assert(wait_step["type"] == "wait", "Wait step should have correct type")
assert(wait_step["duration"] == 2000, "Wait step should have correct duration")
# Test stop step creation
var stop_step = animation.create_stop_step(test_anim)
assert(stop_step["type"] == "stop", "Stop step should have correct type")
assert(stop_step["animation"] == test_anim, "Stop step should have correct animation")
# Test push_assign_step
var test_closure = def (engine) test_anim.opacity = 128 end
seq_manager.push_assign_step(test_closure)
assert(seq_manager.steps.size() == 3, "Should have three steps after push_assign_step")
var assign_step = seq_manager.steps[2]
assert(assign_step["type"] == "assign", "Assign step should have correct type")
assert(assign_step["closure"] == test_closure, "Assign step should have correct closure")
print("✓ Step creation tests passed")
end
@ -93,21 +100,19 @@ def test_sequence_manager_execution()
anim2.loop = true
anim2.name = "anim2"
# Create sequence steps
var steps = []
steps.push(animation.create_play_step(anim1, 1000))
steps.push(animation.create_wait_step(500))
steps.push(animation.create_play_step(anim2, 2000))
steps.push(animation.create_stop_step(anim1))
# Create sequence using fluent interface
seq_manager.push_play_step(anim1, 1000)
.push_wait_step(500)
.push_play_step(anim2, 2000)
# Test sequence start
tasmota.set_millis(10000)
engine.start() # Start the engine
engine.on_tick(10000) # Update engine time
seq_manager.start_sequence(steps)
seq_manager.start()
assert(seq_manager.is_running == true, "Sequence should be running after start")
assert(seq_manager.steps.size() == 4, "Sequence should have 4 steps")
assert(seq_manager.steps.size() == 3, "Sequence should have 3 steps")
assert(seq_manager.step_index == 0, "Should start at step 0")
# Check that first animation was started
@ -134,38 +139,37 @@ def test_sequence_manager_timing()
test_anim.loop = true
test_anim.name = "test"
# Create simple sequence with timed steps
var steps = []
steps.push(animation.create_play_step(test_anim, 1000)) # 1 second
steps.push(animation.create_wait_step(500)) # 0.5 seconds
# Create simple sequence with timed steps using fluent interface
seq_manager.push_play_step(test_anim, 1000) # 1 second
.push_wait_step(500) # 0.5 seconds
# Start sequence at time 20000
tasmota.set_millis(20000)
engine.add_sequence_manager(seq_manager)
engine.start() # Start the engine
engine.on_tick(20000) # Update engine time
seq_manager.start_sequence(steps)
# Update immediately - should still be on first step
seq_manager.update()
seq_manager.update(engine.time_ms)
assert(seq_manager.step_index == 0, "Should still be on first step immediately")
assert(seq_manager.is_running == true, "Sequence should still be running")
# Update after 500ms - should still be on first step
tasmota.set_millis(20500)
engine.on_tick(20500) # Update engine time
seq_manager.update()
seq_manager.update(engine.time_ms)
assert(seq_manager.step_index == 0, "Should still be on first step after 500ms")
# Update after 1000ms - should advance to second step (wait)
tasmota.set_millis(21000)
engine.on_tick(21000) # Update engine time
seq_manager.update()
seq_manager.update(engine.time_ms)
assert(seq_manager.step_index == 1, "Should advance to second step after 1000ms")
# Update after additional 500ms - should complete sequence
tasmota.set_millis(21500)
engine.on_tick(21500) # Update engine time
seq_manager.update()
seq_manager.update(engine.time_ms)
assert(seq_manager.is_running == false, "Sequence should complete after all steps")
print("✓ Timing tests passed")
@ -192,15 +196,15 @@ def test_sequence_manager_step_info()
test_anim.duration = 0
test_anim.loop = true
test_anim.name = "test"
var steps = []
steps.push(animation.create_play_step(test_anim, 2000))
steps.push(animation.create_wait_step(1000))
# Create sequence using fluent interface
seq_manager.push_play_step(test_anim, 2000)
.push_wait_step(1000)
# Start sequence
tasmota.set_millis(30000)
engine.add_sequence_manager(seq_manager)
engine.start() # Start the engine
engine.on_tick(30000) # Update engine time
seq_manager.start_sequence(steps)
# Get step info
step_info = seq_manager.get_current_step_info()
@ -230,18 +234,18 @@ def test_sequence_manager_stop()
test_anim.duration = 0
test_anim.loop = true
test_anim.name = "test"
var steps = []
steps.push(animation.create_play_step(test_anim, 5000))
# Create sequence using fluent interface
seq_manager.push_play_step(test_anim, 5000)
# Start sequence
tasmota.set_millis(40000)
engine.start() # Start the engine
engine.on_tick(40000) # Update engine time
seq_manager.start_sequence(steps)
seq_manager.start()
assert(seq_manager.is_running == true, "Sequence should be running")
# Stop sequence
seq_manager.stop_sequence()
seq_manager.stop()
assert(seq_manager.is_running == false, "Sequence should not be running after stop")
assert(engine.size() == 0, "Engine should have no animations after stop")
@ -268,24 +272,83 @@ def test_sequence_manager_is_running()
test_anim.duration = 0
test_anim.loop = true
test_anim.name = "test"
var steps = []
steps.push(animation.create_play_step(test_anim, 1000))
# Create sequence using fluent interface
seq_manager.push_play_step(test_anim, 1000)
tasmota.set_millis(50000)
engine.add_sequence_manager(seq_manager)
engine.start() # Start the engine
engine.on_tick(50000) # Update engine time
seq_manager.start_sequence(steps)
assert(seq_manager.is_sequence_running() == true, "Sequence should be running after start")
# Complete sequence
tasmota.set_millis(51000)
engine.on_tick(51000) # Update engine time
seq_manager.update()
seq_manager.update(engine.time_ms)
assert(seq_manager.is_sequence_running() == false, "Sequence should not be running after completion")
print("✓ Running state tests passed")
end
def test_sequence_manager_assignment_steps()
print("=== SequenceManager Assignment Steps Tests ===")
# Create strip and engine
var strip = global.Leds(30)
var engine = animation.create_engine(strip)
var seq_manager = animation.SequenceManager(engine)
# Create test animation using new parameterized API
var color_provider = animation.static_color(engine)
color_provider.color = 0xFFFF0000
var test_anim = animation.solid(engine)
test_anim.color = color_provider
test_anim.priority = 0
test_anim.duration = 0
test_anim.loop = true
test_anim.name = "test"
test_anim.opacity = 255 # Initial opacity
# Create brightness value provider for assignment
var brightness_provider = animation.static_value(engine)
brightness_provider.value = 128
# Create assignment closure that changes animation opacity
var assignment_closure = def (engine) test_anim.opacity = brightness_provider.produce_value("value", engine.time_ms) end
# Create sequence with assignment step using fluent interface
seq_manager.push_play_step(test_anim, 500) # Play for 0.5s
.push_assign_step(assignment_closure) # Assign new opacity
.push_play_step(test_anim, 500) # Play for another 0.5s
# Start sequence
tasmota.set_millis(80000)
engine.add_sequence_manager(seq_manager)
engine.start() # Start the engine
engine.on_tick(80000) # Update engine time
# Verify initial state
assert(seq_manager.is_running == true, "Sequence should be running")
assert(seq_manager.step_index == 0, "Should start at step 0")
assert(test_anim.opacity == 255, "Animation should have initial opacity")
# Advance past assignment step (after 500ms)
# Assignment steps are executed atomically and advance immediately
tasmota.set_millis(80502)
engine.on_tick(80502) # Update engine time
seq_manager.update(80502)
assert(seq_manager.step_index == 2, "Should advance past assignment step immediately")
assert(test_anim.opacity == 128, "Animation opacity should be changed by assignment")
# Complete sequence (second play step should finish after 500ms more)
tasmota.set_millis(81002) # 80502 + 500ms = 81002
engine.on_tick(81002) # Update engine time
seq_manager.update(81002)
assert(seq_manager.is_running == false, "Sequence should complete")
print("✓ Assignment steps tests passed")
end
def test_sequence_manager_complex_sequence()
print("=== SequenceManager Complex Sequence Tests ===")
@ -322,61 +385,46 @@ def test_sequence_manager_complex_sequence()
blue_anim.loop = true
blue_anim.name = "blue"
# Create complex sequence
var steps = []
steps.push(animation.create_play_step(red_anim, 1000)) # Play red for 1s
steps.push(animation.create_play_step(green_anim, 800)) # Play green for 0.8s
steps.push(animation.create_wait_step(200)) # Wait 0.2s
steps.push(animation.create_play_step(blue_anim, 1500)) # Play blue for 1.5s
steps.push(animation.create_stop_step(red_anim)) # Stop red
steps.push(animation.create_stop_step(green_anim)) # Stop green
# Create complex sequence using fluent interface
seq_manager.push_play_step(red_anim, 1000) # Play red for 1s
.push_play_step(green_anim, 800) # Play green for 0.8s
.push_wait_step(200) # Wait 0.2s
.push_play_step(blue_anim, 1500) # Play blue for 1.5s
# Start sequence
tasmota.set_millis(60000)
engine.add_sequence_manager(seq_manager)
engine.start() # Start the engine
engine.on_tick(60000) # Update engine time
seq_manager.start_sequence(steps)
# Test sequence progression step by step
# After 1000ms: red completes, should advance to green (step 1)
tasmota.set_millis(61000)
engine.on_tick(61000) # Update engine time
seq_manager.update()
seq_manager.update(61000)
assert(seq_manager.step_index == 1, "Should advance to step 1 (green) after red completes")
assert(seq_manager.is_running == true, "Sequence should still be running")
# After 1800ms: green completes, should advance to wait (step 2)
tasmota.set_millis(61800)
engine.on_tick(61800) # Update engine time
seq_manager.update()
seq_manager.update(61800)
assert(seq_manager.step_index == 2, "Should advance to step 2 (wait) after green completes")
assert(seq_manager.is_running == true, "Sequence should still be running")
# After 2000ms: wait completes, should advance to blue (step 3)
tasmota.set_millis(62000)
engine.on_tick(62000) # Update engine time
seq_manager.update()
seq_manager.update(62000)
assert(seq_manager.step_index == 3, "Should advance to step 3 (blue) after wait completes")
assert(seq_manager.is_running == true, "Sequence should still be running")
# After 3500ms: blue completes, should advance to stop red (step 4)
# After 3500ms: blue completes, sequence should complete (we removed stop steps)
tasmota.set_millis(63500)
engine.on_tick(63500) # Update engine time
seq_manager.update()
assert(seq_manager.step_index == 4, "Should advance to step 4 (stop red) after blue completes")
assert(seq_manager.is_running == true, "Sequence should still be running")
# Stop steps execute immediately, so another update should advance to step 5 and then complete
seq_manager.update()
# The sequence should complete when step_index reaches the end
if seq_manager.is_running
# If still running, do one more update to complete
seq_manager.update()
end
assert(seq_manager.is_running == false, "Complex sequence should complete after all stop steps")
seq_manager.update(63500)
assert(seq_manager.is_running == false, "Complex sequence should complete after blue step")
print("✓ Complex sequence tests passed")
end
@ -401,25 +449,21 @@ def test_sequence_manager_integration()
test_anim.duration = 0
test_anim.loop = true
test_anim.name = "test"
var steps = []
steps.push(animation.create_play_step(test_anim, 1000))
# Create sequence using fluent interface
seq_manager.push_play_step(test_anim, 1000)
# Start sequence
tasmota.set_millis(70000)
engine.start() # Start the engine
engine.on_tick(70000) # Update engine time
seq_manager.start_sequence(steps)
# The engine should automatically start the sequence manager when engine.start() is called
assert(seq_manager.is_running == true, "Sequence should be running after engine start")
# Test that engine's on_tick calls sequence manager update
# The engine has a 5ms minimum delta check, so we need to account for that
tasmota.set_millis(71000)
# Start the engine to initialize last_update
engine.start()
engine.on_tick(70000) # Initialize last_update
# Now call on_tick after the sequence should complete
engine.on_tick(71000) # This should call seq_manager.update()
# After 1 second, the sequence should complete
tasmota.set_millis(71005) # Add 5ms buffer for engine's minimum delta check
engine.on_tick(71005) # This should call seq_manager.update()
# The sequence should complete after the 1-second duration
assert(seq_manager.is_running == false, "Sequence should complete after 1 second duration")
@ -442,6 +486,7 @@ def run_all_sequence_manager_tests()
test_sequence_manager_step_info()
test_sequence_manager_stop()
test_sequence_manager_is_running()
test_sequence_manager_assignment_steps()
test_sequence_manager_complex_sequence()
test_sequence_manager_integration()
@ -461,6 +506,7 @@ return {
"test_sequence_manager_step_info": test_sequence_manager_step_info,
"test_sequence_manager_stop": test_sequence_manager_stop,
"test_sequence_manager_is_running": test_sequence_manager_is_running,
"test_sequence_manager_assignment_steps": test_sequence_manager_assignment_steps,
"test_sequence_manager_complex_sequence": test_sequence_manager_complex_sequence,
"test_sequence_manager_integration": test_sequence_manager_integration
}

View File

@ -175,7 +175,7 @@ def test_complex_forward_references()
assert(string.find(berry_code, "var primary_color_") >= 0, "Should define primary color")
assert(string.find(berry_code, "var gradient_pattern_") >= 0, "Should define gradient pattern")
assert(string.find(berry_code, "var complex_anim_") >= 0, "Should define complex animation")
assert(string.find(berry_code, "var demo_ = (def (engine)") >= 0, "Should define sequence")
assert(string.find(berry_code, "var demo_ = animation.SequenceManager(engine)") >= 0, "Should define sequence")
print("✓ Complex forward references test passed")
return true

View File

@ -46,6 +46,7 @@ def run_all_tests()
"lib/libesp32/berry_animation/src/tests/frame_buffer_test.be",
"lib/libesp32/berry_animation/src/tests/nillable_parameter_test.be",
"lib/libesp32/berry_animation/src/tests/parameterized_object_test.be", # Tests parameter management base class
"lib/libesp32/berry_animation/src/tests/bytes_type_test.be", # Tests bytes type validation in parameterized objects
"lib/libesp32/berry_animation/src/tests/animation_test.be",
"lib/libesp32/berry_animation/src/tests/animation_engine_test.be",
"lib/libesp32/berry_animation/src/tests/fast_loop_integration_test.be",
@ -57,6 +58,7 @@ def run_all_tests()
"lib/libesp32/berry_animation/src/tests/pulse_animation_test.be",
"lib/libesp32/berry_animation/src/tests/breathe_animation_test.be",
"lib/libesp32/berry_animation/src/tests/color_cycle_animation_test.be",
"lib/libesp32/berry_animation/src/tests/color_cycle_bytes_test.be", # Tests ColorCycleColorProvider with bytes palette
"lib/libesp32/berry_animation/src/tests/rich_palette_animation_test.be",
"lib/libesp32/berry_animation/src/tests/rich_palette_animation_class_test.be",
"lib/libesp32/berry_animation/src/tests/comet_animation_test.be",

View File

@ -7,5 +7,56 @@ def rand_demo(engine)
return math.rand() % 256
end
# Factory function for rainbow palette
#
# @param engine: AnimationEngine - Animation engine reference (required for user function signature)
# @param num_colors: int - Number of colors in the rainbow (default: 6)
# @return bytes - A palette object containing rainbow colors in VRGB format
def color_wheel_palette(engine, num_colors)
# Default parameters
if num_colors == nil || num_colors < 2
num_colors = 6
end
# Create a rainbow palette as bytes object
var palette = bytes()
var i = 0
while i < num_colors
# Calculate hue (0 to 360 degrees)
var hue = tasmota.scale_uint(i, 0, num_colors, 0, 360)
# Convert HSV to RGB (simplified conversion)
var r, g, b
var h_section = (hue / 60) % 6
var f = (hue / 60) - h_section
var v = 255 # Value (brightness)
var p = 0 # Saturation is 100%, so p = 0
var q = int(v * (1 - f))
var t = int(v * f)
if h_section == 0
r = v; g = t; b = p
elif h_section == 1
r = q; g = v; b = p
elif h_section == 2
r = p; g = v; b = t
elif h_section == 3
r = p; g = q; b = v
elif h_section == 4
r = t; g = p; b = v
else
r = v; g = p; b = q
end
# Create ARGB color (fully opaque) and add to palette
var color = (255 << 24) | (r << 16) | (g << 8) | b
palette.add(color, -4) # Add as 4-byte big-endian
i += 1
end
return palette
end
# Register all user functions with the animation module
animation.register_user_function("rand_demo", rand_demo)
animation.register_user_function("color_wheel_palette", color_wheel_palette)