Support for LibreTiny platform (RTL8710, BK7231 & other modules) (#3509)

Co-authored-by: Kuba Szczodrzyński <kuba@szczodrzynski.pl>
Co-authored-by: Sam Neirinck <git@samneirinck.com>
Co-authored-by: David Buezas <dbuezas@users.noreply.github.com>
Co-authored-by: Stroe Andrei Catalin <catalin2402@gmail.com>
Co-authored-by: Sam Neirinck <github@samneirinck.be>
Co-authored-by: Péter Sárközi <xmisterhu@gmail.com>
Co-authored-by: Hajo Noerenberg <hn@users.noreply.github.com>
This commit is contained in:
Kuba Szczodrzyński 2023-09-05 00:16:08 +02:00 committed by GitHub
parent 22c0b0abaa
commit a9630ac847
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
78 changed files with 6085 additions and 89 deletions

View file

@ -42,6 +42,7 @@ esphome/components/bedjet/climate/* @jhansche
esphome/components/bedjet/fan/* @jhansche esphome/components/bedjet/fan/* @jhansche
esphome/components/bh1750/* @OttoWinter esphome/components/bh1750/* @OttoWinter
esphome/components/binary_sensor/* @esphome/core esphome/components/binary_sensor/* @esphome/core
esphome/components/bk72xx/* @kuba2k2
esphome/components/bl0939/* @ziceva esphome/components/bl0939/* @ziceva
esphome/components/bl0940/* @tobias- esphome/components/bl0940/* @tobias-
esphome/components/bl0942/* @dbuezas esphome/components/bl0942/* @dbuezas
@ -146,6 +147,8 @@ esphome/components/kuntze/* @ssieb
esphome/components/lcd_menu/* @numo68 esphome/components/lcd_menu/* @numo68
esphome/components/ld2410/* @regevbr @sebcaps esphome/components/ld2410/* @regevbr @sebcaps
esphome/components/ledc/* @OttoWinter esphome/components/ledc/* @OttoWinter
esphome/components/libretiny/* @kuba2k2
esphome/components/libretiny_pwm/* @kuba2k2
esphome/components/light/* @esphome/core esphome/components/light/* @esphome/core
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
esphome/components/lock/* @esphome/core esphome/components/lock/* @esphome/core
@ -234,6 +237,7 @@ esphome/components/rgbct/* @jesserockz
esphome/components/rp2040/* @jesserockz esphome/components/rp2040/* @jesserockz
esphome/components/rp2040_pio_led_strip/* @Papa-DMan esphome/components/rp2040_pio_led_strip/* @Papa-DMan
esphome/components/rp2040_pwm/* @jesserockz esphome/components/rp2040_pwm/* @jesserockz
esphome/components/rtl87xx/* @kuba2k2
esphome/components/rtttl/* @glmnet esphome/components/rtttl/* @glmnet
esphome/components/safe_mode/* @jsuanet @paulmonigatti esphome/components/safe_mode/* @jsuanet @paulmonigatti
esphome/components/scd4x/* @martgras @sjtrny esphome/components/scd4x/* @martgras @sjtrny

View file

@ -26,6 +26,8 @@ from esphome.const import (
CONF_ESPHOME, CONF_ESPHOME,
CONF_PLATFORMIO_OPTIONS, CONF_PLATFORMIO_OPTIONS,
CONF_SUBSTITUTIONS, CONF_SUBSTITUTIONS,
PLATFORM_BK72XX,
PLATFORM_RTL87XX,
PLATFORM_ESP32, PLATFORM_ESP32,
PLATFORM_ESP8266, PLATFORM_ESP8266,
PLATFORM_RP2040, PLATFORM_RP2040,
@ -278,20 +280,25 @@ def upload_using_esptool(config, port):
return run_esptool(115200) return run_esptool(115200)
def upload_using_platformio(config, port):
from esphome import platformio_api
upload_args = ["-t", "upload", "-t", "nobuild"]
if port is not None:
upload_args += ["--upload-port", port]
return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args)
def upload_program(config, args, host): def upload_program(config, args, host):
if get_port_type(host) == "SERIAL": if get_port_type(host) == "SERIAL":
if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266): if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266):
return upload_using_esptool(config, host) return upload_using_esptool(config, host)
if CORE.target_platform in (PLATFORM_RP2040): if CORE.target_platform in (PLATFORM_RP2040):
from esphome import platformio_api return upload_using_platformio(config, args.device)
upload_args = ["-t", "upload"] if CORE.target_platform in (PLATFORM_BK72XX, PLATFORM_RTL87XX):
if args.device is not None: return upload_using_platformio(config, host)
upload_args += ["--upload-port", args.device]
return platformio_api.run_platformio_cli_run(
config, CORE.verbose, *upload_args
)
return 1 # Unknown target platform return 1 # Unknown target platform

View file

@ -1,7 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome import pins from esphome import pins
from esphome.const import CONF_INPUT from esphome.const import CONF_ANALOG, CONF_INPUT
from esphome.core import CORE from esphome.core import CORE
from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32 import get_esp32_variant
@ -166,8 +166,6 @@ def validate_adc_pin(value):
return pins.internal_gpio_input_pin_schema(value) return pins.internal_gpio_input_pin_schema(value)
if CORE.is_esp8266: if CORE.is_esp8266:
from esphome.components.esp8266.gpio import CONF_ANALOG
value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})( value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})(
value value
) )
@ -184,4 +182,9 @@ def validate_adc_pin(value):
raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC") raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC")
return pins.internal_gpio_input_pin_schema(value) return pins.internal_gpio_input_pin_schema(value)
if CORE.is_libretiny:
return pins.gpio_pin_schema(
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True
)(value)
raise NotImplementedError raise NotImplementedError

View file

@ -92,13 +92,13 @@ extern "C"
void ADCSensor::dump_config() { void ADCSensor::dump_config() {
LOG_SENSOR("", "ADC Sensor", this); LOG_SENSOR("", "ADC Sensor", this);
#ifdef USE_ESP8266 #if defined(USE_ESP8266) || defined(USE_LIBRETINY)
#ifdef USE_ADC_SENSOR_VCC #ifdef USE_ADC_SENSOR_VCC
ESP_LOGCONFIG(TAG, " Pin: VCC"); ESP_LOGCONFIG(TAG, " Pin: VCC");
#else #else
LOG_PIN(" Pin: ", pin_); LOG_PIN(" Pin: ", pin_);
#endif #endif
#endif // USE_ESP8266 #endif // USE_ESP8266 || USE_LIBRETINY
#ifdef USE_ESP32 #ifdef USE_ESP32
LOG_PIN(" Pin: ", pin_); LOG_PIN(" Pin: ", pin_);
@ -254,6 +254,15 @@ float ADCSensor::sample() {
} }
#endif #endif
#ifdef USE_LIBRETINY
float ADCSensor::sample() {
if (output_raw_) {
return analogRead(this->pin_->get_pin()); // NOLINT
}
return analogReadVoltage(this->pin_->get_pin()) / 1000.0f; // NOLINT
}
#endif // USE_LIBRETINY
#ifdef USE_ESP8266 #ifdef USE_ESP8266
std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; } std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; }
#endif #endif

View file

@ -1051,6 +1051,10 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) {
resp.manufacturer = "Espressif"; resp.manufacturer = "Espressif";
#elif defined(USE_RP2040) #elif defined(USE_RP2040)
resp.manufacturer = "Raspberry Pi"; resp.manufacturer = "Raspberry Pi";
#elif defined(USE_BK72XX)
resp.manufacturer = "Beken";
#elif defined(USE_RTL87XX)
resp.manufacturer = "Realtek";
#elif defined(USE_HOST) #elif defined(USE_HOST)
resp.manufacturer = "Host"; resp.manufacturer = "Host";
#endif #endif

View file

@ -8,15 +8,15 @@ CODEOWNERS = ["@OttoWinter"]
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema({}), cv.Schema({}),
cv.only_with_arduino, cv.only_with_arduino,
cv.only_on(["esp32", "esp8266"]), cv.only_on(["esp32", "esp8266", "bk72xx", "rtl87xx"]),
) )
@coroutine_with_priority(200.0) @coroutine_with_priority(200.0)
async def to_code(config): async def to_code(config):
if CORE.is_esp32: if CORE.is_esp32 or CORE.is_libretiny:
# https://github.com/esphome/AsyncTCP/blob/master/library.json # https://github.com/esphome/AsyncTCP/blob/master/library.json
cg.add_library("esphome/AsyncTCP-esphome", "1.2.2") cg.add_library("esphome/AsyncTCP-esphome", "2.0.1")
elif CORE.is_esp8266: elif CORE.is_esp8266:
# https://github.com/esphome/ESPAsyncTCP # https://github.com/esphome/ESPAsyncTCP
cg.add_library("esphome/ESPAsyncTCP-esphome", "1.2.3") cg.add_library("esphome/ESPAsyncTCP-esphome", "1.2.3")

View file

@ -0,0 +1,51 @@
# This file was auto-generated by libretiny/generate_components.py
# Do not modify its contents.
# For custom pin validators, put validate_pin() or validate_usage()
# in gpio.py file in this directory.
# For changing schema/pin schema, put COMPONENT_SCHEMA or COMPONENT_PIN_SCHEMA
# in schema.py file in this directory.
from esphome import pins
from esphome.components import libretiny
from esphome.components.libretiny.const import (
COMPONENT_BK72XX,
KEY_COMPONENT_DATA,
KEY_LIBRETINY,
LibreTinyComponent,
)
from esphome.core import CORE
from .boards import BK72XX_BOARDS, BK72XX_BOARD_PINS
CODEOWNERS = ["@kuba2k2"]
AUTO_LOAD = ["libretiny"]
COMPONENT_DATA = LibreTinyComponent(
name=COMPONENT_BK72XX,
boards=BK72XX_BOARDS,
board_pins=BK72XX_BOARD_PINS,
pin_validation=None,
usage_validation=None,
)
def _set_core_data(config):
CORE.data[KEY_LIBRETINY] = {}
CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA] = COMPONENT_DATA
return config
CONFIG_SCHEMA = libretiny.BASE_SCHEMA
PIN_SCHEMA = libretiny.gpio.BASE_PIN_SCHEMA
CONFIG_SCHEMA.prepend_extra(_set_core_data)
async def to_code(config):
return await libretiny.component_to_code(config)
@pins.PIN_SCHEMA_REGISTRY.register("bk72xx", PIN_SCHEMA)
async def pin_to_code(config):
return await libretiny.gpio.component_pin_to_code(config)

File diff suppressed because it is too large Load diff

View file

@ -21,7 +21,7 @@ CONFIG_SCHEMA = cv.All(
), ),
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
cv.only_on(["esp32", "esp8266"]), cv.only_on(["esp32", "esp8266", "bk72xx", "rtl87xx"]),
) )
@ -39,3 +39,5 @@ async def to_code(config):
cg.add_library("WiFi", None) cg.add_library("WiFi", None)
if CORE.is_esp8266: if CORE.is_esp8266:
cg.add_library("DNSServer", None) cg.add_library("DNSServer", None)
if CORE.is_libretiny:
cg.add_library("DNSServer", None)

View file

@ -28,7 +28,7 @@
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
#ifdef USE_RP2040 #ifdef USE_RP2040
#include <Arduino.h> #include <Arduino.h>
#else #elif defined(USE_ESP32) || defined(USE_ESP8266)
#include <Esp.h> #include <Esp.h>
#endif #endif
#endif #endif
@ -45,6 +45,8 @@ static uint32_t get_free_heap() {
return heap_caps_get_free_size(MALLOC_CAP_INTERNAL); return heap_caps_get_free_size(MALLOC_CAP_INTERNAL);
#elif defined(USE_RP2040) #elif defined(USE_RP2040)
return rp2040.getFreeHeap(); return rp2040.getFreeHeap();
#elif defined(USE_LIBRETINY)
return lt_heap_get_free();
#endif #endif
} }
@ -75,7 +77,7 @@ void DebugComponent::dump_config() {
this->free_heap_ = get_free_heap(); this->free_heap_ = get_free_heap();
ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_); ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_);
#if defined(USE_ARDUINO) && !defined(USE_RP2040) #if defined(USE_ARDUINO) && (defined(USE_ESP32) || defined(USE_ESP8266))
const char *flash_mode; const char *flash_mode;
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance) switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
case FM_QIO: case FM_QIO:
@ -107,7 +109,7 @@ void DebugComponent::dump_config() {
device_info += "|Flash: " + to_string(ESP.getFlashChipSize() / 1024) + // NOLINT device_info += "|Flash: " + to_string(ESP.getFlashChipSize() / 1024) + // NOLINT
"kB Speed:" + to_string(ESP.getFlashChipSpeed() / 1000000) + "MHz Mode:"; // NOLINT "kB Speed:" + to_string(ESP.getFlashChipSpeed() / 1000000) + "MHz Mode:"; // NOLINT
device_info += flash_mode; device_info += flash_mode;
#endif // USE_ARDUINO #endif // USE_ARDUINO && (USE_ESP32 || USE_ESP8266)
#ifdef USE_ESP32 #ifdef USE_ESP32
esp_chip_info_t info; esp_chip_info_t info;
@ -340,6 +342,27 @@ void DebugComponent::dump_config() {
device_info += "CPU Frequency: " + to_string(rp2040.f_cpu()); device_info += "CPU Frequency: " + to_string(rp2040.f_cpu());
#endif // USE_RP2040 #endif // USE_RP2040
#ifdef USE_LIBRETINY
ESP_LOGD(TAG, "LibreTiny Version: %s", lt_get_version());
ESP_LOGD(TAG, "Chip: %s (%04x) @ %u MHz", lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz());
ESP_LOGD(TAG, "Chip ID: 0x%06X", lt_cpu_get_mac_id());
ESP_LOGD(TAG, "Board: %s", lt_get_board_code());
ESP_LOGD(TAG, "Flash: %u KiB / RAM: %u KiB", lt_flash_get_size() / 1024, lt_ram_get_size() / 1024);
ESP_LOGD(TAG, "Reset Reason: %s", lt_get_reboot_reason_name(lt_get_reboot_reason()));
device_info += "|Version: ";
device_info += LT_BANNER_STR + 10;
device_info += "|Reset Reason: ";
device_info += lt_get_reboot_reason_name(lt_get_reboot_reason());
device_info += "|Chip Name: ";
device_info += lt_cpu_get_model_name();
device_info += "|Chip ID: 0x" + format_hex(lt_cpu_get_mac_id());
device_info += "|Flash: " + to_string(lt_flash_get_size() / 1024) + " KiB";
device_info += "|RAM: " + to_string(lt_ram_get_size() / 1024) + " KiB";
reset_reason = lt_get_reboot_reason_name(lt_get_reboot_reason());
#endif // USE_LIBRETINY
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
if (this->device_info_ != nullptr) { if (this->device_info_ != nullptr) {
if (device_info.length() > 255) if (device_info.length() > 255)
@ -384,6 +407,8 @@ void DebugComponent::update() {
this->block_sensor_->publish_state(ESP.getMaxFreeBlockSize()); this->block_sensor_->publish_state(ESP.getMaxFreeBlockSize());
#elif defined(USE_ESP32) #elif defined(USE_ESP32)
this->block_sensor_->publish_state(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)); this->block_sensor_->publish_state(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL));
#elif defined(USE_LIBRETINY)
this->block_sensor_->publish_state(lt_heap_get_max_alloc());
#endif #endif
} }

View file

@ -2,6 +2,7 @@ import logging
from dataclasses import dataclass from dataclasses import dataclass
from esphome.const import ( from esphome.const import (
CONF_ANALOG,
CONF_ID, CONF_ID,
CONF_INPUT, CONF_INPUT,
CONF_INVERTED, CONF_INVERTED,
@ -140,7 +141,6 @@ def validate_supports(value):
return value return value
CONF_ANALOG = "analog"
ESP8266_PIN_SCHEMA = cv.All( ESP8266_PIN_SCHEMA = cv.All(
{ {
cv.GenerateID(): cv.declare_id(ESP8266GPIOPin), cv.GenerateID(): cv.declare_id(ESP8266GPIOPin),

View file

@ -42,7 +42,8 @@ pin_with_input_and_output_support = cv.All(
) )
CONFIG_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.All(
cv.Schema(
{ {
cv.GenerateID(): _bus_declare_type, cv.GenerateID(): _bus_declare_type,
cv.Optional(CONF_SDA, default="SDA"): pin_with_input_and_output_support, cv.Optional(CONF_SDA, default="SDA"): pin_with_input_and_output_support,
@ -58,7 +59,9 @@ CONFIG_SCHEMA = cv.Schema(
), ),
cv.Optional(CONF_SCAN, default=True): cv.boolean, cv.Optional(CONF_SCAN, default=True): cv.boolean,
} }
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA),
cv.only_on(["esp32", "esp8266", "rp2040"]),
)
@coroutine_with_priority(1.0) @coroutine_with_priority(1.0)

View file

@ -29,6 +29,8 @@ std::string build_json(const json_build_t &f) {
const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
#elif defined(USE_RP2040) #elif defined(USE_RP2040)
const size_t free_heap = rp2040.getFreeHeap(); const size_t free_heap = rp2040.getFreeHeap();
#elif defined(USE_LIBRETINY)
const size_t free_heap = lt_heap_get_free();
#endif #endif
size_t request_size = std::min(free_heap, (size_t) 512); size_t request_size = std::min(free_heap, (size_t) 512);
@ -71,6 +73,8 @@ void parse_json(const std::string &data, const json_parse_t &f) {
const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT); const size_t free_heap = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
#elif defined(USE_RP2040) #elif defined(USE_RP2040)
const size_t free_heap = rp2040.getFreeHeap(); const size_t free_heap = rp2040.getFreeHeap();
#elif defined(USE_LIBRETINY)
const size_t free_heap = lt_heap_get_free();
#endif #endif
bool pass = false; bool pass = false;
size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5)); size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5));

View file

@ -0,0 +1,336 @@
import json
import logging
from os.path import dirname, isfile, join
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,
__version__,
)
from esphome.core import CORE
from . import gpio # noqa
from .const import (
CONF_GPIO_RECOVER,
CONF_LOGLEVEL,
CONF_SDK_SILENT,
CONF_UART_PORT,
FAMILIES,
FAMILY_COMPONENT,
FAMILY_FRIENDLY,
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 = []
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 CONF_FAMILY not in value:
if board not in component.boards:
raise cv.Invalid(
"This board is unknown, please set the family manually. "
"Also, make sure the chosen chip component is correct.",
path=[CONF_BOARD],
)
value = value.copy()
value[CONF_FAMILY] = component.boards[board][KEY_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],
)
# warn anyway if the board wasn't found
if board not in component.boards:
_LOGGER.warning(
"This board is unknown. Make sure the chosen chip component is correct.",
)
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=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 = dirname(storage_json.firmware_bin_path)
outputs = join(build_dir, "firmware.json")
if not isfile(outputs):
return types
with open(outputs, 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
# NOTE: Keep this in mind when updating the recommended version:
# * For all constants below, update platformio.ini (in this repo)
ARDUINO_VERSIONS = {
"dev": (cv.Version(0, 0, 0), "https://github.com/kuba2k2/libretiny.git"),
"latest": (cv.Version(0, 0, 0), None),
"recommended": (cv.Version(1, 3, 0), None),
}
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, default=None): 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]}")
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]])
# force using arduino framework
cg.add_platformio_option("framework", "arduino")
cg.add_build_flag("-DUSE_ARDUINO")
# disable library compatibility checks
cg.add_platformio_option("lib_ldf_mode", "off")
# 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 str(framework[CONF_VERSION]) != "0.0.0":
cg.add_platformio_option("platform", f"libretiny @ {framework[CONF_VERSION]}")
elif framework[CONF_SOURCE]:
cg.add_platformio_option("platform", framework[CONF_SOURCE])
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 framework[CONF_UART_PORT] is not None:
lt_options["LT_UART_DEFAULT_PORT"] = framework[CONF_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__)
await cg.register_component(var, config)

View file

@ -0,0 +1,90 @@
from dataclasses import dataclass
from typing import Callable
import esphome.codegen as cg
@dataclass
class LibreTinyComponent:
name: str
boards: dict[str, dict[str, str]]
board_pins: dict[str, dict[str, int]]
pin_validation: Callable[[int], int]
usage_validation: Callable[[dict], dict]
CONF_LIBRETINY = "libretiny"
CONF_LOGLEVEL = "loglevel"
CONF_SDK_SILENT = "sdk_silent"
CONF_GPIO_RECOVER = "gpio_recover"
CONF_UART_PORT = "uart_port"
LT_LOGLEVELS = [
"VERBOSE",
"TRACE",
"DEBUG",
"INFO",
"WARN",
"ERROR",
"FATAL",
"NONE",
]
LT_DEBUG_MODULES = [
"WIFI",
"CLIENT",
"SERVER",
"SSL",
"OTA",
"FDB",
"MDNS",
"LWIP",
"LWIP_ASSERT",
]
KEY_LIBRETINY = "libretiny"
KEY_BOARD = "board"
KEY_COMPONENT = "component"
KEY_COMPONENT_DATA = "component_data"
KEY_FAMILY = "family"
# COMPONENTS - auto-generated! Do not modify this block.
COMPONENT_BK72XX = "bk72xx"
COMPONENT_RTL87XX = "rtl87xx"
# COMPONENTS - end
# FAMILIES - auto-generated! Do not modify this block.
FAMILY_BK7231N = "BK7231N"
FAMILY_BK7231Q = "BK7231Q"
FAMILY_BK7231T = "BK7231T"
FAMILY_BK7251 = "BK7251"
FAMILY_RTL8710B = "RTL8710B"
FAMILY_RTL8720C = "RTL8720C"
FAMILIES = [
FAMILY_BK7231N,
FAMILY_BK7231Q,
FAMILY_BK7231T,
FAMILY_BK7251,
FAMILY_RTL8710B,
FAMILY_RTL8720C,
]
FAMILY_FRIENDLY = {
FAMILY_BK7231N: "BK7231N",
FAMILY_BK7231Q: "BK7231Q",
FAMILY_BK7231T: "BK7231T",
FAMILY_BK7251: "BK7251",
FAMILY_RTL8710B: "RTL8710B",
FAMILY_RTL8720C: "RTL8720C",
}
FAMILY_COMPONENT = {
FAMILY_BK7231N: COMPONENT_BK72XX,
FAMILY_BK7231Q: COMPONENT_BK72XX,
FAMILY_BK7231T: COMPONENT_BK72XX,
FAMILY_BK7251: COMPONENT_BK72XX,
FAMILY_RTL8710B: COMPONENT_RTL87XX,
FAMILY_RTL8720C: COMPONENT_RTL87XX,
}
# FAMILIES - end
libretiny_ns = cg.esphome_ns.namespace("libretiny")
LTComponent = libretiny_ns.class_("LTComponent", cg.PollingComponent)

View file

@ -0,0 +1,40 @@
#ifdef USE_LIBRETINY
#include "core.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "preferences.h"
void setup();
void loop();
namespace esphome {
void IRAM_ATTR HOT yield() { ::yield(); }
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); }
void arch_init() {
libretiny::setup_preferences();
lt_wdt_enable(10000L);
#if LT_GPIO_RECOVER
lt_gpio_recover();
#endif
}
void arch_restart() {
lt_reboot();
while (1) {
}
}
void IRAM_ATTR HOT arch_feed_wdt() { lt_wdt_feed(); }
uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); }
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
} // namespace esphome
#endif // USE_LIBRETINY

View file

@ -0,0 +1,11 @@
#pragma once
#ifdef USE_LIBRETINY
#include <Arduino.h>
namespace esphome {
namespace libretiny {} // namespace libretiny
} // namespace esphome
#endif // USE_LIBRETINY

View file

@ -0,0 +1,329 @@
# Copyright (c) Kuba Szczodrzyński 2023-06-01.
# pylint: skip-file
# flake8: noqa
import json
import re
from pathlib import Path
from black import FileMode, format_str
from ltchiptool import Board, Family
from ltchiptool.util.lvm import LVM
BASE_CODE_INIT = """
# This file was auto-generated by libretiny/generate_components.py
# Do not modify its contents.
# For custom pin validators, put validate_pin() or validate_usage()
# in gpio.py file in this directory.
# For changing schema/pin schema, put COMPONENT_SCHEMA or COMPONENT_PIN_SCHEMA
# in schema.py file in this directory.
from esphome import pins
from esphome.components import libretiny
from esphome.components.libretiny.const import (
COMPONENT_{COMPONENT},
KEY_COMPONENT_DATA,
KEY_LIBRETINY,
LibreTinyComponent,
)
from esphome.core import CORE
{IMPORTS}
CODEOWNERS = ["@kuba2k2"]
AUTO_LOAD = ["libretiny"]
COMPONENT_DATA = LibreTinyComponent(
name=COMPONENT_{COMPONENT},
boards={COMPONENT}_BOARDS,
board_pins={COMPONENT}_BOARD_PINS,
pin_validation={PIN_VALIDATION},
usage_validation={USAGE_VALIDATION},
)
def _set_core_data(config):
CORE.data[KEY_LIBRETINY] = {}
CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA] = COMPONENT_DATA
return config
CONFIG_SCHEMA = {SCHEMA}
PIN_SCHEMA = {PIN_SCHEMA}
CONFIG_SCHEMA.prepend_extra(_set_core_data)
async def to_code(config):
return await libretiny.component_to_code(config)
@pins.PIN_SCHEMA_REGISTRY.register("{COMPONENT_LOWER}", PIN_SCHEMA)
async def pin_to_code(config):
return await libretiny.gpio.component_pin_to_code(config)
"""
BASE_CODE_BOARDS = """
# This file was auto-generated by libretiny/generate_components.py
# Do not modify its contents.
from esphome.components.libretiny.const import {FAMILIES}
{COMPONENT}_BOARDS = {BOARDS_JSON}
{COMPONENT}_BOARD_PINS = {PINS_JSON}
BOARDS = {COMPONENT}_BOARDS
"""
# variable names in component extension code
VAR_SCHEMA = "COMPONENT_SCHEMA"
VAR_PIN_SCHEMA = "COMPONENT_PIN_SCHEMA"
VAR_GPIO_PIN = "validate_pin"
VAR_GPIO_USAGE = "validate_usage"
# lines for code snippets
SCHEMA_BASE = "libretiny.BASE_SCHEMA"
SCHEMA_EXTRA = f"libretiny.BASE_SCHEMA.extend({VAR_SCHEMA})"
PIN_SCHEMA_BASE = "libretiny.gpio.BASE_PIN_SCHEMA"
PIN_SCHEMA_EXTRA = f"libretiny.BASE_PIN_SCHEMA.extend({VAR_PIN_SCHEMA})"
# supported root components
COMPONENT_MAP = {
"rtl87xx": "realtek-amb",
"bk72xx": "beken-72xx",
}
def subst(code: str, key: str, value: str) -> str:
return code.replace(f"{{{key}}}", value)
def subst_all(code: str, value: str) -> str:
return re.sub(r"{.+?}", value, code)
def subst_many(code: str, *templates: tuple[str, str]) -> str:
while True:
prev_code = code
for key, value in templates:
code = subst(code, key, value)
if code == prev_code:
break
return code
def check_base_code(code: str) -> None:
code = subst_all(code, "DUMMY")
formatted = format_str(code, mode=FileMode())
if code.strip() != formatted.strip():
print(formatted)
raise RuntimeError("Base code is not formatted properly")
def write_component_code(
component_dir: Path,
component: str,
) -> None:
code = BASE_CODE_INIT
gpio_py = component_dir.joinpath("gpio.py")
schema_py = component_dir.joinpath("schema.py")
init_py = component_dir.joinpath("__init__.py")
# gather all imports
imports = {
"gpio": set(),
"schema": set(),
"boards": {"{COMPONENT}_BOARDS", "{COMPONENT}_BOARD_PINS"},
}
# substitution values
values = dict(
COMPONENT=component.upper(),
COMPONENT_LOWER=component.lower(),
SCHEMA=SCHEMA_BASE,
PIN_SCHEMA=PIN_SCHEMA_BASE,
PIN_VALIDATION="None",
USAGE_VALIDATION="None",
)
# parse gpio.py file to find custom validators
if gpio_py.is_file():
gpio_code = gpio_py.read_text()
if VAR_GPIO_PIN in gpio_code:
values["PIN_VALIDATION"] = VAR_GPIO_PIN
imports["gpio"].add(VAR_GPIO_PIN)
# parse schema.py file to find schema extension
if schema_py.is_file():
schema_code = schema_py.read_text()
if VAR_SCHEMA in schema_code:
values["SCHEMA"] = SCHEMA_EXTRA
imports["schema"].add(VAR_SCHEMA)
if VAR_PIN_SCHEMA in schema_code:
values["PIN_SCHEMA"] = PIN_SCHEMA_EXTRA
imports["schema"].add(VAR_PIN_SCHEMA)
# add import lines if needed
import_lines = "\n".join(
f"from .{m} import {', '.join(sorted(v))}" for m, v in imports.items() if v
)
code = subst_many(
code,
("IMPORTS", import_lines),
*values.items(),
)
# format with black
code = format_str(code, mode=FileMode())
# write back to file
init_py.write_text(code)
def write_component_boards(
component_dir: Path,
component: str,
boards: list[Board],
) -> list[Family]:
code = BASE_CODE_BOARDS
variants_dir = Path(LVM.path(), "boards", "variants")
boards_py = component_dir.joinpath("boards.py")
pin_regex = r"#define PIN_(\w+)\s+(\d+)"
pin_number_regex = r"0*(\d+)$"
# families to import
families = set()
# found root families
root_families = []
# substitution values
values = dict(
COMPONENT=component.upper(),
)
# resulting JSON objects
boards_json = {}
pins_json = {}
# go through all boards found for this root family
for board in boards:
family = "FAMILY_" + board.family.short_name
boards_json[board.name] = {
"name": board.title,
"family": family,
}
families.add(family)
if board.family not in root_families:
root_families.append(board.family)
board_h = variants_dir.joinpath(f"{board.name}.h")
board_code = board_h.read_text()
board_pins = {}
for match in re.finditer(pin_regex, board_code):
pin_name = match[1]
pin_value = match[2]
board_pins[pin_name] = int(pin_value)
# trim leading zeroes in GPIO numbers
pin_name = re.sub(pin_number_regex, r"\1", pin_name)
board_pins[pin_name] = int(pin_value)
pins_json[board.name] = board_pins
# make the JSONs format as non-inline
boards_json = json.dumps(boards_json).replace("}", ",}")
pins_json = json.dumps(pins_json).replace("}", ",}")
# remove quotes from family constants
for family in families:
boards_json = boards_json.replace(f'"{family}"', family)
code = subst_many(
code,
("FAMILIES", ", ".join(sorted(families))),
("BOARDS_JSON", boards_json),
("PINS_JSON", pins_json),
*values.items(),
)
# format with black
code = format_str(code, mode=FileMode())
# write back to file
boards_py.write_text(code)
return root_families
def write_const(
components_dir: Path,
components: set[str],
families: dict[str, str],
) -> None:
const_py = components_dir.joinpath("libretiny").joinpath("const.py")
if not const_py.is_file():
raise FileNotFoundError(const_py)
code = const_py.read_text()
components = sorted(components)
v2f = families
families = sorted(families)
# regex for finding the component list block
comp_regex = r"(# COMPONENTS.+?\n)(.*?)(\n# COMPONENTS)"
# build component constants
comp_str = "\n".join(f'COMPONENT_{f} = "{f.lower()}"' for f in components)
# replace the 2nd regex group only
repl = lambda m: m.group(1) + comp_str + m.group(3)
code = re.sub(comp_regex, repl, code, flags=re.DOTALL | re.MULTILINE)
# regex for finding the family list block
fam_regex = r"(# FAMILIES.+?\n)(.*?)(\n# FAMILIES)"
# build family constants
fam_defs = "\n".join(f'FAMILY_{v} = "{v}"' for v in families)
fam_list = ", ".join(f"FAMILY_{v}" for v in families)
fam_friendly = ", ".join(f'FAMILY_{v}: "{v}"' for v in families)
fam_component = ", ".join(f"FAMILY_{v}: COMPONENT_{v2f[v]}" for v in families)
fam_lines = [
fam_defs,
"FAMILIES = [",
fam_list,
",]",
"FAMILY_FRIENDLY = {",
fam_friendly,
",}",
"FAMILY_COMPONENT = {",
fam_component,
",}",
]
var_str = "\n".join(fam_lines)
# replace the 2nd regex group only
repl = lambda m: m.group(1) + var_str + m.group(3)
code = re.sub(fam_regex, repl, code, flags=re.DOTALL | re.MULTILINE)
# format with black
code = format_str(code, mode=FileMode())
# write back to file
const_py.write_text(code)
if __name__ == "__main__":
# safety check if code is properly formatted
check_base_code(BASE_CODE_INIT)
# list all boards from ltchiptool
components_dir = Path(__file__).parent.parent
boards = [Board(b) for b in Board.get_list()]
# keep track of all supported root- and chip-families
components = set()
families = {}
# loop through supported components
for component, family_name in COMPONENT_MAP.items():
family = Family.get(name=family_name)
# make family component directory
component_dir = components_dir.joinpath(component)
component_dir.mkdir(exist_ok=True)
# filter boards list
family_boards = [b for b in boards if family in b.family.inheritance]
# write __init__.py
write_component_code(component_dir, component)
# write boards.py
component_families = write_component_boards(
component_dir, component, family_boards
)
# store current root component name
components.add(component.upper())
# add all chip families
for family in component_families:
families[family.short_name] = component.upper()
# update libretiny/const.py
write_const(components_dir, components, families)

View file

@ -0,0 +1,216 @@
import logging
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins
from esphome.const import (
CONF_ANALOG,
CONF_ID,
CONF_INPUT,
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
CONF_OPEN_DRAIN,
CONF_OUTPUT,
CONF_PULLDOWN,
CONF_PULLUP,
)
from esphome.core import CORE
from .const import (
KEY_BOARD,
KEY_COMPONENT_DATA,
KEY_LIBRETINY,
LibreTinyComponent,
libretiny_ns,
)
_LOGGER = logging.getLogger(__name__)
ArduinoInternalGPIOPin = libretiny_ns.class_(
"ArduinoInternalGPIOPin", cg.InternalGPIOPin
)
def _is_name_deprecated(value):
return value[0] in "DA" and value[1:].isnumeric()
def _lookup_board_pins(board):
component: LibreTinyComponent = CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA]
board_pins = component.board_pins.get(board, {})
# Resolve aliased board pins (shorthand when two boards have the same pin configuration)
while isinstance(board_pins, str):
board_pins = board_pins[board_pins]
return board_pins
def _lookup_pin(value):
board: str = CORE.data[KEY_LIBRETINY][KEY_BOARD]
board_pins = _lookup_board_pins(board)
# check numeric pin values
if isinstance(value, int):
if value in board_pins.values() or not board_pins:
# accept if pin number present in board pins
# if board is not found, just accept all numeric values
return value
raise cv.Invalid(f"Pin number '{value}' is not usable for board {board}.")
# check textual pin names
if isinstance(value, str):
if not board_pins:
# can't remap without known pin name
raise cv.Invalid(
f"Board {board} wasn't found. "
f"Use 'GPIO#' (numeric value) instead of '{value}'."
)
if value in board_pins:
# pin name found, remap to numeric value
if _is_name_deprecated(value):
number = board_pins[value]
# find all alternative pin names (except the deprecated)
names = (
k
for k, v in board_pins.items()
if v == number and not _is_name_deprecated(k)
)
# sort by shortest
# favor P# or PA# names
names = sorted(
names,
key=lambda x: len(x) - 99 if x[0] == "P" else len(x),
)
_LOGGER.warning(
"Using D# and A# pin numbering is deprecated. "
"Please replace '%s' with one of: %s",
value,
", ".join(names),
)
return board_pins[value]
# pin name not found and not numeric
raise cv.Invalid(f"Cannot resolve pin name '{value}' for board {board}.")
# unknown type of the value
raise cv.Invalid(f"Unrecognized pin value '{value}'.")
def _translate_pin(value):
if isinstance(value, dict) or value is None:
raise cv.Invalid(
"This variable only supports pin numbers, not full pin schemas "
"(with inverted and mode)."
)
if isinstance(value, int):
return value
try:
return int(value)
except ValueError:
pass
# translate GPIO* and P* to a number, if possible
# otherwise return unchanged value (i.e. pin PA05)
try:
if value.startswith("GPIO"):
value = int(value[4:])
elif value.startswith("P"):
value = int(value[1:])
except ValueError:
pass
return value
def validate_gpio_pin(value):
value = _translate_pin(value)
value = _lookup_pin(value)
component: LibreTinyComponent = CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA]
if component.pin_validation:
value = component.pin_validation(value)
return value
def validate_gpio_usage(value):
mode = value[CONF_MODE]
is_analog = mode[CONF_ANALOG]
is_input = mode[CONF_INPUT]
is_output = mode[CONF_OUTPUT]
is_open_drain = mode[CONF_OPEN_DRAIN]
is_pullup = mode[CONF_PULLUP]
is_pulldown = mode[CONF_PULLDOWN]
if is_open_drain and not is_output:
raise cv.Invalid(
"Open-drain only works with output mode", [CONF_MODE, CONF_OPEN_DRAIN]
)
if is_analog and not is_input:
raise cv.Invalid("Analog pins must be an input", [CONF_MODE, CONF_ANALOG])
if is_analog:
# expect analog pin numbers to be available as either ADC# or A#
number = value[CONF_NUMBER]
board: str = CORE.data[KEY_LIBRETINY][KEY_BOARD]
board_pins = _lookup_board_pins(board)
analog_pins = [
v
for k, v in board_pins.items()
if k[0] == "A" and k[1:].isnumeric() or k[0:3] == "ADC"
]
if number not in analog_pins:
raise cv.Invalid(
f"Pin '{number}' is not an analog pin", [CONF_MODE, CONF_ANALOG]
)
# (input, output, open_drain, pullup, pulldown)
supported_modes = {
# INPUT
(True, False, False, False, False),
# OUTPUT
(False, True, False, False, False),
# INPUT_PULLUP
(True, False, False, True, False),
# INPUT_PULLDOWN
(True, False, False, False, True),
# OUTPUT_OPEN_DRAIN
(False, True, True, False, False),
}
key = (is_input, is_output, is_open_drain, is_pullup, is_pulldown)
if key not in supported_modes:
raise cv.Invalid("This pin mode is not supported", [CONF_MODE])
component: LibreTinyComponent = CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA]
if component.usage_validation:
value = component.usage_validation(value)
return value
BASE_PIN_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ArduinoInternalGPIOPin),
cv.Required(CONF_NUMBER): validate_gpio_pin,
cv.Optional(CONF_MODE, default={}): cv.Schema(
{
cv.Optional(CONF_ANALOG, default=False): cv.boolean,
cv.Optional(CONF_INPUT, default=False): cv.boolean,
cv.Optional(CONF_OUTPUT, default=False): cv.boolean,
cv.Optional(CONF_OPEN_DRAIN, default=False): cv.boolean,
cv.Optional(CONF_PULLUP, default=False): cv.boolean,
cv.Optional(CONF_PULLDOWN, default=False): cv.boolean,
}
),
cv.Optional(CONF_INVERTED, default=False): cv.boolean,
},
)
BASE_PIN_SCHEMA.add_extra(validate_gpio_usage)
async def component_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
num = config[CONF_NUMBER]
cg.add(var.set_pin(num))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var

View file

@ -0,0 +1,105 @@
#ifdef USE_LIBRETINY
#include "gpio_arduino.h"
#include "esphome/core/log.h"
namespace esphome {
namespace libretiny {
static const char *const TAG = "lt.gpio";
static int IRAM_ATTR flags_to_mode(gpio::Flags flags) {
if (flags == gpio::FLAG_INPUT) {
return INPUT;
} else if (flags == gpio::FLAG_OUTPUT) {
return OUTPUT;
} else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) {
return INPUT_PULLUP;
} else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLDOWN)) {
return INPUT_PULLDOWN;
} else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) {
return OUTPUT_OPEN_DRAIN;
} else {
return 0;
}
}
struct ISRPinArg {
uint8_t pin;
bool inverted;
};
ISRInternalGPIOPin ArduinoInternalGPIOPin::to_isr() const {
auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory)
arg->pin = pin_;
arg->inverted = inverted_;
return ISRInternalGPIOPin((void *) arg);
}
void ArduinoInternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
PinStatus arduino_mode = (PinStatus) 255;
switch (type) {
case gpio::INTERRUPT_RISING_EDGE:
arduino_mode = inverted_ ? FALLING : RISING;
break;
case gpio::INTERRUPT_FALLING_EDGE:
arduino_mode = inverted_ ? RISING : FALLING;
break;
case gpio::INTERRUPT_ANY_EDGE:
arduino_mode = CHANGE;
break;
case gpio::INTERRUPT_LOW_LEVEL:
arduino_mode = inverted_ ? HIGH : LOW;
break;
case gpio::INTERRUPT_HIGH_LEVEL:
arduino_mode = inverted_ ? LOW : HIGH;
break;
}
attachInterruptParam(pin_, func, arduino_mode, arg);
}
void ArduinoInternalGPIOPin::pin_mode(gpio::Flags flags) {
pinMode(pin_, flags_to_mode(flags)); // NOLINT
}
std::string ArduinoInternalGPIOPin::dump_summary() const {
char buffer[32];
snprintf(buffer, sizeof(buffer), "%u", pin_);
return buffer;
}
bool ArduinoInternalGPIOPin::digital_read() {
return bool(digitalRead(pin_)) ^ inverted_; // NOLINT
}
void ArduinoInternalGPIOPin::digital_write(bool value) {
digitalWrite(pin_, value ^ inverted_); // NOLINT
}
void ArduinoInternalGPIOPin::detach_interrupt() const {
detachInterrupt(pin_); // NOLINT
}
} // namespace libretiny
using namespace libretiny;
bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
return bool(digitalRead(arg->pin)) ^ arg->inverted; // NOLINT
}
void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
digitalWrite(arg->pin, value ^ arg->inverted); // NOLINT
}
void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
detachInterrupt(arg->pin);
}
void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
pinMode(arg->pin, flags_to_mode(flags)); // NOLINT
}
} // namespace esphome
#endif // USE_LIBRETINY

View file

@ -0,0 +1,36 @@
#pragma once
#ifdef USE_LIBRETINY
#include "esphome/core/hal.h"
namespace esphome {
namespace libretiny {
class ArduinoInternalGPIOPin : public InternalGPIOPin {
public:
void set_pin(uint8_t pin) { pin_ = pin; }
void set_inverted(bool inverted) { inverted_ = inverted; }
void set_flags(gpio::Flags flags) { flags_ = flags; }
void setup() override { pin_mode(flags_); }
void pin_mode(gpio::Flags flags) override;
bool digital_read() override;
void digital_write(bool value) override;
std::string dump_summary() const override;
void detach_interrupt() const override;
ISRInternalGPIOPin to_isr() const override;
uint8_t get_pin() const override { return pin_; }
bool is_inverted() const override { return inverted_; }
protected:
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
uint8_t pin_;
bool inverted_;
gpio::Flags flags_;
};
} // namespace libretiny
} // namespace esphome
#endif // USE_LIBRETINY

View file

@ -0,0 +1,29 @@
#include "lt_component.h"
#ifdef USE_LIBRETINY
#include "esphome/core/log.h"
namespace esphome {
namespace libretiny {
static const char *const TAG = "lt.component";
void LTComponent::dump_config() {
ESP_LOGCONFIG(TAG, "LibreTiny:");
ESP_LOGCONFIG(TAG, " Version: %s", LT_BANNER_STR + 10);
ESP_LOGCONFIG(TAG, " Loglevel: %u", LT_LOGLEVEL);
#ifdef USE_TEXT_SENSOR
if (this->version_ != nullptr) {
this->version_->publish_state(LT_BANNER_STR + 10);
}
#endif // USE_TEXT_SENSOR
}
float LTComponent::get_setup_priority() const { return setup_priority::LATE; }
} // namespace libretiny
} // namespace esphome
#endif // USE_LIBRETINY

View file

@ -0,0 +1,36 @@
#pragma once
#ifdef USE_LIBRETINY
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
namespace esphome {
namespace libretiny {
class LTComponent : public Component {
public:
float get_setup_priority() const override;
void dump_config() override;
#ifdef USE_TEXT_SENSOR
void set_version_sensor(text_sensor::TextSensor *version) { version_ = version; }
#endif // USE_TEXT_SENSOR
protected:
#ifdef USE_TEXT_SENSOR
text_sensor::TextSensor *version_{nullptr};
#endif // USE_TEXT_SENSOR
};
} // namespace libretiny
} // namespace esphome
#endif // USE_LIBRETINY

View file

@ -0,0 +1,182 @@
#ifdef USE_LIBRETINY
#include "esphome/core/preferences.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <flashdb.h>
#include <cstring>
#include <vector>
#include <string>
namespace esphome {
namespace libretiny {
static const char *const TAG = "lt.preferences";
struct NVSData {
std::string key;
std::vector<uint8_t> data;
};
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
public:
std::string key;
fdb_kvdb_t db;
fdb_blob_t blob;
bool save(const uint8_t *data, size_t len) override {
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
if (obj.key == key) {
obj.data.assign(data, data + len);
return true;
}
}
NVSData save{};
save.key = key;
save.data.assign(data, data + len);
s_pending_save.emplace_back(save);
ESP_LOGVV(TAG, "s_pending_save: key: %s, len: %d", key.c_str(), len);
return true;
}
bool load(uint8_t *data, size_t len) override {
// try find in pending saves and load from that
for (auto &obj : s_pending_save) {
if (obj.key == key) {
if (obj.data.size() != len) {
// size mismatch
return false;
}
memcpy(data, obj.data.data(), len);
return true;
}
}
fdb_blob_make(blob, data, len);
size_t actual_len = fdb_kv_get_blob(db, key.c_str(), blob);
if (actual_len != len) {
ESP_LOGVV(TAG, "NVS length does not match (%u!=%u)", actual_len, len);
return false;
} else {
ESP_LOGVV(TAG, "fdb_kv_get_blob: key: %s, len: %d", key.c_str(), len);
}
return true;
}
};
class LibreTinyPreferences : public ESPPreferences {
public:
struct fdb_kvdb db;
struct fdb_blob blob;
void open() {
//
fdb_err_t err = fdb_kvdb_init(&db, "esphome", "kvs", NULL, NULL);
if (err != FDB_NO_ERR) {
LT_E("fdb_kvdb_init(...) failed: %d", err);
} else {
LT_I("Preferences initialized");
}
}
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override {
return make_preference(length, type);
}
ESPPreferenceObject make_preference(size_t length, uint32_t type) override {
auto *pref = new LibreTinyPreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->db = &db;
pref->blob = &blob;
uint32_t keyval = type;
pref->key = str_sprintf("%u", keyval);
return ESPPreferenceObject(pref);
}
bool sync() override {
if (s_pending_save.empty())
return true;
ESP_LOGD(TAG, "Saving %d preferences to flash...", s_pending_save.size());
// goal try write all pending saves even if one fails
int cached = 0, written = 0, failed = 0;
fdb_err_t last_err = FDB_NO_ERR;
std::string last_key{};
// go through vector from back to front (makes erase easier/more efficient)
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
const auto &save = s_pending_save[i];
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", save.key.c_str());
if (is_changed(&db, save)) {
ESP_LOGV(TAG, "sync: key: %s, len: %d", save.key.c_str(), save.data.size());
fdb_blob_make(&blob, save.data.data(), save.data.size());
fdb_err_t err = fdb_kv_set_blob(&db, save.key.c_str(), &blob);
if (err != FDB_NO_ERR) {
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%u) failed: %d", save.key.c_str(), save.data.size(), err);
failed++;
last_err = err;
last_key = save.key;
continue;
}
written++;
} else {
ESP_LOGD(TAG, "FDB data not changed; skipping %s len=%u", save.key.c_str(), save.data.size());
cached++;
}
s_pending_save.erase(s_pending_save.begin() + i);
}
ESP_LOGD(TAG, "Saving %d preferences to flash: %d cached, %d written, %d failed", cached + written + failed, cached,
written, failed);
if (failed > 0) {
ESP_LOGE(TAG, "Error saving %d preferences to flash. Last error=%d for key=%s", failed, last_err,
last_key.c_str());
}
return failed == 0;
}
bool is_changed(const fdb_kvdb_t db, const NVSData &to_save) {
NVSData stored_data{};
struct fdb_kv kv;
fdb_kv_t kvp = fdb_kv_get_obj(db, to_save.key.c_str(), &kv);
if (kvp == nullptr) {
ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", to_save.key.c_str());
return true;
}
stored_data.data.reserve(kv.value_len);
fdb_blob_make(&blob, stored_data.data.data(), kv.value_len);
size_t actual_len = fdb_kv_get_blob(db, to_save.key.c_str(), &blob);
if (actual_len != kv.value_len) {
ESP_LOGV(TAG, "fdb_kv_get_blob('%s') len mismatch: %u != %u", to_save.key.c_str(), actual_len, kv.value_len);
return true;
}
return to_save.data != stored_data.data;
}
bool reset() override {
ESP_LOGD(TAG, "Cleaning up preferences in flash...");
s_pending_save.clear();
fdb_kv_set_default(&db);
fdb_kvdb_deinit(&db);
return true;
}
};
void setup_preferences() {
auto *prefs = new LibreTinyPreferences(); // NOLINT(cppcoreguidelines-owning-memory)
prefs->open();
global_preferences = prefs;
}
} // namespace libretiny
ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome
#endif // USE_LIBRETINY

View file

@ -0,0 +1,13 @@
#pragma once
#ifdef USE_LIBRETINY
namespace esphome {
namespace libretiny {
void setup_preferences();
} // namespace libretiny
} // namespace esphome
#endif // USE_LIBRETINY

View file

@ -0,0 +1,31 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import text_sensor
from esphome.const import (
CONF_VERSION,
ENTITY_CATEGORY_DIAGNOSTIC,
ICON_CELLPHONE_ARROW_DOWN,
)
from .const import CONF_LIBRETINY, LTComponent
DEPENDENCIES = ["libretiny"]
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_LIBRETINY): cv.use_id(LTComponent),
cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema(
icon=ICON_CELLPHONE_ARROW_DOWN,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
)
async def to_code(config):
lt_component = await cg.get_variable(config[CONF_LIBRETINY])
if CONF_VERSION in config:
sens = await text_sensor.new_text_sensor(config[CONF_VERSION])
cg.add(lt_component.set_version_sensor(sens))

View file

@ -0,0 +1 @@
CODEOWNERS = ["@kuba2k2"]

View file

@ -0,0 +1,53 @@
#include "libretiny_pwm.h"
#include "esphome/core/log.h"
#ifdef USE_LIBRETINY
namespace esphome {
namespace libretiny_pwm {
static const char *const TAG = "libretiny.pwm";
void LibreTinyPWM::write_state(float state) {
if (!this->initialized_) {
ESP_LOGW(TAG, "LibreTinyPWM output hasn't been initialized yet!");
return;
}
if (this->pin_->is_inverted())
state = 1.0f - state;
this->duty_ = state;
const uint32_t max_duty = (uint32_t(1) << this->bit_depth_) - 1;
const float duty_rounded = roundf(state * max_duty);
auto duty = static_cast<uint32_t>(duty_rounded);
analogWrite(this->pin_->get_pin(), duty); // NOLINT
}
void LibreTinyPWM::setup() {
this->update_frequency(this->frequency_);
this->turn_off();
}
void LibreTinyPWM::dump_config() {
ESP_LOGCONFIG(TAG, "PWM Output:");
LOG_PIN(" Pin ", this->pin_);
ESP_LOGCONFIG(TAG, " Frequency: %.1f Hz", this->frequency_);
}
void LibreTinyPWM::update_frequency(float frequency) {
this->frequency_ = frequency;
// force changing the frequency by removing PWM mode
this->pin_->pin_mode(gpio::FLAG_INPUT);
analogWriteResolution(this->bit_depth_); // NOLINT
analogWriteFrequency(frequency); // NOLINT
this->initialized_ = true;
// re-apply duty
this->write_state(this->duty_);
}
} // namespace libretiny_pwm
} // namespace esphome
#endif

View file

@ -0,0 +1,55 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/automation.h"
#include "esphome/components/output/float_output.h"
#ifdef USE_LIBRETINY
namespace esphome {
namespace libretiny_pwm {
class LibreTinyPWM : public output::FloatOutput, public Component {
public:
explicit LibreTinyPWM(InternalGPIOPin *pin) : pin_(pin) {}
void set_frequency(float frequency) { this->frequency_ = frequency; }
/// Dynamically change frequency at runtime
void update_frequency(float frequency) override;
/// Setup LibreTinyPWM.
void setup() override;
void dump_config() override;
/// HARDWARE setup priority
float get_setup_priority() const override { return setup_priority::HARDWARE; }
/// Override FloatOutput's write_state.
void write_state(float state) override;
protected:
InternalGPIOPin *pin_;
uint8_t bit_depth_{10};
float frequency_{};
float duty_{0.0f};
bool initialized_ = false;
};
template<typename... Ts> class SetFrequencyAction : public Action<Ts...> {
public:
SetFrequencyAction(LibreTinyPWM *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(float, frequency);
void play(Ts... x) {
float freq = this->frequency_.value(x...);
this->parent_->update_frequency(freq);
}
protected:
LibreTinyPWM *parent_;
};
} // namespace libretiny_pwm
} // namespace esphome
#endif

View file

@ -0,0 +1,49 @@
from esphome import pins, automation
from esphome.components import output
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
CONF_FREQUENCY,
CONF_ID,
CONF_PIN,
)
DEPENDENCIES = ["libretiny"]
libretinypwm_ns = cg.esphome_ns.namespace("libretiny_pwm")
LibreTinyPWM = libretinypwm_ns.class_("LibreTinyPWM", output.FloatOutput, cg.Component)
SetFrequencyAction = libretinypwm_ns.class_("SetFrequencyAction", automation.Action)
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(LibreTinyPWM),
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema,
cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.frequency,
}
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
gpio = await cg.gpio_pin_expression(config[CONF_PIN])
var = cg.new_Pvariable(config[CONF_ID], gpio)
await cg.register_component(var, config)
await output.register_output(var, config)
cg.add(var.set_frequency(config[CONF_FREQUENCY]))
@automation.register_action(
"output.libretiny_pwm.set_frequency",
SetFrequencyAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(LibreTinyPWM),
cv.Required(CONF_FREQUENCY): cv.templatable(cv.int_),
}
),
)
async def libretiny_pwm_set_frequency_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_FREQUENCY], args, float)
cg.add(var.set_frequency(template_))
return var

View file

@ -17,6 +17,8 @@ from esphome.const import (
CONF_TAG, CONF_TAG,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_TX_BUFFER_SIZE, CONF_TX_BUFFER_SIZE,
PLATFORM_BK72XX,
PLATFORM_RTL87XX,
PLATFORM_ESP32, PLATFORM_ESP32,
PLATFORM_ESP8266, PLATFORM_ESP8266,
PLATFORM_RP2040, PLATFORM_RP2040,
@ -31,6 +33,11 @@ from esphome.components.esp32.const import (
VARIANT_ESP32C2, VARIANT_ESP32C2,
VARIANT_ESP32C6, VARIANT_ESP32C6,
) )
from esphome.components.libretiny import get_libretiny_component, get_libretiny_family
from esphome.components.libretiny.const import (
COMPONENT_BK72XX,
COMPONENT_RTL87XX,
)
CODEOWNERS = ["@esphome/core"] CODEOWNERS = ["@esphome/core"]
logger_ns = cg.esphome_ns.namespace("logger") logger_ns = cg.esphome_ns.namespace("logger")
@ -70,6 +77,7 @@ UART2 = "UART2"
UART0_SWAP = "UART0_SWAP" UART0_SWAP = "UART0_SWAP"
USB_SERIAL_JTAG = "USB_SERIAL_JTAG" USB_SERIAL_JTAG = "USB_SERIAL_JTAG"
USB_CDC = "USB_CDC" USB_CDC = "USB_CDC"
DEFAULT = "DEFAULT"
UART_SELECTION_ESP32 = { UART_SELECTION_ESP32 = {
VARIANT_ESP32: [UART0, UART1, UART2], VARIANT_ESP32: [UART0, UART1, UART2],
@ -82,6 +90,11 @@ UART_SELECTION_ESP32 = {
UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1] UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1]
UART_SELECTION_LIBRETINY = {
COMPONENT_BK72XX: [DEFAULT, UART1, UART2],
COMPONENT_RTL87XX: [DEFAULT, UART0, UART1, UART2],
}
ESP_IDF_UARTS = [USB_CDC, USB_SERIAL_JTAG] ESP_IDF_UARTS = [USB_CDC, USB_SERIAL_JTAG]
UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1] UART_SELECTION_RP2040 = [USB_CDC, UART0, UART1]
@ -93,6 +106,7 @@ HARDWARE_UART_TO_UART_SELECTION = {
UART2: logger_ns.UART_SELECTION_UART2, UART2: logger_ns.UART_SELECTION_UART2,
USB_CDC: logger_ns.UART_SELECTION_USB_CDC, USB_CDC: logger_ns.UART_SELECTION_USB_CDC,
USB_SERIAL_JTAG: logger_ns.UART_SELECTION_USB_SERIAL_JTAG, USB_SERIAL_JTAG: logger_ns.UART_SELECTION_USB_SERIAL_JTAG,
DEFAULT: logger_ns.UART_SELECTION_DEFAULT,
} }
HARDWARE_UART_TO_SERIAL = { HARDWARE_UART_TO_SERIAL = {
@ -100,6 +114,7 @@ HARDWARE_UART_TO_SERIAL = {
UART0_SWAP: cg.global_ns.Serial, UART0_SWAP: cg.global_ns.Serial,
UART1: cg.global_ns.Serial1, UART1: cg.global_ns.Serial1,
UART2: cg.global_ns.Serial2, UART2: cg.global_ns.Serial2,
DEFAULT: cg.global_ns.Serial,
} }
is_log_level = cv.one_of(*LOG_LEVELS, upper=True) is_log_level = cv.one_of(*LOG_LEVELS, upper=True)
@ -116,6 +131,13 @@ def uart_selection(value):
return cv.one_of(*UART_SELECTION_ESP8266, upper=True)(value) return cv.one_of(*UART_SELECTION_ESP8266, upper=True)(value)
if CORE.is_rp2040: if CORE.is_rp2040:
return cv.one_of(*UART_SELECTION_RP2040, upper=True)(value) return cv.one_of(*UART_SELECTION_RP2040, upper=True)(value)
if CORE.is_libretiny:
family = get_libretiny_family()
if family in UART_SELECTION_LIBRETINY:
return cv.one_of(*UART_SELECTION_LIBRETINY[family], upper=True)(value)
component = get_libretiny_component()
if component in UART_SELECTION_LIBRETINY:
return cv.one_of(*UART_SELECTION_LIBRETINY[component], upper=True)(value)
raise NotImplementedError raise NotImplementedError
@ -148,8 +170,18 @@ CONFIG_SCHEMA = cv.All(
esp8266=UART0, esp8266=UART0,
esp32=UART0, esp32=UART0,
rp2040=USB_CDC, rp2040=USB_CDC,
bk72xx=DEFAULT,
rtl87xx=DEFAULT,
): cv.All( ): cv.All(
cv.only_on([PLATFORM_ESP8266, PLATFORM_ESP32, PLATFORM_RP2040]), cv.only_on(
[
PLATFORM_ESP8266,
PLATFORM_ESP32,
PLATFORM_RP2040,
PLATFORM_BK72XX,
PLATFORM_RTL87XX,
]
),
uart_selection, uart_selection,
), ),
cv.Optional(CONF_LEVEL, default="DEBUG"): is_log_level, cv.Optional(CONF_LEVEL, default="DEBUG"): is_log_level,

View file

@ -158,6 +158,7 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate
this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT
} }
#ifndef USE_LIBRETINY
void Logger::pre_setup() { void Logger::pre_setup() {
if (this->baud_rate_ > 0) { if (this->baud_rate_ > 0) {
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
@ -266,12 +267,58 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized"); ESP_LOGI(TAG, "Log initialized");
} }
#else // USE_LIBRETINY
void Logger::pre_setup() {
if (this->baud_rate_ > 0) {
switch (this->uart_) {
#if LT_HW_UART0
case UART_SELECTION_UART0:
this->hw_serial_ = &Serial0;
Serial0.begin(this->baud_rate_);
break;
#endif
#if LT_HW_UART1
case UART_SELECTION_UART1:
this->hw_serial_ = &Serial1;
Serial1.begin(this->baud_rate_);
break;
#endif
#if LT_HW_UART2
case UART_SELECTION_UART2:
this->hw_serial_ = &Serial2;
Serial2.begin(this->baud_rate_);
break;
#endif
default:
this->hw_serial_ = &Serial;
Serial.begin(this->baud_rate_);
if (this->uart_ != UART_SELECTION_DEFAULT) {
ESP_LOGW(TAG, " The chosen logger UART port is not available on this board."
"The default port was used instead.");
}
break;
}
// change lt_log() port to match default Serial
if (this->uart_ == UART_SELECTION_DEFAULT) {
this->uart_ = (UARTSelection) (LT_UART_DEFAULT_SERIAL + 1);
lt_log_set_port(LT_UART_DEFAULT_SERIAL);
} else {
lt_log_set_port(this->uart_ - 1);
}
}
global_logger = this;
ESP_LOGI(TAG, "Log initialized");
}
#endif // USE_LIBRETINY
void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
void Logger::set_log_level(const std::string &tag, int log_level) { void Logger::set_log_level(const std::string &tag, int log_level) {
this->log_levels_.push_back(LogLevelOverride{tag, log_level}); this->log_levels_.push_back(LogLevelOverride{tag, log_level});
} }
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY)
UARTSelection Logger::get_uart() const { return this->uart_; } UARTSelection Logger::get_uart() const { return this->uart_; }
#endif #endif
@ -299,15 +346,18 @@ const char *const UART_SELECTIONS[] = {
#endif // USE_ESP32 #endif // USE_ESP32
#ifdef USE_ESP8266 #ifdef USE_ESP8266
const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"}; const char *const UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"};
#endif #endif // USE_ESP8266
#ifdef USE_RP2040 #ifdef USE_RP2040
const char *const UART_SELECTIONS[] = {"UART0", "UART1", "USB_CDC"}; const char *const UART_SELECTIONS[] = {"UART0", "UART1", "USB_CDC"};
#endif // USE_ESP8266 #endif // USE_RP2040
#ifdef USE_LIBRETINY
const char *const UART_SELECTIONS[] = {"DEFAULT", "UART0", "UART1", "UART2"};
#endif // USE_LIBRETINY
void Logger::dump_config() { void Logger::dump_config() {
ESP_LOGCONFIG(TAG, "Logger:"); ESP_LOGCONFIG(TAG, "Logger:");
ESP_LOGCONFIG(TAG, " Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]); ESP_LOGCONFIG(TAG, " Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]);
ESP_LOGCONFIG(TAG, " Log Baud Rate: %" PRIu32, this->baud_rate_); ESP_LOGCONFIG(TAG, " Log Baud Rate: %" PRIu32, this->baud_rate_);
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY)
ESP_LOGCONFIG(TAG, " Hardware UART: %s", UART_SELECTIONS[this->uart_]); ESP_LOGCONFIG(TAG, " Hardware UART: %s", UART_SELECTIONS[this->uart_]);
#endif #endif

View file

@ -25,12 +25,18 @@ namespace esphome {
namespace logger { namespace logger {
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY)
/** Enum for logging UART selection /** Enum for logging UART selection
* *
* Advanced configuration (pin selection, etc) is not supported. * Advanced configuration (pin selection, etc) is not supported.
*/ */
enum UARTSelection { enum UARTSelection {
#ifdef USE_LIBRETINY
UART_SELECTION_DEFAULT = 0,
UART_SELECTION_UART0,
UART_SELECTION_UART1,
UART_SELECTION_UART2,
#else
UART_SELECTION_UART0 = 0, UART_SELECTION_UART0 = 0,
UART_SELECTION_UART1, UART_SELECTION_UART1,
#if defined(USE_ESP32) #if defined(USE_ESP32)
@ -53,8 +59,9 @@ enum UARTSelection {
#ifdef USE_RP2040 #ifdef USE_RP2040
UART_SELECTION_USB_CDC, UART_SELECTION_USB_CDC,
#endif // USE_RP2040 #endif // USE_RP2040
#endif // USE_LIBRETINY
}; };
#endif // USE_ESP32 || USE_ESP8266 #endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY
class Logger : public Component { class Logger : public Component {
public: public:
@ -69,7 +76,7 @@ class Logger : public Component {
#ifdef USE_ESP_IDF #ifdef USE_ESP_IDF
uart_port_t get_uart_num() const { return uart_num_; } uart_port_t get_uart_num() const { return uart_num_; }
#endif #endif
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY)
void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; }
/// Get the UART used by the logger. /// Get the UART used by the logger.
UARTSelection get_uart() const; UARTSelection get_uart() const;
@ -146,6 +153,9 @@ class Logger : public Component {
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040)
UARTSelection uart_{UART_SELECTION_UART0}; UARTSelection uart_{UART_SELECTION_UART0};
#endif #endif
#ifdef USE_LIBRETINY
UARTSelection uart_{UART_SELECTION_DEFAULT};
#endif
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
Stream *hw_serial_{nullptr}; Stream *hw_serial_{nullptr};
#endif #endif

View file

@ -22,6 +22,11 @@
#define MD5_CTX_TYPE br_md5_context #define MD5_CTX_TYPE br_md5_context
#endif #endif
#if defined(USE_LIBRETINY)
#include <MD5.h>
#define MD5_CTX_TYPE LT_MD5_CTX_T
#endif
namespace esphome { namespace esphome {
namespace md5 { namespace md5 {

View file

@ -44,6 +44,9 @@ void MDNSComponent::compile_records_() {
#endif #endif
#ifdef USE_RP2040 #ifdef USE_RP2040
platform = "RP2040"; platform = "RP2040";
#endif
#ifdef USE_LIBRETINY
platform = lt_cpu_get_model_name();
#endif #endif
if (platform != nullptr) { if (platform != nullptr) {
service.txt_records.push_back({"platform", platform}); service.txt_records.push_back({"platform", platform});

View file

@ -0,0 +1,43 @@
#ifdef USE_LIBRETINY
#include "esphome/components/network/ip_address.h"
#include "esphome/components/network/util.h"
#include "esphome/core/log.h"
#include "mdns_component.h"
#include <mDNS.h>
namespace esphome {
namespace mdns {
void MDNSComponent::setup() {
this->compile_records_();
MDNS.begin(this->hostname_.c_str());
for (const auto &service : this->services_) {
// Strip the leading underscore from the proto and service_type. While it is
// part of the wire protocol to have an underscore, and for example ESP-IDF
// expects the underscore to be there, the ESP8266 implementation always adds
// the underscore itself.
auto *proto = service.proto.c_str();
while (*proto == '_') {
proto++;
}
auto *service_type = service.service_type.c_str();
while (*service_type == '_') {
service_type++;
}
MDNS.addService(service_type, proto, service.port);
for (const auto &record : service.txt_records) {
MDNS.addServiceTxt(service_type, proto, record.key.c_str(), record.value.c_str());
}
}
}
void MDNSComponent::on_shutdown() {}
} // namespace mdns
} // namespace esphome
#endif

View file

@ -41,7 +41,14 @@ CONFIG_SCHEMA = cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(OTAComponent), cv.GenerateID(): cv.declare_id(OTAComponent),
cv.Optional(CONF_SAFE_MODE, default=True): cv.boolean, cv.Optional(CONF_SAFE_MODE, default=True): cv.boolean,
cv.SplitDefault(CONF_PORT, esp8266=8266, esp32=3232, rp2040=2040): cv.port, cv.SplitDefault(
CONF_PORT,
esp8266=8266,
esp32=3232,
rp2040=2040,
bk72xx=8892,
rtl87xx=8892,
): cv.port,
cv.Optional(CONF_PASSWORD): cv.string, cv.Optional(CONF_PASSWORD): cv.string,
cv.Optional( cv.Optional(
CONF_REBOOT_TIMEOUT, default="5min" CONF_REBOOT_TIMEOUT, default="5min"

View file

@ -0,0 +1,46 @@
#include "esphome/core/defines.h"
#ifdef USE_LIBRETINY
#include "ota_backend_arduino_libretiny.h"
#include "ota_component.h"
#include "ota_backend.h"
#include <Update.h>
namespace esphome {
namespace ota {
OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size) {
bool ret = Update.begin(image_size, U_FLASH);
if (ret) {
return OTA_RESPONSE_OK;
}
uint8_t error = Update.getError();
if (error == UPDATE_ERROR_SIZE)
return OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE;
return OTA_RESPONSE_ERROR_UNKNOWN;
}
void ArduinoLibreTinyOTABackend::set_update_md5(const char *md5) { Update.setMD5(md5); }
OTAResponseTypes ArduinoLibreTinyOTABackend::write(uint8_t *data, size_t len) {
size_t written = Update.write(data, len);
if (written != len) {
return OTA_RESPONSE_ERROR_WRITING_FLASH;
}
return OTA_RESPONSE_OK;
}
OTAResponseTypes ArduinoLibreTinyOTABackend::end() {
if (!Update.end())
return OTA_RESPONSE_ERROR_UPDATE_END;
return OTA_RESPONSE_OK;
}
void ArduinoLibreTinyOTABackend::abort() { Update.abort(); }
} // namespace ota
} // namespace esphome
#endif // USE_LIBRETINY

View file

@ -0,0 +1,24 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_LIBRETINY
#include "ota_component.h"
#include "ota_backend.h"
namespace esphome {
namespace ota {
class ArduinoLibreTinyOTABackend : public OTABackend {
public:
OTAResponseTypes begin(size_t image_size) override;
void set_update_md5(const char *md5) override;
OTAResponseTypes write(uint8_t *data, size_t len) override;
OTAResponseTypes end() override;
void abort() override;
bool supports_compression() override { return false; }
};
} // namespace ota
} // namespace esphome
#endif // USE_LIBRETINY

View file

@ -3,6 +3,7 @@
#include "ota_backend_arduino_esp32.h" #include "ota_backend_arduino_esp32.h"
#include "ota_backend_arduino_esp8266.h" #include "ota_backend_arduino_esp8266.h"
#include "ota_backend_arduino_rp2040.h" #include "ota_backend_arduino_rp2040.h"
#include "ota_backend_arduino_libretiny.h"
#include "ota_backend_esp_idf.h" #include "ota_backend_esp_idf.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@ -39,6 +40,9 @@ std::unique_ptr<OTABackend> make_ota_backend() {
#ifdef USE_RP2040 #ifdef USE_RP2040
return make_unique<ArduinoRP2040OTABackend>(); return make_unique<ArduinoRP2040OTABackend>();
#endif // USE_RP2040 #endif // USE_RP2040
#ifdef USE_LIBRETINY
return make_unique<ArduinoLibreTinyOTABackend>();
#endif
} }
OTAComponent::OTAComponent() { global_ota_component = this; } OTAComponent::OTAComponent() { global_ota_component = this; }

View file

@ -31,7 +31,11 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
cv.percentage_int, cv.Range(min=0) cv.percentage_int, cv.Range(min=0)
), ),
cv.SplitDefault( cv.SplitDefault(
CONF_BUFFER_SIZE, esp32="10000b", esp8266="1000b" CONF_BUFFER_SIZE,
esp32="10000b",
esp8266="1000b",
bk72xx="1000b",
rtl87xx="1000b",
): cv.validate_bytes, ): cv.validate_bytes,
cv.Optional(CONF_FILTER, default="50us"): cv.All( cv.Optional(CONF_FILTER, default="50us"): cv.All(
cv.positive_time_period_microseconds, cv.positive_time_period_microseconds,

View file

@ -6,7 +6,7 @@
namespace esphome { namespace esphome {
namespace remote_receiver { namespace remote_receiver {
#ifdef USE_ESP8266 #if defined(USE_ESP8266) || defined(USE_LIBRETINY)
struct RemoteReceiverComponentStore { struct RemoteReceiverComponentStore {
static void gpio_intr(RemoteReceiverComponentStore *arg); static void gpio_intr(RemoteReceiverComponentStore *arg);
@ -55,7 +55,7 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase,
esp_err_t error_code_{ESP_OK}; esp_err_t error_code_{ESP_OK};
#endif #endif
#ifdef USE_ESP8266 #if defined(USE_ESP8266) || defined(USE_LIBRETINY)
RemoteReceiverComponentStore store_; RemoteReceiverComponentStore store_;
HighFrequencyLoopRequester high_freq_; HighFrequencyLoopRequester high_freq_;
#endif #endif

View file

@ -0,0 +1,122 @@
#include "remote_receiver.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#ifdef USE_LIBRETINY
namespace esphome {
namespace remote_receiver {
static const char *const TAG = "remote_receiver.libretiny";
void IRAM_ATTR HOT RemoteReceiverComponentStore::gpio_intr(RemoteReceiverComponentStore *arg) {
const uint32_t now = micros();
// If the lhs is 1 (rising edge) we should write to an uneven index and vice versa
const uint32_t next = (arg->buffer_write_at + 1) % arg->buffer_size;
const bool level = arg->pin.digital_read();
if (level != next % 2)
return;
// If next is buffer_read, we have hit an overflow
if (next == arg->buffer_read_at)
return;
const uint32_t last_change = arg->buffer[arg->buffer_write_at];
const uint32_t time_since_change = now - last_change;
if (time_since_change <= arg->filter_us)
return;
arg->buffer[arg->buffer_write_at = next] = now;
}
void RemoteReceiverComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up Remote Receiver...");
this->pin_->setup();
auto &s = this->store_;
s.filter_us = this->filter_us_;
s.pin = this->pin_->to_isr();
s.buffer_size = this->buffer_size_;
this->high_freq_.start();
if (s.buffer_size % 2 != 0) {
// Make sure divisible by two. This way, we know that every 0bxxx0 index is a space and every 0bxxx1 index is a mark
s.buffer_size++;
}
s.buffer = new uint32_t[s.buffer_size];
void *buf = (void *) s.buffer;
memset(buf, 0, s.buffer_size * sizeof(uint32_t));
// First index is a space.
if (this->pin_->digital_read()) {
s.buffer_write_at = s.buffer_read_at = 1;
} else {
s.buffer_write_at = s.buffer_read_at = 0;
}
this->pin_->attach_interrupt(RemoteReceiverComponentStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE);
}
void RemoteReceiverComponent::dump_config() {
ESP_LOGCONFIG(TAG, "Remote Receiver:");
LOG_PIN(" Pin: ", this->pin_);
if (this->pin_->digital_read()) {
ESP_LOGW(TAG, "Remote Receiver Signal starts with a HIGH value. Usually this means you have to "
"invert the signal using 'inverted: True' in the pin schema!");
}
ESP_LOGCONFIG(TAG, " Buffer Size: %u", this->buffer_size_);
ESP_LOGCONFIG(TAG, " Tolerance: %u%%", this->tolerance_);
ESP_LOGCONFIG(TAG, " Filter out pulses shorter than: %u us", this->filter_us_);
ESP_LOGCONFIG(TAG, " Signal is done after %u us of no changes", this->idle_us_);
}
void RemoteReceiverComponent::loop() {
auto &s = this->store_;
// copy write at to local variables, as it's volatile
const uint32_t write_at = s.buffer_write_at;
const uint32_t dist = (s.buffer_size + write_at - s.buffer_read_at) % s.buffer_size;
// signals must at least one rising and one leading edge
if (dist <= 1)
return;
const uint32_t now = micros();
if (now - s.buffer[write_at] < this->idle_us_) {
// The last change was fewer than the configured idle time ago.
return;
}
ESP_LOGVV(TAG, "read_at=%u write_at=%u dist=%u now=%u end=%u", s.buffer_read_at, write_at, dist, now,
s.buffer[write_at]);
// Skip first value, it's from the previous idle level
s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size;
uint32_t prev = s.buffer_read_at;
s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size;
const uint32_t reserve_size = 1 + (s.buffer_size + write_at - s.buffer_read_at) % s.buffer_size;
this->temp_.clear();
this->temp_.reserve(reserve_size);
int32_t multiplier = s.buffer_read_at % 2 == 0 ? 1 : -1;
for (uint32_t i = 0; prev != write_at; i++) {
int32_t delta = s.buffer[s.buffer_read_at] - s.buffer[prev];
if (uint32_t(delta) >= this->idle_us_) {
// already found a space longer than idle. There must have been two pulses
break;
}
ESP_LOGVV(TAG, " i=%u buffer[%u]=%u - buffer[%u]=%u -> %d", i, s.buffer_read_at, s.buffer[s.buffer_read_at], prev,
s.buffer[prev], multiplier * delta);
this->temp_.push_back(multiplier * delta);
prev = s.buffer_read_at;
s.buffer_read_at = (s.buffer_read_at + 1) % s.buffer_size;
multiplier *= -1;
}
s.buffer_read_at = (s.buffer_size + s.buffer_read_at - 1) % s.buffer_size;
this->temp_.push_back(this->idle_us_ * multiplier);
this->call_listeners_dumpers_();
}
} // namespace remote_receiver
} // namespace esphome
#endif

View file

@ -28,7 +28,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
protected: protected:
void send_internal(uint32_t send_times, uint32_t send_wait) override; void send_internal(uint32_t send_times, uint32_t send_wait) override;
#ifdef USE_ESP8266 #if defined(USE_ESP8266) || defined(USE_LIBRETINY)
void calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, uint32_t *off_time_period); void calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period, uint32_t *off_time_period);
void mark_(uint32_t on_time, uint32_t off_time, uint32_t usec); void mark_(uint32_t on_time, uint32_t off_time, uint32_t usec);

View file

@ -0,0 +1,104 @@
#include "remote_transmitter.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#ifdef USE_LIBRETINY
namespace esphome {
namespace remote_transmitter {
static const char *const TAG = "remote_transmitter";
void RemoteTransmitterComponent::setup() {
this->pin_->setup();
this->pin_->digital_write(false);
}
void RemoteTransmitterComponent::dump_config() {
ESP_LOGCONFIG(TAG, "Remote Transmitter...");
ESP_LOGCONFIG(TAG, " Carrier Duty: %u%%", this->carrier_duty_percent_);
LOG_PIN(" Pin: ", this->pin_);
}
void RemoteTransmitterComponent::calculate_on_off_time_(uint32_t carrier_frequency, uint32_t *on_time_period,
uint32_t *off_time_period) {
if (carrier_frequency == 0) {
*on_time_period = 0;
*off_time_period = 0;
return;
}
uint32_t period = (1000000UL + carrier_frequency / 2) / carrier_frequency; // round(1000000/freq)
period = std::max(uint32_t(1), period);
*on_time_period = (period * this->carrier_duty_percent_) / 100;
*off_time_period = period - *on_time_period;
}
void RemoteTransmitterComponent::await_target_time_() {
const uint32_t current_time = micros();
if (this->target_time_ == 0) {
this->target_time_ = current_time;
} else {
while (this->target_time_ > micros()) {
// busy loop that ensures micros is constantly called
}
}
}
void RemoteTransmitterComponent::mark_(uint32_t on_time, uint32_t off_time, uint32_t usec) {
this->await_target_time_();
this->pin_->digital_write(true);
const uint32_t target = this->target_time_ + usec;
if (this->carrier_duty_percent_ < 100 && (on_time > 0 || off_time > 0)) {
while (true) { // Modulate with carrier frequency
this->target_time_ += on_time;
if (this->target_time_ >= target)
break;
this->await_target_time_();
this->pin_->digital_write(false);
this->target_time_ += off_time;
if (this->target_time_ >= target)
break;
this->await_target_time_();
this->pin_->digital_write(true);
}
}
this->target_time_ = target;
}
void RemoteTransmitterComponent::space_(uint32_t usec) {
this->await_target_time_();
this->pin_->digital_write(false);
this->target_time_ += usec;
}
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
ESP_LOGD(TAG, "Sending remote code...");
uint32_t on_time, off_time;
this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time);
this->target_time_ = 0;
for (uint32_t i = 0; i < send_times; i++) {
InterruptLock lock;
for (int32_t item : this->temp_.get_data()) {
if (item > 0) {
const auto length = uint32_t(item);
this->mark_(on_time, off_time, length);
} else {
const auto length = uint32_t(-item);
this->space_(length);
}
App.feed_wdt();
}
this->await_target_time_(); // wait for duration of last pulse
this->pin_->digital_write(false);
if (i + 1 < send_times)
this->target_time_ += send_wait;
}
}
} // namespace remote_transmitter
} // namespace esphome
#endif

View file

@ -1,6 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ANALOG,
CONF_ID, CONF_ID,
CONF_INPUT, CONF_INPUT,
CONF_INVERTED, CONF_INVERTED,
@ -76,8 +77,6 @@ def validate_supports(value):
return value return value
CONF_ANALOG = "analog"
RP2040_PIN_SCHEMA = cv.All( RP2040_PIN_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {

View file

@ -0,0 +1,51 @@
# This file was auto-generated by libretiny/generate_components.py
# Do not modify its contents.
# For custom pin validators, put validate_pin() or validate_usage()
# in gpio.py file in this directory.
# For changing schema/pin schema, put COMPONENT_SCHEMA or COMPONENT_PIN_SCHEMA
# in schema.py file in this directory.
from esphome import pins
from esphome.components import libretiny
from esphome.components.libretiny.const import (
COMPONENT_RTL87XX,
KEY_COMPONENT_DATA,
KEY_LIBRETINY,
LibreTinyComponent,
)
from esphome.core import CORE
from .boards import RTL87XX_BOARDS, RTL87XX_BOARD_PINS
CODEOWNERS = ["@kuba2k2"]
AUTO_LOAD = ["libretiny"]
COMPONENT_DATA = LibreTinyComponent(
name=COMPONENT_RTL87XX,
boards=RTL87XX_BOARDS,
board_pins=RTL87XX_BOARD_PINS,
pin_validation=None,
usage_validation=None,
)
def _set_core_data(config):
CORE.data[KEY_LIBRETINY] = {}
CORE.data[KEY_LIBRETINY][KEY_COMPONENT_DATA] = COMPONENT_DATA
return config
CONFIG_SCHEMA = libretiny.BASE_SCHEMA
PIN_SCHEMA = libretiny.gpio.BASE_PIN_SCHEMA
CONFIG_SCHEMA.prepend_extra(_set_core_data)
async def to_code(config):
return await libretiny.component_to_code(config)
@pins.PIN_SCHEMA_REGISTRY.register("rtl87xx", PIN_SCHEMA)
async def pin_to_code(config):
return await libretiny.gpio.component_pin_to_code(config)

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
#include "sntp_component.h" #include "sntp_component.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#ifdef USE_ESP32 #if defined(USE_ESP32) || defined(USE_LIBRETINY)
#include "lwip/apps/sntp.h" #include "lwip/apps/sntp.h"
#ifdef USE_ESP_IDF #ifdef USE_ESP_IDF
#include "esp_sntp.h" #include "esp_sntp.h"
@ -26,7 +26,7 @@ static const char *const TAG = "sntp";
void SNTPComponent::setup() { void SNTPComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up SNTP..."); ESP_LOGCONFIG(TAG, "Setting up SNTP...");
#ifdef USE_ESP32 #if defined(USE_ESP32) || defined(USE_LIBRETINY)
if (sntp_enabled()) { if (sntp_enabled()) {
sntp_stop(); sntp_stop();
} }

View file

@ -5,6 +5,7 @@ CODEOWNERS = ["@esphome/core"]
CONF_IMPLEMENTATION = "implementation" CONF_IMPLEMENTATION = "implementation"
IMPLEMENTATION_LWIP_TCP = "lwip_tcp" IMPLEMENTATION_LWIP_TCP = "lwip_tcp"
IMPLEMENTATION_LWIP_SOCKETS = "lwip_sockets"
IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets" IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets"
CONFIG_SCHEMA = cv.Schema( CONFIG_SCHEMA = cv.Schema(
@ -14,9 +15,15 @@ CONFIG_SCHEMA = cv.Schema(
esp8266=IMPLEMENTATION_LWIP_TCP, esp8266=IMPLEMENTATION_LWIP_TCP,
esp32=IMPLEMENTATION_BSD_SOCKETS, esp32=IMPLEMENTATION_BSD_SOCKETS,
rp2040=IMPLEMENTATION_LWIP_TCP, rp2040=IMPLEMENTATION_LWIP_TCP,
bk72xx=IMPLEMENTATION_LWIP_SOCKETS,
rtl87xx=IMPLEMENTATION_LWIP_SOCKETS,
host=IMPLEMENTATION_BSD_SOCKETS, host=IMPLEMENTATION_BSD_SOCKETS,
): cv.one_of( ): cv.one_of(
IMPLEMENTATION_LWIP_TCP, IMPLEMENTATION_BSD_SOCKETS, lower=True, space="_" IMPLEMENTATION_LWIP_TCP,
IMPLEMENTATION_LWIP_SOCKETS,
IMPLEMENTATION_BSD_SOCKETS,
lower=True,
space="_",
), ),
} }
) )
@ -26,5 +33,7 @@ async def to_code(config):
impl = config[CONF_IMPLEMENTATION] impl = config[CONF_IMPLEMENTATION]
if impl == IMPLEMENTATION_LWIP_TCP: if impl == IMPLEMENTATION_LWIP_TCP:
cg.add_define("USE_SOCKET_IMPL_LWIP_TCP") cg.add_define("USE_SOCKET_IMPL_LWIP_TCP")
elif impl == IMPLEMENTATION_LWIP_SOCKETS:
cg.add_define("USE_SOCKET_IMPL_LWIP_SOCKETS")
elif impl == IMPLEMENTATION_BSD_SOCKETS: elif impl == IMPLEMENTATION_BSD_SOCKETS:
cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS") cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS")

View file

@ -120,6 +120,35 @@ struct iovec {
#endif // USE_SOCKET_IMPL_LWIP_TCP #endif // USE_SOCKET_IMPL_LWIP_TCP
#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS
// standard lwIP's compatibility macros will interfere
// with Socket class function names - disable the macros
// and use real function names instead
#undef LWIP_COMPAT_SOCKETS
#define LWIP_COMPAT_SOCKETS 0
#include "lwip/sockets.h"
#include <sys/types.h>
#ifdef USE_ARDUINO
// arduino-esp32 declares a global var called INADDR_NONE which is replaced
// by the define
#ifdef INADDR_NONE
#undef INADDR_NONE
#endif
// not defined for ESP32
using socklen_t = uint32_t;
#define ESPHOME_INADDR_ANY ((uint32_t) 0x00000000UL)
#define ESPHOME_INADDR_NONE ((uint32_t) 0xFFFFFFFFUL)
#else // !USE_ESP32
#define ESPHOME_INADDR_ANY INADDR_ANY
#define ESPHOME_INADDR_NONE INADDR_NONE
#endif
#endif // USE_SOCKET_IMPL_LWIP_SOCKETS
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS #ifdef USE_SOCKET_IMPL_BSD_SOCKETS
#include <cstdint> #include <cstdint>

View file

@ -0,0 +1,115 @@
#include "socket.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS
#include <cstring>
namespace esphome {
namespace socket {
std::string format_sockaddr(const struct sockaddr_storage &storage) {
if (storage.ss_family == AF_INET) {
const struct sockaddr_in *addr = reinterpret_cast<const struct sockaddr_in *>(&storage);
char buf[INET_ADDRSTRLEN];
const char *ret = lwip_inet_ntop(AF_INET, &addr->sin_addr, buf, sizeof(buf));
if (ret == nullptr)
return {};
return std::string{buf};
}
#if LWIP_IPV6
else if (storage.ss_family == AF_INET6) {
const struct sockaddr_in6 *addr = reinterpret_cast<const struct sockaddr_in6 *>(&storage);
char buf[INET6_ADDRSTRLEN];
const char *ret = lwip_inet_ntop(AF_INET6, &addr->sin6_addr, buf, sizeof(buf));
if (ret == nullptr)
return {};
return std::string{buf};
}
#endif
return {};
}
class LwIPSocketImpl : public Socket {
public:
LwIPSocketImpl(int fd) : fd_(fd) {}
~LwIPSocketImpl() override {
if (!closed_) {
close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall)
}
}
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
int fd = lwip_accept(fd_, addr, addrlen);
if (fd == -1)
return {};
return make_unique<LwIPSocketImpl>(fd);
}
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(fd_, addr, addrlen); }
int close() override {
int ret = lwip_close(fd_);
closed_ = true;
return ret;
}
int shutdown(int how) override { return lwip_shutdown(fd_, how); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { return lwip_getpeername(fd_, addr, addrlen); }
std::string getpeername() override {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
int err = this->getpeername((struct sockaddr *) &storage, &len);
if (err != 0)
return {};
return format_sockaddr(storage);
}
int getsockname(struct sockaddr *addr, socklen_t *addrlen) override { return lwip_getsockname(fd_, addr, addrlen); }
std::string getsockname() override {
struct sockaddr_storage storage;
socklen_t len = sizeof(storage);
int err = this->getsockname((struct sockaddr *) &storage, &len);
if (err != 0)
return {};
return format_sockaddr(storage);
}
int getsockopt(int level, int optname, void *optval, socklen_t *optlen) override {
return lwip_getsockopt(fd_, level, optname, optval, optlen);
}
int setsockopt(int level, int optname, const void *optval, socklen_t optlen) override {
return lwip_setsockopt(fd_, level, optname, optval, optlen);
}
int listen(int backlog) override { return lwip_listen(fd_, backlog); }
ssize_t read(void *buf, size_t len) override { return lwip_read(fd_, buf, len); }
ssize_t readv(const struct iovec *iov, int iovcnt) override { return lwip_readv(fd_, iov, iovcnt); }
ssize_t write(const void *buf, size_t len) override { return lwip_write(fd_, buf, len); }
ssize_t send(void *buf, size_t len, int flags) { return lwip_send(fd_, buf, len, flags); }
ssize_t writev(const struct iovec *iov, int iovcnt) override { return lwip_writev(fd_, iov, iovcnt); }
ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override {
return lwip_sendto(fd_, buf, len, flags, to, tolen);
}
int setblocking(bool blocking) override {
int fl = lwip_fcntl(fd_, F_GETFL, 0);
if (blocking) {
fl &= ~O_NONBLOCK;
} else {
fl |= O_NONBLOCK;
}
lwip_fcntl(fd_, F_SETFL, fl);
return 0;
}
protected:
int fd_;
bool closed_ = false;
};
std::unique_ptr<Socket> socket(int domain, int type, int protocol) {
int ret = lwip_socket(domain, type, protocol);
if (ret == -1)
return nullptr;
return std::unique_ptr<Socket>{new LwIPSocketImpl(ret)};
}
} // namespace socket
} // namespace esphome
#endif // USE_SOCKET_IMPL_LWIP_SOCKETS

View file

@ -48,6 +48,7 @@ CONFIG_SCHEMA = cv.All(
} }
), ),
cv.has_at_least_one_key(CONF_MISO_PIN, CONF_MOSI_PIN), cv.has_at_least_one_key(CONF_MISO_PIN, CONF_MOSI_PIN),
cv.only_on(["esp32", "esp8266", "rp2040"]),
) )

View file

@ -42,6 +42,9 @@ ESP8266UartComponent = uart_ns.class_(
"ESP8266UartComponent", UARTComponent, cg.Component "ESP8266UartComponent", UARTComponent, cg.Component
) )
RP2040UartComponent = uart_ns.class_("RP2040UartComponent", UARTComponent, cg.Component) RP2040UartComponent = uart_ns.class_("RP2040UartComponent", UARTComponent, cg.Component)
LibreTinyUARTComponent = uart_ns.class_(
"LibreTinyUARTComponent", UARTComponent, cg.Component
)
UARTDevice = uart_ns.class_("UARTDevice") UARTDevice = uart_ns.class_("UARTDevice")
UARTWriteAction = uart_ns.class_("UARTWriteAction", automation.Action) UARTWriteAction = uart_ns.class_("UARTWriteAction", automation.Action)
@ -92,6 +95,8 @@ def _uart_declare_type(value):
return cv.declare_id(IDFUARTComponent)(value) return cv.declare_id(IDFUARTComponent)(value)
if CORE.is_rp2040: if CORE.is_rp2040:
return cv.declare_id(RP2040UartComponent)(value) return cv.declare_id(RP2040UartComponent)(value)
if CORE.is_libretiny:
return cv.declare_id(LibreTinyUARTComponent)(value)
raise NotImplementedError raise NotImplementedError

View file

@ -0,0 +1,168 @@
#ifdef USE_LIBRETINY
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "uart_component_libretiny.h"
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif
#if LT_ARD_HAS_SOFTSERIAL
#include <SoftwareSerial.h>
#endif
namespace esphome {
namespace uart {
static const char *const TAG = "uart.lt";
static const char *UART_TYPE[] = {
"hardware",
"software",
};
uint16_t LibreTinyUARTComponent::get_config() {
uint16_t config = 0;
switch (this->parity_) {
case UART_CONFIG_PARITY_NONE:
config |= SERIAL_PARITY_NONE;
break;
case UART_CONFIG_PARITY_EVEN:
config |= SERIAL_PARITY_EVEN;
break;
case UART_CONFIG_PARITY_ODD:
config |= SERIAL_PARITY_ODD;
break;
}
config |= (this->data_bits_ - 4) << 8;
config |= 0x10 + (this->stop_bits_ - 1) * 0x20;
return config;
}
void LibreTinyUARTComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up UART...");
int8_t tx_pin = tx_pin_ == nullptr ? -1 : tx_pin_->get_pin();
int8_t rx_pin = rx_pin_ == nullptr ? -1 : rx_pin_->get_pin();
bool tx_inverted = tx_pin_ != nullptr && tx_pin_->is_inverted();
bool rx_inverted = rx_pin_ != nullptr && rx_pin_->is_inverted();
if (false)
return;
#if LT_HW_UART0
else if ((tx_pin == -1 || tx_pin == PIN_SERIAL0_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL0_RX)) {
this->serial_ = &Serial0;
this->hardware_idx_ = 0;
}
#endif
#if LT_HW_UART1
else if ((tx_pin == -1 || tx_pin == PIN_SERIAL1_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL1_RX)) {
this->serial_ = &Serial1;
this->hardware_idx_ = 1;
}
#endif
#if LT_HW_UART2
else if ((tx_pin == -1 || tx_pin == PIN_SERIAL2_TX) && (rx_pin == -1 || rx_pin == PIN_SERIAL2_RX)) {
this->serial_ = &Serial2;
this->hardware_idx_ = 2;
}
#endif
else {
#if LT_ARD_HAS_SOFTSERIAL
this->serial_ = new SoftwareSerial(rx_pin, tx_pin, rx_inverted || tx_inverted);
#else
this->serial_ = &Serial;
ESP_LOGE(TAG, " SoftwareSerial is not implemented for this chip. Only hardware pins are supported:");
#if LT_HW_UART0
ESP_LOGE(TAG, " TX=%u, RX=%u", PIN_SERIAL0_TX, PIN_SERIAL0_RX);
#endif
#if LT_HW_UART1
ESP_LOGE(TAG, " TX=%u, RX=%u", PIN_SERIAL1_TX, PIN_SERIAL1_RX);
#endif
#if LT_HW_UART2
ESP_LOGE(TAG, " TX=%u, RX=%u", PIN_SERIAL2_TX, PIN_SERIAL2_RX);
#endif
this->mark_failed();
return;
#endif
}
this->serial_->begin(this->baud_rate_, get_config());
}
void LibreTinyUARTComponent::dump_config() {
bool is_software = this->hardware_idx_ == -1;
ESP_LOGCONFIG(TAG, "UART Bus:");
ESP_LOGCONFIG(TAG, " Type: %s", UART_TYPE[is_software]);
if (!is_software) {
ESP_LOGCONFIG(TAG, " Port number: %d", this->hardware_idx_);
}
LOG_PIN(" TX Pin: ", tx_pin_);
LOG_PIN(" RX Pin: ", rx_pin_);
if (this->rx_pin_ != nullptr) {
ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_);
}
ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_);
ESP_LOGCONFIG(TAG, " Data Bits: %u", this->data_bits_);
ESP_LOGCONFIG(TAG, " Parity: %s", LOG_STR_ARG(parity_to_str(this->parity_)));
ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_);
this->check_logger_conflict();
}
void LibreTinyUARTComponent::write_array(const uint8_t *data, size_t len) {
this->serial_->write(data, len);
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(UART_DIRECTION_TX, data[i]);
}
#endif
}
bool LibreTinyUARTComponent::peek_byte(uint8_t *data) {
if (!this->check_read_timeout_())
return false;
*data = this->serial_->peek();
return true;
}
bool LibreTinyUARTComponent::read_array(uint8_t *data, size_t len) {
if (!this->check_read_timeout_(len))
return false;
this->serial_->readBytes(data, len);
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
}
#endif
return true;
}
int LibreTinyUARTComponent::available() { return this->serial_->available(); }
void LibreTinyUARTComponent::flush() {
ESP_LOGVV(TAG, " Flushing...");
this->serial_->flush();
}
void LibreTinyUARTComponent::check_logger_conflict() {
#ifdef USE_LOGGER
if (this->hardware_idx_ == -1 || logger::global_logger->get_baud_rate() == 0) {
return;
}
if (this->serial_ == logger::global_logger->get_hw_serial()) {
ESP_LOGW(TAG, " You're using the same serial port for logging and the UART component. Please "
"disable logging over the serial port by setting logger->baud_rate to 0.");
}
#endif
}
} // namespace uart
} // namespace esphome
#endif // USE_LIBRETINY

