mirror of
https://github.com/esphome/esphome.git
synced 2026-07-02 21:33:16 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 648f5e1b06 | |||
| 65fc10d627 | |||
| 41cf842d5d | |||
| 792dfbcbbf | |||
| 06c7ac37d1 | |||
| 0666cb8635 |
@@ -501,6 +501,7 @@ esphome/components/ssd1331_base/* @kbx81
|
||||
esphome/components/ssd1331_spi/* @kbx81
|
||||
esphome/components/ssd1351_base/* @kbx81
|
||||
esphome/components/ssd1351_spi/* @kbx81
|
||||
esphome/components/st7123/* @miniskipper
|
||||
esphome/components/st7567_base/* @latonita
|
||||
esphome/components/st7567_i2c/* @latonita
|
||||
esphome/components/st7567_spi/* @latonita
|
||||
|
||||
@@ -21,9 +21,10 @@ export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms"
|
||||
export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages"
|
||||
export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache"
|
||||
|
||||
# Keep the native ESP-IDF install on the persistent cache root, not the
|
||||
# Keep the native toolchain installs on the persistent cache root, not the
|
||||
# container's ephemeral user cache dir (re-downloaded on every restart).
|
||||
export ESPHOME_ESP_IDF_PREFIX="$(dirname "${pio_cache_base}")/idf"
|
||||
export ESPHOME_SDK_NRF_PREFIX="$(dirname "${pio_cache_base}")/sdk-nrf"
|
||||
|
||||
# If /build is mounted, use that as the build path
|
||||
# otherwise use path in /config (so that builds aren't lost on container restart)
|
||||
|
||||
@@ -15,9 +15,10 @@ export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms"
|
||||
export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages"
|
||||
export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache"
|
||||
|
||||
# Keep the native ESP-IDF install on the persistent /data volume, not the
|
||||
# Keep the native toolchain installs on the persistent /data volume, not the
|
||||
# container's ephemeral user cache dir (wiped on every add-on update/restart).
|
||||
export ESPHOME_ESP_IDF_PREFIX=/data/cache/idf
|
||||
export ESPHOME_SDK_NRF_PREFIX=/data/cache/sdk-nrf
|
||||
|
||||
if bashio::config.true 'leave_front_door_open'; then
|
||||
export DISABLE_HA_AUTHENTICATION=true
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
#include "epaper_waveshare_bwr.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
enum class BwrState : uint8_t {
|
||||
BWR_BLACK,
|
||||
BWR_WHITE,
|
||||
BWR_RED,
|
||||
};
|
||||
|
||||
static BwrState color_to_bwr(Color color) {
|
||||
if (color.r > color.g + color.b && color.r > 127) {
|
||||
return BwrState::BWR_RED;
|
||||
}
|
||||
if (color.r + color.g + color.b >= 382) {
|
||||
return BwrState::BWR_WHITE;
|
||||
}
|
||||
return BwrState::BWR_BLACK;
|
||||
}
|
||||
|
||||
// UC8179 3-color display buffer layout:
|
||||
// - 1 bit per pixel, 8 pixels per byte
|
||||
// - Buffer first half: Black/White plane (1=black, 0=white)
|
||||
// - Buffer second half: Red plane (1=red, 0=white)
|
||||
// - Total: row_width * height * 2 bytes
|
||||
|
||||
void EPaperWaveshareBWR::draw_pixel_at(int x, int y, Color color) {
|
||||
if (!this->rotate_coordinates_(x, y))
|
||||
return;
|
||||
|
||||
const uint32_t pos = (x / 8) + (y * this->row_width_);
|
||||
const uint8_t bit = 0x80 >> (x & 0x07);
|
||||
const uint32_t red_offset = this->buffer_length_ / 2u;
|
||||
|
||||
const auto bwr = color_to_bwr(color);
|
||||
|
||||
if (bwr == BwrState::BWR_BLACK) {
|
||||
this->buffer_[pos] |= bit;
|
||||
} else {
|
||||
this->buffer_[pos] &= ~bit;
|
||||
}
|
||||
|
||||
if (bwr == BwrState::BWR_RED) {
|
||||
this->buffer_[red_offset + pos] |= bit;
|
||||
} else {
|
||||
this->buffer_[red_offset + pos] &= ~bit;
|
||||
}
|
||||
}
|
||||
|
||||
void EPaperWaveshareBWR::fill(Color color) {
|
||||
const size_t half_buffer = this->buffer_length_ / 2u;
|
||||
const auto bwr = color_to_bwr(color);
|
||||
|
||||
if (bwr == BwrState::BWR_BLACK) {
|
||||
// Black plane: 0xFF (black), Red plane: 0x00 (no red)
|
||||
for (size_t i = 0; i < half_buffer; i++)
|
||||
this->buffer_[i] = 0xFF;
|
||||
for (size_t i = 0; i < half_buffer; i++)
|
||||
this->buffer_[half_buffer + i] = 0x00;
|
||||
} else if (bwr == BwrState::BWR_RED) {
|
||||
// Black plane: 0x00 (no black), Red plane: 0xFF (red)
|
||||
for (size_t i = 0; i < half_buffer; i++)
|
||||
this->buffer_[i] = 0x00;
|
||||
for (size_t i = 0; i < half_buffer; i++)
|
||||
this->buffer_[half_buffer + i] = 0xFF;
|
||||
} else {
|
||||
// Black plane: 0x00 (no black), Red plane: 0x00 (no red)
|
||||
this->buffer_.fill(0x00);
|
||||
}
|
||||
}
|
||||
|
||||
bool HOT EPaperWaveshareBWR::transfer_data() {
|
||||
const uint32_t start_time = millis();
|
||||
const size_t buffer_length = this->buffer_length_;
|
||||
const size_t half_buffer = buffer_length / 2u;
|
||||
|
||||
uint8_t bytes_to_send[MAX_TRANSFER_SIZE];
|
||||
|
||||
// Phase 1: send Black/White plane (first half) via command 0x10 (DTM1)
|
||||
// UC8179 DTM1 (0x10): inverted to get 0=black, 1=white
|
||||
if (this->current_data_index_ < half_buffer) {
|
||||
if (this->current_data_index_ == 0) {
|
||||
this->command(0x10); // DATA START TRANSMISSION 1 (black channel)
|
||||
}
|
||||
this->start_data_();
|
||||
while (this->current_data_index_ < half_buffer) {
|
||||
const size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, half_buffer - this->current_data_index_);
|
||||
for (size_t i = 0; i < bytes_to_copy; i++) {
|
||||
bytes_to_send[i] = ~this->buffer_[this->current_data_index_ + i];
|
||||
}
|
||||
this->write_array(bytes_to_send, bytes_to_copy);
|
||||
this->current_data_index_ += bytes_to_copy;
|
||||
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
||||
this->disable();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this->disable();
|
||||
}
|
||||
|
||||
// Phase 2: send Red plane (second half) via command 0x13 (DTM2)
|
||||
// UC8179 DTM2 (0x13): 1=red, 0=white
|
||||
if (this->current_data_index_ < buffer_length) {
|
||||
if (this->current_data_index_ == half_buffer) {
|
||||
this->command(0x13); // DATA START TRANSMISSION 2 (red channel)
|
||||
}
|
||||
this->start_data_();
|
||||
while (this->current_data_index_ < buffer_length) {
|
||||
const size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, buffer_length - this->current_data_index_);
|
||||
for (size_t i = 0; i < bytes_to_copy; i++) {
|
||||
bytes_to_send[i] = this->buffer_[this->current_data_index_ + i];
|
||||
}
|
||||
this->write_array(bytes_to_send, bytes_to_copy);
|
||||
this->current_data_index_ += bytes_to_copy;
|
||||
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
||||
this->disable();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this->disable();
|
||||
}
|
||||
|
||||
this->current_data_index_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void EPaperWaveshareBWR::power_on() {
|
||||
this->cmd_data(0x01, {0x07, 0x17, 0x3F, 0x3F}); // POWER SETTING
|
||||
this->command(0x04); // POWER ON
|
||||
}
|
||||
|
||||
void EPaperWaveshareBWR::refresh_screen(bool /*partial*/) {
|
||||
this->command(0x12); // DISPLAY REFRESH
|
||||
}
|
||||
|
||||
void EPaperWaveshareBWR::power_off() {
|
||||
this->command(0x02); // POWER OFF
|
||||
}
|
||||
|
||||
void EPaperWaveshareBWR::deep_sleep() {
|
||||
this->cmd_data(0x07, {0xA5}); // DEEP SLEEP with check code
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "epaper_spi.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
/**
|
||||
* Waveshare 3-color e-paper displays (UC8179 controller).
|
||||
* Supports: 7.5" V2 BWR (EDP_7in5b_V2), 800x480 pixels.
|
||||
*
|
||||
* Color scheme: Black, White, Red (BWR)
|
||||
* Buffer layout: 1 bit per pixel, separate planes
|
||||
* - Buffer first half: Black/White plane (1=black, 0=white)
|
||||
* - Buffer second half: Red plane (1=red, 0=no red)
|
||||
* - Total buffer: width * height / 4 bytes (2 * width * height / 8)
|
||||
*
|
||||
* The init sequence (INITIALISE state) sends panel configuration only.
|
||||
* Power-on (0x01 + 0x04) is sent in the POWER_ON state after data transfer;
|
||||
* the state machine then busy-waits before triggering REFRESH_SCREEN (0x12).
|
||||
*/
|
||||
class EPaperWaveshareBWR : public EPaperBase {
|
||||
public:
|
||||
EPaperWaveshareBWR(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
|
||||
size_t init_sequence_length)
|
||||
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) {
|
||||
this->buffer_length_ = this->row_width_ * height * 2;
|
||||
}
|
||||
|
||||
void fill(Color color) override;
|
||||
|
||||
protected:
|
||||
void draw_pixel_at(int x, int y, Color color) override;
|
||||
bool transfer_data() override;
|
||||
void refresh_screen(bool partial) override;
|
||||
void power_on() override;
|
||||
void power_off() override;
|
||||
void deep_sleep() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Waveshare Black/White/Red e-paper displays using UC8179 controller.
|
||||
|
||||
Supported models:
|
||||
- waveshare-7.5in-bv2-bwr: 800x480 pixels (7.5" BWR display, EDP_7in5b_V2)
|
||||
|
||||
These displays use the UC8179 controller. Panel configuration is sent during
|
||||
the INITIALISE state. Power-on is handled in the POWER_ON state, after data
|
||||
transfer, so the state machine's built-in busy wait covers the power-on delay.
|
||||
"""
|
||||
|
||||
from . import EpaperModel
|
||||
|
||||
|
||||
class WaveshareBWR(EpaperModel):
|
||||
"""EpaperModel class for Waveshare Black/White/Red displays using UC8179 controller."""
|
||||
|
||||
def __init__(self, name, **defaults):
|
||||
super().__init__(name, "EPaperWaveshareBWR", **defaults)
|
||||
|
||||
def get_init_sequence(self, config):
|
||||
"""Generate initialization sequence for UC8179 BWR displays.
|
||||
|
||||
Panel configuration only — power-on is handled separately in power_on()
|
||||
after data transfer, with the state machine busy-waiting before refresh.
|
||||
"""
|
||||
width, height = self.get_dimensions(config)
|
||||
return (
|
||||
# PANEL SETTING (KWR mode)
|
||||
(0x00, 0x0F),
|
||||
# RESOLUTION SETTING (width x height)
|
||||
(
|
||||
0x61,
|
||||
(width >> 8) & 0xFF,
|
||||
width & 0xFF,
|
||||
(height >> 8) & 0xFF,
|
||||
height & 0xFF,
|
||||
),
|
||||
# DUAL SPI MODE (disabled)
|
||||
(0x15, 0x00),
|
||||
# VCOM AND DATA INTERVAL SETTING
|
||||
(0x50, 0x11, 0x07),
|
||||
# TCON SETTING
|
||||
(0x60, 0x22),
|
||||
# RESOLUTION GATE SETTING
|
||||
(0x65, 0x00, 0x00, 0x00, 0x00),
|
||||
)
|
||||
|
||||
|
||||
# Model: Waveshare 7.5" V2 BWR (EDP_7in5b_V2) — 800x480, UC8179 controller
|
||||
WaveshareBWR(
|
||||
"waveshare-7.5in-bv2-bwr",
|
||||
width=800,
|
||||
height=480,
|
||||
data_rate="10MHz",
|
||||
minimum_update_interval="30s",
|
||||
)
|
||||
@@ -24,7 +24,6 @@ from esphome.const import (
|
||||
CONF_IGNORE_EFUSE_CUSTOM_MAC,
|
||||
CONF_IGNORE_EFUSE_MAC_CRC,
|
||||
CONF_LOG_LEVEL,
|
||||
CONF_LOGGER,
|
||||
CONF_NAME,
|
||||
CONF_OTA,
|
||||
CONF_PATH,
|
||||
@@ -2215,21 +2214,6 @@ async def to_code(config):
|
||||
# This saves ~250 bytes of RAM (tag cache) and associated code
|
||||
add_idf_sdkconfig_option("CONFIG_LOG_TAG_LEVEL_IMPL_NONE", True)
|
||||
|
||||
# ESP-IDF Log V2 centralizes formatting inside esp_log(), removing the
|
||||
# per-site macro expansions of V1 (saves ~4KB flash, ~180B RAM). Only enable
|
||||
# on IDF >= 6.1, where CONFIG_LOG_API_CONSTRAINED_ENV_SAFE=n lets
|
||||
# ESP_DRAM_LOGx / ESP_EARLY_LOGx expand directly to esp_rom_printf and drops
|
||||
# the ~1.2KB esp_rom_vprintf that would otherwise sit in IRAM. Below 6.1 the
|
||||
# option does not exist, so we stay on V1 unchanged. Also requires the
|
||||
# logger component: the esp_log_format override in core/log.cpp integrates
|
||||
# V2 with ESPHome's logger hook, and without the logger there is no hook to
|
||||
# integrate with, so such builds stay on V1 as well.
|
||||
if idf_version() >= cv.Version(6, 1, 0) and CONF_LOGGER in CORE.config:
|
||||
add_idf_sdkconfig_option("CONFIG_LOG_VERSION_1", False)
|
||||
add_idf_sdkconfig_option("CONFIG_LOG_VERSION_2", True)
|
||||
add_idf_sdkconfig_option("CONFIG_LOG_API_CONSTRAINED_ENV_SAFE", False)
|
||||
cg.add_define("USE_ESP32_LOG_V2")
|
||||
|
||||
# Reduce PHY TX power in the event of a brownout
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True)
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ from esphome.framework_helpers import (
|
||||
get_project_link_flags,
|
||||
run_command_ok,
|
||||
)
|
||||
from esphome.helpers import write_file_if_changed
|
||||
from esphome.helpers import rmtree, write_file_if_changed
|
||||
from esphome.storage_json import StorageJSON
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -411,6 +411,17 @@ async def _dfu_to_code(dfu_config):
|
||||
def copy_files() -> None:
|
||||
"""Copy files to the build directory."""
|
||||
|
||||
# Library conversion to Zephyr modules is wired into the sdk-nrf
|
||||
# CMakeLists only; the PlatformIO toolchain's forked platform package
|
||||
# cannot compile external libraries at all, so the build would fail at
|
||||
# link time anyway. Fail fast with a clear message instead.
|
||||
if CORE.using_toolchain_platformio and CORE.platformio_libraries:
|
||||
raise EsphomeError(
|
||||
f"Libraries ({', '.join(sorted(CORE.platformio_libraries))}) are "
|
||||
"not supported on the nRF52 'platformio' toolchain; use toolchain "
|
||||
"'sdk-nrf' to build them as Zephyr modules."
|
||||
)
|
||||
|
||||
if CORE.using_toolchain_platformio and (
|
||||
zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT
|
||||
or zephyr_data()[KEY_BOARD] == "xiao_ble"
|
||||
@@ -697,15 +708,31 @@ def process_stacktrace(config: ConfigType, line: str, backtrace_state: bool) ->
|
||||
return False
|
||||
|
||||
|
||||
def _generate_cmake_lists() -> None:
|
||||
def _generate_cmake_lists() -> bool:
|
||||
"""Write the project CMakeLists.txt, returning True if it changed."""
|
||||
compile_flags = get_project_compile_flags()
|
||||
link_flags = get_project_link_flags()
|
||||
|
||||
# Convert any PlatformIO libraries added via cg.add_library() into Zephyr
|
||||
# modules and discover them through EXTRA_ZEPHYR_MODULES (a CMake list, set
|
||||
# before find_package(Zephyr) so the modules are picked up). Only
|
||||
# framework-agnostic libraries actually compile under Zephyr.
|
||||
from esphome.components.zephyr.library import generate_zephyr_modules
|
||||
|
||||
module_dirs = generate_zephyr_modules(list(CORE.platformio_libraries.values()))
|
||||
|
||||
lines = [
|
||||
"cmake_minimum_required(VERSION 3.20.0)",
|
||||
"",
|
||||
'set(Zephyr_DIR "$ENV{ZEPHYR_BASE}/share/zephyr-package/cmake/")',
|
||||
"",
|
||||
]
|
||||
|
||||
if module_dirs:
|
||||
modules = ";".join(str(d).replace("\\", "/") for d in module_dirs)
|
||||
lines += [f'set(EXTRA_ZEPHYR_MODULES "{modules}")', ""]
|
||||
|
||||
lines += [
|
||||
"find_package(Zephyr REQUIRED)",
|
||||
"",
|
||||
f"project({CORE.name})",
|
||||
@@ -732,7 +759,7 @@ def _generate_cmake_lists() -> None:
|
||||
")",
|
||||
]
|
||||
|
||||
write_file_if_changed(
|
||||
return write_file_if_changed(
|
||||
CORE.relative_build_path("zephyr", "CMakeLists.txt"),
|
||||
"\n".join(lines) + "\n",
|
||||
)
|
||||
@@ -751,12 +778,23 @@ def run_compile(args, config: ConfigType) -> bool:
|
||||
paths = get_build_paths()
|
||||
env = get_build_env()
|
||||
|
||||
_generate_cmake_lists()
|
||||
cmake_lists_changed = _generate_cmake_lists()
|
||||
|
||||
board = zephyr_data()[KEY_BOARD]
|
||||
build_dir = CORE.relative_pioenvs_path(CORE.name)
|
||||
source_dir = CORE.relative_build_path("zephyr")
|
||||
|
||||
# A missing CMake cache (dropped by zephyr's copy_files() on config
|
||||
# change) or a changed CMakeLists.txt requires a pristine build: Zephyr
|
||||
# caches Kconfig/devicetree state that survives a plain cmake re-run.
|
||||
# West can't do the wipe — its pristine modes only recognize a build dir
|
||||
# by reading ZEPHYR_BASE from the very cache that was dropped.
|
||||
if (
|
||||
cmake_lists_changed or not (build_dir / "CMakeCache.txt").is_file()
|
||||
) and build_dir.is_dir():
|
||||
_LOGGER.info("Build inputs changed, cleaning %s", build_dir)
|
||||
rmtree(build_dir)
|
||||
|
||||
west_cmd = [
|
||||
str(paths["python_executable"]),
|
||||
"-m",
|
||||
|
||||
@@ -4,6 +4,8 @@ from pathlib import Path
|
||||
import platform
|
||||
import tempfile
|
||||
|
||||
import platformdirs
|
||||
|
||||
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.framework_helpers import (
|
||||
@@ -15,6 +17,7 @@ from esphome.framework_helpers import (
|
||||
run_command_ok,
|
||||
str_to_lst_of_str,
|
||||
)
|
||||
from esphome.helpers import get_str_env
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -38,20 +41,28 @@ SDK_NG_MINIMAL_MIRRORS = str_to_lst_of_str(
|
||||
)
|
||||
|
||||
|
||||
def _get_tools_path() -> Path:
|
||||
return CORE.data_dir / "sdk-nrf"
|
||||
def get_sdk_nrf_tools_path() -> Path:
|
||||
# A blank ESPHOME_SDK_NRF_PREFIX must be treated as unset: Path("")
|
||||
# resolves to the CWD, which clean-all would then delete.
|
||||
if prefix := get_str_env("ESPHOME_SDK_NRF_PREFIX", "").strip():
|
||||
path = Path(prefix).expanduser()
|
||||
else:
|
||||
# Machine-global (OS user cache dir) so all projects share one install;
|
||||
# see espidf.framework.get_idf_tools_path for the location rationale.
|
||||
path = Path(platformdirs.user_cache_dir("esphome", appauthor=False)) / "sdk-nrf"
|
||||
return path.resolve()
|
||||
|
||||
|
||||
def _get_python_env_path(version: str) -> Path:
|
||||
return _get_tools_path() / "penvs" / version
|
||||
return get_sdk_nrf_tools_path() / "penvs" / version
|
||||
|
||||
|
||||
def _get_framework_path(version: str) -> Path:
|
||||
return _get_tools_path() / "frameworks" / version
|
||||
return get_sdk_nrf_tools_path() / "frameworks" / version
|
||||
|
||||
|
||||
def _get_toolchain_path(version: str) -> Path:
|
||||
return _get_tools_path() / "toolchains" / version
|
||||
return get_sdk_nrf_tools_path() / "toolchains" / version
|
||||
|
||||
|
||||
_SITECUSTOMIZE = """\
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
|
||||
CODEOWNERS = ["@miniskipper"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
st7123_ns = cg.esphome_ns.namespace("st7123")
|
||||
@@ -0,0 +1,32 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, touchscreen
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN
|
||||
|
||||
from .. import st7123_ns
|
||||
|
||||
ST7123Touchscreen = st7123_ns.class_(
|
||||
"ST7123Touchscreen",
|
||||
touchscreen.Touchscreen,
|
||||
i2c.I2CDevice,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ST7123Touchscreen),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
).extend(i2c.i2c_device_schema(0x55))
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await touchscreen.register_touchscreen(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
|
||||
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
|
||||
if reset_pin := config.get(CONF_RESET_PIN):
|
||||
cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin)))
|
||||
@@ -0,0 +1,108 @@
|
||||
#include "st7123_touchscreen.h"
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::st7123 {
|
||||
|
||||
static const char *const TAG = "st7123.touchscreen";
|
||||
|
||||
void ST7123Touchscreen::setup() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(false); // TP_RESX is active low, assert for at least tRSTW (2ms)
|
||||
delay(5);
|
||||
this->reset_pin_->digital_write(true);
|
||||
// The controller needs up to 20ms to initialize after reset before it can be accessed.
|
||||
this->setup_time_ = millis() + 30;
|
||||
}
|
||||
}
|
||||
|
||||
void ST7123Touchscreen::update() {
|
||||
// check if setup is complete
|
||||
if (this->setup_time_ != 0) {
|
||||
if (this->setup_time_ > millis())
|
||||
return;
|
||||
|
||||
uint8_t status;
|
||||
if (this->read_register16(ST7123_REG_STATUS, &status, 1) != i2c::ERROR_OK) {
|
||||
this->mark_failed(LOG_STR("Failed to read status register")); // will stop updates
|
||||
return;
|
||||
}
|
||||
if ((status & 0x0F) == ST7123_STATUS_INIT) {
|
||||
ESP_LOGD(TAG, "Controller still initializing");
|
||||
return;
|
||||
}
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
// INT is held high when idle and pulses low when touch data is ready.
|
||||
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Status is %X", status);
|
||||
|
||||
uint8_t data;
|
||||
if (this->read_register16(ST7123_REG_MAX_TOUCHES, &data, 1) == i2c::ERROR_OK && data != 0 &&
|
||||
data <= ST7123_MAX_TOUCHES) {
|
||||
this->max_touches_ = data;
|
||||
}
|
||||
|
||||
// If no calibration was supplied, read the native coordinate resolution from the controller.
|
||||
if (this->x_raw_max_ == this->x_raw_min_ || this->y_raw_max_ == this->y_raw_min_) {
|
||||
uint8_t res[4];
|
||||
if (this->read_register16(ST7123_REG_MAX_X, res, sizeof(res)) == i2c::ERROR_OK) {
|
||||
this->x_raw_max_ = encode_uint16(res[0] & ST7123_COORD_HIGH_MASK, res[1]);
|
||||
this->y_raw_max_ = encode_uint16(res[2] & ST7123_COORD_HIGH_MASK, res[3]);
|
||||
if (this->swap_x_y_)
|
||||
std::swap(this->x_raw_max_, this->y_raw_max_);
|
||||
} else {
|
||||
this->mark_failed(LOG_STR("Failed to read calibration"));
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Read dimensions %d/%d", this->x_raw_max_, this->y_raw_max_);
|
||||
}
|
||||
this->setup_time_ = 0; // flag setup complete
|
||||
}
|
||||
Touchscreen::update();
|
||||
}
|
||||
|
||||
void ST7123Touchscreen::update_touches() {
|
||||
// Read the reporting table from the advanced touch info register through the last touch point.
|
||||
// Reading from this register also clears the INT pin so the controller can report the next frame.
|
||||
uint8_t data[(ST7123_REG_TOUCH_DATA - ST7123_REG_ADV_TOUCH_INFO) + ST7123_MAX_TOUCHES * ST7123_TOUCH_STRIDE];
|
||||
const size_t len = (ST7123_REG_TOUCH_DATA - ST7123_REG_ADV_TOUCH_INFO) + this->max_touches_ * ST7123_TOUCH_STRIDE;
|
||||
if (this->read_register16(ST7123_REG_ADV_TOUCH_INFO, data, len) != i2c::ERROR_OK) {
|
||||
this->skip_update_ = true;
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
|
||||
const uint8_t *points = data + (ST7123_REG_TOUCH_DATA - ST7123_REG_ADV_TOUCH_INFO);
|
||||
for (uint8_t i = 0; i != this->max_touches_; i++) {
|
||||
const uint8_t *p = points + i * ST7123_TOUCH_STRIDE;
|
||||
if ((p[0] & ST7123_TOUCH_VALID) == 0)
|
||||
continue;
|
||||
uint16_t x = encode_uint16(p[0] & ST7123_COORD_HIGH_MASK, p[1]);
|
||||
uint16_t y = encode_uint16(p[2] & ST7123_COORD_HIGH_MASK, p[3]);
|
||||
uint8_t intensity = p[5];
|
||||
ESP_LOGV(TAG, "Touch %u: x=%u, y=%u, intensity=%u", i, x, y, intensity);
|
||||
this->add_raw_touch_position_(i, x, y, intensity);
|
||||
}
|
||||
}
|
||||
|
||||
void ST7123Touchscreen::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"ST7123 Touchscreen:\n"
|
||||
" Max touches: %u\n"
|
||||
" X Raw Min: %d, X Raw Max: %d\n"
|
||||
" Y Raw Min: %d, Y Raw Max: %d",
|
||||
this->max_touches_, this->x_raw_min_, this->x_raw_max_, this->y_raw_min_, this->y_raw_max_);
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
}
|
||||
|
||||
} // namespace esphome::st7123
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/touchscreen/touchscreen.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::st7123 {
|
||||
|
||||
// Sitronix ST7123 capacitive touch controller.
|
||||
// Registers are addressed with a 16-bit big-endian address (sent MSB first).
|
||||
static constexpr uint16_t ST7123_REG_STATUS = 0x0001; // [7:4] error code, [3:0] device status
|
||||
static constexpr uint16_t ST7123_REG_MAX_X = 0x0005; // 0x0005..0x0006 X resolution, 0x0007..0x0008 Y resolution
|
||||
static constexpr uint16_t ST7123_REG_MAX_TOUCHES = 0x0009;
|
||||
static constexpr uint16_t ST7123_REG_ADV_TOUCH_INFO = 0x0010; // start of the reporting table
|
||||
static constexpr uint16_t ST7123_REG_TOUCH_DATA = 0x0014; // first touch point
|
||||
|
||||
// Device status field of the status register.
|
||||
static constexpr uint8_t ST7123_STATUS_INIT = 0x1;
|
||||
|
||||
// Each touch point occupies 7 bytes: X high, X low, Y high, Y low, area, intensity, reserved.
|
||||
static constexpr uint8_t ST7123_TOUCH_STRIDE = 7;
|
||||
// Bit 7 of the X high byte indicates a valid touch point.
|
||||
static constexpr uint8_t ST7123_TOUCH_VALID = 0x80;
|
||||
// The X and Y high bytes only use the low 6 bits.
|
||||
static constexpr uint8_t ST7123_COORD_HIGH_MASK = 0x3F;
|
||||
// The ST7123 can report at most 10 touch points.
|
||||
static constexpr uint8_t ST7123_MAX_TOUCHES = 10;
|
||||
|
||||
class ST7123Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
void update_touches() override;
|
||||
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
GPIOPin *reset_pin_{nullptr};
|
||||
uint8_t max_touches_{ST7123_MAX_TOUCHES};
|
||||
uint32_t setup_time_{1};
|
||||
};
|
||||
|
||||
} // namespace esphome::st7123
|
||||
@@ -46,42 +46,59 @@ DEFAULT_BAUD_RATE = 9600
|
||||
|
||||
|
||||
class Type:
|
||||
def __init__(self, name, vid, pid, cls, max_channels=1, baud_rate_required=True):
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
vid,
|
||||
pid,
|
||||
cls,
|
||||
max_channels=1,
|
||||
baud_rate_required=True,
|
||||
max_baud=1_000_000,
|
||||
):
|
||||
self.name = name
|
||||
cls = cls or name
|
||||
self.vid = vid
|
||||
self.pid = pid
|
||||
self.cls = usb_uart_ns.class_(f"USBUartType{cls}", USBUartComponent)
|
||||
self.max_channels = max_channels
|
||||
self._max_channels = max_channels
|
||||
self.baud_rate_required = baud_rate_required
|
||||
self.max_baud = max_baud
|
||||
|
||||
@property
|
||||
def max_channels(self) -> int:
|
||||
return (
|
||||
3
|
||||
if (
|
||||
CORE.is_esp32
|
||||
and get_esp32_variant() != VARIANT_ESP32P4
|
||||
and self._max_channels > 3
|
||||
)
|
||||
else self._max_channels
|
||||
)
|
||||
|
||||
|
||||
uart_types = (
|
||||
Type("CDC_ACM", 0, 0, "CdcAcm", 1, baud_rate_required=False),
|
||||
Type("CH34X", 0x1A86, 0x55D5, "CH34X", 4),
|
||||
Type("CH340", 0x1A86, 0x7523, "CH34X", 1),
|
||||
Type("CP210X", 0x10C4, 0xEA60, "CP210X", 3),
|
||||
Type("CH34X", 0x1A86, 0x55D5, "CH34X", 4, max_baud=2_000_000),
|
||||
Type("CH340", 0x1A86, 0x7523, "CH34X", 1, max_baud=2_000_000),
|
||||
Type("CP210X", 0x10C4, 0xEA60, "CP210X", 3, max_baud=2_000_000),
|
||||
Type("ESP_JTAG", 0x303A, 0x1001, "CdcAcm", 1, baud_rate_required=False),
|
||||
Type("FT232", 0x0403, 0x6001, "FT23XX", 1),
|
||||
Type("FT2232", 0x0403, 0x6010, "FT23XX", 2),
|
||||
Type("FT4232", 0x0403, 0x6011, "FT23XX", 4),
|
||||
Type("PL2303", 0x067B, 0x2303, "PL2303", 1),
|
||||
Type("PL2303GB", 0x067B, 0x23B3, "PL2303", 1),
|
||||
Type("PL2303GC", 0x067B, 0x23A3, "PL2303", 1),
|
||||
Type("PL2303GE", 0x067B, 0x23E3, "PL2303", 1),
|
||||
Type("PL2303GL", 0x067B, 0x23D3, "PL2303", 1),
|
||||
Type("PL2303GS", 0x067B, 0x23F3, "PL2303", 1),
|
||||
Type("PL2303GT", 0x067B, 0x23C3, "PL2303", 1),
|
||||
Type("FT232", 0x0403, 0x6001, "FT23XX", 1, max_baud=3_000_000),
|
||||
Type("FT2232", 0x0403, 0x6010, "FT23XX", 2, max_baud=12_000_000),
|
||||
Type("FT4232", 0x0403, 0x6011, "FT23XX", 4, max_baud=12_000_000),
|
||||
Type("PL2303", 0x067B, 0x2303, "PL2303", 1, max_baud=6_000_000),
|
||||
Type("PL2303GB", 0x067B, 0x23B3, "PL2303", 1, max_baud=6_000_000),
|
||||
Type("PL2303GC", 0x067B, 0x23A3, "PL2303", 1, max_baud=6_000_000),
|
||||
Type("PL2303GE", 0x067B, 0x23E3, "PL2303", 1, max_baud=6_000_000),
|
||||
Type("PL2303GL", 0x067B, 0x23D3, "PL2303", 1, max_baud=6_000_000),
|
||||
Type("PL2303GS", 0x067B, 0x23F3, "PL2303", 1, max_baud=6_000_000),
|
||||
Type("PL2303GT", 0x067B, 0x23C3, "PL2303", 1, max_baud=6_000_000),
|
||||
Type("STM32_VCP", 0x0483, 0x5740, "CdcAcm", 1, baud_rate_required=False),
|
||||
)
|
||||
|
||||
|
||||
def channel_schema(channels, baud_rate_required):
|
||||
# For now S3 is restricted to 3 channels since each needs 2 endpoints, plus the control endpoint, and
|
||||
# there are only a total of 8 endpoints available.
|
||||
# This will need updating when the 8 channel devices that multiplex over an endpoint are added.
|
||||
if CORE.is_esp32 and get_esp32_variant() != VARIANT_ESP32P4 and channels > 3:
|
||||
channels = 3
|
||||
def channel_schema(type_: "Type") -> cv.Schema:
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_CHANNELS): cv.All(
|
||||
@@ -94,11 +111,11 @@ def channel_schema(channels, baud_rate_required):
|
||||
),
|
||||
(
|
||||
cv.Required(CONF_BAUD_RATE)
|
||||
if baud_rate_required
|
||||
if type_.baud_rate_required
|
||||
else cv.Optional(
|
||||
CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE
|
||||
)
|
||||
): cv.int_range(min=300, max=1000000),
|
||||
): cv.int_range(min=300, max=type_.max_baud),
|
||||
cv.Optional(CONF_STOP_BITS, default="1"): cv.enum(
|
||||
UART_STOP_BITS_OPTIONS, upper=True
|
||||
),
|
||||
@@ -117,7 +134,10 @@ def channel_schema(channels, baud_rate_required):
|
||||
}
|
||||
)
|
||||
),
|
||||
cv.Length(max=channels),
|
||||
cv.Length(
|
||||
max=type_.max_channels,
|
||||
msg=f"Device type {type_.name} supports a maximum of {type_.max_channels} channels",
|
||||
),
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -127,7 +147,7 @@ CONFIG_SCHEMA = cv.ensure_list(
|
||||
cv.typed_schema(
|
||||
{
|
||||
it.name: usb_device_schema(it.cls, it.vid, it.pid).extend(
|
||||
channel_schema(it.max_channels, it.baud_rate_required)
|
||||
channel_schema(it)
|
||||
)
|
||||
for it in uart_types
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ from esphome.const import CONF_BOARD, KEY_CORE, KEY_FRAMEWORK_VERSION
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.helpers import copy_file_if_changed, write_file_if_changed
|
||||
from esphome.types import ConfigType
|
||||
from esphome.writer import clean_cmake_cache
|
||||
|
||||
from .const import (
|
||||
CONF_CDC_ACM,
|
||||
@@ -203,7 +204,20 @@ def zephyr_add_user(key, value):
|
||||
user[key] += [value]
|
||||
|
||||
|
||||
def copy_files():
|
||||
def _write_file_if_changed_or_remove_when_empty(path: Path, content: str) -> bool:
|
||||
"""Write content to path, or remove a stale file when content is empty.
|
||||
|
||||
Returns True if the file changed on disk.
|
||||
"""
|
||||
if content:
|
||||
return write_file_if_changed(path, content)
|
||||
if path.is_file():
|
||||
path.unlink()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def copy_files() -> None:
|
||||
user = zephyr_data()[KEY_USER]
|
||||
if user:
|
||||
entries = " ".join(
|
||||
@@ -219,6 +233,8 @@ def copy_files():
|
||||
"""
|
||||
)
|
||||
|
||||
changed = False
|
||||
|
||||
for image, want_opts in zephyr_data()[KEY_PRJ_CONF].items():
|
||||
prj_conf = (
|
||||
"\n".join(
|
||||
@@ -233,26 +249,25 @@ def copy_files():
|
||||
else:
|
||||
path = CORE.relative_build_path("zephyr/prj.conf")
|
||||
|
||||
write_file_if_changed(CORE.relative_build_path(path), prj_conf)
|
||||
changed |= write_file_if_changed(path, prj_conf)
|
||||
|
||||
for image, content in zephyr_data()[KEY_OVERLAY].items():
|
||||
if image:
|
||||
path = CORE.relative_build_path(f"sysbuild/{image}.overlay")
|
||||
else:
|
||||
path = CORE.relative_build_path("zephyr/app.overlay")
|
||||
write_file_if_changed(path, content)
|
||||
changed |= write_file_if_changed(path, content)
|
||||
|
||||
for filename, path in zephyr_data()[KEY_EXTRA_BUILD_FILES].items():
|
||||
copy_file_if_changed(
|
||||
changed |= copy_file_if_changed(
|
||||
path,
|
||||
CORE.relative_build_path(filename),
|
||||
)
|
||||
|
||||
pm_static = "\n".join(str(item) for item in zephyr_data()[KEY_PM_STATIC])
|
||||
if pm_static:
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("zephyr/pm_static.yml"), pm_static
|
||||
)
|
||||
changed |= _write_file_if_changed_or_remove_when_empty(
|
||||
CORE.relative_build_path("zephyr/pm_static.yml"), pm_static
|
||||
)
|
||||
|
||||
kconfig = zephyr_data()[KEY_KCONFIG]
|
||||
if kconfig:
|
||||
@@ -267,4 +282,12 @@ def copy_files():
|
||||
+ "\n"
|
||||
+ kconfig
|
||||
)
|
||||
write_file_if_changed(CORE.relative_build_path("zephyr/Kconfig"), kconfig)
|
||||
changed |= _write_file_if_changed_or_remove_when_empty(
|
||||
CORE.relative_build_path("zephyr/Kconfig"), kconfig
|
||||
)
|
||||
|
||||
if changed:
|
||||
# A configure-time input changed; drop the CMake cache so the build
|
||||
# can't reuse stale configure results (the native sdk-nrf toolchain
|
||||
# rebuilds pristine when the cache is missing).
|
||||
clean_cmake_cache()
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
"""Zephyr backend for the shared PlatformIO library converter.
|
||||
|
||||
For each PlatformIO library added via ``cg.add_library()``, emit a Zephyr
|
||||
external module (``zephyr/module.yml`` + ``zephyr/CMakeLists.txt`` built with the
|
||||
``zephyr_library*`` API) into the shared ``pio_components`` cache. The caller
|
||||
wires the resulting module directories into the build via
|
||||
``EXTRA_ZEPHYR_MODULES``; Zephyr then compiles each module and links it into the
|
||||
final image.
|
||||
|
||||
Only framework-agnostic libraries (plain C/C++ that doesn't depend on the Arduino
|
||||
API) will actually compile under Zephyr — this converter shares the
|
||||
fetch/parse/cache plumbing, not API compatibility.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from esphome import yaml_util
|
||||
from esphome.core import EsphomeError, Library
|
||||
from esphome.helpers import write_file_if_changed
|
||||
from esphome.platformio.library import (
|
||||
DEFAULT_BUILD_FLAGS,
|
||||
DEFAULT_BUILD_INCLUDE_DIR,
|
||||
DEFAULT_BUILD_SRC_FILTER,
|
||||
SRC_FILE_EXTENSIONS,
|
||||
ConvertedLibrary,
|
||||
LibraryBackend,
|
||||
PathType,
|
||||
collect_filtered_files,
|
||||
convert_libraries,
|
||||
ensure_list,
|
||||
split_list_by_condition,
|
||||
)
|
||||
|
||||
# Zephyr libraries declare frameworks rarely and the PIO ``platforms`` token for
|
||||
# nRF is seldom present, so the platform check is disabled (None) and only the
|
||||
# framework mismatch warning fires.
|
||||
ZEPHYR_FRAMEWORK = "zephyr"
|
||||
|
||||
|
||||
def _escape(p: PathType) -> str:
|
||||
# In CMakeLists.txt, backslashes need to be escaped (mirrors the ESP-IDF
|
||||
# backend's escape_entry). Doubling -- rather than rewriting '\' -> '/' --
|
||||
# preserves content, so it's safe for arbitrary build flags (e.g. a -D value
|
||||
# containing a backslash) as well as Windows paths.
|
||||
return f'"{str(p)}"'.replace("\\", "\\\\")
|
||||
|
||||
|
||||
def generate_module_yml(component: ConvertedLibrary) -> str:
|
||||
"""Render the ``zephyr/module.yml`` manifest for a converted library."""
|
||||
return yaml_util.dump(
|
||||
{
|
||||
"name": component.get_require_name(),
|
||||
"build": {"cmake": "zephyr"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def generate_cmakelists_txt(component: ConvertedLibrary) -> str:
|
||||
"""Render the ``zephyr/CMakeLists.txt`` that builds a converted library.
|
||||
|
||||
Sources/includes are emitted as absolute paths since the CMakeLists lives in
|
||||
the library's ``zephyr/`` subdir while its sources sit alongside it. Include
|
||||
dirs are published globally so the app (and sibling libraries) can include the
|
||||
library's headers, mirroring ESP-IDF's public ``INCLUDE_DIRS``.
|
||||
"""
|
||||
build = component.data.get("build", {})
|
||||
|
||||
build_src_dir = build.get("srcDir")
|
||||
if not build_src_dir:
|
||||
for d in ["src", "Src", "."]:
|
||||
if (component.path / Path(d)).is_dir():
|
||||
build_src_dir = d
|
||||
break
|
||||
|
||||
build_include_dir = build.get("includeDir", DEFAULT_BUILD_INCLUDE_DIR)
|
||||
build_src_filter = ensure_list(build.get("srcFilter", DEFAULT_BUILD_SRC_FILTER))
|
||||
build_flags = ensure_list(build.get("flags", DEFAULT_BUILD_FLAGS))
|
||||
|
||||
src_files = collect_filtered_files(
|
||||
component.path / Path(build_src_dir), build_src_filter
|
||||
)
|
||||
src_files = sorted(
|
||||
str(Path(p).resolve())
|
||||
for p in src_files
|
||||
if Path(p).suffix in SRC_FILE_EXTENSIONS
|
||||
)
|
||||
|
||||
include_dir_flags, build_flags = split_list_by_condition(
|
||||
build_flags, lambda a: a[2:].strip() if a.startswith("-I") else None
|
||||
)
|
||||
link_directories, build_flags = split_list_by_condition(
|
||||
build_flags, lambda a: a[2:].strip() if a.startswith("-L") else None
|
||||
)
|
||||
link_libraries, build_flags = split_list_by_condition(
|
||||
build_flags, lambda a: a[2:].strip() if a.startswith("-l") else None
|
||||
)
|
||||
|
||||
include_dirs = [build_include_dir, build_src_dir, *include_dir_flags]
|
||||
include_dirs = [
|
||||
str((component.path / Path(d)).resolve())
|
||||
for d in include_dirs
|
||||
if (component.path / Path(d)).is_dir()
|
||||
]
|
||||
|
||||
lines = [f"zephyr_library_named({component.get_require_name()})"]
|
||||
if src_files:
|
||||
lines += [
|
||||
"zephyr_library_sources(",
|
||||
*[f" {_escape(p)}" for p in src_files],
|
||||
")",
|
||||
]
|
||||
if include_dirs:
|
||||
lines += [
|
||||
"zephyr_include_directories(",
|
||||
*[f" {_escape(p)}" for p in include_dirs],
|
||||
")",
|
||||
]
|
||||
if build_flags:
|
||||
lines += [
|
||||
"zephyr_library_compile_options(",
|
||||
*[f" {_escape(f)}" for f in build_flags],
|
||||
")",
|
||||
]
|
||||
# Best-effort link wiring; most Zephyr-portable libraries don't need it.
|
||||
link_flags = [f"-L{d}" for d in link_directories] + [
|
||||
f"-l{lib}" for lib in link_libraries
|
||||
]
|
||||
if link_flags:
|
||||
lines += [
|
||||
"zephyr_link_libraries(",
|
||||
*[f" {_escape(f)}" for f in link_flags],
|
||||
")",
|
||||
]
|
||||
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _emit_zephyr_module(component: ConvertedLibrary) -> None:
|
||||
zephyr_dir = component.path / "zephyr"
|
||||
write_file_if_changed(zephyr_dir / "module.yml", generate_module_yml(component))
|
||||
write_file_if_changed(
|
||||
zephyr_dir / "CMakeLists.txt", generate_cmakelists_txt(component)
|
||||
)
|
||||
|
||||
|
||||
def generate_zephyr_modules(libraries: list[Library]) -> list[Path]:
|
||||
"""Convert ``libraries`` to Zephyr modules and return all module directories.
|
||||
|
||||
The returned list includes transitive dependencies (each converted library is
|
||||
its own module). Every directory should be added to ``EXTRA_ZEPHYR_MODULES``;
|
||||
Zephyr links all module libraries into the image, so cross-library symbols
|
||||
resolve without explicit dependency declarations.
|
||||
|
||||
Raises ``EsphomeError`` if two libraries resolve to the same Zephyr module
|
||||
name -- each module's CMakeLists calls ``zephyr_library_named(<name>)``, so a
|
||||
duplicate would otherwise fail the build with a CMake "target already exists".
|
||||
The converter already warns when a library is referenced under inconsistent
|
||||
specs (bare ``name`` vs ``owner/name``, git vs registry); this turns that into
|
||||
an actionable error at the Zephyr boundary where it is fatal.
|
||||
"""
|
||||
module_dirs: list[Path] = []
|
||||
by_name: dict[str, Path] = {}
|
||||
|
||||
def emit(component: ConvertedLibrary) -> None:
|
||||
name = component.get_require_name()
|
||||
if name in by_name:
|
||||
raise EsphomeError(
|
||||
f"Two libraries resolve to the same Zephyr module '{name}' "
|
||||
f"({by_name[name]} and {component.path}). Reference the library "
|
||||
f"consistently (e.g. always as 'owner/name') so it resolves once."
|
||||
)
|
||||
by_name[name] = component.path
|
||||
_emit_zephyr_module(component)
|
||||
module_dirs.append(component.path)
|
||||
|
||||
backend = LibraryBackend(
|
||||
platform=None, framework=ZEPHYR_FRAMEWORK, emit=emit, cache_key="zephyr"
|
||||
)
|
||||
convert_libraries(libraries, backend)
|
||||
return module_dirs
|
||||
@@ -246,7 +246,6 @@
|
||||
#define BLUETOOTH_PROXY_MAX_CONNECTIONS 3
|
||||
#define BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE 16
|
||||
#define USE_CAPTIVE_PORTAL
|
||||
#define USE_ESP32_LOG_V2
|
||||
#define USE_ESP32_BLE
|
||||
#define USE_ESP32_BLE_MAX_CONNECTIONS 3
|
||||
#define USE_ESP32_BLE_CLIENT
|
||||
|
||||
@@ -86,109 +86,3 @@ int HOT esp_idf_log_vprintf_(const char *format, va_list args) { // NOLINT
|
||||
#endif
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
// Only compiled when the logger component is present: the override exists to
|
||||
// integrate V2 with ESPHome's logger hook. Without the logger no hook is ever
|
||||
// installed, so liblog's stock esp_log_format links in and console output
|
||||
// stays bone-stock ESP-IDF (V2's flash savings still apply).
|
||||
#if defined(USE_ESP32_LOG_V2) && defined(USE_LOGGER) && !defined(BOOTLOADER_BUILD)
|
||||
// Override esp_log_format to prevent V2's 3-call vprintf fragmentation.
|
||||
// Without this, Log V2 calls the vprintf hook 3 times per message (header,
|
||||
// body, newline) which creates 3 separate log entries in ESPHome's logger.
|
||||
// This strong definition overrides the archive symbol from ESP-IDF's liblog,
|
||||
// affecting all callers including precompiled blobs (e.g. wifi).
|
||||
#include <esp_private/log_message.h>
|
||||
#include <esp_log_write.h>
|
||||
|
||||
// Format an ESP-IDF log message directly to the console, bypassing the
|
||||
// ESPHome logger hook. Used when the hook isn't installed (early boot) or
|
||||
// can't be used safely (constrained env: PHY init, efuse reads -- fwrite
|
||||
// locks crash on USB JTAG devices).
|
||||
// Formats in ESPHome style with ANSI colors into a 512-byte stack buffer,
|
||||
// then outputs atomically via esp_rom_printf.
|
||||
// This path is cold on 99.9% of builds -- it only runs during early boot
|
||||
// and at DEBUG framework log level (default is ERROR).
|
||||
static void __attribute__((noinline)) esp_log_format_direct_(esp_log_msg_t *message) { // NOLINT
|
||||
// ESP-IDF levels: NONE=0 ERROR=1 WARN=2 INFO=3 DEBUG=4 VERBOSE=5
|
||||
// Color digits: E=1(red) W=3(yellow) I=2(green) D=6(cyan) V=7(gray)
|
||||
static const char COLOR_DIGIT[] = {'\0', '1', '3', '2', '6', '7'};
|
||||
static const char LVL[] = {'\0', 'E', 'W', 'I', 'D', 'V'};
|
||||
// Format into stack buffer and output atomically via esp_rom_printf.
|
||||
// Can't use fwrite (locks crash during early boot and PHY init).
|
||||
char buf[512];
|
||||
int pos = 0;
|
||||
uint8_t level = message->config.opts.log_level;
|
||||
if (level > 0 && level < sizeof(LVL)) {
|
||||
pos = snprintf(buf, sizeof(buf), "\033[0;3%cm[%c][%s]: ", COLOR_DIGIT[level], LVL[level],
|
||||
message->tag ? message->tag : "idf");
|
||||
// Clamp: snprintf returns the untruncated length (or negative on error),
|
||||
// so an oversized tag or an encoding error would otherwise leave pos
|
||||
// outside the buffer and break every bound check below.
|
||||
if (pos < 0) {
|
||||
pos = 0;
|
||||
} else if (pos >= (int) sizeof(buf)) {
|
||||
pos = sizeof(buf) - 1;
|
||||
}
|
||||
}
|
||||
if (pos < (int) sizeof(buf) - 2) {
|
||||
int body = vsnprintf(buf + pos, sizeof(buf) - pos, message->format, message->args);
|
||||
if (body > 0)
|
||||
pos += (body < (int) sizeof(buf) - pos) ? body : (int) sizeof(buf) - pos - 1;
|
||||
}
|
||||
if (level > 0 && level < sizeof(LVL) && pos < (int) sizeof(buf) - 6) {
|
||||
pos += snprintf(buf + pos, sizeof(buf) - pos, "\033[0m");
|
||||
}
|
||||
if (pos < (int) sizeof(buf) - 1) {
|
||||
buf[pos++] = '\n';
|
||||
}
|
||||
buf[pos] = '\0';
|
||||
esp_rom_printf("%s", buf);
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
// Override esp_log_format from liblog.a to prevent V2's 3-call vprintf
|
||||
// fragmentation. Deliberately NOT IRAM_ATTR: every caller that must work with
|
||||
// flash cache disabled (ESP_DRAM_LOGx, ESP_EARLY_LOGx) bypasses esp_log()
|
||||
// entirely under CONFIG_LOG_API_CONSTRAINED_ENV_SAFE=n, and both paths below
|
||||
// immediately call flash-resident code anyway, so IRAM placement would only
|
||||
// spend the IRAM this change exists to save.
|
||||
void esp_log_format(esp_log_msg_t *message) {
|
||||
extern vprintf_like_t esp_log_vprint_func;
|
||||
extern int vprintf(const char *, __gnuc_va_list); // NOLINT
|
||||
if (esp_log_vprint_func == &vprintf || message->config.opts.constrained_env) [[unlikely]] {
|
||||
// Early boot or constrained env (PHY init, efuse reads, scheduler not
|
||||
// running). Can't use the ESPHome hook -- fwrite locks crash during PHY
|
||||
// init on USB JTAG devices and newlib isn't initialized during early boot.
|
||||
// Format to stack buffer with vsnprintf + esp_rom_printf instead.
|
||||
//
|
||||
// Note: if called from an ISR with flash cache disabled, this will crash
|
||||
// because the format string and tag are in flash. This is the same as V1
|
||||
// where ESP_EARLY_LOGx from ISR also used flash-resident format strings
|
||||
// via esp_rom_printf. No ESP-IDF code is known to log from ISR with
|
||||
// cache disabled.
|
||||
esp_log_format_direct_(message);
|
||||
return;
|
||||
}
|
||||
// After hook installed, normal environment. Never call the esp_log_vprintf
|
||||
// inline here: it would pull in esp_rom_vprintf (1.2KB IRAM).
|
||||
if (esp_log_vprint_func == &esphome::esp_idf_log_vprintf_ && esphome::logger::global_logger != nullptr) {
|
||||
// The hook is ESPHome's own (the common case). V2 keeps the component tag
|
||||
// (wifi, phy_init, ...) and per-message severity separate from the format
|
||||
// string, so call the logger directly with both: lines render with the
|
||||
// real tag and level (e.g. "[E][wifi]: ...") including color and per-tag
|
||||
// filtering, details the (format, args) hook signature cannot carry.
|
||||
// IDF levels: NONE=0 E=1 W=2 I=3 D=4 V=5; ESPHome inserts CONFIG at 4.
|
||||
static const uint8_t LEVEL_MAP[] = {ESPHOME_LOG_LEVEL_NONE, ESPHOME_LOG_LEVEL_ERROR, ESPHOME_LOG_LEVEL_WARN,
|
||||
ESPHOME_LOG_LEVEL_INFO, ESPHOME_LOG_LEVEL_DEBUG, ESPHOME_LOG_LEVEL_VERBOSE};
|
||||
uint8_t idf_level = message->config.opts.log_level;
|
||||
uint8_t level = idf_level < sizeof(LEVEL_MAP) ? LEVEL_MAP[idf_level] : ESPHOME_LOG_LEVEL_VERBOSE;
|
||||
esphome::logger::global_logger->log_vprintf_(level, message->tag ? message->tag : "esp-idf", 0, message->format,
|
||||
message->args);
|
||||
return;
|
||||
}
|
||||
// A custom hook was installed via esp_log_set_vprintf: honor it and forward
|
||||
// the message body as-is.
|
||||
esp_log_vprint_func(message->format, message->args);
|
||||
}
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
||||
@@ -264,5 +264,6 @@ def generate_idf_components(libraries: list[Library]) -> list[IDFComponent]:
|
||||
platform=ESP32_PLATFORM,
|
||||
framework=_idf_framework(),
|
||||
emit=_emit_idf_component,
|
||||
cache_key="idf",
|
||||
)
|
||||
return convert_libraries(libraries, backend)
|
||||
|
||||
@@ -75,7 +75,7 @@ ESP_IDF_CONSTRAINTS_MIRRORS = str_to_lst_of_str(
|
||||
)
|
||||
|
||||
|
||||
def _get_idf_tools_path() -> Path:
|
||||
def get_idf_tools_path() -> Path:
|
||||
"""
|
||||
Get the path to the ESP-IDF tools directory.
|
||||
|
||||
@@ -141,7 +141,7 @@ def _check_windows_path_length() -> None:
|
||||
"""
|
||||
if platform.system() != "Windows" or _windows_long_paths_enabled():
|
||||
return
|
||||
tools_path = str(_get_idf_tools_path())
|
||||
tools_path = str(get_idf_tools_path())
|
||||
projected = len(tools_path) + _TOOLCHAIN_NESTED_PATH_LEN
|
||||
if projected <= _WINDOWS_MAX_PATH:
|
||||
return
|
||||
@@ -180,7 +180,7 @@ def _get_framework_path(version: str) -> Path:
|
||||
Returns:
|
||||
Path object pointing to the framework directory
|
||||
"""
|
||||
return _get_idf_tools_path() / "frameworks" / f"{version}"
|
||||
return get_idf_tools_path() / "frameworks" / f"{version}"
|
||||
|
||||
|
||||
def _get_python_env_path(version: str) -> Path:
|
||||
@@ -193,7 +193,7 @@ def _get_python_env_path(version: str) -> Path:
|
||||
Returns:
|
||||
Path object pointing to the Python environment directory
|
||||
"""
|
||||
return _get_idf_tools_path() / "penvs" / f"{version}"
|
||||
return get_idf_tools_path() / "penvs" / f"{version}"
|
||||
|
||||
|
||||
def _check_stamp(file: PathType, data: dict[str, str]) -> bool:
|
||||
@@ -707,7 +707,7 @@ def _check_esp_idf_python_env_install(
|
||||
|
||||
esp_idf_version = _get_idf_version(framework_path, env=env)
|
||||
constraint_file_path = (
|
||||
_get_idf_tools_path() / f"espidf.constraints.v{esp_idf_version}.txt"
|
||||
get_idf_tools_path() / f"espidf.constraints.v{esp_idf_version}.txt"
|
||||
)
|
||||
_LOGGER.debug("ESP-IDF version %s", esp_idf_version)
|
||||
|
||||
@@ -798,7 +798,7 @@ def check_esp_idf_install(
|
||||
_check_windows_path_length()
|
||||
|
||||
env = {}
|
||||
env["IDF_TOOLS_PATH"] = str(_get_idf_tools_path())
|
||||
env["IDF_TOOLS_PATH"] = str(get_idf_tools_path())
|
||||
env["IDF_PATH"] = ""
|
||||
|
||||
targets = targets or ESPHOME_IDF_DEFAULT_TARGETS
|
||||
@@ -867,7 +867,7 @@ def _ccache_env() -> dict[str, str]:
|
||||
|
||||
defaults = {
|
||||
"IDF_CCACHE_ENABLE": "1",
|
||||
"CCACHE_DIR": str(_get_idf_tools_path() / "ccache"),
|
||||
"CCACHE_DIR": str(get_idf_tools_path() / "ccache"),
|
||||
"CCACHE_NOHASHDIR": "true",
|
||||
"CCACHE_DEPEND": "1",
|
||||
"CCACHE_BASEDIR": str(Path(CORE.build_path).resolve()),
|
||||
@@ -894,7 +894,7 @@ def get_framework_env(
|
||||
"""
|
||||
# 1. Initialize base environment with extra ESP-IDF environment variables
|
||||
env = env.copy() if env else {}
|
||||
env["IDF_TOOLS_PATH"] = str(_get_idf_tools_path())
|
||||
env["IDF_TOOLS_PATH"] = str(get_idf_tools_path())
|
||||
env["IDF_PATH"] = ""
|
||||
|
||||
# 2. Get existing PATH from env or os.environ
|
||||
|
||||
@@ -68,7 +68,9 @@ ESPHOME_DATA_EXTRA_CMAKE_KEY = "EXTRA_CMAKE"
|
||||
|
||||
|
||||
class Source:
|
||||
def download(self, dir_suffix: str, force: bool = False, salt: str = "") -> Path:
|
||||
def download(
|
||||
self, dir_suffix: str, force: bool = False, salt: str = "", namespace: str = ""
|
||||
) -> Path:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -76,8 +78,14 @@ class URLSource(Source):
|
||||
def __init__(self, url: str):
|
||||
self.url = url
|
||||
|
||||
def download(self, dir_suffix: str, force: bool = False, salt: str = "") -> Path:
|
||||
def download(
|
||||
self, dir_suffix: str, force: bool = False, salt: str = "", namespace: str = ""
|
||||
) -> Path:
|
||||
# Namespace the cache per backend (e.g. pio_components/idf, .../zephyr) so
|
||||
# the build files each backend writes into the library dir can't collide.
|
||||
base_dir = Path(CORE.data_dir) / DOMAIN
|
||||
if namespace:
|
||||
base_dir = base_dir / namespace
|
||||
h = hashlib.new("sha256")
|
||||
h.update(self.url.encode())
|
||||
if salt:
|
||||
@@ -113,12 +121,19 @@ class GitSource(Source):
|
||||
self.url = url
|
||||
self.ref = ref
|
||||
|
||||
def download(self, dir_suffix: str, force: bool = False, salt: str = "") -> Path:
|
||||
def download(
|
||||
self, dir_suffix: str, force: bool = False, salt: str = "", namespace: str = ""
|
||||
) -> Path:
|
||||
domain = DOMAIN
|
||||
if namespace:
|
||||
domain = f"{domain}/{namespace}"
|
||||
if salt:
|
||||
domain = f"{domain}/{salt}"
|
||||
path, _ = git.clone_or_update(
|
||||
url=self.url,
|
||||
ref=self.ref,
|
||||
refresh=git.NEVER_REFRESH if not force else None,
|
||||
domain=f"{DOMAIN}/{salt}" if salt else DOMAIN,
|
||||
domain=domain,
|
||||
submodules=[],
|
||||
subpath=Path(dir_suffix),
|
||||
)
|
||||
@@ -167,16 +182,16 @@ class ConvertedLibrary:
|
||||
def get_require_name(self):
|
||||
return self.get_sanitized_name().replace("/", "__")
|
||||
|
||||
def download(self, force: bool = False, salt: str = ""):
|
||||
def download(self, force: bool = False, salt: str = "", namespace: str = ""):
|
||||
"""Fetch the library into the shared cache and record its ``path``.
|
||||
|
||||
The cache directory is named after the sanitized library name; backends
|
||||
rely on that name to identify the unit they build (e.g. ESP-IDF uses the
|
||||
directory name as the component name, replacing ``/`` with ``__`` via
|
||||
``get_require_name``).
|
||||
``get_require_name``). ``namespace`` keeps each backend's cache separate.
|
||||
"""
|
||||
self.path = self.source.download(
|
||||
self.get_sanitized_name(), force=force, salt=salt
|
||||
self.get_sanitized_name(), force=force, salt=salt, namespace=namespace
|
||||
)
|
||||
|
||||
|
||||
@@ -188,11 +203,15 @@ class LibraryBackend:
|
||||
``emit`` writes the toolchain-specific build files into a resolved library's
|
||||
``path`` (e.g. the ESP-IDF ``CMakeLists.txt`` + ``idf_component.yml``, or a
|
||||
Zephyr ``module.yml`` + ``CMakeLists.txt``).
|
||||
``cache_key`` namespaces the download cache (``pio_components/<cache_key>/``)
|
||||
so the differing build files two backends emit into a library dir never
|
||||
collide when the same config dir hosts both an ESP-IDF and a Zephyr build.
|
||||
"""
|
||||
|
||||
platform: str
|
||||
platform: str | None
|
||||
framework: str
|
||||
emit: Callable[["ConvertedLibrary"], None]
|
||||
cache_key: str
|
||||
|
||||
|
||||
def ensure_list[T](obj: T | list[T]) -> list[T]:
|
||||
@@ -306,7 +325,7 @@ def split_list_by_condition(
|
||||
return matched, non_matched
|
||||
|
||||
|
||||
def check_library_data(data: dict, platform: str, framework: str):
|
||||
def check_library_data(data: dict, platform: str | None, framework: str):
|
||||
"""
|
||||
Check whether a library manifest is compatible with the target toolchain.
|
||||
|
||||
@@ -319,7 +338,9 @@ def check_library_data(data: dict, platform: str, framework: str):
|
||||
Args:
|
||||
data: PIO library manifest dict being processed.
|
||||
platform: The PlatformIO platform token the build targets (e.g.
|
||||
``espressif32``).
|
||||
``espressif32``). ``None`` skips the platform check entirely — useful
|
||||
for targets (e.g. Zephyr) where PIO manifests rarely declare the
|
||||
platform yet portable libraries still build.
|
||||
framework: The active framework name (e.g. ``espidf``, ``arduino``,
|
||||
``zephyr``) the manifest is expected to declare.
|
||||
|
||||
@@ -332,7 +353,7 @@ def check_library_data(data: dict, platform: str, framework: str):
|
||||
platforms = ensure_list(platforms)
|
||||
|
||||
# Check if library supports the target platform
|
||||
valid_platforms = "*" in platforms or platform in platforms
|
||||
valid_platforms = platform is None or "*" in platforms or platform in platforms
|
||||
|
||||
if not valid_platforms:
|
||||
raise InvalidLibrary(f"Unsupported library platforms: {platforms}")
|
||||
@@ -613,7 +634,7 @@ def convert_libraries(
|
||||
component = ConvertedLibrary(
|
||||
_owner_pkgname_to_name(owner, name), version, URLSource(url)
|
||||
)
|
||||
component.download(salt=salt)
|
||||
component.download(salt=salt, namespace=backend.cache_key)
|
||||
|
||||
library_json_path = component.path / "library.json"
|
||||
library_properties_path = component.path / "library.properties"
|
||||
|
||||
+14
-7
@@ -653,14 +653,21 @@ def clean_all(configuration: list[str]):
|
||||
elif item.is_dir() and item.name != "storage":
|
||||
rmtree(item)
|
||||
|
||||
# The native ESP-IDF install lives in a machine-global cache dir, outside
|
||||
# any .esphome data dir, so the per-config loop above won't reach it.
|
||||
from esphome.espidf.framework import _get_idf_tools_path
|
||||
# The native toolchain installs live in a machine-global cache dir that
|
||||
# the per-config loop above can't reach. Wipe the default cache root
|
||||
# (also catches leftovers from older install layouts), then the resolved
|
||||
# install paths for the ESPHOME_*_PREFIX overrides (docker/add-on/CI)
|
||||
# that live outside it.
|
||||
import platformdirs
|
||||
|
||||
idf_install_path = _get_idf_tools_path()
|
||||
if idf_install_path.is_dir():
|
||||
_LOGGER.info("Deleting %s", idf_install_path)
|
||||
rmtree(idf_install_path)
|
||||
from esphome.components.nrf52.framework import get_sdk_nrf_tools_path
|
||||
from esphome.espidf.framework import get_idf_tools_path
|
||||
|
||||
cache_root = Path(platformdirs.user_cache_dir("esphome", appauthor=False)).resolve()
|
||||
for install_path in (cache_root, get_idf_tools_path(), get_sdk_nrf_tools_path()):
|
||||
if install_path.is_dir():
|
||||
_LOGGER.info("Deleting %s", install_path)
|
||||
rmtree(install_path)
|
||||
|
||||
# Clean PlatformIO project files
|
||||
try:
|
||||
|
||||
@@ -203,3 +203,24 @@ display:
|
||||
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
|
||||
it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK);
|
||||
it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0));
|
||||
|
||||
# Waveshare 7.5" V2 BWR (800x480, UC8179 controller, EDP_7in5b_V2)
|
||||
- platform: epaper_spi
|
||||
spi_id: spi_bus
|
||||
model: waveshare-7.5in-bv2-bwr
|
||||
cs_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO5
|
||||
dc_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO17
|
||||
reset_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO16
|
||||
busy_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO4
|
||||
lambda: |-
|
||||
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
|
||||
it.circle(it.get_width() / 2, it.get_height() / 2, 100, Color::BLACK);
|
||||
it.circle(it.get_width() / 2, it.get_height() / 2, 60, Color(255, 0, 0));
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
display:
|
||||
- platform: ssd1306_i2c
|
||||
i2c_id: i2c_bus
|
||||
id: st7123_ssd1306_i2c_display
|
||||
model: SSD1306_128X64
|
||||
reset_pin: ${display_reset_pin}
|
||||
pages:
|
||||
- id: st7123_page1
|
||||
lambda: |-
|
||||
it.rectangle(0, 0, it.get_width(), it.get_height());
|
||||
|
||||
touchscreen:
|
||||
- platform: st7123
|
||||
i2c_id: i2c_bus
|
||||
id: st7123_touchscreen
|
||||
display: st7123_ssd1306_i2c_display
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
reset_pin: ${reset_pin}
|
||||
@@ -0,0 +1,9 @@
|
||||
substitutions:
|
||||
display_reset_pin: "10"
|
||||
interrupt_pin: "20"
|
||||
reset_pin: "21"
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -481,7 +481,7 @@ def test_generate_idf_components_dedupes_shared_dependency(
|
||||
"esphome/C": {"name": "C"},
|
||||
}
|
||||
|
||||
def fake_download(self, force=False, salt=""):
|
||||
def fake_download(self, force=False, salt="", namespace=""):
|
||||
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
|
||||
(self.path / "src").mkdir(parents=True, exist_ok=True)
|
||||
(self.path / "src" / "x.c").write_text("int x;")
|
||||
@@ -543,7 +543,7 @@ def test_generate_idf_components_lib_ignore_filters_top_level_and_dependencies(
|
||||
|
||||
download_salts: list[str] = []
|
||||
|
||||
def fake_download(self, force=False, salt=""):
|
||||
def fake_download(self, force=False, salt="", namespace=""):
|
||||
download_salts.append(salt)
|
||||
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
|
||||
(self.path / "src").mkdir(parents=True, exist_ok=True)
|
||||
@@ -597,7 +597,7 @@ def test_generate_idf_components_handles_dependency_cycle(
|
||||
},
|
||||
}
|
||||
|
||||
def fake_download(self, force=False, salt=""):
|
||||
def fake_download(self, force=False, salt="", namespace=""):
|
||||
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
|
||||
(self.path / "src").mkdir(parents=True, exist_ok=True)
|
||||
(self.path / "src" / "x.c").write_text("int x;")
|
||||
@@ -654,7 +654,7 @@ def test_generate_idf_components_git_overrides_registry_warns(
|
||||
"esphome/shared": {"name": "shared"},
|
||||
}
|
||||
|
||||
def fake_download(self, force=False, salt=""):
|
||||
def fake_download(self, force=False, salt="", namespace=""):
|
||||
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
|
||||
(self.path / "src").mkdir(parents=True, exist_ok=True)
|
||||
(self.path / "src" / "x.c").write_text("int x;")
|
||||
@@ -691,7 +691,7 @@ def test_generate_idf_components_missing_manifest_raises(
|
||||
) -> None:
|
||||
# A library with neither library.json nor library.properties is invalid;
|
||||
# fail loudly rather than silently generating build files for it.
|
||||
def fake_download(self, force=False, salt=""):
|
||||
def fake_download(self, force=False, salt="", namespace=""):
|
||||
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
|
||||
(self.path / "src").mkdir(parents=True, exist_ok=True)
|
||||
# no library.json / library.properties written
|
||||
@@ -733,7 +733,7 @@ def test_generate_idf_components_warns_on_noncanonical_duplicate(
|
||||
"owner/shared": {"name": "shared"},
|
||||
}
|
||||
|
||||
def fake_download(self, force=False, salt=""):
|
||||
def fake_download(self, force=False, salt="", namespace=""):
|
||||
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
|
||||
(self.path / "src").mkdir(parents=True, exist_ok=True)
|
||||
(self.path / "src" / "x.c").write_text("int x;")
|
||||
@@ -766,7 +766,7 @@ def test_generate_idf_components_incompatible_top_level_raises(
|
||||
) -> None:
|
||||
# A top-level library that isn't ESP-IDF/esp32 compatible must fail fast,
|
||||
# not be silently dropped.
|
||||
def fake_download(self, force=False, salt=""):
|
||||
def fake_download(self, force=False, salt="", namespace=""):
|
||||
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
|
||||
(self.path / "src").mkdir(parents=True, exist_ok=True)
|
||||
(self.path / "library.json").write_text(
|
||||
@@ -804,7 +804,7 @@ def test_generate_idf_components_incompatible_dependency_skipped(
|
||||
"esphome/B": {"name": "B", "platforms": ["espressif8266"]},
|
||||
}
|
||||
|
||||
def fake_download(self, force=False, salt=""):
|
||||
def fake_download(self, force=False, salt="", namespace=""):
|
||||
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
|
||||
(self.path / "src").mkdir(parents=True, exist_ok=True)
|
||||
(self.path / "library.json").write_text(json.dumps(manifests[self.name]))
|
||||
@@ -847,6 +847,13 @@ def test_url_source_salt_changes_cache_path(
|
||||
assert source.download("lib") == expected[""]
|
||||
assert source.download("lib", salt="abcd1234") == expected["abcd1234"]
|
||||
|
||||
# A backend namespace adds a pio_components/<namespace>/ subdir.
|
||||
digest = hashlib.sha256(url.encode()).hexdigest()[:8]
|
||||
ns_expected = base / "idf" / digest / "lib"
|
||||
ns_expected.mkdir(parents=True)
|
||||
(ns_expected / ".esphome_extracted").touch()
|
||||
assert source.download("lib", namespace="idf") == ns_expected
|
||||
|
||||
|
||||
def test_git_source_salt_scopes_domain(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""The salt becomes a subdirectory of the git clone domain."""
|
||||
@@ -863,7 +870,14 @@ def test_git_source_salt_scopes_domain(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
source = GitSource("https://github.com/esphome/noise-c.git", "v1.0")
|
||||
source.download("noise-c")
|
||||
source.download("noise-c", salt="abcd1234")
|
||||
assert domains == ["pio_components", "pio_components/abcd1234"]
|
||||
source.download("noise-c", namespace="idf")
|
||||
source.download("noise-c", namespace="zephyr", salt="abcd1234")
|
||||
assert domains == [
|
||||
"pio_components",
|
||||
"pio_components/abcd1234",
|
||||
"pio_components/idf",
|
||||
"pio_components/zephyr/abcd1234",
|
||||
]
|
||||
|
||||
|
||||
def test_idf_component_download_passes_salt() -> None:
|
||||
@@ -873,7 +887,9 @@ def test_idf_component_download_passes_salt() -> None:
|
||||
source.download.return_value = Path("/converted/owner/name")
|
||||
|
||||
c = IDFComponent("owner/name", "1.0", source=source)
|
||||
c.download(force=True, salt="abcd1234")
|
||||
c.download(force=True, salt="abcd1234", namespace="idf")
|
||||
|
||||
source.download.assert_called_once_with("owner/name", force=True, salt="abcd1234")
|
||||
source.download.assert_called_once_with(
|
||||
"owner/name", force=True, salt="abcd1234", namespace="idf"
|
||||
)
|
||||
assert c.path == Path("/converted/owner/name")
|
||||
|
||||
@@ -21,7 +21,6 @@ from esphome.espidf.framework import (
|
||||
_clone_idf_with_submodules,
|
||||
_get_framework_path,
|
||||
_get_idf_tool_paths,
|
||||
_get_idf_tools_path,
|
||||
_get_idf_version,
|
||||
_get_python_env_path,
|
||||
_get_python_version,
|
||||
@@ -32,6 +31,7 @@ from esphome.espidf.framework import (
|
||||
_write_stamp,
|
||||
check_esp_idf_install,
|
||||
get_framework_env,
|
||||
get_idf_tools_path,
|
||||
)
|
||||
from esphome.framework_helpers import _tar_extract_all, get_python_env_executable_path
|
||||
|
||||
@@ -639,7 +639,7 @@ def test_write_stamp_writes_json(tmp_path: Path) -> None:
|
||||
def test_get_framework_env_with_python_env(tmp_path: Path) -> None:
|
||||
with (
|
||||
patch(
|
||||
"esphome.espidf.framework._get_idf_tools_path",
|
||||
"esphome.espidf.framework.get_idf_tools_path",
|
||||
return_value=tmp_path / "tools",
|
||||
),
|
||||
patch("esphome.espidf.framework._get_idf_version", return_value="5.1.2"),
|
||||
@@ -664,7 +664,7 @@ def test_get_framework_env_with_python_env(tmp_path: Path) -> None:
|
||||
def test_get_framework_env_without_python_env_uses_os_path(tmp_path: Path) -> None:
|
||||
with (
|
||||
patch(
|
||||
"esphome.espidf.framework._get_idf_tools_path",
|
||||
"esphome.espidf.framework.get_idf_tools_path",
|
||||
return_value=tmp_path / "tools",
|
||||
),
|
||||
patch("esphome.espidf.framework._get_idf_version", return_value="5.1.2"),
|
||||
@@ -687,7 +687,7 @@ def _ccache_patches(tmp_path: Path, which: str | None, build_path: Path | None):
|
||||
return (
|
||||
patch("esphome.espidf.framework.shutil.which", return_value=which),
|
||||
patch(
|
||||
"esphome.espidf.framework._get_idf_tools_path",
|
||||
"esphome.espidf.framework.get_idf_tools_path",
|
||||
return_value=tmp_path / "tools",
|
||||
),
|
||||
patch(
|
||||
@@ -761,7 +761,7 @@ def test_ccache_env_raises_without_build_path(tmp_path: Path) -> None:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _check_stamp / _write_idf_version_txt / _get_idf_tools_path
|
||||
# _check_stamp / _write_idf_version_txt / get_idf_tools_path
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -798,14 +798,14 @@ def test_write_idf_version_txt_skips_when_present(tmp_path: Path) -> None:
|
||||
assert (tmp_path / "version.txt").read_text(encoding="utf-8") == "existing\n"
|
||||
|
||||
|
||||
def test_get_idf_tools_path_env_override(tmp_path: Path) -> None:
|
||||
def testget_idf_tools_path_env_override(tmp_path: Path) -> None:
|
||||
override = str(tmp_path / "custom-idf")
|
||||
with patch.dict("os.environ", {"ESPHOME_ESP_IDF_PREFIX": override}):
|
||||
assert _get_idf_tools_path() == Path(override)
|
||||
assert get_idf_tools_path() == Path(override)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["", " "])
|
||||
def test_get_idf_tools_path_blank_env_falls_back_to_default(
|
||||
def testget_idf_tools_path_blank_env_falls_back_to_default(
|
||||
value: str, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""A blank ESPHOME_ESP_IDF_PREFIX is treated as unset, not as CWD.
|
||||
@@ -819,10 +819,10 @@ def test_get_idf_tools_path_blank_env_falls_back_to_default(
|
||||
expected = (
|
||||
Path(platformdirs.user_cache_dir("esphome", appauthor=False)) / "idf"
|
||||
).resolve()
|
||||
assert _get_idf_tools_path() == expected
|
||||
assert get_idf_tools_path() == expected
|
||||
|
||||
|
||||
def test_get_idf_tools_path_default_uses_user_cache(
|
||||
def testget_idf_tools_path_default_uses_user_cache(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Without the env override the install root is the machine-global OS user
|
||||
@@ -833,7 +833,7 @@ def test_get_idf_tools_path_default_uses_user_cache(
|
||||
expected = (
|
||||
Path(platformdirs.user_cache_dir("esphome", appauthor=False)) / "idf"
|
||||
).resolve()
|
||||
assert _get_idf_tools_path() == expected
|
||||
assert get_idf_tools_path() == expected
|
||||
|
||||
|
||||
def test_write_idf_version_txt_warns_on_write_error(tmp_path: Path) -> None:
|
||||
@@ -908,7 +908,7 @@ def test_check_windows_path_length_noop_when_long_paths_enabled(
|
||||
patch(
|
||||
"esphome.espidf.framework._windows_long_paths_enabled", return_value=True
|
||||
),
|
||||
patch("esphome.espidf.framework._get_idf_tools_path") as get_path_mock,
|
||||
patch("esphome.espidf.framework.get_idf_tools_path") as get_path_mock,
|
||||
caplog.at_level(logging.WARNING),
|
||||
):
|
||||
_check_windows_path_length()
|
||||
@@ -925,7 +925,7 @@ def test_check_windows_path_length_short_path_silent(
|
||||
"esphome.espidf.framework._windows_long_paths_enabled", return_value=False
|
||||
),
|
||||
patch(
|
||||
"esphome.espidf.framework._get_idf_tools_path",
|
||||
"esphome.espidf.framework.get_idf_tools_path",
|
||||
return_value=_SHORT_IDF_PATH,
|
||||
),
|
||||
caplog.at_level(logging.WARNING),
|
||||
@@ -943,7 +943,7 @@ def test_check_windows_path_length_long_path_warns(
|
||||
"esphome.espidf.framework._windows_long_paths_enabled", return_value=False
|
||||
),
|
||||
patch(
|
||||
"esphome.espidf.framework._get_idf_tools_path",
|
||||
"esphome.espidf.framework.get_idf_tools_path",
|
||||
return_value=_LONG_IDF_PATH,
|
||||
),
|
||||
caplog.at_level(logging.WARNING),
|
||||
|
||||
@@ -10,12 +10,28 @@ from esphome.components.nrf52.framework import (
|
||||
_TOOLCHAIN_VERSION,
|
||||
_get_toolchain_platform_info,
|
||||
check_and_install,
|
||||
get_sdk_nrf_tools_path,
|
||||
)
|
||||
from esphome.config_validation import Version
|
||||
from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION
|
||||
from esphome.core import CORE, EsphomeError
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolate_sdk_nrf_install_path(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Pin the sdk-nrf install root to a tmp dir for every test.
|
||||
|
||||
The default location is the OS user cache dir, so without this any test
|
||||
that builds framework paths or pre-creates the install dir would touch
|
||||
the real ``~/.cache/esphome`` on the developer's machine. Tests that need
|
||||
to exercise the override or default-resolution logic clear/override the
|
||||
env themselves.
|
||||
"""
|
||||
monkeypatch.setenv("ESPHOME_SDK_NRF_PREFIX", str(tmp_path / "sdk_nrf_install"))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("system", "machine", "expected"),
|
||||
[
|
||||
@@ -52,7 +68,7 @@ _TEST_SDK_VERSION = "2.9.0"
|
||||
def nrf52_dirs(setup_core: Path) -> SimpleNamespace:
|
||||
"""Populate CORE and pre-create SDK directories so sentinel.touch() succeeds."""
|
||||
CORE.data[KEY_CORE] = {KEY_FRAMEWORK_VERSION: Version.parse(_TEST_SDK_VERSION)}
|
||||
tools = CORE.data_dir / "sdk-nrf"
|
||||
tools = get_sdk_nrf_tools_path()
|
||||
python_env = tools / "penvs" / f"v{_TEST_SDK_VERSION}"
|
||||
framework = tools / "frameworks" / f"v{_TEST_SDK_VERSION}"
|
||||
toolchain_dir = tools / "toolchains" / _TOOLCHAIN_VERSION
|
||||
@@ -226,3 +242,46 @@ class TestCheckAndInstall:
|
||||
assert substitutions["sysname"] == "linux"
|
||||
assert substitutions["machine"] == "x86_64"
|
||||
assert substitutions["extension"] == "tar.xz"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_sdk_nrf_tools_path tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def testget_tools_path_env_override(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
override = tmp_path / "custom" / "sdk-nrf"
|
||||
monkeypatch.setenv("ESPHOME_SDK_NRF_PREFIX", str(override))
|
||||
assert get_sdk_nrf_tools_path() == override.resolve()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["", " "])
|
||||
def testget_tools_path_blank_env_falls_back_to_default(
|
||||
value: str, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""A blank ESPHOME_SDK_NRF_PREFIX is treated as unset, not as CWD.
|
||||
|
||||
Path("") would resolve to the working directory, which clean-all could
|
||||
then delete by accident.
|
||||
"""
|
||||
import platformdirs
|
||||
|
||||
monkeypatch.setenv("ESPHOME_SDK_NRF_PREFIX", value)
|
||||
expected = (
|
||||
Path(platformdirs.user_cache_dir("esphome", appauthor=False)) / "sdk-nrf"
|
||||
).resolve()
|
||||
assert get_sdk_nrf_tools_path() == expected
|
||||
|
||||
|
||||
def testget_tools_path_default_is_global_cache(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
import platformdirs
|
||||
|
||||
monkeypatch.delenv("ESPHOME_SDK_NRF_PREFIX", raising=False)
|
||||
expected = (
|
||||
Path(platformdirs.user_cache_dir("esphome", appauthor=False)) / "sdk-nrf"
|
||||
).resolve()
|
||||
assert get_sdk_nrf_tools_path() == expected
|
||||
|
||||
@@ -26,7 +26,9 @@ from esphome.platformio.library import (
|
||||
|
||||
|
||||
def _backend(emit=lambda component: None) -> LibraryBackend:
|
||||
return LibraryBackend(platform="espressif32", framework="espidf", emit=emit)
|
||||
return LibraryBackend(
|
||||
platform="espressif32", framework="espidf", emit=emit, cache_key="idf"
|
||||
)
|
||||
|
||||
|
||||
def test_check_library_data_accepts_wildcards():
|
||||
@@ -134,7 +136,7 @@ def test_resolve_registry_version_raises_without_pkg_file(monkeypatch):
|
||||
def _patch_download_with_manifests(monkeypatch, tmp_path, manifests, *, properties=()):
|
||||
"""Fake ConvertedLibrary.download to materialize canned manifests on disk."""
|
||||
|
||||
def fake_download(self, force=False, salt=""):
|
||||
def fake_download(self, force=False, salt="", namespace=""):
|
||||
self.path = tmp_path / self.get_sanitized_name().replace("/", "__")
|
||||
self.path.mkdir(parents=True, exist_ok=True)
|
||||
if self.name in properties:
|
||||
|
||||
@@ -68,12 +68,16 @@ def _isolate_platformio_paths(tmp_path_factory: pytest.TempPathFactory) -> Any:
|
||||
test_clean_all_partial_exists) install their own inner patch which
|
||||
stacks on top of this one and wins for the duration of their block.
|
||||
|
||||
Also pin ``ESPHOME_ESP_IDF_PREFIX`` to a nonexistent tmp dir for the
|
||||
same reason: ``clean_all`` removes the now machine-global ESP-IDF
|
||||
install, which otherwise defaults to the real ``~/.cache/esphome``.
|
||||
Also pin ``ESPHOME_ESP_IDF_PREFIX`` and ``ESPHOME_SDK_NRF_PREFIX`` to
|
||||
nonexistent tmp dirs, and patch ``platformdirs.user_cache_dir``, for the
|
||||
same reason: ``clean_all`` removes the machine-global toolchain installs
|
||||
and their default cache root, which otherwise resolve to the real
|
||||
``~/.cache/esphome``.
|
||||
"""
|
||||
pio_root = tmp_path_factory.mktemp("isolated_pio") / "nonexistent"
|
||||
idf_root = tmp_path_factory.mktemp("isolated_idf") / "nonexistent"
|
||||
sdk_nrf_root = tmp_path_factory.mktemp("isolated_sdk_nrf") / "nonexistent"
|
||||
cache_root = tmp_path_factory.mktemp("isolated_cache") / "nonexistent"
|
||||
mock_cfg = MagicMock()
|
||||
mock_cfg.get.side_effect = lambda section, option: (
|
||||
str(pio_root / option) if section == "platformio" else ""
|
||||
@@ -83,7 +87,14 @@ def _isolate_platformio_paths(tmp_path_factory: pytest.TempPathFactory) -> Any:
|
||||
"platformio.project.config.ProjectConfig.get_instance",
|
||||
return_value=mock_cfg,
|
||||
),
|
||||
patch.dict("os.environ", {"ESPHOME_ESP_IDF_PREFIX": str(idf_root)}),
|
||||
patch.dict(
|
||||
"os.environ",
|
||||
{
|
||||
"ESPHOME_ESP_IDF_PREFIX": str(idf_root),
|
||||
"ESPHOME_SDK_NRF_PREFIX": str(sdk_nrf_root),
|
||||
},
|
||||
),
|
||||
patch("platformdirs.user_cache_dir", return_value=str(cache_root)),
|
||||
):
|
||||
yield
|
||||
|
||||
@@ -1022,6 +1033,55 @@ def test_clean_all_removes_global_idf_install(
|
||||
assert str(idf_install.resolve()) in caplog.text
|
||||
|
||||
|
||||
@patch("esphome.writer.CORE")
|
||||
def test_clean_all_removes_global_sdk_nrf_install(
|
||||
mock_core: MagicMock,
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""clean_all removes the machine-global native sdk-nrf install dir."""
|
||||
sdk_nrf_install = tmp_path / "sdk_nrf_install"
|
||||
(sdk_nrf_install / "frameworks").mkdir(parents=True)
|
||||
monkeypatch.setenv("ESPHOME_SDK_NRF_PREFIX", str(sdk_nrf_install))
|
||||
|
||||
config_dir = tmp_path / "config"
|
||||
config_dir.mkdir()
|
||||
|
||||
from esphome.writer import clean_all
|
||||
|
||||
with caplog.at_level("INFO"):
|
||||
clean_all([str(config_dir)])
|
||||
|
||||
assert not sdk_nrf_install.exists()
|
||||
assert str(sdk_nrf_install.resolve()) in caplog.text
|
||||
|
||||
|
||||
@patch("esphome.writer.CORE")
|
||||
def test_clean_all_removes_default_cache_root(
|
||||
mock_core: MagicMock,
|
||||
tmp_path: Path,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""clean_all removes the default cache root (stale/orphaned installs)."""
|
||||
cache_root = tmp_path / "cache_root"
|
||||
(cache_root / "some-old-toolchain").mkdir(parents=True)
|
||||
|
||||
config_dir = tmp_path / "config"
|
||||
config_dir.mkdir()
|
||||
|
||||
from esphome.writer import clean_all
|
||||
|
||||
with (
|
||||
patch("platformdirs.user_cache_dir", return_value=str(cache_root)),
|
||||
caplog.at_level("INFO"),
|
||||
):
|
||||
clean_all([str(config_dir)])
|
||||
|
||||
assert not cache_root.exists()
|
||||
assert str(cache_root.resolve()) in caplog.text
|
||||
|
||||
|
||||
@patch("esphome.writer.CORE")
|
||||
def test_clean_all_with_yaml_build_path(
|
||||
mock_core: MagicMock,
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
"""Tests for the Zephyr backend of the shared PlatformIO library converter."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
import esphome.components.zephyr.library as zlib
|
||||
from esphome.components.zephyr.library import (
|
||||
generate_cmakelists_txt,
|
||||
generate_module_yml,
|
||||
generate_zephyr_modules,
|
||||
)
|
||||
from esphome.core import EsphomeError, Library
|
||||
from esphome.platformio.library import ConvertedLibrary, URLSource
|
||||
|
||||
|
||||
def _make_component(path: Path, name: str = "mylib") -> ConvertedLibrary:
|
||||
c = ConvertedLibrary(name, "1.0", source=URLSource("http://dummy"))
|
||||
c.path = path
|
||||
return c
|
||||
|
||||
|
||||
def test_generate_module_yml_uses_sanitized_name():
|
||||
c = ConvertedLibrary("owner/My Lib", "1.0", source=URLSource("http://dummy"))
|
||||
out = generate_module_yml(c)
|
||||
# "/" -> "__" and " " -> "_" so it's a valid Zephyr module name.
|
||||
assert "name: owner__My_Lib" in out
|
||||
assert "cmake: zephyr" in out
|
||||
|
||||
|
||||
def test_generate_cmakelists_txt_basic(tmp_path):
|
||||
c = _make_component(tmp_path)
|
||||
src = tmp_path / "src"
|
||||
src.mkdir()
|
||||
(src / "main.c").write_text("int main() {}")
|
||||
c.data = {}
|
||||
|
||||
out = generate_cmakelists_txt(c)
|
||||
|
||||
assert "zephyr_library_named(mylib)" in out
|
||||
assert "zephyr_library_sources(" in out
|
||||
# Sources are emitted as absolute paths (CMakeLists lives in zephyr/ subdir),
|
||||
# backslash-escaped for CMake (matching the output on Windows).
|
||||
assert str((src / "main.c").resolve()).replace("\\", "\\\\") in out
|
||||
|
||||
|
||||
def test_generate_cmakelists_txt_flags_and_includes(tmp_path):
|
||||
c = _make_component(tmp_path)
|
||||
(tmp_path / "src").mkdir()
|
||||
(tmp_path / "src" / "a.c").write_text("")
|
||||
(tmp_path / "include").mkdir()
|
||||
c.data = {"build": {"flags": ["-Iinclude", "-DFOO", "-Wall", "-Llibdir", "-lm"]}}
|
||||
|
||||
out = generate_cmakelists_txt(c)
|
||||
|
||||
assert "zephyr_include_directories(" in out
|
||||
assert str((tmp_path / "include").resolve()).replace("\\", "\\\\") in out
|
||||
assert "zephyr_library_compile_options(" in out
|
||||
assert "-DFOO" in out
|
||||
assert "-Wall" in out
|
||||
assert "zephyr_link_libraries(" in out
|
||||
assert "-Llibdir" in out
|
||||
assert "-lm" in out
|
||||
|
||||
|
||||
def test_generate_zephyr_modules_collects_all_dirs_and_writes(tmp_path, monkeypatch):
|
||||
# Two converted libraries: one top-level, one transitive dependency. The
|
||||
# converter calls backend.emit for both; generate_zephyr_modules must return
|
||||
# *all* module dirs (not just top-level) so every module is discoverable.
|
||||
top = _make_component(tmp_path / "top", "top")
|
||||
(top.path / "src").mkdir(parents=True)
|
||||
(top.path / "src" / "t.c").write_text("")
|
||||
dep = _make_component(tmp_path / "dep", "dep")
|
||||
(dep.path / "src").mkdir(parents=True)
|
||||
(dep.path / "src" / "d.c").write_text("")
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_convert(libraries, backend):
|
||||
captured["platform"] = backend.platform
|
||||
captured["framework"] = backend.framework
|
||||
backend.emit(top)
|
||||
backend.emit(dep)
|
||||
return [top]
|
||||
|
||||
monkeypatch.setattr(zlib, "convert_libraries", fake_convert)
|
||||
|
||||
dirs = generate_zephyr_modules([Library("top", "1.0", None)])
|
||||
|
||||
assert dirs == [top.path, dep.path]
|
||||
# Platform check disabled for Zephyr; framework declared as zephyr.
|
||||
assert captured["platform"] is None
|
||||
assert captured["framework"] == "zephyr"
|
||||
for comp in (top, dep):
|
||||
assert (comp.path / "zephyr" / "module.yml").is_file()
|
||||
assert (comp.path / "zephyr" / "CMakeLists.txt").is_file()
|
||||
|
||||
|
||||
def test_generate_zephyr_modules_errors_on_duplicate_module_name(tmp_path, monkeypatch):
|
||||
# The same library referenced under inconsistent specs (e.g. bare vs
|
||||
# owner-qualified, or git vs registry) resolves to two components with the
|
||||
# same Zephyr module name, which would collide in zephyr_library_named().
|
||||
a = _make_component(tmp_path / "a", "esphome/noise-c")
|
||||
a.path.mkdir(parents=True)
|
||||
b = _make_component(tmp_path / "b", "esphome/noise-c")
|
||||
b.path.mkdir(parents=True)
|
||||
assert a.get_require_name() == b.get_require_name()
|
||||
|
||||
def fake_convert(libraries, backend):
|
||||
backend.emit(a)
|
||||
backend.emit(b)
|
||||
return [a]
|
||||
|
||||
monkeypatch.setattr(zlib, "convert_libraries", fake_convert)
|
||||
|
||||
with pytest.raises(EsphomeError, match="same Zephyr module"):
|
||||
generate_zephyr_modules([Library("esphome/noise-c", "1.0", None)])
|
||||
Reference in New Issue
Block a user