Tasmota Berry 动画框架 Animation DSL 转译指南:从声明式语法到可执行 Berry 代码
【免费下载链接】TasmotaAlternative firmware for ESP8266 and ESP32 based devices with easy configuration using webUI, OTA updates, automation using timers or rules, expandability and entirely local control over MQTT, HTTP, Serial or KNX. Full documentation at项目地址: https://gitcode.com/GitHub_Trending/ta/Tasmota
导读
Animation DSL(领域特定语言)是 Tasmota 内置 Berry 动画框架(lib/libesp32/berry_animation)提供的一套声明式动画定义语言,它允许你用近乎自然语言的语法描述动画,再由转译器(transpiler)在编译期生成等价的 Berry 代码。本文以官方参考文档 Dsl_Transpilation.md 为主体,结合 animation_dsl.be 与 transpiler.be 源码,系统讲解 DSL 的模块导入、API、语法、符号解析、import/berry 代码块转译、模板、事件系统、错误处理与性能优化,读完你既能直接用 DSL 编写动画,也能理解其底层转译原理。
一、模块导入与 DSL 使用前提
DSL 功能由独立模块提供,使用时必须先导入动画核心框架,再导入 DSL 编译器与运行时:
import animation # Core framework (required) import animation_dsl # DSL compiler and runtime (required for DSL)从源码看,animation_dsl.be 将animation_dsl注册为全局模块,并在加载时依次导入dsl/token.be、dsl/lexer.be、dsl/transpiler.be、dsl/symbol_table.be、dsl/named_colors.be以及 Web UI 组件。其中:
- lexer:把 DSL 源码切成 token 流(dsl/lexer.be);
- transpiler:单遍(single-pass)把 token 流转换为 Berry 代码(dsl/transpiler.be);
- symbol_table:编译期符号表,负责动态探测、类型校验与冲突检测(dsl/symbol_table.be);
- named_colors:内置命名颜色表(dsl/named_colors.be)。
为什么使用 DSL?
- 声明式语法:描述"想要什么"而非"如何实现";
- 可读性强:接近自然语言的表达;
- 快速原型:动画创意可以快速迭代验证;
- 事件驱动:内置对交互式动画的支持;
- 组合能力:轻松实现动画的叠加与排序。
DSL 与编程式 API 的取舍
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 复杂动画序列 | DSL | 声明式、易读、易维护 |
| 交互式/事件驱动动画 | DSL | 内置事件系统 |
| 快速原型与实验 | DSL | 迭代成本低 |
| 非程序员创作动画 | DSL | 语法门槛低 |
| 可复用动画组件 | 编程式 API | 组件化更自然 |
| 性能敏感场景 | 编程式 API | DSL 有编译开销(约 10–50ms) |
| 需要细粒度控制 | 编程式 API | 直接操作 Berry 对象 |
| 与既有 Berry 代码集成 | 编程式 API | 避免额外抽象层 |
| 固件体积受限 | 编程式 API | DSL 模块可从固件中排除 |
二、DSL 核心 API 函数
2.1animation_dsl.compile(source)
将 DSL 源码编译为 Berry 代码但不执行,适合调试与查看生成结果:
var dsl_source = "color red = 0xFF0000\n" "animation red_anim = solid(color=red)\n" "run red_anim" var berry_code = animation_dsl.compile(dsl_source) print(berry_code) # Shows generated Berry code源码中compile实际指向compile_dsl_source(animation_dsl.be),而真正的转译入口compile_dsl(source)位于 transpiler.be:先由create_lexer(source)生成词法分析器,再构造SimpleDSLTranspiler实例并调用transpile()。
2.2animation_dsl.execute(source)
一步完成"编译 + 执行":
animation_dsl.execute("color blue = 0x0000FF\n" "animation blue_anim = solid(color=blue)\n" "run blue_anim for 5s")其实现(animation_dsl.be)为:先compile(source)得到 Berry 代码,再compile(berry_code)编译成可调用函数并立即执行。
2.3animation_dsl.load_file(filename)
从文件读取 DSL 源码并执行:
# Create a DSL file var f = open("my_animation.dsl", "w") f.write("color green = 0x00FF00\n" "animation pulse_green = breathe(color=green, period=2s)\n" "run pulse_green") f.close() # Load and execute animation_dsl.load_file("my_animation.dsl")源码(animation_dsl.be)在文件打开失败时抛出io_error。
2.4 额外工具:animation_dsl.compile_file(filename)
文档正文之外,模块还提供了批量编译.anim文件为.be文件的工具函数(animation_dsl.be):要求输入必须以.anim结尾,输出同名.be文件并附带生成说明头注释。这在将动画固化为 Berry 模块时非常有用,仓库 dsl/all_wled_palettes.anim 即这类源文件示例。
三、DSL 语言速览
DSL 使用带命名参数的声明式语法,所有动画都采用 engine-first(引擎优先)模式创建,参数逐个赋值以获得最大灵活性。
关键语法特性
- 导入语句:
import module_name,用于加载 Berry 模块 - 命名参数:所有函数调用均使用
name=value语法 - 时间单位:
2s、500ms、1m、1h - 十六进制颜色:
0xFF0000、0x80FF0000(ARGB 格式) - 命名颜色:
red、blue、white等 - 注释:
# This is a comment - 属性赋值:
animation.property = value - 用户函数:
function_name()调用自定义函数
基本结构示例
# Import statements (optional, for user functions or custom modules) import user_functions # Optional strip configuration strip length 60 # Color definitions color red = 0xFF0000 color blue = 0x0000FF # Animation definitions with named parameters animation pulse_red = breathe(color=red, period=2s) animation comet_blue = comet(color=blue, tail_length=10, speed=1500) # Property assignments with user functions pulse_red.priority = 10 pulse_red.opacity = breathing_effect() comet_blue.direction = -1 # Execution run pulse_red上述 DSL 会被转译为:每个动画获得一个 engine 参数,命名参数逐个赋值。典型的生成结果是(对应 Transpiler_Architecture.md 中的 Engine-First 模式):
# Auto-generated strip initialization (using Tasmota configuration) var engine = animation.init_strip() var pulse_ = animation.breathe(engine) pulse_.color = animation.red pulse_.period = 2000四、编译期符号解析(Symbol Resolution)
DSL 转译器在转译时(而非运行时)对标识符(如SINE、red)做智能符号解析,借助 Berry 的内省(introspection)能力判断符号是否存在于animation模块中,从而优化生成代码并消除运行时查找。
# If SINE exists in animation module animation wave = wave(waveform=SINE) # Transpiles to: animation.SINE (direct access) # If custom_color doesn't exist in animation module color custom_color = 0xFF0000 animation solid_red = solid(color=custom_color) # Transpiles to: custom_color_ (user-defined variable)符号类别
内置符号(解析为animation.<symbol>):
- 动画工厂函数:
solid、breathe、comet - 值提供器:
triangle、smooth、sine、static_value - 颜色提供器:
color_cycle、breathe_color、rich_palette_color - 常量:
PALETTE_RAINBOW、SINE、TRIANGLE等
用户定义符号(解析为<symbol>_):
- 自定义颜色:
my_red、fire_color - 自定义动画:
pulse_effect、rainbow_wave - 变量:
brightness_level、cycle_time
属性赋值解析
属性赋值使用同一套解析逻辑:
# Built-in symbol (if 'engine' existed in animation module) engine.brightness = 200 # Would transpile to: animation.engine.brightness = 200 # User-defined symbol my_animation.priority = 10 # Transpiles to: my_animation_.priority = 10底层实现:SymbolTable 与惰性探测
从 Transpiler_Architecture.md 与 dsl/symbol_table.be 可以看到,这套机制由SymbolTable支撑:
- 动态探测:首次遇到符号时用内省缓存其类型(palette、constant、math_function、user_function、value_provider、animation 构造器等);
- MockEngine 校验:用轻量
MockEngine(time_ms: 0、默认 strip 长度 30)实例化工厂函数,判断返回值是否为 value provider 或 animation 实例; - 引用生成:
SymbolEntry.get_reference()依据is_builtin标志统一生成animation.X或x_引用; - 冲突预防:同类型可重新赋值,不同类型则抛出
symbol_redefinition_error,例如color red = 0xFF0000之后再animation red = solid(...)会报错,color max = ...也会与内置数学函数max冲突。
五、import 语句的转译
DSL 用import关键字加载 Berry 模块,为加载用户函数与自定义模块提供干净入口。
# DSL Import Syntax import user_functions import my_custom_module import math转译行为
import 语句被直接转译为带引号模块名的 Berry import:
# DSL Code import user_functions # Transpiles to Berry Code import "user_functions"导入处理流程
- 早期处理:import 语句在转译早期被处理;
- 模块加载:通过标准 Berry import 机制加载模块;
- 函数注册:用户函数模块应通过
animation.register_user_function()注册函数; - 不做校验:DSL 不在编译期验证模块是否存在(运行期由 Berry 负责)。
完整导入工作流
Step 1:创建用户函数模块(user_functions.be)
import animation def rand_demo(engine) import math return math.rand() % 256 end # Register for DSL use animation.register_user_function("rand_demo", rand_demo)Step 2:在 DSL 中使用
import user_functions animation test = solid(color=blue) test.opacity = rand_demo() run testStep 3:生成的 Berry 代码
import animation var engine = animation.init_strip() import "user_functions" var test_ = animation.solid(engine) test_.color = 0xFF0000FF test_.opacity = animation.create_closure_value(engine, def (engine) return animation.get_user_function('rand_demo')(engine) end) engine.add(test_) engine.run()注意test_.opacity被包进了闭包:因为用户函数的结果会随时间变化,转译器将其标记为动态表达式,通过animation.create_closure_value()封装为逐帧求值。
六、Berry 代码块转译
DSL 支持用berry关键字配合三引号字符串嵌入任意 Berry 代码,为复杂逻辑提供逃生舱,同时保持 DSL 的声明式特性。
# DSL Berry Code Block berry """ import math var custom_value = math.pi * 2 print("Custom calculation:", custom_value) """转译行为
Berry 代码块原样复制到生成的 Berry 代码中,并附带注释标记:
# DSL Code berry """ var test_var = 42 print("Hello from berry block") """ # Transpiles to Berry Code # Berry code block var test_var = 42 print("Hello from berry block") # End berry code block与 DSL 对象交互
Berry 代码块可通过下划线后缀命名约定访问 DSL 生成的对象:
# DSL Code animation pulse = breathe(color=red, period=2s) berry """ pulse_.opacity = 200 pulse_.priority = 10 """ # Transpiles to Berry Code var pulse_ = animation.breathe(engine) pulse_.color = animation.red pulse_.period = 2000 # Berry code block pulse_.opacity = 200 pulse_.priority = 10 # End berry code block仓库中的测试 dsl_berry_code_blocks_test.be 对三引号词法与代码块转译做了专项覆盖。
七、模板系统:复用动画定义
DSL 支持两类模板:普通模板(函数)与模板动画(类)。
7.1 模板动画(Template Animation)
模板动画生成继承engine_proxy的可复用动画类:
# DSL Template Animation template animation shutter_effect { param colors type palette nillable true param duration type time min 0 max 3600 default 5 nillable false set strip_len = strip_length() color col = color_cycle(colors=colors, period=0) animation shutter = beacon( color = col beacon_size = strip_len / 2 ) sequence seq repeat forever { play shutter for duration col.next = 1 } run seq }转译为:
class shutter_effect_animation : animation.engine_proxy static var PARAMS = animation.enc_params({ "colors": {"type": "palette", "nillable": true}, "duration": {"type": "time", "min": 0, "max": 3600, "default": 5, "nillable": false} }) def init(engine) super(self).init(engine) var strip_len_ = animation.strip_length(engine) var col_ = animation.color_cycle(engine) col_.colors = animation.create_closure_value(engine, def (engine) return self.colors end) col_.period = 0 var shutter_ = animation.beacon(engine) shutter_.color = col_ shutter_.beacon_size = animation.create_closure_value(engine, def (engine) return animation.resolve(strip_len_) / 2 end) var seq_ = animation.sequence_manager(engine, -1) .push_play_step(shutter_, animation.resolve(self.duration)) .push_closure_step(def (engine) col_.next = 1 end) self.add(seq_) end end关键特性:
- 参数以
self.<param>访问并包装进闭包; - 约束(min、max、default、nillable)编码进
PARAMS; - 使用
self.add()而非engine.add(); - 可用不同参数多次实例化。
继承参数的动态发现:模板动画会自动继承engine_proxy类层级中的参数(id、priority、duration、loop、opacity、color、is_running)。转译器在编译期创建临时engine_proxy实例向上遍历类层级动态收集参数(见 Transpiler_Architecture.md 的_add_inherited_params_to_template()),因此模板内可直接使用duration、opacity等继承参数而无需显式声明。
7.2 普通模板(Regular Template)
普通模板生成 Berry 函数:
# DSL Template template pulse_effect { param color type color param speed animation pulse = breathe(color=color, period=speed) run pulse }转译为:
def pulse_effect_template(engine, color_, speed_) var pulse_ = animation.breathe(engine) pulse_.color = color_ pulse_.period = speed_ engine.add(pulse_) end animation.register_user_function('pulse_effect', pulse_effect_template)7.3 两类模板对比
| 维度 | 模板动画(template animation) | 普通模板(template) |
|---|---|---|
| 生成产物 | 继承engine_proxy的类 | Berry 函数 |
| 参数访问 | self.<param> | <param>_ |
| 参数约束 | 支持 min/max/default/nillable | 不支持 |
| 组合方式 | self.add() | engine.add() |
| 实例化 | 可多次实例化 | 按函数调用 |
模板-only 优化:若一个 DSL 文件只包含模板定义,转译器会跳过 engine 初始化与engine.run()生成,输出纯粹的函数库(参见 Transpiler_Architecture.md)。相关测试见 dsl_template_animation_test.be。
八、用户自定义函数
将自定义 Berry 函数注册到 DSL 中供动画使用。用户函数必须以engine为首参,其后跟随用户提供的参数:
# Define custom function in Berry - engine must be first parameter def custom_twinkle(engine, color, count, period) var anim = animation.twinkle(engine) anim.color = color anim.count = count return anim end # Register the function for DSL use animation.register_user_function("twinkle", custom_twinkle)# Use in DSL - engine is automatically passed as first argument animation gold_twinkle = twinkle(0xFFD700, 8, 500ms) animation blue_twinkle = twinkle(blue, 12, 300ms) run gold_twinkle重要:DSL 转译器会自动把engine作为第一个实参传给所有用户函数。函数签名必须包含engine首参,但 DSL 使用者调用时无需提供它。
从 Transpiler_Architecture.md 的闭包生成示例可见,用户函数在计算表达式中的调用会被改写为animation.get_user_function('rand_demo')(engine)形式。更全面的示例与最佳实践见 User_Functions.md。
九、事件系统
定义响应触发器的事件处理器:
# Define animations for different states color normal = 0x000080 color alert = 0xFF0000 animation normal_state = solid(color=normal) animation alert_state = breathe(color=alert, period=500ms) # Event handlers on button_press { run alert_state for 3s run normal_state } on sensor_trigger { run alert_state for 5s wait 1s run normal_state } # Default state run normal_stateon <event> { ... }块由转译器中的process_event_handler()处理(见 Transpiler_Architecture.md 的处理流程),事件处理器体内支持run、wait等语句,可在触发时切换动画状态。
十、嵌套函数调用
DSL 支持嵌套函数调用以完成复杂组合:
# Nested calls in animation definitions (now supported) animation complex = breathe( color=red, period=2s ) # Nested calls in run statements sequence demo { play breathe(color=blue, period=1s) for 10s }表达式层面对嵌套调用由递归下降解析器中的process_nested_function_call()支持(见 Transpiler_Architecture.md 的 Expression Processing Chain)。
十一、错误处理与编译期校验
DSL 编译器在转译期校验类与参数,在执行前捕获错误:
var invalid_dsl = "color red = #INVALID_COLOR\n" "animation bad = unknown_function(red)\n" "animation pulse = breathe(invalid_param=123)" try animation_dsl.execute(invalid_dsl) except .. as e print("DSL Error:", e) end转译期校验细则
动画工厂校验:
# Error: Function doesn't exist animation bad = nonexistent_animation(color=red) # Transpiler error: "Animation factory function 'nonexistent_animation' does not exist" # Error: Function exists but doesn't create animation animation bad2 = math_function(value=10) # Transpiler error: "Function 'math_function' does not create an animation instance"参数校验:
# Error: Invalid parameter name in constructor animation pulse = breathe(invalid_param=123) # Transpiler error: "Parameter 'invalid_param' is not valid for breathe" # Error: Invalid parameter name in property assignment animation pulse = breathe(color=red, period=2s) pulse.wrong_arg = 15 # Transpiler error: "Animation 'PulseAnimation' does not have parameter 'wrong_arg'" # Error: Parameter constraint violation animation comet = comet(tail_length=-5) # Transpiler error: "Parameter 'tail_length' value -5 violates constraint: min=1"颜色提供器校验:
# Error: Color provider doesn't exist color bad = nonexistent_color_provider(period=2s) # Transpiler error: "Color provider factory 'nonexistent_color_provider' does not exist" # Error: Function exists but doesn't create color provider color bad2 = breathe(color=red) # Transpiler error: "Function 'breathe' does not create a color provider instance"引用校验:
# Error: Undefined color reference animation pulse = breathe(color=undefined_color) # Transpiler error: "Undefined reference: 'undefined_color'" # Error: Undefined animation reference in run statement run nonexistent_animation # Transpiler error: "Undefined reference 'nonexistent_animation' in run" # Error: Undefined animation reference in sequence sequence demo { play nonexistent_animation for 5s } # Transpiler error: "Undefined reference 'nonexistent_animation' in sequence play"函数调用安全性校验:
# Error: Dangerous function creation in computed expression set strip_len3 = (strip_length() + 1) / 2 # Transpiler error: "Function 'strip_length()' cannot be used in computed expressions. # This creates a new instance at each evaluation. Use either: # set var_name = strip_length() # Single function call # set computed = (existing_var + 1) / 2 # Computation with existing values"为什么需要这项校验:转译器阻止"在会被包进闭包的计算表达式中调用创建实例的函数"这类危险模式。否则每次闭包求值都会新建实例,导致内存泄漏、性能退化以及多个时序状态导致的运行不一致。
安全替代写法:
# ✅ CORRECT: Separate function call from computation set strip_len = strip_length() # Single function call set strip_len3 = (strip_len + 1) / 2 # Computation with existing value模板参数校验:
# Error: Duplicate parameter names template bad_template { param color type color param color type number # Error: duplicate parameter name } # Transpiler error: "Duplicate parameter name 'color' in template" # Error: Reserved keyword as parameter name template reserved_template { param animation type color # Error: conflicts with reserved keyword } # Transpiler error: "Parameter name 'animation' conflicts with reserved keyword" # Error: Built-in color name as parameter template color_template { param red type number # Error: conflicts with built-in color } # Transpiler error: "Parameter name 'red' conflicts with built-in color name" # Error: Invalid type annotation template type_template { param value type invalid_type # Error: invalid type } # Transpiler error: "Invalid parameter type 'invalid_type'. Valid types are: [...]" # Warning: Unused parameter (compilation succeeds) template unused_template { param used_color type color param unused_param type number # Warning: never used animation test = solid(color=used_color) run test } # Transpiler warning: "Template 'unused_template' parameter 'unused_param' is declared but never used"错误分类
- 语法错误:DSL 语法无效(词法/解析错误);
- 工厂校验:不存在的或无效的动画/颜色提供器工厂;
- 参数校验:构造器或属性赋值中出现无效参数名;
- 模板校验:模板参数名、类型或使用模式非法;
- 约束校验:参数值违反约束(min/max、枚举、类型);
- 引用校验:使用未定义的色彩、动画或变量;
- 类型校验:参数类型错误或不兼容的赋值;
- 安全性校验:可能导致内存泄漏或性能问题的危险模式;
- 运行时错误:Berry 代码执行期错误(校验充分时很少发生)。
警告分类
转译器还会产生不阻止编译的警告,提示潜在代码质量问题:
- 未使用参数:模板中声明但从未在模板体内使用的参数;
- 代码质量:更好的编码实践建议。
警告行为:
- 警告以注释形式写入生成的 Berry 代码;
- 存在警告时编译仍然成功;
- 警告在保持代码质量的同时不过度约束开发者。
从实现上看,transpiler.be 的transpile()会把所有 warning 以# Compilation warnings:注释追加到输出末尾。相关测试见 dsl_parameter_validation_test.be、dsl_undefined_identifier_test.be 与 dsl_value_provider_validation_test.be。
十二、性能考量
DSL 与编程式 API 的性能对比
- DSL 编译开销:约 10–50ms,取决于复杂度;
- 生成代码性能:与手写 Berry 代码一致;
- 内存占用:编译期使用临时内存。
转译器的"超简单遍架构"(ultra-simplified single-pass)本身就是性能设计:token 流从头到尾只处理一遍、符号表增量构建、直接生成代码而不构造大型 AST 中间结构、惰性符号探测与结果缓存(详见 Transpiler_Architecture.md 的 Performance Considerations)。
优化建议
一次编译、多次运行:
var compiled = animation_dsl.compile(dsl_source) var fn = compile(compiled) # Run multiple times without recompilation fn() # First execution fn() # Subsequent executions are faster性能关键代码使用编程式 API:
# DSL for high-level structure animation_dsl.execute( "sequence main {\n" "play performance_critical_anim for 10s\n" "}\n" "run main" ) # Programmatic for performance-critical animations var performance_critical_anim = animation.create_optimized_animation()
十三、集成示例
与 Tasmota 规则系统集成
# In autoexec.be import animation import animation_dsl def handle_rule_trigger(event) if event == "motion" animation_dsl.execute("color alert = 0xFF0000\n" "animation alert_anim = breathe(color=alert, period=500ms)\n" "run alert_anim for 5s") elif event == "door" animation_dsl.execute("color welcome = 0x00FF00\n" "animation welcome_anim = breathe(color=welcome, period=2s)\n" "run welcome_anim for 8s") end end # Register with Tasmota's rule system tasmota.add_rule("motion", handle_rule_trigger)与 Web 界面集成
# Create web endpoints for DSL execution import webserver def web_execute_dsl() var dsl_code = webserver.arg("dsl") if dsl_code try animation_dsl.execute(dsl_code) webserver.content_response("DSL executed successfully") except .. as e webserver.content_response(f"DSL Error: {e}") end else webserver.content_response("No DSL code provided") end end webserver.on("/execute_dsl", web_execute_dsl)动画框架本身也自带了 Web UI 组件(webui/animation_web_ui.be),由 animation_dsl.be 在模块初始化时挂载到animation.web_ui。
十四、最佳实践
结构化组织 DSL 文件:
# Strip configuration first strip length 60 # Colors next color red = 0xFF0000 color blue = 0x0000FF # Animations with named parameters animation red_solid = solid(color=red) animation pulse_red = breathe(color=red, period=2s) # Property assignments pulse_red.priority = 10 # Sequences sequence demo { play pulse_red for 5s } # Execution last run demo使用有意义的命名:
# Good color warning_red = 0xFF0000 animation door_alert = breathe(color=warning_red, period=500ms) # Avoid color c1 = 0xFF0000 animation a1 = breathe(color=c1, period=500ms)为 DSL 写注释:
# Security system colors color normal_blue = 0x000080 # Idle state color alert_red = 0xFF0000 # Alert state color success_green = 0x00FF00 # Success state # Main security animation sequence sequence security_demo { play solid(color=normal_blue) for 10s # Normal operation play breathe(color=alert_red, period=500ms) for 3s # Alert play breathe(color=success_green, period=2s) for 5s # Success confirmation }大型项目分文件组织:
# Load DSL modules animation_dsl.load_file("colors.dsl") # Color definitions animation_dsl.load_file("animations.dsl") # Animation library animation_dsl.load_file("sequences.dsl") # Sequence definitions animation_dsl.load_file("main.dsl") # Main execution
十五、进一步阅读
- 转译器内部架构与表达式处理链:Transpiler_Architecture.md
- DSL 完整语言参考:Dsl_Reference.md
- 用户函数指南:User_Functions.md
- 动画快速上手:Quick_Start.md
- 动画类层级:Animation_Class_Hierarchy.md
- 示例与动画效果库:Examples.md、anim_examples、anim_tutorials
- 常见问题排查:Troubleshooting.md
总结来说,Animation DSL 以声明式语法降低了动画创作门槛,同时通过编译期符号解析、参数校验与闭包生成,让生成的 Berry 代码与手写代码性能一致。理解本文介绍的转译链路(词法分析 → 单遍转译 → 符号表校验 → Berry 代码生成 → engine 执行),你就能在 Tasmota 设备上高效、安全地构建从简单纯色到复杂事件驱动序列的各类动画。
【免费下载链接】TasmotaAlternative firmware for ESP8266 and ESP32 based devices with easy configuration using webUI, OTA updates, automation using timers or rules, expandability and entirely local control over MQTT, HTTP, Serial or KNX. Full documentation at项目地址: https://gitcode.com/GitHub_Trending/ta/Tasmota
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考