View file

@ -0,0 +1,43 @@
#pragma once
#ifdef USE_LIBRETINY
#include <vector>
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "uart_component.h"
namespace esphome {
namespace uart {
class LibreTinyUARTComponent : public UARTComponent, public Component {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::BUS; }
void write_array(const uint8_t *data, size_t len) override;
bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override;
int available() override;
void flush() override;
uint16_t get_config();
HardwareSerial *get_hw_serial() { return this->serial_; }
int8_t get_hw_serial_number() { return this->hardware_idx_; }
protected:
void check_logger_conflict() override;
HardwareSerial *serial_{nullptr};
int8_t hardware_idx_{-1};
};
} // namespace uart
} // namespace esphome
#endif // USE_LIBRETINY

View file

@ -79,13 +79,18 @@ CONFIG_SCHEMA = cv.All(
), ),
cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean, cv.Optional(CONF_INCLUDE_INTERNAL, default=False): cv.boolean,
cv.SplitDefault( cv.SplitDefault(
CONF_OTA, esp8266=True, esp32_arduino=True, esp32_idf=False CONF_OTA,
esp8266=True,
esp32_arduino=True,
esp32_idf=False,
bk72xx=True,
rtl87xx=True,
): cv.boolean, ): cv.boolean,
cv.Optional(CONF_LOG, default=True): cv.boolean, cv.Optional(CONF_LOG, default=True): cv.boolean,
cv.Optional(CONF_LOCAL): cv.boolean, cv.Optional(CONF_LOCAL): cv.boolean,
} }
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
cv.only_on(["esp32", "esp8266"]), cv.only_on(["esp32", "esp8266", "bk72xx", "rtl87xx"]),
default_url, default_url,
validate_local, validate_local,
validate_ota, validate_ota,

