Berry animation simplify sequence_manager (#24293)
This commit is contained in:
parent
36424dd8e7
commit
3503cee120
@ -10,6 +10,7 @@ template animation shutter_bidir {
|
|||||||
|
|
||||||
# since 'strip_length()' is a value provider, it must be assigned to a variable before being used
|
# since 'strip_length()' is a value provider, it must be assigned to a variable before being used
|
||||||
set strip_len = strip_length()
|
set strip_len = strip_length()
|
||||||
|
set strip_len_2 = (strip_len + 1) / 2 # half length rounded
|
||||||
|
|
||||||
# Define animated value for the size of the shutter, evolving linearly in time (sawtooth from 0% to 100%)
|
# Define animated value for the size of the shutter, evolving linearly in time (sawtooth from 0% to 100%)
|
||||||
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)
|
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)
|
||||||
@ -21,22 +22,18 @@ template animation shutter_bidir {
|
|||||||
|
|
||||||
# Shutter moving in in-out
|
# Shutter moving in in-out
|
||||||
animation shutter_inout_animation = beacon_animation(
|
animation shutter_inout_animation = beacon_animation(
|
||||||
color = col2
|
color = col2 # Use two rotating colors
|
||||||
back_color = col1
|
back_color = col1 # Use two rotating colors
|
||||||
pos = 0
|
pos = strip_len_2 - (shutter_size + 1) / 2
|
||||||
beacon_size = shutter_size
|
beacon_size = shutter_size
|
||||||
slew_size = 0
|
|
||||||
priority = 5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# shutter moving in out-in
|
# shutter moving in out-in
|
||||||
animation shutter_outin_animation = beacon_animation(
|
animation shutter_outin_animation = beacon_animation(
|
||||||
color = col1
|
color = col1
|
||||||
back_color = col2
|
back_color = col2
|
||||||
pos = 0
|
pos = strip_len_2 - (strip_len - shutter_size + 1) / 2
|
||||||
beacon_size = strip_len - shutter_size
|
beacon_size = strip_len - shutter_size
|
||||||
slew_size = 0
|
|
||||||
priority = 5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# this is the overall sequence composed of two sub-sequences
|
# this is the overall sequence composed of two sub-sequences
|
||||||
|
|||||||
@ -1186,62 +1186,65 @@ run main
|
|||||||
|
|
||||||
### 8.3 Advanced Template with Conditional Flags
|
### 8.3 Advanced Template with Conditional Flags
|
||||||
|
|
||||||
<a href="https://tasmota.github.io/docs/Tasmota-Berry-emulator/index.html?example=chap_8_30_template_shutter" target="_blank"><img src="../../_media/berry_animation/chap_8_30.png" alt="Template Shutter"></a>
|
<a href="https://tasmota.github.io/docs/Tasmota-Berry-emulator/index.html?example=chap_8_30_template_shutter_bidir_flags" target="_blank"><img src="../../_media/berry_animation/chap_8_30.png" alt="Template Shutter"></a>
|
||||||
|
|
||||||
Templates support `bool` parameters that can be used with `if` statements inside sequences. This allows users to enable or disable parts of the animation at instantiation time. Here we create a bidirectional shutter that can optionally run in-out, out-in, or both directions.
|
Templates support `bool` parameters that can be used with `if` statements inside sequences. This allows users to enable or disable parts of the animation at instantiation time. Here we create a bidirectional shutter (based on chapter 6.4) that can optionally run in-out, out-in, or both directions.
|
||||||
|
|
||||||
**Dynamic parameter changes:**
|
**Dynamic parameter changes:**
|
||||||
|
|
||||||
You can also modify template parameters at runtime using Berry code. Note that DSL variable names get an underscore suffix in Berry to avoid collisions with reserved words (e.g., `main` becomes `main_`). For example: `main_.inout = false` disables the in-out animation while it's running.
|
You can also modify template parameters at runtime using Berry code. Note that DSL variable names get an underscore suffix in Berry to avoid collisions with reserved words (e.g., `main` becomes `main_`). For example: `main_.inout = false` disables the in-out animation while it's running.
|
||||||
|
|
||||||
```berry
|
```berry
|
||||||
# Template with conditional flags for bidirectional shutter
|
# Template to illustrate the parameters and flags
|
||||||
|
|
||||||
|
# Define a template to package the shutter in-out-in from 6.40
|
||||||
|
# with flags to enable or disable in-out or out-in
|
||||||
template animation shutter_bidir {
|
template animation shutter_bidir {
|
||||||
param colors type palette
|
param colors type palette
|
||||||
param period default 2s
|
param period default 2s
|
||||||
param inout type bool default true # Enable in-out animation
|
param inout type bool default true # define to true to enable 'inout' part
|
||||||
param outin type bool default true # Enable out-in animation
|
param outin type bool default true # define to true to enable 'outin' part
|
||||||
|
|
||||||
|
# since 'strip_length()' is a value provider, it must be assigned to a variable before being used
|
||||||
set strip_len = strip_length()
|
set strip_len = strip_length()
|
||||||
|
set strip_len_2 = (strip_len + 1) / 2 # half length rounded
|
||||||
|
|
||||||
|
# Define animated value for the size of the shutter, evolving linearly in time (sawtooth from 0% to 100%)
|
||||||
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)
|
set shutter_size = sawtooth(min_value = 0, max_value = strip_len, duration = period)
|
||||||
|
|
||||||
# Two rotating color providers
|
# Define two rotating palettes, shifted by one color
|
||||||
color col1 = color_cycle(colors=colors, period=0)
|
color col1 = color_cycle(colors=colors, period=0)
|
||||||
color col2 = color_cycle(colors=colors, period=0)
|
color col2 = color_cycle(colors=colors, period=0)
|
||||||
col2.next = 1
|
col2.next = 1 # move 'col2' to the next color so it's shifte by one compared to 'col1'
|
||||||
|
|
||||||
# In-out shutter
|
# Shutter moving in in-out
|
||||||
animation shutter_inout_animation = beacon_animation(
|
animation shutter_inout_animation = beacon_animation(
|
||||||
color = col2
|
color = col2 # Use two rotating colors
|
||||||
back_color = col1
|
back_color = col1 # Use two rotating colors
|
||||||
pos = 0
|
pos = strip_len_2 - (shutter_size + 1) / 2
|
||||||
beacon_size = shutter_size
|
beacon_size = shutter_size
|
||||||
slew_size = 0
|
|
||||||
priority = 5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Out-in shutter
|
# shutter moving in out-in
|
||||||
animation shutter_outin_animation = beacon_animation(
|
animation shutter_outin_animation = beacon_animation(
|
||||||
color = col1
|
color = col1
|
||||||
back_color = col2
|
back_color = col2
|
||||||
pos = 0
|
pos = strip_len_2 - (strip_len - shutter_size + 1) / 2
|
||||||
beacon_size = strip_len - shutter_size
|
beacon_size = strip_len - shutter_size
|
||||||
slew_size = 0
|
|
||||||
priority = 5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sequence with conditional blocks
|
# this is the overall sequence composed of two sub-sequences
|
||||||
|
# the first in ascending mode, the second in descending
|
||||||
sequence shutter_seq repeat forever {
|
sequence shutter_seq repeat forever {
|
||||||
if inout { # Only if inout is true
|
if inout { # un only if 'ascending' is true
|
||||||
repeat col1.palette_size times {
|
repeat col1.palette_size times { # run the shutter animation
|
||||||
restart shutter_size
|
restart shutter_size # resync all times for this animation, to avoid temporal drift
|
||||||
play shutter_inout_animation for period
|
play shutter_inout_animation for period # run the animation
|
||||||
col1.next = 1
|
col1.next = 1 # then move to next color for both palettes
|
||||||
col2.next = 1
|
col2.next = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if outin { # Only if outin is true
|
if outin { # run only if 'descending' is true
|
||||||
repeat col1.palette_size times {
|
repeat col1.palette_size times {
|
||||||
restart shutter_size
|
restart shutter_size
|
||||||
play shutter_outin_animation for period
|
play shutter_outin_animation for period
|
||||||
@ -1253,7 +1256,7 @@ template animation shutter_bidir {
|
|||||||
run shutter_seq
|
run shutter_seq
|
||||||
}
|
}
|
||||||
|
|
||||||
# Define palette
|
# define a palette of rainbow colors including white with constant brightness
|
||||||
palette rainbow_with_white = [
|
palette rainbow_with_white = [
|
||||||
0xFC0000 # Red
|
0xFC0000 # Red
|
||||||
0xFF8000 # Orange
|
0xFF8000 # Orange
|
||||||
@ -1265,7 +1268,6 @@ palette rainbow_with_white = [
|
|||||||
0xCCCCCC # White
|
0xCCCCCC # White
|
||||||
]
|
]
|
||||||
|
|
||||||
# Use the template
|
|
||||||
animation main = shutter_bidir(colors = rainbow_with_white, period = 1.5s)
|
animation main = shutter_bidir(colors = rainbow_with_white, period = 1.5s)
|
||||||
run main
|
run main
|
||||||
```
|
```
|
||||||
|
|||||||
@ -4,16 +4,29 @@
|
|||||||
#
|
#
|
||||||
# Extends ParameterizedObject to provide parameter management and playable interface,
|
# Extends ParameterizedObject to provide parameter management and playable interface,
|
||||||
# allowing sequences to be treated uniformly with animations by the engine.
|
# allowing sequences to be treated uniformly with animations by the engine.
|
||||||
|
#
|
||||||
|
# Memory-optimized: Uses two parallel arrays instead of array of maps
|
||||||
|
# - step_durations: encodes type and duration in a single integer
|
||||||
|
# - step_refs: stores references (animation, closure, sequence_manager, or nil)
|
||||||
|
#
|
||||||
|
# Duration encoding:
|
||||||
|
# >= 0: play (ref=animation) or wait (ref=nil)
|
||||||
|
# -2: closure (ref=closure function)
|
||||||
|
# -3: subsequence (ref=sequence_manager)
|
||||||
|
|
||||||
import "./core/param_encoder" as encode_constraints
|
import "./core/param_encoder" as encode_constraints
|
||||||
|
|
||||||
class SequenceManager : animation.parameterized_object
|
class SequenceManager : animation.parameterized_object
|
||||||
|
# static var DURATION_CLOSURE = -2
|
||||||
|
# static var DURATION_SUBSEQUENCE = -3
|
||||||
|
|
||||||
# Non-parameter instance variables
|
# Non-parameter instance variables
|
||||||
var active_sequence # Currently running sequence
|
var active_sequence # Currently running sequence
|
||||||
var sequence_state # Current sequence execution state
|
var sequence_state # Current sequence execution state
|
||||||
var step_index # Current step in the sequence
|
var step_index # Current step in the sequence
|
||||||
var step_start_time # When current step started
|
var step_start_time # When current step started
|
||||||
var steps # List of sequence steps
|
var step_durations # List of duration/type values (int)
|
||||||
|
var step_refs # List of references (animation, closure, sequence_manager, or nil)
|
||||||
|
|
||||||
# Repeat-specific properties
|
# Repeat-specific properties
|
||||||
var repeat_count # Number of times to repeat this sequence (-1 for forever, 0 for no repeat)
|
var repeat_count # Number of times to repeat this sequence (-1 for forever, 0 for no repeat)
|
||||||
@ -29,7 +42,8 @@ class SequenceManager : animation.parameterized_object
|
|||||||
self.sequence_state = {}
|
self.sequence_state = {}
|
||||||
self.step_index = 0
|
self.step_index = 0
|
||||||
self.step_start_time = 0
|
self.step_start_time = 0
|
||||||
self.steps = []
|
self.step_durations = []
|
||||||
|
self.step_refs = []
|
||||||
|
|
||||||
# Repeat logic
|
# Repeat logic
|
||||||
self.repeat_count = repeat_count != nil ? repeat_count : 1 # Default: run once (can be function or number)
|
self.repeat_count = repeat_count != nil ? repeat_count : 1 # Default: run once (can be function or number)
|
||||||
@ -37,51 +51,35 @@ class SequenceManager : animation.parameterized_object
|
|||||||
self.is_repeat_sequence = repeat_count != nil && repeat_count != 1
|
self.is_repeat_sequence = repeat_count != nil && repeat_count != 1
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add a step to this sequence
|
|
||||||
def push_step(step)
|
|
||||||
self.steps.push(step)
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
# Add a play step directly
|
# Add a play step directly
|
||||||
def push_play_step(animation_ref, duration)
|
def push_play_step(animation_ref, duration)
|
||||||
self.steps.push({
|
self.step_durations.push(duration != nil ? duration : 0)
|
||||||
"type": "play",
|
self.step_refs.push(animation_ref)
|
||||||
"animation": animation_ref,
|
|
||||||
"duration": duration != nil ? duration : 0
|
|
||||||
})
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add a wait step directly
|
# Add a wait step directly
|
||||||
def push_wait_step(duration)
|
def push_wait_step(duration)
|
||||||
self.steps.push({
|
self.step_durations.push(duration)
|
||||||
"type": "wait",
|
self.step_refs.push(nil)
|
||||||
"duration": duration
|
|
||||||
})
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add a closure step directly (used for both assign and log steps)
|
# Add a closure step directly (used for both assign and log steps)
|
||||||
def push_closure_step(closure)
|
def push_closure_step(closure)
|
||||||
self.steps.push({
|
self.step_durations.push(-2 #-self.DURATION_CLOSURE-#)
|
||||||
"type": "closure",
|
self.step_refs.push(closure)
|
||||||
"closure": closure
|
|
||||||
})
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
# Add a repeat subsequence step directly
|
# Add a repeat subsequence step directly
|
||||||
def push_repeat_subsequence(sequence_manager)
|
def push_repeat_subsequence(sequence_manager)
|
||||||
self.steps.push({
|
self.step_durations.push(-3 #-self.DURATION_SUBSEQUENCE-#)
|
||||||
"type": "subsequence",
|
self.step_refs.push(sequence_manager)
|
||||||
"sequence_manager": sequence_manager
|
|
||||||
})
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
# Start this sequence
|
# Start this sequence
|
||||||
# FIXED: More conservative engine clearing to avoid black frames
|
|
||||||
def start(time_ms)
|
def start(time_ms)
|
||||||
# Stop any current sequence
|
# Stop any current sequence
|
||||||
if self.is_running
|
if self.is_running
|
||||||
@ -113,23 +111,19 @@ class SequenceManager : animation.parameterized_object
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Start executing if we have steps
|
# Start executing if we have steps
|
||||||
if size(self.steps) > 0
|
var num_steps = size(self.step_durations)
|
||||||
|
if num_steps > 0
|
||||||
# Execute all consecutive closure steps at the beginning atomically
|
# Execute all consecutive closure steps at the beginning atomically
|
||||||
while self.step_index < size(self.steps)
|
while self.step_index < num_steps && self.step_durations[self.step_index] == -2 #-self.DURATION_CLOSURE-#
|
||||||
var step = self.steps[self.step_index]
|
var closure_func = self.step_refs[self.step_index]
|
||||||
if step["type"] == "closure"
|
|
||||||
var closure_func = step["closure"]
|
|
||||||
if closure_func != nil
|
if closure_func != nil
|
||||||
closure_func(self.engine)
|
closure_func(self.engine)
|
||||||
end
|
end
|
||||||
self.step_index += 1
|
self.step_index += 1
|
||||||
else
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Now execute the next non-closure step (usually play)
|
# Now execute the next non-closure step (usually play)
|
||||||
if self.step_index < size(self.steps)
|
if self.step_index < num_steps
|
||||||
self.execute_current_step(time_ms)
|
self.execute_current_step(time_ms)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -148,14 +142,15 @@ class SequenceManager : animation.parameterized_object
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Stop any currently playing animations
|
# Stop any currently playing animations
|
||||||
if self.step_index < size(self.steps)
|
var num_steps = size(self.step_durations)
|
||||||
var current_step = self.steps[self.step_index]
|
if self.step_index < num_steps
|
||||||
if current_step["type"] == "play"
|
var dur = self.step_durations[self.step_index]
|
||||||
var anim = current_step["animation"]
|
var ref = self.step_refs[self.step_index]
|
||||||
self.engine.remove(anim)
|
if dur == -3 #-self.DURATION_SUBSEQUENCE-#
|
||||||
elif current_step["type"] == "subsequence"
|
ref.stop()
|
||||||
var sub_seq = current_step["sequence_manager"]
|
elif dur != -2 #-self.DURATION_CLOSURE-# && ref != nil
|
||||||
sub_seq.stop()
|
# play step (dur is >= 0 or function, ref is animation)
|
||||||
|
self.engine.remove(ref)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -167,215 +162,198 @@ class SequenceManager : animation.parameterized_object
|
|||||||
|
|
||||||
# Stop all sub-sequences in our steps
|
# Stop all sub-sequences in our steps
|
||||||
def stop_all_subsequences()
|
def stop_all_subsequences()
|
||||||
for step : self.steps
|
var num_steps = size(self.step_durations)
|
||||||
if step["type"] == "subsequence"
|
var i = 0
|
||||||
var sub_seq = step["sequence_manager"]
|
while i < num_steps
|
||||||
sub_seq.stop()
|
if self.step_durations[i] == -3 #-self.DURATION_SUBSEQUENCE-#
|
||||||
|
self.step_refs[i].stop()
|
||||||
end
|
end
|
||||||
|
i += 1
|
||||||
end
|
end
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
# Update sequence state - called from fast_loop
|
# Update sequence state - called from fast_loop
|
||||||
def update(current_time)
|
def update(current_time)
|
||||||
if !self.is_running || size(self.steps) == 0
|
var num_steps = size(self.step_durations)
|
||||||
|
if !self.is_running || num_steps == 0 || self.step_index >= num_steps
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
# Safety check: ensure step_index is valid
|
var dur = self.step_durations[self.step_index]
|
||||||
if self.step_index >= size(self.steps)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
var current_step = self.steps[self.step_index]
|
# Only closure/subsequence have negative int markers, play/wait have >= 0 or function
|
||||||
|
if dur == -3 #-self.DURATION_SUBSEQUENCE-#
|
||||||
# Handle different step types
|
# Handle sub-sequence
|
||||||
if current_step["type"] == "subsequence"
|
var sub_seq = self.step_refs[self.step_index]
|
||||||
# Handle sub-sequence (including repeat sequences)
|
|
||||||
var sub_seq = current_step["sequence_manager"]
|
|
||||||
sub_seq.update(current_time)
|
sub_seq.update(current_time)
|
||||||
if !sub_seq.is_running
|
if !sub_seq.is_running
|
||||||
# Sub-sequence finished, advance to next step
|
|
||||||
self.advance_to_next_step(current_time)
|
self.advance_to_next_step(current_time)
|
||||||
end
|
end
|
||||||
elif current_step["type"] == "closure"
|
elif dur == -2 #-self.DURATION_CLOSURE-#
|
||||||
# Closure steps are handled in batches by advance_to_next_step
|
# Closure steps are handled in batches
|
||||||
# This should not happen in normal flow, but handle it just in case
|
|
||||||
self.execute_closure_steps_batch(current_time)
|
self.execute_closure_steps_batch(current_time)
|
||||||
else
|
else
|
||||||
# Handle regular steps with duration
|
# Handle regular steps with duration (play or wait)
|
||||||
if current_step.contains("duration") && current_step["duration"] != nil
|
# dur is either int >= 0, function, or bool
|
||||||
# Resolve duration - it can be a number or a closure
|
var duration_value = dur
|
||||||
var duration_value = current_step["duration"]
|
if type(dur) == "function"
|
||||||
if type(duration_value) == "function"
|
duration_value = dur(self.engine)
|
||||||
# Duration is a closure - call it to get the actual value
|
|
||||||
duration_value = duration_value(self.engine)
|
|
||||||
end
|
end
|
||||||
|
duration_value = int(duration_value) # Force int, works for bool
|
||||||
|
|
||||||
if duration_value > 0
|
if duration_value > 0
|
||||||
var elapsed = current_time - self.step_start_time
|
if current_time - self.step_start_time >= duration_value
|
||||||
if elapsed >= duration_value
|
|
||||||
self.advance_to_next_step(current_time)
|
self.advance_to_next_step(current_time)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
# Duration is 0 or nil - complete immediately
|
# Duration is 0 - complete immediately
|
||||||
self.advance_to_next_step(current_time)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
# Steps without duration complete immediately
|
|
||||||
self.advance_to_next_step(current_time)
|
self.advance_to_next_step(current_time)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Execute the current step
|
# Execute the current step
|
||||||
def execute_current_step(current_time)
|
# skip_budget limits how many consecutive immediate skips to prevent infinite loops
|
||||||
if self.step_index >= size(self.steps)
|
def execute_current_step(current_time, skip_budget)
|
||||||
self.complete_iteration(current_time)
|
if skip_budget == nil
|
||||||
|
skip_budget = size(self.step_durations) + 1 # Default: allow skipping all steps once
|
||||||
|
end
|
||||||
|
|
||||||
|
var num_steps = size(self.step_durations)
|
||||||
|
if self.step_index >= num_steps
|
||||||
|
self.complete_iteration(current_time, skip_budget)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
var step = self.steps[self.step_index]
|
var dur = self.step_durations[self.step_index]
|
||||||
|
var ref = self.step_refs[self.step_index]
|
||||||
|
|
||||||
if step["type"] == "play"
|
if dur == -3 #-self.DURATION_SUBSEQUENCE-#
|
||||||
var anim = step["animation"]
|
# Start sub-sequence
|
||||||
|
ref.start(current_time)
|
||||||
# Check if animation is nil (safety check)
|
# If subsequence immediately completed (e.g., repeat_count resolved to 0),
|
||||||
if anim == nil
|
# advance to next step immediately to avoid black frame (if we have budget)
|
||||||
|
if !ref.is_running && skip_budget > 0
|
||||||
|
self.advance_to_next_step(current_time, skip_budget - 1)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
elif dur == -2 #-self.DURATION_CLOSURE-#
|
||||||
# Check if animation is already in the engine (avoid duplicate adds)
|
# Closure steps should be handled in batches
|
||||||
|
if ref != nil
|
||||||
|
ref(self.engine)
|
||||||
|
end
|
||||||
|
elif ref != nil
|
||||||
|
# Play step (dur >= 0 and ref is animation)
|
||||||
|
# Check if animation is already in the engine
|
||||||
var animations = self.engine.get_animations()
|
var animations = self.engine.get_animations()
|
||||||
var already_added = false
|
var already_added = false
|
||||||
for existing_anim : animations
|
for existing_anim : animations
|
||||||
if existing_anim == anim
|
if existing_anim == ref
|
||||||
already_added = true
|
already_added = true
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if !already_added
|
if !already_added
|
||||||
self.engine.add(anim)
|
self.engine.add(ref)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Always restart the animation to ensure proper timing
|
# Always restart the animation to ensure proper timing
|
||||||
anim.start(current_time)
|
ref.start(current_time)
|
||||||
|
|
||||||
elif step["type"] == "wait"
|
|
||||||
# Wait steps are handled by the update loop checking duration
|
|
||||||
# No animation needed for wait
|
|
||||||
|
|
||||||
elif step["type"] == "stop"
|
|
||||||
var anim = step["animation"]
|
|
||||||
self.engine.remove(anim)
|
|
||||||
|
|
||||||
elif step["type"] == "closure"
|
|
||||||
# Closure steps should be handled in batches by execute_closure_steps_batch
|
|
||||||
# This should not happen in normal flow, but handle it for safety
|
|
||||||
var closure_func = step["closure"]
|
|
||||||
if closure_func != nil
|
|
||||||
closure_func(self.engine)
|
|
||||||
end
|
|
||||||
|
|
||||||
elif step["type"] == "subsequence"
|
|
||||||
# Start sub-sequence (including repeat sequences)
|
|
||||||
var sub_seq = step["sequence_manager"]
|
|
||||||
sub_seq.start(current_time)
|
|
||||||
end
|
end
|
||||||
|
# else: wait step (dur >= 0 and ref is nil) - nothing to do
|
||||||
|
|
||||||
self.step_start_time = current_time
|
self.step_start_time = current_time
|
||||||
end
|
end
|
||||||
|
|
||||||
# Advance to the next step in the sequence
|
# Advance to the next step in the sequence
|
||||||
# FIXED: Atomic transition to eliminate black frames
|
def advance_to_next_step(current_time, skip_budget)
|
||||||
def advance_to_next_step(current_time)
|
var num_steps = size(self.step_durations)
|
||||||
# Get current step info BEFORE advancing
|
|
||||||
var current_step = self.steps[self.step_index]
|
|
||||||
var current_anim = nil
|
|
||||||
|
|
||||||
# Store reference to current animation but DON'T remove it yet
|
if skip_budget == nil
|
||||||
if current_step["type"] == "play" && current_step.contains("duration")
|
skip_budget = num_steps + 1
|
||||||
current_anim = current_step["animation"]
|
end
|
||||||
|
|
||||||
|
# Get current animation ref BEFORE advancing (for atomic transition)
|
||||||
|
# If dur is not a special marker and ref is not nil, it's a play step
|
||||||
|
var dur = self.step_durations[self.step_index]
|
||||||
|
var ref = self.step_refs[self.step_index]
|
||||||
|
var current_anim = nil
|
||||||
|
if dur != -2 #-self.DURATION_CLOSURE-# && dur != -3 #-self.DURATION_SUBSEQUENCE-# && ref != nil
|
||||||
|
current_anim = ref
|
||||||
end
|
end
|
||||||
|
|
||||||
self.step_index += 1
|
self.step_index += 1
|
||||||
|
|
||||||
if self.step_index >= size(self.steps)
|
if self.step_index >= num_steps
|
||||||
# Only remove animation when completing iteration
|
|
||||||
if current_anim != nil
|
if current_anim != nil
|
||||||
self.engine.remove(current_anim)
|
self.engine.remove(current_anim)
|
||||||
end
|
end
|
||||||
self.complete_iteration(current_time)
|
self.complete_iteration(current_time, skip_budget)
|
||||||
else
|
else
|
||||||
# Execute closures and start next animation BEFORE removing current one
|
self.execute_closure_steps_batch_atomic(current_time, current_anim, skip_budget)
|
||||||
self.execute_closure_steps_batch_atomic(current_time, current_anim)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Execute all consecutive closure steps in a batch to avoid black frames
|
# Execute all consecutive closure steps in a batch to avoid black frames
|
||||||
def execute_closure_steps_batch(current_time)
|
def execute_closure_steps_batch(current_time)
|
||||||
|
var num_steps = size(self.step_durations)
|
||||||
|
|
||||||
# Execute all consecutive closure steps
|
# Execute all consecutive closure steps
|
||||||
while self.step_index < size(self.steps)
|
while self.step_index < num_steps && self.step_durations[self.step_index] == -2 #-self.DURATION_CLOSURE-#
|
||||||
var step = self.steps[self.step_index]
|
var closure_func = self.step_refs[self.step_index]
|
||||||
if step["type"] == "closure"
|
|
||||||
# Execute closure function
|
|
||||||
var closure_func = step["closure"]
|
|
||||||
if closure_func != nil
|
if closure_func != nil
|
||||||
closure_func(self.engine)
|
closure_func(self.engine)
|
||||||
end
|
end
|
||||||
self.step_index += 1
|
self.step_index += 1
|
||||||
else
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Now execute the next non-closure step
|
# Now execute the next non-closure step
|
||||||
if self.step_index < size(self.steps)
|
if self.step_index < num_steps
|
||||||
self.execute_current_step(current_time)
|
self.execute_current_step(current_time)
|
||||||
else
|
else
|
||||||
self.complete_iteration(current_time)
|
self.complete_iteration(current_time)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# ADDED: Atomic batch execution to eliminate black frames
|
# Atomic batch execution to eliminate black frames
|
||||||
def execute_closure_steps_batch_atomic(current_time, previous_anim)
|
def execute_closure_steps_batch_atomic(current_time, previous_anim, skip_budget)
|
||||||
|
if skip_budget == nil
|
||||||
|
skip_budget = size(self.step_durations) + 1
|
||||||
|
end
|
||||||
|
|
||||||
|
var num_steps = size(self.step_durations)
|
||||||
|
|
||||||
# Execute all consecutive closure steps
|
# Execute all consecutive closure steps
|
||||||
while self.step_index < size(self.steps)
|
while self.step_index < num_steps && self.step_durations[self.step_index] == -2 #-self.DURATION_CLOSURE-#
|
||||||
var step = self.steps[self.step_index]
|
var closure_func = self.step_refs[self.step_index]
|
||||||
if step["type"] == "closure"
|
|
||||||
var closure_func = step["closure"]
|
|
||||||
if closure_func != nil
|
if closure_func != nil
|
||||||
closure_func(self.engine)
|
closure_func(self.engine)
|
||||||
end
|
end
|
||||||
self.step_index += 1
|
self.step_index += 1
|
||||||
else
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# CRITICAL FIX: Handle the case where the next step is the SAME animation
|
# Check if the next step is the SAME animation
|
||||||
# This prevents removing and re-adding the same animation, which causes black frames
|
|
||||||
var next_step = nil
|
|
||||||
var is_same_animation = false
|
var is_same_animation = false
|
||||||
|
if self.step_index < num_steps && previous_anim != nil
|
||||||
if self.step_index < size(self.steps)
|
var next_dur = self.step_durations[self.step_index]
|
||||||
next_step = self.steps[self.step_index]
|
var next_ref = self.step_refs[self.step_index]
|
||||||
if next_step["type"] == "play" && previous_anim != nil
|
# If not a special marker and same animation ref
|
||||||
is_same_animation = (next_step["animation"] == previous_anim)
|
if next_dur != -2 #-self.DURATION_CLOSURE-# && next_dur != -3 #-self.DURATION_SUBSEQUENCE-# && next_ref == previous_anim
|
||||||
|
is_same_animation = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if is_same_animation
|
if is_same_animation
|
||||||
# Same animation continuing - don't remove/re-add, but DO restart for timing sync
|
# Same animation continuing - don't remove/re-add, but DO restart for timing sync
|
||||||
self.step_start_time = current_time
|
self.step_start_time = current_time
|
||||||
# CRITICAL: Still need to restart the animation to sync with sequence timing
|
|
||||||
previous_anim.start(current_time)
|
previous_anim.start(current_time)
|
||||||
else
|
else
|
||||||
# Different animation or no next animation
|
# Different animation or no next animation
|
||||||
# Start the next animation BEFORE removing the previous one
|
# Start the next animation BEFORE removing the previous one
|
||||||
if self.step_index < size(self.steps)
|
if self.step_index < num_steps
|
||||||
self.execute_current_step(current_time)
|
self.execute_current_step(current_time, skip_budget)
|
||||||
end
|
end
|
||||||
|
|
||||||
# NOW it's safe to remove the previous animation (no gap)
|
# NOW it's safe to remove the previous animation (no gap)
|
||||||
@ -385,14 +363,17 @@ class SequenceManager : animation.parameterized_object
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Handle completion
|
# Handle completion
|
||||||
if self.step_index >= size(self.steps)
|
if self.step_index >= num_steps
|
||||||
self.complete_iteration(current_time)
|
self.complete_iteration(current_time, skip_budget)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Complete current iteration and check if we should repeat
|
# Complete current iteration and check if we should repeat
|
||||||
# FIXED: Ensure atomic transitions during repeat iterations
|
def complete_iteration(current_time, skip_budget)
|
||||||
def complete_iteration(current_time)
|
if skip_budget == nil
|
||||||
|
skip_budget = size(self.step_durations) + 1
|
||||||
|
end
|
||||||
|
|
||||||
self.current_iteration += 1
|
self.current_iteration += 1
|
||||||
|
|
||||||
# Update iteration context in engine stack if this is a repeat sequence
|
# Update iteration context in engine stack if this is a repeat sequence
|
||||||
@ -405,26 +386,29 @@ class SequenceManager : animation.parameterized_object
|
|||||||
|
|
||||||
# Check if we should continue repeating
|
# Check if we should continue repeating
|
||||||
if resolved_repeat_count == -1 || self.current_iteration < resolved_repeat_count
|
if resolved_repeat_count == -1 || self.current_iteration < resolved_repeat_count
|
||||||
|
# If we've exhausted skip budget, stop here and let next update() continue
|
||||||
|
# This prevents infinite loops when all steps are skipped
|
||||||
|
if skip_budget <= 0
|
||||||
|
self.step_index = 0
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
# Start next iteration - execute all initial closures atomically
|
# Start next iteration - execute all initial closures atomically
|
||||||
self.step_index = 0
|
self.step_index = 0
|
||||||
|
var num_steps = size(self.step_durations)
|
||||||
|
|
||||||
# Execute all consecutive closure steps at the beginning atomically
|
# Execute all consecutive closure steps at the beginning atomically
|
||||||
while self.step_index < size(self.steps)
|
while self.step_index < num_steps && self.step_durations[self.step_index] == -2 #-self.DURATION_CLOSURE-#
|
||||||
var step = self.steps[self.step_index]
|
var closure_func = self.step_refs[self.step_index]
|
||||||
if step["type"] == "closure"
|
|
||||||
var closure_func = step["closure"]
|
|
||||||
if closure_func != nil
|
if closure_func != nil
|
||||||
closure_func(self.engine)
|
closure_func(self.engine)
|
||||||
end
|
end
|
||||||
self.step_index += 1
|
self.step_index += 1
|
||||||
else
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Now execute the next non-closure step (usually play)
|
# Now execute the next non-closure step (usually play)
|
||||||
if self.step_index < size(self.steps)
|
if self.step_index < num_steps
|
||||||
self.execute_current_step(current_time)
|
self.execute_current_step(current_time, skip_budget - 1)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
# All iterations complete
|
# All iterations complete
|
||||||
@ -455,23 +439,6 @@ class SequenceManager : animation.parameterized_object
|
|||||||
def is_sequence_running()
|
def is_sequence_running()
|
||||||
return self.is_running
|
return self.is_running
|
||||||
end
|
end
|
||||||
|
|
||||||
# # Get current step info for debugging
|
|
||||||
# def get_current_step_info()
|
|
||||||
# if !self.is_running || self.step_index >= size(self.steps)
|
|
||||||
# return nil
|
|
||||||
# end
|
|
||||||
|
|
||||||
# return {
|
|
||||||
# "step_index": self.step_index,
|
|
||||||
# "total_steps": size(self.steps),
|
|
||||||
# "current_step": self.steps[self.step_index],
|
|
||||||
# "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
|
end
|
||||||
|
|
||||||
return {'sequence_manager': SequenceManager }
|
return {'sequence_manager': SequenceManager }
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -21,8 +21,10 @@ def test_sequence_manager_basic()
|
|||||||
# Test initialization
|
# Test initialization
|
||||||
var seq_manager = animation.sequence_manager(engine)
|
var seq_manager = animation.sequence_manager(engine)
|
||||||
assert(seq_manager.engine == 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.step_durations != nil, "Step durations list should be initialized")
|
||||||
assert(seq_manager.steps.size() == 0, "Steps list should be empty initially")
|
assert(seq_manager.step_refs != nil, "Step refs list should be initialized")
|
||||||
|
assert(size(seq_manager.step_durations) == 0, "Step durations list should be empty initially")
|
||||||
|
assert(size(seq_manager.step_refs) == 0, "Step refs list should be empty initially")
|
||||||
assert(seq_manager.step_index == 0, "Step index should be 0 initially")
|
assert(seq_manager.step_index == 0, "Step index should be 0 initially")
|
||||||
assert(seq_manager.is_running == false, "Sequence should not be running initially")
|
assert(seq_manager.is_running == false, "Sequence should not be running initially")
|
||||||
|
|
||||||
@ -48,26 +50,22 @@ def test_sequence_manager_step_creation()
|
|||||||
|
|
||||||
# Test push_play_step
|
# Test push_play_step
|
||||||
seq_manager.push_play_step(test_anim, 5000)
|
seq_manager.push_play_step(test_anim, 5000)
|
||||||
assert(seq_manager.steps.size() == 1, "Should have one step after push_play_step")
|
assert(size(seq_manager.step_durations) == 1, "Should have one step after push_play_step")
|
||||||
var play_step = seq_manager.steps[0]
|
assert(seq_manager.step_durations[0] == 5000, "Play step should have correct duration")
|
||||||
assert(play_step["type"] == "play", "Play step should have correct type")
|
assert(seq_manager.step_refs[0] == test_anim, "Play step should have correct animation")
|
||||||
assert(play_step["animation"] == test_anim, "Play step should have correct animation")
|
|
||||||
assert(play_step["duration"] == 5000, "Play step should have correct duration")
|
|
||||||
|
|
||||||
# Test push_wait_step
|
# Test push_wait_step
|
||||||
seq_manager.push_wait_step(2000)
|
seq_manager.push_wait_step(2000)
|
||||||
assert(seq_manager.steps.size() == 2, "Should have two steps after push_wait_step")
|
assert(size(seq_manager.step_durations) == 2, "Should have two steps after push_wait_step")
|
||||||
var wait_step = seq_manager.steps[1]
|
assert(seq_manager.step_durations[1] == 2000, "Wait step should have correct duration")
|
||||||
assert(wait_step["type"] == "wait", "Wait step should have correct type")
|
assert(seq_manager.step_refs[1] == nil, "Wait step should have nil ref")
|
||||||
assert(wait_step["duration"] == 2000, "Wait step should have correct duration")
|
|
||||||
|
|
||||||
# Test push_closure_step
|
# Test push_closure_step
|
||||||
var test_closure = def (engine) test_anim.opacity = 128 end
|
var test_closure = def (engine) test_anim.opacity = 128 end
|
||||||
seq_manager.push_closure_step(test_closure)
|
seq_manager.push_closure_step(test_closure)
|
||||||
assert(seq_manager.steps.size() == 3, "Should have three steps after push_closure_step")
|
assert(size(seq_manager.step_durations) == 3, "Should have three steps after push_closure_step")
|
||||||
var assign_step = seq_manager.steps[2]
|
assert(seq_manager.step_durations[2] == -2, "Closure step should have correct duration marker (-2)")
|
||||||
assert(assign_step["type"] == "closure", "Assign step should have correct type")
|
assert(seq_manager.step_refs[2] == test_closure, "Closure step should have correct closure")
|
||||||
assert(assign_step["closure"] == test_closure, "Assign step should have correct closure")
|
|
||||||
|
|
||||||
print("✓ Step creation tests passed")
|
print("✓ Step creation tests passed")
|
||||||
end
|
end
|
||||||
@ -109,7 +107,7 @@ def test_sequence_manager_execution()
|
|||||||
seq_manager.start()
|
seq_manager.start()
|
||||||
|
|
||||||
assert(seq_manager.is_running == true, "Sequence should be running after start")
|
assert(seq_manager.is_running == true, "Sequence should be running after start")
|
||||||
assert(seq_manager.steps.size() == 3, "Sequence should have 3 steps")
|
assert(size(seq_manager.step_durations) == 3, "Sequence should have 3 steps")
|
||||||
assert(seq_manager.step_index == 0, "Should start at step 0")
|
assert(seq_manager.step_index == 0, "Should start at step 0")
|
||||||
|
|
||||||
# Check that first animation was started
|
# Check that first animation was started
|
||||||
@ -921,6 +919,189 @@ def test_sequence_manager_boolean_repeat_counts()
|
|||||||
print("✓ Boolean repeat count tests passed")
|
print("✓ Boolean repeat count tests passed")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_sequence_manager_false_conditional_immediate_skip()
|
||||||
|
print("=== SequenceManager False Conditional Immediate Skip Tests ===")
|
||||||
|
|
||||||
|
# This test verifies that when a conditional subsequence (if block) has a false condition,
|
||||||
|
# the parent sequence immediately advances to the next step without waiting for a tick.
|
||||||
|
# This prevents the "black frame" issue where no animation runs for one tick.
|
||||||
|
|
||||||
|
# Create strip and engine
|
||||||
|
var strip = global.Leds(30)
|
||||||
|
var engine = animation.create_engine(strip)
|
||||||
|
|
||||||
|
# Create test animations
|
||||||
|
var color_provider1 = animation.static_color(engine)
|
||||||
|
color_provider1.color = 0xFFFF0000 # Red
|
||||||
|
var anim1 = animation.solid(engine)
|
||||||
|
anim1.color = color_provider1
|
||||||
|
anim1.priority = 0
|
||||||
|
anim1.duration = 0
|
||||||
|
anim1.loop = true
|
||||||
|
|
||||||
|
var color_provider2 = animation.static_color(engine)
|
||||||
|
color_provider2.color = 0xFF00FF00 # Green
|
||||||
|
var anim2 = animation.solid(engine)
|
||||||
|
anim2.color = color_provider2
|
||||||
|
anim2.priority = 0
|
||||||
|
anim2.duration = 0
|
||||||
|
anim2.loop = true
|
||||||
|
|
||||||
|
# Create a parent sequence with:
|
||||||
|
# 1. A conditional subsequence that is FALSE (should skip immediately)
|
||||||
|
# 2. A play step for anim2
|
||||||
|
|
||||||
|
# Create the false conditional subsequence (simulating "if false { ... }")
|
||||||
|
var false_condition = def (engine) return false end
|
||||||
|
var false_subseq = animation.sequence_manager(engine, false_condition)
|
||||||
|
false_subseq.push_play_step(anim1, 1000) # This should never execute
|
||||||
|
|
||||||
|
# Create the parent sequence
|
||||||
|
var parent_seq = animation.sequence_manager(engine, 1) # Run once
|
||||||
|
parent_seq.push_repeat_subsequence(false_subseq) # This should skip immediately
|
||||||
|
parent_seq.push_play_step(anim2, 500) # This should start immediately after the skip
|
||||||
|
|
||||||
|
# Start the parent sequence
|
||||||
|
tasmota.set_millis(200000)
|
||||||
|
engine.add(parent_seq)
|
||||||
|
engine.run()
|
||||||
|
engine.on_tick(200000)
|
||||||
|
|
||||||
|
# At this point, the false subsequence should have been skipped immediately,
|
||||||
|
# and anim2 should already be playing (step_index should be 1)
|
||||||
|
assert(parent_seq.is_running == true, "Parent sequence should be running")
|
||||||
|
assert(parent_seq.step_index == 1, f"Parent should have advanced past false conditional to step 1, got {parent_seq.step_index}")
|
||||||
|
|
||||||
|
# Verify anim2 is in the engine (not anim1)
|
||||||
|
var animations = engine.get_animations()
|
||||||
|
var anim2_found = false
|
||||||
|
var anim1_found = false
|
||||||
|
for anim : animations
|
||||||
|
if anim == anim2
|
||||||
|
anim2_found = true
|
||||||
|
end
|
||||||
|
if anim == anim1
|
||||||
|
anim1_found = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
assert(anim2_found == true, "anim2 should be playing after false conditional skip")
|
||||||
|
assert(anim1_found == false, "anim1 should NOT be playing (false conditional was skipped)")
|
||||||
|
|
||||||
|
# Test 2: Multiple consecutive false conditionals should all skip immediately
|
||||||
|
# Create fresh engine and animations
|
||||||
|
var strip2 = global.Leds(30)
|
||||||
|
var engine2 = animation.create_engine(strip2)
|
||||||
|
|
||||||
|
var color_provider3 = animation.static_color(engine2)
|
||||||
|
color_provider3.color = 0xFFFF0000 # Red
|
||||||
|
var anim3 = animation.solid(engine2)
|
||||||
|
anim3.color = color_provider3
|
||||||
|
anim3.priority = 0
|
||||||
|
anim3.duration = 0
|
||||||
|
anim3.loop = true
|
||||||
|
|
||||||
|
var color_provider4 = animation.static_color(engine2)
|
||||||
|
color_provider4.color = 0xFF00FF00 # Green
|
||||||
|
var anim4 = animation.solid(engine2)
|
||||||
|
anim4.color = color_provider4
|
||||||
|
anim4.priority = 0
|
||||||
|
anim4.duration = 0
|
||||||
|
anim4.loop = true
|
||||||
|
|
||||||
|
var false_condition2 = def (engine) return false end
|
||||||
|
|
||||||
|
var false_subseq2 = animation.sequence_manager(engine2, false_condition2)
|
||||||
|
false_subseq2.push_play_step(anim3, 1000)
|
||||||
|
|
||||||
|
var false_subseq3 = animation.sequence_manager(engine2, false_condition2)
|
||||||
|
false_subseq3.push_play_step(anim3, 1000)
|
||||||
|
|
||||||
|
var parent_seq2 = animation.sequence_manager(engine2, 1)
|
||||||
|
parent_seq2.push_repeat_subsequence(false_subseq2) # Skip
|
||||||
|
parent_seq2.push_repeat_subsequence(false_subseq3) # Skip
|
||||||
|
parent_seq2.push_play_step(anim4, 500) # Should start immediately
|
||||||
|
|
||||||
|
engine2.add(parent_seq2)
|
||||||
|
|
||||||
|
tasmota.set_millis(201000)
|
||||||
|
engine2.run()
|
||||||
|
engine2.on_tick(201000)
|
||||||
|
|
||||||
|
# Should have skipped both false conditionals and be at step 2
|
||||||
|
assert(parent_seq2.is_running == true, "Parent sequence 2 should be running")
|
||||||
|
assert(parent_seq2.step_index == 2, f"Parent should have advanced past both false conditionals to step 2, got {parent_seq2.step_index}")
|
||||||
|
|
||||||
|
print("✓ False conditional immediate skip tests passed")
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_sequence_manager_all_false_conditionals_no_infinite_loop()
|
||||||
|
print("=== SequenceManager All False Conditionals No Infinite Loop Tests ===")
|
||||||
|
|
||||||
|
# This test verifies that when ALL conditional subsequences in a repeat-forever
|
||||||
|
# sequence are false, the sequence doesn't go into an infinite loop.
|
||||||
|
# Instead, it should yield control back after one pass through all steps.
|
||||||
|
|
||||||
|
# Create strip and engine
|
||||||
|
var strip = global.Leds(30)
|
||||||
|
var engine = animation.create_engine(strip)
|
||||||
|
|
||||||
|
# Create test animation (should never be used)
|
||||||
|
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
|
||||||
|
|
||||||
|
# Track how many times closures are executed
|
||||||
|
var closure_count = 0
|
||||||
|
|
||||||
|
# Create two false conditional subsequences (simulating "if inout" and "if outin" both false)
|
||||||
|
var false_condition = def (engine) return false end
|
||||||
|
|
||||||
|
var false_subseq1 = animation.sequence_manager(engine, false_condition)
|
||||||
|
false_subseq1.push_play_step(test_anim, 1000)
|
||||||
|
|
||||||
|
var false_subseq2 = animation.sequence_manager(engine, false_condition)
|
||||||
|
false_subseq2.push_play_step(test_anim, 1000)
|
||||||
|
|
||||||
|
# Create parent sequence that repeats forever with both false conditionals
|
||||||
|
var parent_seq = animation.sequence_manager(engine, -1) # Repeat forever
|
||||||
|
parent_seq.push_closure_step(def (engine) closure_count += 1 end) # Track iterations
|
||||||
|
parent_seq.push_repeat_subsequence(false_subseq1) # if inout (false)
|
||||||
|
parent_seq.push_repeat_subsequence(false_subseq2) # if outin (false)
|
||||||
|
|
||||||
|
# Start the sequence
|
||||||
|
tasmota.set_millis(300000)
|
||||||
|
engine.add(parent_seq)
|
||||||
|
engine.run()
|
||||||
|
engine.on_tick(300000)
|
||||||
|
|
||||||
|
# The sequence should be running but not in an infinite loop
|
||||||
|
assert(parent_seq.is_running == true, "Parent sequence should still be running")
|
||||||
|
|
||||||
|
# The closure should have been executed a limited number of times (not infinite)
|
||||||
|
# With skip_budget, it should execute at most a few times before yielding
|
||||||
|
assert(closure_count < 100, f"Closure should not execute infinitely, got {closure_count}")
|
||||||
|
|
||||||
|
# Simulate a few update cycles - should not hang
|
||||||
|
var update_count = 0
|
||||||
|
var max_updates = 10
|
||||||
|
while update_count < max_updates
|
||||||
|
tasmota.set_millis(300000 + update_count * 100)
|
||||||
|
engine.on_tick(300000 + update_count * 100)
|
||||||
|
parent_seq.update(300000 + update_count * 100)
|
||||||
|
update_count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
# Should still be running (repeat forever) but not have executed infinitely
|
||||||
|
assert(parent_seq.is_running == true, "Parent sequence should still be running after updates")
|
||||||
|
assert(closure_count < 1000, f"Closure count should be bounded, got {closure_count}")
|
||||||
|
|
||||||
|
print("✓ All false conditionals no infinite loop tests passed")
|
||||||
|
end
|
||||||
|
|
||||||
# Run all tests
|
# Run all tests
|
||||||
def run_all_sequence_manager_tests()
|
def run_all_sequence_manager_tests()
|
||||||
print("Starting SequenceManager Unit Tests...")
|
print("Starting SequenceManager Unit Tests...")
|
||||||
@ -943,6 +1124,8 @@ def run_all_sequence_manager_tests()
|
|||||||
test_sequence_manager_zero_iterations()
|
test_sequence_manager_zero_iterations()
|
||||||
test_sequence_manager_zero_palette_size()
|
test_sequence_manager_zero_palette_size()
|
||||||
test_sequence_manager_boolean_repeat_counts()
|
test_sequence_manager_boolean_repeat_counts()
|
||||||
|
test_sequence_manager_false_conditional_immediate_skip()
|
||||||
|
test_sequence_manager_all_false_conditionals_no_infinite_loop()
|
||||||
|
|
||||||
print("\n🎉 All SequenceManager tests passed!")
|
print("\n🎉 All SequenceManager tests passed!")
|
||||||
return true
|
return true
|
||||||
@ -970,5 +1153,7 @@ return {
|
|||||||
"test_sequence_manager_complex_parametric_scenario": test_sequence_manager_complex_parametric_scenario,
|
"test_sequence_manager_complex_parametric_scenario": test_sequence_manager_complex_parametric_scenario,
|
||||||
"test_sequence_manager_zero_iterations": test_sequence_manager_zero_iterations,
|
"test_sequence_manager_zero_iterations": test_sequence_manager_zero_iterations,
|
||||||
"test_sequence_manager_zero_palette_size": test_sequence_manager_zero_palette_size,
|
"test_sequence_manager_zero_palette_size": test_sequence_manager_zero_palette_size,
|
||||||
"test_sequence_manager_boolean_repeat_counts": test_sequence_manager_boolean_repeat_counts
|
"test_sequence_manager_boolean_repeat_counts": test_sequence_manager_boolean_repeat_counts,
|
||||||
|
"test_sequence_manager_false_conditional_immediate_skip": test_sequence_manager_false_conditional_immediate_skip,
|
||||||
|
"test_sequence_manager_all_false_conditionals_no_infinite_loop": test_sequence_manager_all_false_conditionals_no_infinite_loop
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user