mirror of
https://github.com/arendst/Tasmota.git
synced 2025-11-25 18:57:41 +00:00
677 lines
17 KiB
Markdown
677 lines
17 KiB
Markdown
# User-Defined Functions
|
|
|
|
Create custom animation functions in Berry and use them seamlessly in the Animation DSL.
|
|
|
|
## Quick Start
|
|
|
|
### 1. Create Your Function
|
|
|
|
Write a Berry function that creates and returns an animation:
|
|
|
|
```berry
|
|
# Define a custom breathing effect
|
|
def my_breathing(engine, color, speed)
|
|
var anim = animation.pulsating_animation(engine)
|
|
anim.color = color
|
|
anim.min_brightness = 50
|
|
anim.max_brightness = 255
|
|
anim.period = speed
|
|
return anim
|
|
end
|
|
```
|
|
|
|
### 2. Register It
|
|
|
|
Make your function available in DSL:
|
|
|
|
```berry
|
|
animation.register_user_function("breathing", my_breathing)
|
|
```
|
|
|
|
### 3. Use It in DSL
|
|
|
|
First, import your user functions module, then call your function directly in computed parameters:
|
|
|
|
```berry
|
|
# Import your user functions module
|
|
import user_functions
|
|
|
|
# Use your custom function in computed parameters
|
|
animation calm = solid(color=blue)
|
|
calm.opacity = breathing_effect()
|
|
|
|
animation energetic = solid(color=red)
|
|
energetic.opacity = breathing_effect()
|
|
|
|
sequence demo {
|
|
play calm for 10s
|
|
play energetic for 5s
|
|
}
|
|
|
|
run demo
|
|
```
|
|
|
|
## Importing User Functions
|
|
|
|
### DSL Import Statement
|
|
|
|
The DSL supports importing Berry modules using the `import` keyword. This is the recommended way to make user functions available in your animations:
|
|
|
|
```berry
|
|
# Import user functions at the beginning of your DSL file
|
|
import user_functions
|
|
|
|
# Now user functions are available directly
|
|
animation test = solid(color=blue)
|
|
test.opacity = my_function()
|
|
```
|
|
|
|
### Import Behavior
|
|
|
|
- **Module Loading**: `import user_functions` transpiles to Berry `import "user_functions"`
|
|
- **Function Registration**: The imported module should register functions using `animation.register_user_function()`
|
|
- **Availability**: Once imported, functions are available throughout the DSL file
|
|
- **No Compile-Time Checking**: The DSL doesn't validate user function existence at compile time
|
|
|
|
### Example User Functions Module
|
|
|
|
Create a file called `user_functions.be`:
|
|
|
|
```berry
|
|
import animation
|
|
|
|
# Define your custom functions
|
|
def rand_demo(engine)
|
|
import math
|
|
return math.rand() % 256 # Random value 0-255
|
|
end
|
|
|
|
def breathing_effect(engine, base_value, amplitude)
|
|
import math
|
|
var time_factor = (engine.time_ms / 1000) % 4 # 4-second cycle
|
|
var breath = math.sin(time_factor * math.pi / 2)
|
|
return int(base_value + breath * amplitude)
|
|
end
|
|
|
|
# Register functions for DSL use
|
|
animation.register_user_function("rand_demo", rand_demo)
|
|
animation.register_user_function("breathing", breathing_effect)
|
|
|
|
print("User functions loaded!")
|
|
```
|
|
|
|
### Using Imported Functions in DSL
|
|
|
|
```berry
|
|
import user_functions
|
|
|
|
# Simple user function call
|
|
animation random_test = solid(color=red)
|
|
random_test.opacity = rand_demo()
|
|
|
|
# User function with parameters
|
|
animation breathing_blue = solid(color=blue)
|
|
breathing_blue.opacity = breathing(128, 64)
|
|
|
|
# User functions in mathematical expressions
|
|
animation complex = solid(color=green)
|
|
complex.opacity = max(50, min(255, rand_demo() + 100))
|
|
|
|
run random_test
|
|
```
|
|
|
|
### Multiple Module Imports
|
|
|
|
You can import multiple modules in the same DSL file:
|
|
|
|
```berry
|
|
import user_functions # Basic user functions
|
|
import fire_effects # Fire animation functions
|
|
import color_utilities # Color manipulation functions
|
|
|
|
animation base = solid(color=random_color())
|
|
base.opacity = breathing(200, 50)
|
|
|
|
animation flames = solid(color=red)
|
|
flames.opacity = fire_intensity(180)
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### Simple Color Effects
|
|
|
|
```berry
|
|
def solid_bright(engine, color, brightness_percent)
|
|
var anim = animation.solid_animation(engine)
|
|
anim.color = color
|
|
anim.brightness = int(brightness_percent * 255 / 100)
|
|
return anim
|
|
end
|
|
|
|
animation.register_user_function("bright", solid_bright)
|
|
```
|
|
|
|
```berry
|
|
animation bright_red = solid(color=red)
|
|
bright_red.opacity = bright(80)
|
|
|
|
animation dim_blue = solid(color=blue)
|
|
dim_blue.opacity = bright(30)
|
|
```
|
|
|
|
### Fire Effects
|
|
|
|
```berry
|
|
def custom_fire(engine, intensity, speed)
|
|
var color_provider = animation.rich_palette(engine)
|
|
color_provider.palette = animation.PALETTE_FIRE
|
|
color_provider.cycle_period = speed
|
|
|
|
var fire_anim = animation.filled(engine)
|
|
fire_anim.color_provider = color_provider
|
|
fire_anim.brightness = intensity
|
|
return fire_anim
|
|
end
|
|
|
|
animation.register_user_function("fire", custom_fire)
|
|
```
|
|
|
|
```berry
|
|
animation campfire = solid(color=red)
|
|
campfire.opacity = fire(200, 2000)
|
|
|
|
animation torch = solid(color=orange)
|
|
torch.opacity = fire(255, 500)
|
|
```
|
|
|
|
### Twinkling Effects
|
|
|
|
```berry
|
|
def twinkles(engine, color, count, period)
|
|
var anim = animation.twinkle_animation(engine)
|
|
anim.color = color
|
|
anim.count = count
|
|
anim.period = period
|
|
return anim
|
|
end
|
|
|
|
animation.register_user_function("twinkles", twinkles)
|
|
```
|
|
|
|
```berry
|
|
animation stars = solid(color=white)
|
|
stars.opacity = twinkles(12, 800ms)
|
|
|
|
animation fairy_dust = solid(color=0xFFD700)
|
|
fairy_dust.opacity = twinkles(8, 600ms)
|
|
```
|
|
|
|
### Position-Based Effects
|
|
|
|
```berry
|
|
def pulse_at(engine, color, position, width, speed)
|
|
var anim = animation.beacon_animation(engine)
|
|
anim.color = color
|
|
anim.position = position
|
|
anim.width = width
|
|
anim.period = speed
|
|
return anim
|
|
end
|
|
|
|
animation.register_user_function("pulse_at", pulse_at)
|
|
```
|
|
|
|
```berry
|
|
animation left_pulse = solid(color=green)
|
|
left_pulse.position = pulse_at(5, 3, 2000)
|
|
|
|
animation right_pulse = solid(color=blue)
|
|
right_pulse.position = pulse_at(25, 3, 2000)
|
|
```
|
|
|
|
## Advanced Examples
|
|
|
|
### Multi-Layer Effects
|
|
|
|
```berry
|
|
def rainbow_twinkle(engine, base_speed, twinkle_density)
|
|
# Create base rainbow animation
|
|
var rainbow_provider = animation.rich_palette(engine)
|
|
rainbow_provider.palette = animation.PALETTE_RAINBOW
|
|
rainbow_provider.cycle_period = base_speed
|
|
|
|
var base_anim = animation.filled(engine)
|
|
base_anim.color_provider = rainbow_provider
|
|
base_anim.priority = 1
|
|
|
|
# Note: This is a simplified example
|
|
# Real multi-layer effects would require engine support
|
|
return base_anim
|
|
end
|
|
|
|
animation.register_user_function("rainbow_sparkle", rainbow_sparkle)
|
|
```
|
|
|
|
### Dynamic Palettes
|
|
|
|
Since DSL palettes only accept hex colors and predefined color names (not custom colors), use user functions for dynamic palettes with custom colors:
|
|
|
|
```berry
|
|
def create_custom_palette(engine, base_color, variation_count, intensity)
|
|
# Create a palette with variations of the base color
|
|
var palette_bytes = bytes()
|
|
|
|
# Extract RGB components from base color
|
|
var r = (base_color >> 16) & 0xFF
|
|
var g = (base_color >> 8) & 0xFF
|
|
var b = base_color & 0xFF
|
|
|
|
# Create palette entries with color variations
|
|
for i : 0..(variation_count-1)
|
|
var position = int(i * 255 / (variation_count - 1))
|
|
var factor = intensity * i / (variation_count - 1) / 255
|
|
|
|
var new_r = int(r * factor)
|
|
var new_g = int(g * factor)
|
|
var new_b = int(b * factor)
|
|
|
|
# Add VRGB entry (Value, Red, Green, Blue)
|
|
palette_bytes.add(position, 1) # Position
|
|
palette_bytes.add(new_r, 1) # Red
|
|
palette_bytes.add(new_g, 1) # Green
|
|
palette_bytes.add(new_b, 1) # Blue
|
|
end
|
|
|
|
return palette_bytes
|
|
end
|
|
|
|
animation.register_user_function("custom_palette", create_custom_palette)
|
|
```
|
|
|
|
```berry
|
|
# Use dynamic palette in DSL
|
|
animation gradient_effect = rich_palette(
|
|
palette=custom_palette(0xFF6B35, 5, 255)
|
|
cycle_period=4s
|
|
)
|
|
|
|
run gradient_effect
|
|
```
|
|
|
|
### Preset Configurations
|
|
|
|
```berry
|
|
def police_lights(engine, flash_speed)
|
|
var anim = animation.pulsating_animation(engine)
|
|
anim.color = 0xFFFF0000 # Red
|
|
anim.min_brightness = 0
|
|
anim.max_brightness = 255
|
|
anim.period = flash_speed
|
|
return anim
|
|
end
|
|
|
|
def warning_strobe(engine)
|
|
return police_lights(engine, 200) # Fast strobe
|
|
end
|
|
|
|
def gentle_alert(engine)
|
|
return police_lights(engine, 1000) # Slow pulse
|
|
end
|
|
|
|
animation.register_user_function("police", police_lights)
|
|
animation.register_user_function("strobe", warning_strobe)
|
|
animation.register_user_function("alert", gentle_alert)
|
|
```
|
|
|
|
```berry
|
|
animation emergency = solid(color=red)
|
|
emergency.opacity = strobe()
|
|
|
|
animation notification = solid(color=yellow)
|
|
notification.opacity = alert()
|
|
|
|
animation custom_police = solid(color=blue)
|
|
custom_police.opacity = police(500)
|
|
```
|
|
|
|
## Function Organization
|
|
|
|
### Single File Approach
|
|
|
|
```berry
|
|
# user_animations.be
|
|
import animation
|
|
|
|
def breathing(engine, color, period)
|
|
# ... implementation
|
|
end
|
|
|
|
def fire_effect(engine, intensity, speed)
|
|
# ... implementation
|
|
end
|
|
|
|
def twinkle_effect(engine, color, count, period)
|
|
# ... implementation
|
|
end
|
|
|
|
# Register all functions
|
|
animation.register_user_function("breathing", breathing)
|
|
animation.register_user_function("fire", fire_effect)
|
|
animation.register_user_function("twinkle", twinkle_effect)
|
|
|
|
print("Custom animations loaded!")
|
|
```
|
|
|
|
### Modular Approach
|
|
|
|
```berry
|
|
# animations/fire.be
|
|
def fire_effect(engine, intensity, speed)
|
|
# ... implementation
|
|
end
|
|
|
|
def torch_effect(engine)
|
|
return fire_effect(engine, 255, 500)
|
|
end
|
|
|
|
return {
|
|
'fire': fire_effect,
|
|
'torch': torch_effect
|
|
}
|
|
```
|
|
|
|
```berry
|
|
# main.be
|
|
import animation
|
|
|
|
# Register functions
|
|
animation.register_user_function("fire", fire_effects['fire'])
|
|
animation.register_user_function("torch", fire_effects['torch'])
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### Function Design
|
|
|
|
1. **Use descriptive names**: `breathing_slow` not `bs`
|
|
2. **Logical parameter order**: color first, then timing, then modifiers
|
|
3. **Sensible defaults**: Make functions work with minimal parameters
|
|
4. **Return animations**: Always return a configured animation object
|
|
|
|
### Parameter Handling
|
|
|
|
```berry
|
|
def flexible_pulse(engine, color, period, min_brightness, max_brightness)
|
|
# Provide defaults for optional parameters
|
|
if min_brightness == nil min_brightness = 50 end
|
|
if max_brightness == nil max_brightness = 255 end
|
|
|
|
var anim = animation.pulsating_animation(engine)
|
|
anim.color = color
|
|
anim.period = period
|
|
anim.min_brightness = min_brightness
|
|
anim.max_brightness = max_brightness
|
|
return anim
|
|
end
|
|
```
|
|
|
|
### Error Handling
|
|
|
|
```berry
|
|
def safe_comet(engine, color, tail_length, speed)
|
|
# Validate parameters
|
|
if tail_length < 1 tail_length = 1 end
|
|
if tail_length > 20 tail_length = 20 end
|
|
if speed < 100 speed = 100 end
|
|
|
|
var anim = animation.comet_animation(engine)
|
|
anim.color = color
|
|
anim.tail_length = tail_length
|
|
anim.speed = speed
|
|
return anim
|
|
end
|
|
```
|
|
|
|
### Documentation
|
|
|
|
```berry
|
|
# Creates a pulsing animation with customizable brightness range
|
|
# Parameters:
|
|
# color: The color to pulse (hex or named color)
|
|
# period: How long one pulse cycle takes (in milliseconds)
|
|
# min_brightness: Minimum brightness (0-255, default: 50)
|
|
# max_brightness: Maximum brightness (0-255, default: 255)
|
|
# Returns: Configured pulse animation
|
|
def breathing_effect(engine, color, period, min_brightness, max_brightness)
|
|
# ... implementation
|
|
end
|
|
```
|
|
|
|
## User Functions in Computed Parameters
|
|
|
|
User functions can be used in computed parameter expressions alongside mathematical functions, creating powerful dynamic animations:
|
|
|
|
### Simple User Function in Computed Parameter
|
|
|
|
```berry
|
|
# Simple user function call in property assignment
|
|
animation base = solid(color=blue, priority=10)
|
|
base.opacity = rand_demo() # User function as computed parameter
|
|
```
|
|
|
|
### User Functions with Mathematical Operations
|
|
|
|
```berry
|
|
# Get strip length for calculations
|
|
set strip_len = strip_length()
|
|
|
|
# Mix user functions with mathematical functions
|
|
animation dynamic_solid = solid(
|
|
color=purple
|
|
opacity=max(50, min(255, rand_demo() + 100)) # Random opacity with bounds
|
|
priority=15
|
|
)
|
|
```
|
|
|
|
### User Functions in Complex Expressions
|
|
|
|
```berry
|
|
# Use user function in arithmetic expressions
|
|
animation random_effect = solid(
|
|
color=cyan
|
|
opacity=abs(rand_demo() - 128) + 64 # Random variation around middle value
|
|
priority=12
|
|
)
|
|
```
|
|
|
|
### How It Works
|
|
|
|
When you use user functions in computed parameters:
|
|
|
|
1. **Automatic Detection**: The transpiler automatically detects user functions in expressions
|
|
2. **Single Closure**: The entire expression is wrapped in a single efficient closure
|
|
3. **Engine Access**: User functions receive `engine` in the closure context
|
|
4. **Mixed Operations**: User functions work seamlessly with mathematical functions and arithmetic
|
|
|
|
**Generated Code Example:**
|
|
```berry
|
|
# DSL code
|
|
animation.opacity = max(100, breathing(red, 2000))
|
|
```
|
|
|
|
**Transpiles to:**
|
|
```berry
|
|
animation.opacity = animation.create_closure_value(engine,
|
|
def (engine, param_name, time_ms)
|
|
return (animation._math.max(100, animation.get_user_function('breathing')(engine, 0xFFFF0000, 2000)))
|
|
end)
|
|
```
|
|
|
|
### Available User Functions
|
|
|
|
The following user functions are available by default:
|
|
|
|
| Function | Parameters | Description |
|
|
|----------|------------|-------------|
|
|
| `rand_demo()` | none | Returns a random value (0-255) for demonstration |
|
|
|
|
### Best Practices for Computed Parameters
|
|
|
|
1. **Keep expressions readable**: Break complex expressions across multiple lines
|
|
2. **Use meaningful variable names**: `set strip_len = strip_length()` not `set s = strip_length()`
|
|
3. **Combine wisely**: Mix user functions with math functions for rich effects
|
|
4. **Test incrementally**: Start simple and build up complex expressions
|
|
|
|
## Loading and Using Functions
|
|
|
|
### In Tasmota autoexec.be
|
|
|
|
```berry
|
|
import animation
|
|
|
|
# Load your custom functions
|
|
load("user_animations.be")
|
|
|
|
# Now they're available in DSL with import
|
|
var dsl_code =
|
|
"import user_functions\n"
|
|
"\n"
|
|
"animation my_fire = solid(color=red)\n"
|
|
"my_fire.opacity = fire(200, 1500)\n"
|
|
"animation my_twinkles = solid(color=white)\n"
|
|
"my_twinkles.opacity = twinkle(8, 400ms)\n"
|
|
"\n"
|
|
"sequence show {\n"
|
|
" play my_fire for 10s\n"
|
|
" play my_twinkles for 5s\n"
|
|
"}\n"
|
|
"\n"
|
|
"run show"
|
|
|
|
animation_dsl.execute(dsl_code)
|
|
```
|
|
|
|
### From Files
|
|
|
|
```berry
|
|
# Save DSL with custom functions
|
|
var my_show =
|
|
"import user_functions\n"
|
|
"\n"
|
|
"animation campfire = solid(color=orange)\n"
|
|
"campfire.opacity = fire(180, 2000)\n"
|
|
"animation stars = solid(color=0xFFFFFF)\n"
|
|
"stars.opacity = twinkle(6, 600ms)\n"
|
|
"\n"
|
|
"sequence night_scene {\n"
|
|
" play campfire for 30s\n"
|
|
" play stars for 10s\n"
|
|
"}\n"
|
|
"\n"
|
|
"run night_scene"
|
|
|
|
# Save to file
|
|
var f = open("night_scene.anim", "w")
|
|
f.write(my_show)
|
|
f.close()
|
|
|
|
# Load and run
|
|
animation_dsl.load_file("night_scene.anim")
|
|
```
|
|
|
|
## Implementation Details
|
|
|
|
### Function Signature Requirements
|
|
|
|
User functions must follow this exact pattern:
|
|
|
|
```berry
|
|
def function_name(engine, param1, param2, ...)
|
|
# engine is ALWAYS the first parameter
|
|
# followed by user-provided parameters
|
|
return animation_object
|
|
end
|
|
```
|
|
|
|
### How the DSL Transpiler Works
|
|
|
|
When you write DSL like this:
|
|
```berry
|
|
animation my_anim = my_function(arg1, arg2)
|
|
```
|
|
|
|
The transpiler generates Berry code like this:
|
|
```berry
|
|
var my_anim_ = animation.get_user_function('my_function')(engine, arg1, arg2)
|
|
```
|
|
|
|
The `engine` parameter is automatically inserted as the first argument.
|
|
|
|
### Registration API
|
|
|
|
```berry
|
|
# Register a function
|
|
animation.register_user_function(name, function)
|
|
|
|
# Check if a function is registered
|
|
if animation.is_user_function("my_function")
|
|
print("Function is registered")
|
|
end
|
|
|
|
# Get a registered function
|
|
var func = animation.get_user_function("my_function")
|
|
|
|
# List all registered functions
|
|
var functions = animation.list_user_functions()
|
|
for name : functions
|
|
print("Registered:", name)
|
|
end
|
|
```
|
|
|
|
### Engine Parameter
|
|
|
|
The `engine` parameter provides:
|
|
- Access to the LED strip: `engine.get_strip_length()`
|
|
- Current time: `engine.time_ms`
|
|
- Animation management context
|
|
|
|
Always use the provided engine when creating animations - don't create your own engine instances.
|
|
|
|
### Return Value Requirements
|
|
|
|
User functions must return an animation object that:
|
|
- Extends `animation.animation` or `animation.pattern`
|
|
- Is properly configured with the engine
|
|
- Has all required parameters set
|
|
|
|
### Error Handling
|
|
|
|
The framework handles errors gracefully:
|
|
- Invalid function names are caught at DSL compile time
|
|
- Runtime errors in user functions are reported with context
|
|
- Failed function calls don't crash the animation system
|
|
|
|
## Troubleshooting
|
|
|
|
### Function Not Found
|
|
```
|
|
Error: Unknown function 'my_function'
|
|
```
|
|
- Ensure the function is registered with `animation.register_user_function()`
|
|
- Check that registration happens before DSL compilation
|
|
- Verify the function name matches exactly (case-sensitive)
|
|
|
|
### Wrong Number of Arguments
|
|
```
|
|
Error: Function call failed
|
|
```
|
|
- Check that your function signature matches the DSL call
|
|
- Remember that `engine` is automatically added as the first parameter
|
|
- Verify all required parameters are provided in the DSL
|
|
|
|
### Animation Not Working
|
|
- Ensure your function returns a valid animation object
|
|
- Check that the animation is properly configured
|
|
- Verify that the engine parameter is used correctly
|
|
|
|
User-defined functions provide a powerful way to extend the Animation DSL with custom effects while maintaining the clean, declarative syntax that makes the DSL easy to use. |