View file

@ -37,4 +37,4 @@ async def to_code(config):
cg.add_library("FS", None) cg.add_library("FS", None)
cg.add_library("Update", None) cg.add_library("Update", None)
# https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json # https://github.com/esphome/ESPAsyncWebServer/blob/master/library.json
cg.add_library("esphome/ESPAsyncWebServer-esphome", "2.1.0") cg.add_library("esphome/ESPAsyncWebServer-esphome", "3.1.0")

View file

@ -5,7 +5,7 @@
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
#include <StreamString.h> #include <StreamString.h>
#ifdef USE_ESP32 #if defined(USE_ESP32) || defined(USE_LIBRETINY)
#include <Update.h> #include <Update.h>
#endif #endif
#ifdef USE_ESP8266 #ifdef USE_ESP8266
@ -50,7 +50,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin
// NOLINTNEXTLINE(readability-static-accessed-through-instance) // NOLINTNEXTLINE(readability-static-accessed-through-instance)
success = Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); success = Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000);
#endif #endif
#ifdef USE_ESP32_FRAMEWORK_ARDUINO #if defined(USE_ESP32_FRAMEWORK_ARDUINO) || defined(USE_LIBRETINY)
if (Update.isRunning()) { if (Update.isRunning()) {
Update.abort(); Update.abort();
} }

