Files
esphome/esphome/components/libretiny/__init__.py
2026-02-10 20:07:09 +00:00

400 lines
14 KiB
Python

import json
import logging
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_BOARD,
CONF_COMPONENT_ID,
CONF_DEBUG,
CONF_FAMILY,
CONF_FRAMEWORK,
CONF_ID,
CONF_NAME,
CONF_OPTIONS,
CONF_PROJECT,
CONF_SOURCE,
CONF_VERSION,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
KEY_TARGET_FRAMEWORK,
KEY_TARGET_PLATFORM,
ThreadModel,
__version__,
)
from esphome.core import CORE
from esphome.storage_json import StorageJSON
from . import gpio # noqa
from .const import (
CONF_GPIO_RECOVER,
CONF_LOGLEVEL,
CONF_SDK_SILENT,
CONF_UART_PORT,
FAMILIES,
FAMILY_BK7231N,
FAMILY_COMPONENT,
FAMILY_FRIENDLY,
FAMILY_RTL8710B,
KEY_BOARD,
KEY_COMPONENT,
KEY_COMPONENT_DATA,
KEY_FAMILY,
KEY_LIBRETINY,
LT_DEBUG_MODULES,
LT_LOGLEVELS,
LibreTinyComponent,
LTComponent,
)
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@kuba2k2"]
AUTO_LOAD = ["preferences"]
IS_TARGET_PLATFORM = True
# BK7231N SDK options to disable unused features.
# Disabling BLE saves ~21KB RAM and ~200KB Flash because BLE init code is
# called unconditionally by the SDK. ESPHome doesn't use BLE on LibreTiny.
#
# This only works on BK7231N (BLE 5.x). Other BK72XX chips using BLE 4.2
# (BK7231T, BK7231Q, BK7251; BK7252 boards use the BK7251 family) have a bug
# where the BLE library still links and references undefined symbols when
# CFG_SUPPORT_BLE=0.
#
# Other options like CFG_TX_EVM_TEST, CFG_RX_SENSITIVITY_TEST, CFG_SUPPORT_BKREG,
# CFG_SUPPORT_OTA_HTTP, and CFG_USE_SPI_SLAVE were evaluated but provide no # NOLINT
# measurable benefit - the linker already strips unreferenced code via -gc-sections.
_BK7231N_SYS_CONFIG_OPTIONS = [
"CFG_SUPPORT_BLE=0",
]
def _detect_variant(value):
if KEY_LIBRETINY not in CORE.data:
raise cv.Invalid("Family component didn't populate core data properly!")
component: LibreTinyComponent = CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA]
board = value[CONF_BOARD]
# read board-default family if not specified
if board not in component.boards:
if CONF_FAMILY not in value:
raise cv.Invalid(
"This board is unknown, if you are sure you want to compile with this board selection, "
f"override with option '{CONF_FAMILY}'",
path=[CONF_BOARD],
)
_LOGGER.warning(
"This board is unknown. Make sure the chosen chip component is correct.",
)
else:
family = component.boards[board][KEY_FAMILY]
if CONF_FAMILY in value and family != value[CONF_FAMILY]:
raise cv.Invalid(
f"Option '{CONF_FAMILY}' does not match selected board.",
path=[CONF_FAMILY],
)
value = value.copy()
value[CONF_FAMILY] = family
# read component name matching this family
value[CONF_COMPONENT_ID] = FAMILY_COMPONENT[value[CONF_FAMILY]]
# make sure the chosen component matches the family
if value[CONF_COMPONENT_ID] != component.name:
raise cv.Invalid(
f"The chosen family doesn't belong to '{component.name}' component. The correct component is '{value[CONF_COMPONENT_ID]}'",
path=[CONF_FAMILY],
)
return value
def _update_core_data(config):
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = config[CONF_COMPONENT_ID]
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino"
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
config[CONF_FRAMEWORK][CONF_VERSION]
)
CORE.data[KEY_LIBRETINY][KEY_BOARD] = config[CONF_BOARD]
CORE.data[KEY_LIBRETINY][KEY_COMPONENT] = config[CONF_COMPONENT_ID]
CORE.data[KEY_LIBRETINY][KEY_FAMILY] = config[CONF_FAMILY]
return config
def get_libretiny_component(core_obj=None):
return (core_obj or CORE).data[KEY_LIBRETINY][KEY_COMPONENT]
def get_libretiny_family(core_obj=None):
return (core_obj or CORE).data[KEY_LIBRETINY][KEY_FAMILY]
def only_on_family(*, supported=None, unsupported=None):
"""Config validator for features only available on some LibreTiny families."""
if supported is not None and not isinstance(supported, list):
supported = [supported]
if unsupported is not None and not isinstance(unsupported, list):
unsupported = [unsupported]
def validator_(obj):
family = get_libretiny_family()
if supported is not None and family not in supported:
raise cv.Invalid(
f"This feature is only available on {', '.join(supported)}"
)
if unsupported is not None and family in unsupported:
raise cv.Invalid(
f"This feature is not available on {', '.join(unsupported)}"
)
return obj
return validator_
def get_download_types(storage_json: StorageJSON = None):
types = [
{
"title": "UF2 package (recommended)",
"description": "For flashing via web_server OTA or with ltchiptool (UART)",
"file": "firmware.uf2",
"download": f"{storage_json.name}.uf2",
},
]
build_dir = storage_json.firmware_bin_path.parent
outputs = build_dir / "firmware.json"
if not outputs.is_file():
return types
with outputs.open(encoding="utf-8") as f:
outputs = json.load(f)
for output in outputs:
if not output["public"]:
continue
suffix = output["filename"].partition(".")[2]
suffix = f"-{suffix}" if "." in suffix else f".{suffix}"
types.append(
{
"title": output["title"],
"description": output["description"],
"file": output["filename"],
"download": storage_json.name + suffix,
}
)
return types
def _notify_old_style(config):
if config:
raise cv.Invalid(
"The LibreTiny component is now split between supported chip families.\n"
"Migrate your config file to include a chip-based configuration, "
"instead of the 'libretiny:' block.\n"
"For example 'bk72xx:' or 'rtl87xx:'."
)
return config
# The dev and latest branches will be at *least* this version, which is what matters.
# Use GitHub releases directly to avoid PlatformIO moderation delays.
ARDUINO_VERSIONS = {
"dev": (cv.Version(1, 12, 1), "https://github.com/libretiny-eu/libretiny.git"),
"latest": (
cv.Version(1, 12, 1),
"https://github.com/libretiny-eu/libretiny.git#v1.12.1",
),
"recommended": (
cv.Version(1, 12, 1),
"https://github.com/libretiny-eu/libretiny.git#v1.12.1",
),
}
def _check_framework_version(value):
value = value.copy()
if value[CONF_VERSION] in ARDUINO_VERSIONS:
if CONF_SOURCE in value:
raise cv.Invalid(
"Framework version needs to be explicitly specified when custom source is used."
)
version, source = ARDUINO_VERSIONS[value[CONF_VERSION]]
else:
version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
source = value.get(CONF_SOURCE, None)
value[CONF_VERSION] = str(version)
value[CONF_SOURCE] = source
return value
def _check_debug_order(value):
debug = value[CONF_DEBUG]
if "NONE" in debug and "NONE" in debug[1:]:
raise cv.Invalid(
"'none' has to be specified before other modules, and only once",
path=[CONF_DEBUG],
)
return value
FRAMEWORK_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
cv.Optional(CONF_SOURCE): cv.string_strict,
cv.Optional(CONF_LOGLEVEL, default="warn"): (
cv.one_of(*LT_LOGLEVELS, upper=True)
),
cv.Optional(CONF_DEBUG, default=[]): cv.ensure_list(
cv.one_of("NONE", *LT_DEBUG_MODULES, upper=True)
),
cv.Optional(CONF_SDK_SILENT, default="all"): (
cv.one_of("all", "auto", "none", lower=True)
),
cv.Optional(CONF_UART_PORT): cv.one_of(0, 1, 2, int=True),
cv.Optional(CONF_GPIO_RECOVER, default=True): cv.boolean,
cv.Optional(CONF_OPTIONS, default={}): {
cv.string_strict: cv.string,
},
}
),
_check_framework_version,
_check_debug_order,
)
CONFIG_SCHEMA = cv.All(_notify_old_style)
BASE_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(LTComponent),
cv.Required(CONF_BOARD): cv.string_strict,
cv.Optional(CONF_FAMILY): cv.one_of(*FAMILIES, upper=True),
cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA,
},
)
BASE_SCHEMA.add_extra(_detect_variant)
BASE_SCHEMA.add_extra(_update_core_data)
# pylint: disable=use-dict-literal
async def component_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
# setup board config
cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_build_flag("-DUSE_LIBRETINY")
cg.add_build_flag(f"-DUSE_{config[CONF_COMPONENT_ID].upper()}")
cg.add_build_flag(f"-DUSE_LIBRETINY_VARIANT_{config[CONF_FAMILY]}")
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_define("ESPHOME_VARIANT", FAMILY_FRIENDLY[config[CONF_FAMILY]])
# Set threading model based on chip architecture
component: LibreTinyComponent = CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA]
if component.supports_atomics:
# RTL87xx (Cortex-M4) and LN882x (Cortex-M4F) have LDREX/STREX
cg.add_define(ThreadModel.MULTI_ATOMICS)
else:
# BK72xx uses ARM968E-S (ARMv5TE) which lacks LDREX/STREX.
# std::atomic RMW operations would require libatomic (not linked to save
# 4-8KB flash). Even if linked, it would use locks, so explicit FreeRTOS
# mutexes are simpler and equivalent.
cg.add_define(ThreadModel.MULTI_NO_ATOMICS)
# RTL8710B needs FreeRTOS 8.2.3+ for xTaskNotifyGive/ulTaskNotifyTake
# required by AsyncTCP 3.4.3+ (https://github.com/esphome/esphome/issues/10220)
# RTL8720C (ambz2) requires FreeRTOS 10.x so this only applies to RTL8710B
if config[CONF_FAMILY] == FAMILY_RTL8710B:
cg.add_platformio_option("custom_versions.freertos", "8.2.3")
# force using arduino framework
cg.add_platformio_option("framework", "arduino")
cg.add_build_flag("-DUSE_ARDUINO")
cg.set_cpp_standard("gnu++20")
# disable library compatibility checks
cg.add_platformio_option("lib_ldf_mode", "off")
cg.add_platformio_option("lib_compat_mode", "soft")
# include <Arduino.h> in every file
cg.add_platformio_option("build_src_flags", "-include Arduino.h")
# dummy version code
cg.add_define("USE_ARDUINO_VERSION_CODE", cg.RawExpression("VERSION_CODE(0, 0, 0)"))
# decrease web server stack size (16k words -> 4k words)
cg.add_build_flag("-DCONFIG_ASYNC_TCP_STACK_SIZE=4096")
# build framework version
# if platform version is a valid version constraint, prefix the default package
framework = config[CONF_FRAMEWORK]
cv.platformio_version_constraint(framework[CONF_VERSION])
if framework[CONF_SOURCE]:
cg.add_platformio_option("platform", framework[CONF_SOURCE])
elif str(framework[CONF_VERSION]) != "0.0.0":
cg.add_platformio_option("platform", f"libretiny @ {framework[CONF_VERSION]}")
else:
cg.add_platformio_option("platform", "libretiny")
# apply LibreTiny options from framework: block
# setup LT logger to work nicely with ESPHome logger
lt_options = dict(
LT_LOGLEVEL="LT_LEVEL_" + framework[CONF_LOGLEVEL],
LT_LOGGER_CALLER=0,
LT_LOGGER_TASK=0,
LT_LOGGER_COLOR=1,
LT_USE_TIME=1,
)
# enable/disable per-module debugging
for module in framework[CONF_DEBUG]:
if module == "NONE":
# disable all modules
for module in LT_DEBUG_MODULES:
lt_options[f"LT_DEBUG_{module}"] = 0
else:
# enable one module
lt_options[f"LT_DEBUG_{module}"] = 1
# set SDK silencing mode
if framework[CONF_SDK_SILENT] == "all":
lt_options["LT_UART_SILENT_ENABLED"] = 1
lt_options["LT_UART_SILENT_ALL"] = 1
elif framework[CONF_SDK_SILENT] == "auto":
lt_options["LT_UART_SILENT_ENABLED"] = 1
lt_options["LT_UART_SILENT_ALL"] = 0
else:
lt_options["LT_UART_SILENT_ENABLED"] = 0
lt_options["LT_UART_SILENT_ALL"] = 0
# set default UART port
if (uart_port := framework.get(CONF_UART_PORT, None)) is not None:
lt_options["LT_UART_DEFAULT_PORT"] = uart_port
# add custom options
lt_options.update(framework[CONF_OPTIONS])
# apply ESPHome options from framework: block
cg.add_define("LT_GPIO_RECOVER", int(framework[CONF_GPIO_RECOVER]))
# build PlatformIO compiler flags
for name, value in sorted(lt_options.items()):
cg.add_build_flag(f"-D{name}={value}")
# custom output firmware name and version
if CONF_PROJECT in config:
cg.add_platformio_option(
"custom_fw_name", "esphome." + config[CONF_PROJECT][CONF_NAME]
)
cg.add_platformio_option(
"custom_fw_version", config[CONF_PROJECT][CONF_VERSION]
)
else:
cg.add_platformio_option("custom_fw_name", "esphome")
cg.add_platformio_option("custom_fw_version", __version__)
# Apply chip-specific SDK options to save RAM/Flash
if config[CONF_FAMILY] == FAMILY_BK7231N:
cg.add_platformio_option(
"custom_options.sys_config#h", _BK7231N_SYS_CONFIG_OPTIONS
)
# Disable LWIP statistics to save RAM - not needed in production
# Must explicitly disable all sub-stats to avoid redefinition warnings
cg.add_platformio_option(
"custom_options.lwip",
["LWIP_STATS=0", "MEM_STATS=0", "MEMP_STATS=0"],
)
await cg.register_component(var, config)