View file

@ -272,7 +272,12 @@ CONFIG_SCHEMA = cv.All(
CONF_REBOOT_TIMEOUT, default="15min" CONF_REBOOT_TIMEOUT, default="15min"
): cv.positive_time_period_milliseconds, ): cv.positive_time_period_milliseconds,
cv.SplitDefault( cv.SplitDefault(
CONF_POWER_SAVE_MODE, esp8266="none", esp32="light", rp2040="light" CONF_POWER_SAVE_MODE,
esp8266="none",
esp32="light",
rp2040="light",
bk72xx="none",
rtl87xx="none",
): cv.enum(WIFI_POWER_SAVE_MODES, upper=True), ): cv.enum(WIFI_POWER_SAVE_MODES, upper=True),
cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean, cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean,
cv.Optional(CONF_USE_ADDRESS): cv.string_strict, cv.Optional(CONF_USE_ADDRESS): cv.string_strict,

View file

@ -15,6 +15,10 @@
#include <WiFi.h> #include <WiFi.h>
#endif #endif
#ifdef USE_LIBRETINY
#include <WiFi.h>
#endif
#ifdef USE_ESP8266 #ifdef USE_ESP8266
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
#include <ESP8266WiFiType.h> #include <ESP8266WiFiType.h>
@ -336,6 +340,11 @@ class WiFiComponent : public Component {
void wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result); void wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result);
#endif #endif
#ifdef USE_LIBRETINY
void wifi_event_callback_(arduino_event_id_t event, arduino_event_info_t info);
void wifi_scan_done_callback_();
#endif
std::string use_address_; std::string use_address_;
std::vector<WiFiAP> sta_; std::vector<WiFiAP> sta_;
std::vector<WiFiSTAPriority> sta_priorities_; std::vector<WiFiSTAPriority> sta_priorities_;

View file

@ -0,0 +1,467 @@
#include "wifi_component.h"
#ifdef USE_LIBRETINY
#include <utility>
#include <algorithm>
#include "lwip/ip_addr.h"
#include "lwip/err.h"
#include "lwip/dns.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/application.h"
#include "esphome/core/util.h"
namespace esphome {
namespace wifi {
static const char *const TAG = "wifi_lt";
static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
uint8_t current_mode = WiFi.getMode();
bool current_sta = current_mode & 0b01;
bool current_ap = current_mode & 0b10;
bool enable_sta = sta.value_or(current_sta);
bool enable_ap = ap.value_or(current_ap);
if (current_sta == enable_sta && current_ap == enable_ap)
return true;
if (enable_sta && !current_sta) {
ESP_LOGV(TAG, "Enabling STA.");
} else if (!enable_sta && current_sta) {
ESP_LOGV(TAG, "Disabling STA.");
}
if (enable_ap && !current_ap) {
ESP_LOGV(TAG, "Enabling AP.");
} else if (!enable_ap && current_ap) {
ESP_LOGV(TAG, "Disabling AP.");
}
uint8_t mode = 0;
if (enable_sta)
mode |= 0b01;
if (enable_ap)
mode |= 0b10;
bool ret = WiFi.mode(static_cast<wifi_mode_t>(mode));
if (!ret) {
ESP_LOGW(TAG, "Setting WiFi mode failed!");
}
return ret;
}
bool WiFiComponent::wifi_apply_output_power_(float output_power) {
int8_t val = static_cast<int8_t>(output_power * 4);
return WiFi.setTxPower(val);
}
bool WiFiComponent::wifi_sta_pre_setup_() {
if (!this->wifi_mode_(true, {}))
return false;
WiFi.setAutoReconnect(false);
delay(10);
return true;
}
bool WiFiComponent::wifi_apply_power_save_() { return WiFi.setSleep(this->power_save_ != WIFI_POWER_SAVE_NONE); }
bool WiFiComponent::wifi_sta_ip_config_(optional<ManualIP> manual_ip) {
// enable STA
if (!this->wifi_mode_(true, {}))
return false;
if (!manual_ip.has_value()) {
return true;
}
WiFi.config(static_cast<uint32_t>(manual_ip->static_ip), static_cast<uint32_t>(manual_ip->gateway),
static_cast<uint32_t>(manual_ip->subnet), static_cast<uint32_t>(manual_ip->dns1),
static_cast<uint32_t>(manual_ip->dns2));
return true;
}
network::IPAddress WiFiComponent::wifi_sta_ip() {
if (!this->has_sta())
return {};
return {WiFi.localIP()};
}
bool WiFiComponent::wifi_apply_hostname_() {
// setting is done in SYSTEM_EVENT_STA_START callback too
WiFi.setHostname(App.get_name().c_str());
return true;
}
bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
// enable STA
if (!this->wifi_mode_(true, {}))
return false;
String ssid = WiFi.SSID();
if (ssid && strcmp(ssid.c_str(), ap.get_ssid().c_str()) != 0) {
WiFi.disconnect();
}
if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
return false;
}
this->wifi_apply_hostname_();
s_sta_connecting = true;
WiFiStatus status = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(),
ap.get_channel().has_value() ? *ap.get_channel() : 0,
ap.get_bssid().has_value() ? ap.get_bssid()->data() : NULL);
if (status != WL_CONNECTED) {
ESP_LOGW(TAG, "esp_wifi_connect failed! %d", status);
return false;
}
return true;
}
const char *get_auth_mode_str(uint8_t mode) {
switch (mode) {
case WIFI_AUTH_OPEN:
return "OPEN";
case WIFI_AUTH_WEP:
return "WEP";
case WIFI_AUTH_WPA_PSK:
return "WPA PSK";
case WIFI_AUTH_WPA2_PSK:
return "WPA2 PSK";
case WIFI_AUTH_WPA_WPA2_PSK:
return "WPA/WPA2 PSK";
default:
return "UNKNOWN";
}
}
using esphome_ip4_addr_t = IPAddress;
std::string format_ip4_addr(const esphome_ip4_addr_t &ip) {
char buf[20];
uint32_t addr = ip;
sprintf(buf, "%u.%u.%u.%u", uint8_t(addr >> 0), uint8_t(addr >> 8), uint8_t(addr >> 16), uint8_t(addr >> 24));
return buf;
}
const char *get_op_mode_str(uint8_t mode) {
switch (mode) {
case WIFI_OFF:
return "OFF";
case WIFI_STA:
return "STA";
case WIFI_AP:
return "AP";
case WIFI_AP_STA:
return "AP+STA";
default:
return "UNKNOWN";
}
}
const char *get_disconnect_reason_str(uint8_t reason) {
switch (reason) {
case WIFI_REASON_AUTH_EXPIRE:
return "Auth Expired";
case WIFI_REASON_AUTH_LEAVE:
return "Auth Leave";
case WIFI_REASON_ASSOC_EXPIRE:
return "Association Expired";
case WIFI_REASON_ASSOC_TOOMANY:
return "Too Many Associations";
case WIFI_REASON_NOT_AUTHED:
return "Not Authenticated";
case WIFI_REASON_NOT_ASSOCED:
return "Not Associated";
case WIFI_REASON_ASSOC_LEAVE:
return "Association Leave";
case WIFI_REASON_ASSOC_NOT_AUTHED:
return "Association not Authenticated";
case WIFI_REASON_DISASSOC_PWRCAP_BAD:
return "Disassociate Power Cap Bad";
case WIFI_REASON_DISASSOC_SUPCHAN_BAD:
return "Disassociate Supported Channel Bad";
case WIFI_REASON_IE_INVALID:
return "IE Invalid";
case WIFI_REASON_MIC_FAILURE:
return "Mic Failure";
case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT:
return "4-Way Handshake Timeout";
case WIFI_REASON_GROUP_KEY_UPDATE_TIMEOUT:
return "Group Key Update Timeout";
case WIFI_REASON_IE_IN_4WAY_DIFFERS:
return "IE In 4-Way Handshake Differs";
case WIFI_REASON_GROUP_CIPHER_INVALID:
return "Group Cipher Invalid";
case WIFI_REASON_PAIRWISE_CIPHER_INVALID:
return "Pairwise Cipher Invalid";
case WIFI_REASON_AKMP_INVALID:
return "AKMP Invalid";
case WIFI_REASON_UNSUPP_RSN_IE_VERSION:
return "Unsupported RSN IE version";
case WIFI_REASON_INVALID_RSN_IE_CAP:
return "Invalid RSN IE Cap";
case WIFI_REASON_802_1X_AUTH_FAILED:
return "802.1x Authentication Failed";
case WIFI_REASON_CIPHER_SUITE_REJECTED:
return "Cipher Suite Rejected";
case WIFI_REASON_BEACON_TIMEOUT:
return "Beacon Timeout";
case WIFI_REASON_NO_AP_FOUND:
return "AP Not Found";
case WIFI_REASON_AUTH_FAIL:
return "Authentication Failed";
case WIFI_REASON_ASSOC_FAIL:
return "Association Failed";
case WIFI_REASON_HANDSHAKE_TIMEOUT:
return "Handshake Failed";
case WIFI_REASON_CONNECTION_FAIL:
return "Connection Failed";
case WIFI_REASON_UNSPECIFIED:
default:
return "Unspecified";
}
}
#define ESPHOME_EVENT_ID_WIFI_READY ARDUINO_EVENT_WIFI_READY
#define ESPHOME_EVENT_ID_WIFI_SCAN_DONE ARDUINO_EVENT_WIFI_SCAN_DONE
#define ESPHOME_EVENT_ID_WIFI_STA_START ARDUINO_EVENT_WIFI_STA_START
#define ESPHOME_EVENT_ID_WIFI_STA_STOP ARDUINO_EVENT_WIFI_STA_STOP
#define ESPHOME_EVENT_ID_WIFI_STA_CONNECTED ARDUINO_EVENT_WIFI_STA_CONNECTED
#define ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED ARDUINO_EVENT_WIFI_STA_DISCONNECTED
#define ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE ARDUINO_EVENT_WIFI_STA_AUTHMODE_CHANGE
#define ESPHOME_EVENT_ID_WIFI_STA_GOT_IP ARDUINO_EVENT_WIFI_STA_GOT_IP
#define ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6 ARDUINO_EVENT_WIFI_STA_GOT_IP6
#define ESPHOME_EVENT_ID_WIFI_STA_LOST_IP ARDUINO_EVENT_WIFI_STA_LOST_IP
#define ESPHOME_EVENT_ID_WIFI_AP_START ARDUINO_EVENT_WIFI_AP_START
#define ESPHOME_EVENT_ID_WIFI_AP_STOP ARDUINO_EVENT_WIFI_AP_STOP
#define ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED ARDUINO_EVENT_WIFI_AP_STACONNECTED
#define ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED ARDUINO_EVENT_WIFI_AP_STADISCONNECTED
#define ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED ARDUINO_EVENT_WIFI_AP_STAIPASSIGNED
#define ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED ARDUINO_EVENT_WIFI_AP_PROBEREQRECVED
#define ESPHOME_EVENT_ID_WIFI_AP_GOT_IP6 ARDUINO_EVENT_WIFI_AP_GOT_IP6
using esphome_wifi_event_id_t = arduino_event_id_t;
using esphome_wifi_event_info_t = arduino_event_info_t;
void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_wifi_event_info_t info) {
switch (event) {
case ESPHOME_EVENT_ID_WIFI_READY: {
ESP_LOGV(TAG, "Event: WiFi ready");
break;
}
case ESPHOME_EVENT_ID_WIFI_SCAN_DONE: {
auto it = info.wifi_scan_done;
ESP_LOGV(TAG, "Event: WiFi Scan Done status=%u number=%u scan_id=%u", it.status, it.number, it.scan_id);
this->wifi_scan_done_callback_();
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_START: {
ESP_LOGV(TAG, "Event: WiFi STA start");
WiFi.setHostname(App.get_name().c_str());
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_STOP: {
ESP_LOGV(TAG, "Event: WiFi STA stop");
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_CONNECTED: {
auto it = info.wifi_sta_connected;
char buf[33];
memcpy(buf, it.ssid, it.ssid_len);
buf[it.ssid_len] = '\0';
ESP_LOGV(TAG, "Event: Connected ssid='%s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", buf,
format_mac_addr(it.bssid).c_str(), it.channel, get_auth_mode_str(it.authmode));
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_DISCONNECTED: {
auto it = info.wifi_sta_disconnected;
char buf[33];
memcpy(buf, it.ssid, it.ssid_len);
buf[it.ssid_len] = '\0';
if (it.reason == WIFI_REASON_NO_AP_FOUND) {
ESP_LOGW(TAG, "Event: Disconnected ssid='%s' reason='Probe Request Unsuccessful'", buf);
} else {
ESP_LOGW(TAG, "Event: Disconnected ssid='%s' bssid=" LOG_SECRET("%s") " reason='%s'", buf,
format_mac_addr(it.bssid).c_str(), get_disconnect_reason_str(it.reason));
}
uint8_t reason = it.reason;
if (reason == WIFI_REASON_AUTH_EXPIRE || reason == WIFI_REASON_BEACON_TIMEOUT ||
reason == WIFI_REASON_NO_AP_FOUND || reason == WIFI_REASON_ASSOC_FAIL ||
reason == WIFI_REASON_HANDSHAKE_TIMEOUT) {
WiFi.disconnect();
this->error_from_callback_ = true;
}
s_sta_connecting = false;
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_AUTHMODE_CHANGE: {
auto it = info.wifi_sta_authmode_change;
ESP_LOGV(TAG, "Event: Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode),
get_auth_mode_str(it.new_mode));
// Mitigate CVE-2020-12638
// https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors
if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) {
ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting...");
// we can't call retry_connect() from this context, so disconnect immediately
// and notify main thread with error_from_callback_
WiFi.disconnect();
this->error_from_callback_ = true;
}
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP: {
// auto it = info.got_ip.ip_info;
ESP_LOGV(TAG, "Event: Got IP static_ip=%s gateway=%s", format_ip4_addr(WiFi.localIP()).c_str(),
format_ip4_addr(WiFi.gatewayIP()).c_str());
s_sta_connecting = false;
break;
}
case ESPHOME_EVENT_ID_WIFI_STA_LOST_IP: {
ESP_LOGV(TAG, "Event: Lost IP");
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_START: {
ESP_LOGV(TAG, "Event: WiFi AP start");
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_STOP: {
ESP_LOGV(TAG, "Event: WiFi AP stop");
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_STACONNECTED: {
auto it = info.wifi_sta_connected;
auto &mac = it.bssid;
ESP_LOGV(TAG, "Event: AP client connected MAC=%s", format_mac_addr(mac).c_str());
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_STADISCONNECTED: {
auto it = info.wifi_sta_disconnected;
auto &mac = it.bssid;
ESP_LOGV(TAG, "Event: AP client disconnected MAC=%s", format_mac_addr(mac).c_str());
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_STAIPASSIGNED: {
ESP_LOGV(TAG, "Event: AP client assigned IP");
break;
}
case ESPHOME_EVENT_ID_WIFI_AP_PROBEREQRECVED: {
auto it = info.wifi_ap_probereqrecved;
ESP_LOGVV(TAG, "Event: AP receive Probe Request MAC=%s RSSI=%d", format_mac_addr(it.mac).c_str(), it.rssi);
break;
}
default:
break;
}
}
void WiFiComponent::wifi_pre_setup_() {
auto f = std::bind(&WiFiComponent::wifi_event_callback_, this, std::placeholders::_1, std::placeholders::_2);
WiFi.onEvent(f);
// Make sure WiFi is in clean state before anything starts
this->wifi_mode_(false, false);
}
WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() {
auto status = WiFi.status();
if (status == WL_CONNECTED) {
return WiFiSTAConnectStatus::CONNECTED;
} else if (status == WL_CONNECT_FAILED || status == WL_CONNECTION_LOST) {
return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED;
} else if (status == WL_NO_SSID_AVAIL) {
return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND;
} else if (s_sta_connecting) {
return WiFiSTAConnectStatus::CONNECTING;
}
return WiFiSTAConnectStatus::IDLE;
}
bool WiFiComponent::wifi_scan_start_(bool passive) {
// enable STA
if (!this->wifi_mode_(true, {}))
return false;
// need to use WiFi because of WiFiScanClass allocations :(
int16_t err = WiFi.scanNetworks(true, true, passive, 200);
if (err != WIFI_SCAN_RUNNING) {
ESP_LOGV(TAG, "WiFi.scanNetworks failed! %d", err);
return false;
}
return true;
}
void WiFiComponent::wifi_scan_done_callback_() {
this->scan_result_.clear();
int16_t num = WiFi.scanComplete();
if (num < 0)
return;
this->scan_result_.reserve(static_cast<unsigned int>(num));
for (int i = 0; i < num; i++) {
String ssid = WiFi.SSID(i);
wifi_auth_mode_t authmode = WiFi.encryptionType(i);
int32_t rssi = WiFi.RSSI(i);
uint8_t *bssid = WiFi.BSSID(i);
int32_t channel = WiFi.channel(i);
WiFiScanResult scan({bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]}, std::string(ssid.c_str()),
channel, rssi, authmode != WIFI_AUTH_OPEN, ssid.length() == 0);
this->scan_result_.push_back(scan);
}
WiFi.scanDelete();
this->scan_done_ = true;
}
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
// enable AP
if (!this->wifi_mode_({}, true))
return false;
if (manual_ip.has_value()) {
return WiFi.softAPConfig(static_cast<uint32_t>(manual_ip->static_ip), static_cast<uint32_t>(manual_ip->gateway),
static_cast<uint32_t>(manual_ip->subnet));
} else {
return WiFi.softAPConfig(IPAddress(192, 168, 4, 1), IPAddress(192, 168, 4, 1), IPAddress(255, 255, 255, 0));
}
}
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
// enable AP
if (!this->wifi_mode_({}, true))
return false;
if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
ESP_LOGV(TAG, "wifi_ap_ip_config_ failed!");
return false;
}
yield();
return WiFi.softAP(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(),
ap.get_channel().value_or(1), ap.get_hidden());
}
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.softAPIP()}; }
bool WiFiComponent::wifi_disconnect_() { return WiFi.disconnect(); }
bssid_t WiFiComponent::wifi_bssid() {
bssid_t bssid{};
uint8_t *raw_bssid = WiFi.BSSID();
if (raw_bssid != nullptr) {
for (size_t i = 0; i < bssid.size(); i++)
bssid[i] = raw_bssid[i];
}
return bssid;
}
std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
int8_t WiFiComponent::wifi_rssi() { return WiFi.RSSI(); }
int32_t WiFiComponent::wifi_channel_() { return WiFi.channel(); }
network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; }
network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {WiFi.gatewayIP()}; }
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {WiFi.dnsIP(num)}; }
void WiFiComponent::wifi_loop_() {}
} // namespace wifi
} // namespace esphome
#endif // USE_LIBRETINY

View file

@ -1500,6 +1500,8 @@ class SplitDefault(Optional):
esp32_arduino=vol.UNDEFINED, esp32_arduino=vol.UNDEFINED,
esp32_idf=vol.UNDEFINED, esp32_idf=vol.UNDEFINED,
rp2040=vol.UNDEFINED, rp2040=vol.UNDEFINED,
bk72xx=vol.UNDEFINED,
rtl87xx=vol.UNDEFINED,
host=vol.UNDEFINED, host=vol.UNDEFINED,
): ):
super().__init__(key) super().__init__(key)
@ -1511,6 +1513,8 @@ class SplitDefault(Optional):
esp32_idf if esp32 is vol.UNDEFINED else esp32 esp32_idf if esp32 is vol.UNDEFINED else esp32
) )
self._rp2040_default = vol.default_factory(rp2040) self._rp2040_default = vol.default_factory(rp2040)
self._bk72xx_default = vol.default_factory(bk72xx)
self._rtl87xx_default = vol.default_factory(rtl87xx)
self._host_default = vol.default_factory(host) self._host_default = vol.default_factory(host)
@property @property
@ -1523,6 +1527,10 @@ class SplitDefault(Optional):
return self._esp32_idf_default return self._esp32_idf_default
if CORE.is_rp2040: if CORE.is_rp2040:
return self._rp2040_default return self._rp2040_default
if CORE.is_bk72xx:
return self._bk72xx_default
if CORE.is_rtl87xx:
return self._rtl87xx_default
if CORE.is_host: if CORE.is_host:
return self._host_default return self._host_default
raise NotImplementedError raise NotImplementedError

View file

@ -11,8 +11,19 @@ PLATFORM_ESP32 = "esp32"
PLATFORM_ESP8266 = "esp8266" PLATFORM_ESP8266 = "esp8266"
PLATFORM_RP2040 = "rp2040" PLATFORM_RP2040 = "rp2040"
PLATFORM_HOST = "host" PLATFORM_HOST = "host"
PLATFORM_BK72XX = "bk72xx"
PLATFORM_RTL87XX = "rtl87xx"
PLATFORM_LIBRETINY_OLDSTYLE = "libretiny"
TARGET_PLATFORMS = [PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_HOST] TARGET_PLATFORMS = [
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
PLATFORM_HOST,
PLATFORM_BK72XX,
PLATFORM_RTL87XX,
PLATFORM_LIBRETINY_OLDSTYLE,
]
SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"} SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"}
HEADER_FILE_EXTENSIONS = {".h", ".hpp", ".tcc"} HEADER_FILE_EXTENSIONS = {".h", ".hpp", ".tcc"}
@ -37,6 +48,7 @@ CONF_ADVANCED = "advanced"
CONF_AFTER = "after" CONF_AFTER = "after"
CONF_ALPHA = "alpha" CONF_ALPHA = "alpha"
CONF_ALTITUDE = "altitude" CONF_ALTITUDE = "altitude"
CONF_ANALOG = "analog"
CONF_AND = "and" CONF_AND = "and"
CONF_AP = "ap" CONF_AP = "ap"
CONF_APPARENT_POWER = "apparent_power" CONF_APPARENT_POWER = "apparent_power"
@ -835,6 +847,7 @@ ICON_BRIEFCASE_DOWNLOAD = "mdi:briefcase-download"
ICON_BRIGHTNESS_5 = "mdi:brightness-5" ICON_BRIGHTNESS_5 = "mdi:brightness-5"
ICON_BRIGHTNESS_6 = "mdi:brightness-6" ICON_BRIGHTNESS_6 = "mdi:brightness-6"
ICON_BUG = "mdi:bug" ICON_BUG = "mdi:bug"
ICON_CELLPHONE_ARROW_DOWN = "mdi:cellphone-arrow-down"
ICON_CHECK_CIRCLE_OUTLINE = "mdi:check-circle-outline" ICON_CHECK_CIRCLE_OUTLINE = "mdi:check-circle-outline"
ICON_CHEMICAL_WEAPON = "mdi:chemical-weapon" ICON_CHEMICAL_WEAPON = "mdi:chemical-weapon"
ICON_CHIP = "mdi:chip" ICON_CHIP = "mdi:chip"

View file

@ -584,6 +584,8 @@ class EsphomeCore:
@property @property
def firmware_bin(self): def firmware_bin(self):
if self.is_libretiny:
return self.relative_pioenvs_path(self.name, "firmware.uf2")
return self.relative_pioenvs_path(self.name, "firmware.bin") return self.relative_pioenvs_path(self.name, "firmware.bin")
@property @property
@ -602,6 +604,18 @@ class EsphomeCore:
def is_rp2040(self): def is_rp2040(self):
return self.target_platform == "rp2040" return self.target_platform == "rp2040"
@property
def is_bk72xx(self):
return self.target_platform == "bk72xx"
@property
def is_rtl87xx(self):
return self.target_platform == "rtl87xx"
@property
def is_libretiny(self):
return self.is_bk72xx or self.is_rtl87xx
@property @property
def is_host(self): def is_host(self):
return self.target_platform == "host" return self.target_platform == "host"

View file

@ -380,7 +380,7 @@ async def to_code(config):
cg.add_build_flag("-Wno-unused-but-set-variable") cg.add_build_flag("-Wno-unused-but-set-variable")
cg.add_build_flag("-Wno-sign-compare") cg.add_build_flag("-Wno-sign-compare")
if CORE.using_arduino: if CORE.using_arduino and not CORE.is_bk72xx:
CORE.add_job(add_arduino_global_workaround) CORE.add_job(add_arduino_global_workaround)
if config[CONF_INCLUDES]: if config[CONF_INCLUDES]:

View file

@ -95,6 +95,10 @@
#define USE_HTTP_REQUEST_ESP8266_HTTPS #define USE_HTTP_REQUEST_ESP8266_HTTPS
#define USE_SOCKET_IMPL_LWIP_TCP #define USE_SOCKET_IMPL_LWIP_TCP
#ifdef USE_LIBRETINY
#define USE_SOCKET_IMPL_LWIP_SOCKETS
#endif
// Dummy firmware payload for shelly_dimmer // Dummy firmware payload for shelly_dimmer
#define USE_SHD_FIRMWARE_MAJOR_VERSION 56 #define USE_SHD_FIRMWARE_MAJOR_VERSION 56
#define USE_SHD_FIRMWARE_MINOR_VERSION 5 #define USE_SHD_FIRMWARE_MINOR_VERSION 5

View file

@ -43,6 +43,10 @@
#include "esp_efuse_table.h" #include "esp_efuse_table.h"
#endif #endif
#ifdef USE_LIBRETINY
#include <WiFi.h> // for macAddress()
#endif
namespace esphome { namespace esphome {
static const char *const TAG = "helpers"; static const char *const TAG = "helpers";
@ -190,6 +194,8 @@ uint32_t random_uint32() {
result |= rosc_hw->randombit; result |= rosc_hw->randombit;
} }
return result; return result;
#elif defined(USE_LIBRETINY)
return rand();
#elif defined(USE_HOST) #elif defined(USE_HOST)
std::random_device dev; std::random_device dev;
std::mt19937 rng(dev()); std::mt19937 rng(dev());
@ -216,6 +222,9 @@ bool random_bytes(uint8_t *data, size_t len) {
*data++ = result; *data++ = result;
} }
return true; return true;
#elif defined(USE_LIBRETINY)
lt_rand_bytes(data, len);
return true;
#elif defined(USE_HOST) #elif defined(USE_HOST)
FILE *fp = fopen("/dev/urandom", "r"); FILE *fp = fopen("/dev/urandom", "r");
if (fp == nullptr) { if (fp == nullptr) {
@ -503,7 +512,7 @@ Mutex::Mutex() {}
void Mutex::lock() {} void Mutex::lock() {}
bool Mutex::try_lock() { return true; } bool Mutex::try_lock() { return true; }
void Mutex::unlock() {} void Mutex::unlock() {}
#elif defined(USE_ESP32) #elif defined(USE_ESP32) || defined(USE_LIBRETINY)
Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); } Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); }
void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); } void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); }
bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; } bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; }
@ -513,7 +522,7 @@ void Mutex::unlock() { xSemaphoreGive(this->handle_); }
#if defined(USE_ESP8266) #if defined(USE_ESP8266)
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); } IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); } IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
#elif defined(USE_ESP32) #elif defined(USE_ESP32) || defined(USE_LIBRETINY)
// only affects the executing core // only affects the executing core
// so should not be used as a mutex lock, only to get accurate timing // so should not be used as a mutex lock, only to get accurate timing
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); } IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
@ -555,6 +564,8 @@ void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parame
wifi_get_macaddr(STATION_IF, mac); wifi_get_macaddr(STATION_IF, mac);
#elif defined(USE_RP2040) && defined(USE_WIFI) #elif defined(USE_RP2040) && defined(USE_WIFI)
WiFi.macAddress(mac); WiFi.macAddress(mac);
#elif defined(USE_LIBRETINY)
WiFi.macAddress(mac);
#endif #endif
} }
std::string get_mac_address() { std::string get_mac_address() {

View file

@ -17,6 +17,9 @@
#if defined(USE_ESP32) #if defined(USE_ESP32)
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/semphr.h> #include <freertos/semphr.h>
#elif defined(USE_LIBRETINY)
#include <FreeRTOS.h>
#include <semphr.h>
#endif #endif
#define HOT __attribute__((hot)) #define HOT __attribute__((hot))
@ -543,7 +546,7 @@ class Mutex {
Mutex &operator=(const Mutex &) = delete; Mutex &operator=(const Mutex &) = delete;
private: private:
#if defined(USE_ESP32) #if defined(USE_ESP32) || defined(USE_LIBRETINY)
SemaphoreHandle_t handle_; SemaphoreHandle_t handle_;
#endif #endif
}; };

View file

@ -17,6 +17,9 @@
#ifdef USE_ESP32_FRAMEWORK_ARDUINO #ifdef USE_ESP32_FRAMEWORK_ARDUINO
#include <esp32-hal-log.h> #include <esp32-hal-log.h>
#endif #endif
#ifdef USE_LIBRETINY
#include <lt_logger.h>
#endif
namespace esphome { namespace esphome {

View file

@ -543,6 +543,7 @@ class DownloadListRequestHandler(BaseHandler):
from esphome.components.esp32 import get_download_types as esp32_types from esphome.components.esp32 import get_download_types as esp32_types
from esphome.components.esp8266 import get_download_types as esp8266_types from esphome.components.esp8266 import get_download_types as esp8266_types
from esphome.components.rp2040 import get_download_types as rp2040_types from esphome.components.rp2040 import get_download_types as rp2040_types
from esphome.components.libretiny import get_download_types as libretiny_types
downloads = [] downloads = []
platform = storage_json.target_platform.lower() platform = storage_json.target_platform.lower()
@ -552,6 +553,10 @@ class DownloadListRequestHandler(BaseHandler):
downloads = esp8266_types(storage_json) downloads = esp8266_types(storage_json)
elif platform == const.PLATFORM_ESP32: elif platform == const.PLATFORM_ESP32:
downloads = esp32_types(storage_json) downloads = esp32_types(storage_json)
elif platform == const.PLATFORM_BK72XX:
downloads = libretiny_types(storage_json)
elif platform == const.PLATFORM_RTL87XX:
downloads = libretiny_types(storage_json)
else: else:
self.send_error(418) self.send_error(418)
return return
@ -826,11 +831,15 @@ class BoardsRequestHandler(BaseHandler):
from esphome.components.esp32.boards import BOARDS as ESP32_BOARDS from esphome.components.esp32.boards import BOARDS as ESP32_BOARDS
from esphome.components.esp8266.boards import BOARDS as ESP8266_BOARDS from esphome.components.esp8266.boards import BOARDS as ESP8266_BOARDS
from esphome.components.rp2040.boards import BOARDS as RP2040_BOARDS from esphome.components.rp2040.boards import BOARDS as RP2040_BOARDS
from esphome.components.bk72xx.boards import BOARDS as BK72XX_BOARDS
from esphome.components.rtl87xx.boards import BOARDS as RTL87XX_BOARDS
platform_to_boards = { platform_to_boards = {
"esp32": ESP32_BOARDS, "esp32": ESP32_BOARDS,
"esp8266": ESP8266_BOARDS, "esp8266": ESP8266_BOARDS,
"rp2040": RP2040_BOARDS, "rp2040": RP2040_BOARDS,
"bk72xx": BK72XX_BOARDS,
"rtl87xx": RTL87XX_BOARDS,
} }
# filter all ESP32 variants by requested platform # filter all ESP32 variants by requested platform
if platform.startswith("esp32"): if platform.startswith("esp32"):

View file

@ -203,6 +203,11 @@ class _Schema(vol.Schema):
self._extra_schemas.append(validator) self._extra_schemas.append(validator)
return self return self
def prepend_extra(self, validator):
validator = _Schema(validator)
self._extra_schemas.insert(0, validator)
return self
@schema_extractor_extended @schema_extractor_extended
def extend(self, *schemas, **kwargs): def extend(self, *schemas, **kwargs):
extra = kwargs.pop("extra", None) extra = kwargs.pop("extra", None)

View file

@ -93,12 +93,24 @@ rp2040:
platform_version: https://github.com/maxgerhardt/platform-raspberrypi.git platform_version: https://github.com/maxgerhardt/platform-raspberrypi.git
""" """
BK72XX_CONFIG = """
bk72xx:
board: {board}
"""
RTL87XX_CONFIG = """
rtl87xx:
board: {board}
"""
HARDWARE_BASE_CONFIGS = { HARDWARE_BASE_CONFIGS = {
"ESP8266": ESP8266_CONFIG, "ESP8266": ESP8266_CONFIG,
"ESP32": ESP32_CONFIG, "ESP32": ESP32_CONFIG,
"ESP32S2": ESP32S2_CONFIG, "ESP32S2": ESP32S2_CONFIG,
"ESP32C3": ESP32C3_CONFIG, "ESP32C3": ESP32C3_CONFIG,
"RP2040": RP2040_CONFIG, "RP2040": RP2040_CONFIG,
"BK72XX": BK72XX_CONFIG,
"RTL87XX": RTL87XX_CONFIG,
} }
@ -156,7 +168,7 @@ def wizard_file(**kwargs):
""" """
# pylint: disable=consider-using-f-string # pylint: disable=consider-using-f-string
if kwargs["platform"] in ["ESP8266", "ESP32"]: if kwargs["platform"] in ["ESP8266", "ESP32", "BK72XX", "RTL87XX"]:
config += """ config += """
# Enable fallback hotspot (captive portal) in case wifi connection fails # Enable fallback hotspot (captive portal) in case wifi connection fails
ap: ap:
@ -182,7 +194,10 @@ captive_portal:
def wizard_write(path, **kwargs): def wizard_write(path, **kwargs):
from esphome.components.esp8266 import boards as esp8266_boards from esphome.components.esp8266 import boards as esp8266_boards
from esphome.components.esp32 import boards as esp32_boards
from esphome.components.rp2040 import boards as rp2040_boards from esphome.components.rp2040 import boards as rp2040_boards
from esphome.components.bk72xx import boards as bk72xx_boards
from esphome.components.rtl87xx import boards as rtl87xx_boards
name = kwargs["name"] name = kwargs["name"]
board = kwargs["board"] board = kwargs["board"]
@ -192,12 +207,19 @@ def wizard_write(path, **kwargs):
kwargs[key] = sanitize_double_quotes(kwargs[key]) kwargs[key] = sanitize_double_quotes(kwargs[key])
if "platform" not in kwargs: if "platform" not in kwargs:
if board in esp8266_boards.ESP8266_BOARD_PINS: if board in esp8266_boards.BOARDS:
platform = "ESP8266" platform = "ESP8266"
elif board in rp2040_boards.RP2040_BOARD_PINS: elif board in esp32_boards.BOARDS:
platform = "RP2040"
else:
platform = "ESP32" platform = "ESP32"
elif board in rp2040_boards.BOARDS:
platform = "RP2040"
elif board in bk72xx_boards.BOARDS:
platform = "BK72XX"
elif board in rtl87xx_boards.BOARDS:
platform = "RTL87XX"
else:
safe_print(color(Fore.RED, f'The board "{board}" is unknown.'))
return False
kwargs["platform"] = platform kwargs["platform"] = platform
hardware = kwargs["platform"] hardware = kwargs["platform"]
@ -206,6 +228,8 @@ def wizard_write(path, **kwargs):
storage_path = ext_storage_path(os.path.dirname(path), os.path.basename(path)) storage_path = ext_storage_path(os.path.dirname(path), os.path.basename(path))
storage.save(storage_path) storage.save(storage_path)
return True
if get_bool_env(ENV_QUICKWIZARD): if get_bool_env(ENV_QUICKWIZARD):
@ -243,6 +267,8 @@ def strip_accents(value):
def wizard(path): def wizard(path):
from esphome.components.esp32 import boards as esp32_boards from esphome.components.esp32 import boards as esp32_boards
from esphome.components.esp8266 import boards as esp8266_boards from esphome.components.esp8266 import boards as esp8266_boards
from esphome.components.bk72xx import boards as bk72xx_boards
from esphome.components.rtl87xx import boards as rtl87xx_boards
if not path.endswith(".yaml") and not path.endswith(".yml"): if not path.endswith(".yaml") and not path.endswith(".yml"):
safe_print( safe_print(
@ -262,7 +288,7 @@ def wizard(path):
sleep(2.0) sleep(2.0)
safe_print( safe_print(
"In 4 steps I'm going to guide you through creating a basic " "In 4 steps I'm going to guide you through creating a basic "
"configuration file for your custom ESP8266/ESP32 firmware. Yay!" "configuration file for your custom firmware. Yay!"
) )
sleep(3.0) sleep(3.0)
safe_print() safe_print()
@ -305,16 +331,18 @@ def wizard(path):
"Now I'd like to know what microcontroller you're using so that I can compile " "Now I'd like to know what microcontroller you're using so that I can compile "
"firmwares for it." "firmwares for it."
) )
wizard_platforms = ["ESP32", "ESP8266", "BK72XX", "RTL87XX"]
safe_print( safe_print(
f"Are you using an {color(Fore.GREEN, 'ESP32')} or {color(Fore.GREEN, 'ESP8266')} platform? (Choose ESP8266 for Sonoff devices)" "Please choose one of the supported microcontrollers "
"(Use ESP8266 for Sonoff devices)."
) )
while True: while True:
sleep(0.5) sleep(0.5)
safe_print() safe_print()
safe_print("Please enter either ESP32 or ESP8266.") platform = input(color(Fore.BOLD_WHITE, f"({'/'.join(wizard_platforms)}): "))
platform = input(color(Fore.BOLD_WHITE, "(ESP32/ESP8266): "))
try: try:
platform = vol.All(vol.Upper, vol.Any("ESP32", "ESP8266"))(platform) platform = vol.All(vol.Upper, vol.Any(*wizard_platforms))(platform.upper())
break break
except vol.Invalid: except vol.Invalid:
safe_print( safe_print(
@ -328,10 +356,14 @@ def wizard(path):
board_link = ( board_link = (
"http://docs.platformio.org/en/latest/platforms/espressif32.html#boards" "http://docs.platformio.org/en/latest/platforms/espressif32.html#boards"
) )
else: elif platform == "ESP8266":
board_link = ( board_link = (
"http://docs.platformio.org/en/latest/platforms/espressif8266.html#boards" "http://docs.platformio.org/en/latest/platforms/espressif8266.html#boards"
) )
elif platform in ["BK72XX", "RTL87XX"]:
board_link = "https://docs.libretiny.eu/docs/status/supported/"
else:
raise NotImplementedError("Unknown platform!")
safe_print(f"Next, I need to know what {color(Fore.GREEN, 'board')} you're using.") safe_print(f"Next, I need to know what {color(Fore.GREEN, 'board')} you're using.")
sleep(0.5) sleep(0.5)
@ -342,11 +374,24 @@ def wizard(path):
# Don't sleep because user needs to copy link # Don't sleep because user needs to copy link
if platform == "ESP32": if platform == "ESP32":
safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'nodemcu-32s')}\".") safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'nodemcu-32s')}\".")
boards = list(esp32_boards.ESP32_BOARD_PINS.keys()) boards_list = esp32_boards.BOARDS.items()
else: elif platform == "ESP8266":
safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'nodemcuv2')}\".") safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'nodemcuv2')}\".")
boards = list(esp8266_boards.ESP8266_BOARD_PINS.keys()) boards_list = esp8266_boards.BOARDS.items()
safe_print(f"Options: {', '.join(sorted(boards))}") elif platform == "BK72XX":
safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'cb2s')}\".")
boards_list = bk72xx_boards.BOARDS.items()
elif platform == "RTL87XX":
safe_print(f"For example \"{color(Fore.BOLD_WHITE, 'wr3')}\".")
boards_list = rtl87xx_boards.BOARDS.items()
else:
raise NotImplementedError("Unknown platform!")
boards = []
safe_print("Options:")
for board_id, board_data in boards_list:
safe_print(f" - {board_id} - {board_data['name']}")
boards.append(board_id)
while True: while True:
board = input(color(Fore.BOLD_WHITE, "(board): ")) board = input(color(Fore.BOLD_WHITE, "(board): "))
@ -420,7 +465,7 @@ def wizard(path):
safe_print("Press ENTER for no password") safe_print("Press ENTER for no password")
password = input(color(Fore.BOLD_WHITE, "(password): ")) password = input(color(Fore.BOLD_WHITE, "(password): "))
wizard_write( if not wizard_write(
path=path, path=path,
name=name, name=name,
platform=platform, platform=platform,
@ -428,7 +473,8 @@ def wizard(path):
ssid=ssid, ssid=ssid,
psk=psk, psk=psk,
password=password, password=password,
) ):
return 1
safe_print() safe_print()
safe_print( safe_print(

View file

@ -4,7 +4,7 @@
; It's *not* used during runtime. ; It's *not* used during runtime.
[platformio] [platformio]
default_envs = esp8266-arduino, esp32-arduino, esp32-idf default_envs = esp8266-arduino, esp32-arduino, esp32-idf, bk72xx-arduino
; Ideally, we want src_dir to be the root directory of the repository, to mimic the runtime build ; Ideally, we want src_dir to be the root directory of the repository, to mimic the runtime build
; environment as best as possible. Unfortunately, the ESP-IDF toolchain really doesn't like this ; environment as best as possible. Unfortunately, the ESP-IDF toolchain really doesn't like this
; being the root directory. Instead, set esphome/ as the source directory, all our sources are in ; being the root directory. Instead, set esphome/ as the source directory, all our sources are in
@ -167,6 +167,16 @@ build_flags =
-DUSE_RP2040 -DUSE_RP2040
-DUSE_RP2040_FRAMEWORK_ARDUINO -DUSE_RP2040_FRAMEWORK_ARDUINO
; This are common settings for the LibreTiny (all variants) using Arduino.
[common:libretiny-arduino]
extends = common:arduino
platform = libretiny
framework = arduino
build_flags =
${common:arduino.build_flags}
-DUSE_LIBRETINY
build_src_flags = -include Arduino.h
; All the actual environments are defined below. ; All the actual environments are defined below.
;;;;;;;; ESP8266 ;;;;;;;; ;;;;;;;; ESP8266 ;;;;;;;;
@ -339,6 +349,35 @@ build_flags =
${common:rp2040-arduino.build_flags} ${common:rp2040-arduino.build_flags}
${flags:runtime.build_flags} ${flags:runtime.build_flags}
;;;;;;;; LibreTiny ;;;;;;;;
[env:bk72xx-arduino]
extends = common:libretiny-arduino
board = generic-bk7231n-qfn32-tuya
build_flags =
${common:libretiny-arduino.build_flags}
${flags:runtime.build_flags}
-DUSE_BK72XX
-DUSE_LIBRETINY_VARIANT_BK7231N
[env:rtl87xxb-arduino]
extends = common:libretiny-arduino
board = generic-rtl8710bn-2mb-788k
build_flags =
${common:libretiny-arduino.build_flags}
${flags:runtime.build_flags}
-DUSE_RTL87XX
-DUSE_LIBRETINY_VARIANT_RTL8710B
[env:rtl87xxc-arduino]
extends = common:libretiny-arduino
board = generic-rtl8720cf-2mb-992k
build_flags =
${common:libretiny-arduino.build_flags}
${flags:runtime.build_flags}
-DUSE_RTL87XX
-DUSE_LIBRETINY_VARIANT_RTL8720C
[env:host] [env:host]
extends = common extends = common
platform = platformio/native platform = platformio/native

View file

@ -512,7 +512,10 @@ def relative_py_search_text(fname, content):
@lint_content_find_check( @lint_content_find_check(
relative_py_search_text, relative_py_search_text,
include=["esphome/components/*.py"], include=["esphome/components/*.py"],
exclude=["esphome/components/web_server/__init__.py"], exclude=[
"esphome/components/libretiny/generate_components.py",
"esphome/components/web_server/__init__.py",
],
) )
def lint_relative_py_import(fname): def lint_relative_py_import(fname):
return ( return (
@ -536,6 +539,7 @@ def lint_relative_py_import(fname):
"esphome/components/esp32/core.cpp", "esphome/components/esp32/core.cpp",
"esphome/components/esp8266/core.cpp", "esphome/components/esp8266/core.cpp",
"esphome/components/rp2040/core.cpp", "esphome/components/rp2040/core.cpp",
"esphome/components/libretiny/core.cpp",
"esphome/components/host/core.cpp", "esphome/components/host/core.cpp",
], ],
) )

28
tests/test9.1.yaml Normal file
View file

@ -0,0 +1,28 @@
# Tests for rtl87xx boards using LibreTiny
---
wifi:
ssid: "ssid"
rtl87xx:
board: generic-rtl8710bn-2mb-788k
esphome:
name: rtl87xx-test
logger:
ota:
captive_portal:
binary_sensor:
- platform: gpio
name: Home Button
pin: GPIO11
sensor:
- platform: adc
id: adc_sensor
name: ADC
pin: PA19
update_interval: 1s

28
tests/test9.yaml Normal file
View file

@ -0,0 +1,28 @@
# Tests for bk7xx boards using LibreTiny
---
wifi:
ssid: "ssid"
bk72xx:
board: cb2s
esphome:
name: bk72xx-test
logger:
ota:
captive_portal:
binary_sensor:
- platform: gpio
name: Home Button
pin: GPIO24
sensor:
- platform: adc
id: adc_sensor
name: ADC
pin: GPIO23
update_interval: 1s

View file

@ -3,6 +3,9 @@
import esphome.wizard as wz import esphome.wizard as wz
import pytest import pytest
from esphome.components.esp8266.boards import ESP8266_BOARD_PINS from esphome.components.esp8266.boards import ESP8266_BOARD_PINS
from esphome.components.esp32.boards import ESP32_BOARD_PINS
from esphome.components.bk72xx.boards import BK72XX_BOARD_PINS
from esphome.components.rtl87xx.boards import RTL87XX_BOARD_PINS
from unittest.mock import MagicMock from unittest.mock import MagicMock
@ -140,11 +143,11 @@ def test_wizard_write_defaults_platform_from_board_esp32(
default_config, tmp_path, monkeypatch default_config, tmp_path, monkeypatch
): ):
""" """
If the platform is not explicitly set, use "ESP32" if the board is not one of the ESP8266 boards If the platform is not explicitly set, use "ESP32" if the board is one of the ESP32 boards
""" """
# Given # Given
del default_config["platform"] del default_config["platform"]
default_config["board"] = "foo" default_config["board"] = [*ESP32_BOARD_PINS][0]
monkeypatch.setattr(wz, "write_file", MagicMock()) monkeypatch.setattr(wz, "write_file", MagicMock())
@ -156,6 +159,46 @@ def test_wizard_write_defaults_platform_from_board_esp32(
assert "esp32:" in generated_config assert "esp32:" in generated_config
def test_wizard_write_defaults_platform_from_board_bk72xx(
default_config, tmp_path, monkeypatch
):
"""
If the platform is not explicitly set, use "BK72XX" if the board is one of BK72XX boards
"""
# Given
del default_config["platform"]
default_config["board"] = [*BK72XX_BOARD_PINS][0]
monkeypatch.setattr(wz, "write_file", MagicMock())
# When
wz.wizard_write(tmp_path, **default_config)
# Then
generated_config = wz.write_file.call_args.args[1]
assert "bk72xx:" in generated_config
def test_wizard_write_defaults_platform_from_board_rtl87xx(
default_config, tmp_path, monkeypatch
):
"""
If the platform is not explicitly set, use "RTL87XX" if the board is one of RTL87XX boards
"""
# Given
del default_config["platform"]
default_config["board"] = [*RTL87XX_BOARD_PINS][0]
monkeypatch.setattr(wz, "write_file", MagicMock())
# When
wz.wizard_write(tmp_path, **default_config)
# Then
generated_config = wz.write_file.call_args.args[1]
assert "rtl87xx:" in generated_config
def test_safe_print_step_prints_step_number_and_description(monkeypatch): def test_safe_print_step_prints_step_number_and_description(monkeypatch):
""" """
The safe_print_step function prints the step number and the passed description The safe_print_step function prints the step number and the passed description
@ -186,7 +229,7 @@ def test_default_input_uses_default_if_no_input_supplied(monkeypatch):
""" """
# Given # Given
monkeypatch.setattr("builtins.input", lambda _: "") monkeypatch.setattr("builtins.input", lambda _=None: "")
default_string = "foobar" default_string = "foobar"
# When # When
@ -203,7 +246,7 @@ def test_default_input_uses_user_supplied_value(monkeypatch):
# Given # Given
user_input = "A value" user_input = "A value"
monkeypatch.setattr("builtins.input", lambda _: user_input) monkeypatch.setattr("builtins.input", lambda _=None: user_input)
default_string = "foobar" default_string = "foobar"
# When # When