mirror of
https://github.com/esphome/esphome.git
synced 2026-07-10 08:55:36 +00:00
Merge remote-tracking branch 'upstream/dev' into integration
This commit is contained in:
+13
-16
@@ -199,8 +199,7 @@ jobs:
|
||||
- common
|
||||
outputs:
|
||||
integration-tests: ${{ steps.determine.outputs.integration-tests }}
|
||||
integration-tests-run-all: ${{ steps.determine.outputs.integration-tests-run-all }}
|
||||
integration-test-files: ${{ steps.determine.outputs.integration-test-files }}
|
||||
integration-test-buckets: ${{ steps.determine.outputs.integration-test-buckets }}
|
||||
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
|
||||
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
|
||||
python-linters: ${{ steps.determine.outputs.python-linters }}
|
||||
@@ -243,8 +242,7 @@ jobs:
|
||||
|
||||
# Extract individual fields
|
||||
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
|
||||
echo "integration-tests-run-all=$(echo "$output" | jq -r '.integration_tests_run_all')" >> $GITHUB_OUTPUT
|
||||
echo "integration-test-files=$(echo "$output" | jq -c '.integration_test_files')" >> $GITHUB_OUTPUT
|
||||
echo "integration-test-buckets=$(echo "$output" | jq -c '.integration_test_buckets')" >> $GITHUB_OUTPUT
|
||||
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
|
||||
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
|
||||
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
|
||||
@@ -267,12 +265,16 @@ jobs:
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
|
||||
integration-tests:
|
||||
name: Run integration tests
|
||||
name: Run integration tests (${{ matrix.bucket.name }})
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: needs.determine-jobs.outputs.integration-tests == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -299,19 +301,14 @@ jobs:
|
||||
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
||||
- name: Run integration tests
|
||||
env:
|
||||
INTEGRATION_TEST_FILES: ${{ needs.determine-jobs.outputs.integration-test-files }}
|
||||
INTEGRATION_TESTS_RUN_ALL: ${{ needs.determine-jobs.outputs.integration-tests-run-all }}
|
||||
# JSON array of test paths; parsed into a bash array below to avoid
|
||||
# shell word-splitting / glob hazards.
|
||||
BUCKET_TESTS: ${{ toJson(matrix.bucket.tests) }}
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
if [[ "$INTEGRATION_TESTS_RUN_ALL" == "true" ]]; then
|
||||
echo "Running all integration tests"
|
||||
pytest -vv --no-cov --tb=native -n auto tests/integration/
|
||||
else
|
||||
# Parse JSON array into bash array to avoid shell expansion issues
|
||||
mapfile -t test_files < <(echo "$INTEGRATION_TEST_FILES" | jq -r '.[]')
|
||||
echo "Running ${#test_files[@]} specific integration tests"
|
||||
pytest -vv --no-cov --tb=native -n auto "${test_files[@]}"
|
||||
fi
|
||||
mapfile -t test_files < <(echo "$BUCKET_TESTS" | jq -r '.[]')
|
||||
echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests"
|
||||
pytest -vv --no-cov --tb=native -n auto "${test_files[@]}"
|
||||
|
||||
cpp-unit-tests:
|
||||
name: Run C++ unit tests
|
||||
|
||||
@@ -62,6 +62,7 @@ CONF_IS_WRGB = "is_wrgb"
|
||||
SUPPORTED_PINS = {
|
||||
libretiny.const.FAMILY_BK7231N: [16],
|
||||
libretiny.const.FAMILY_BK7231T: [16],
|
||||
libretiny.const.FAMILY_BK7238: [16],
|
||||
libretiny.const.FAMILY_BK7251: [16],
|
||||
}
|
||||
|
||||
|
||||
@@ -13,14 +13,21 @@
|
||||
* and printf() calls in SDK components are only in debug/assert paths
|
||||
* (gpio_dump_io_configuration, ringbuf diagnostics) that are either
|
||||
* GC'd or never called. Crash backtraces and panic output are
|
||||
* unaffected — they use esp_rom_printf() which is a ROM function
|
||||
* unaffected; they use esp_rom_printf() which is a ROM function
|
||||
* and does not go through libc.
|
||||
*
|
||||
* These stubs redirect through vsnprintf() (which uses _svfprintf_r
|
||||
* already in the binary) and fwrite(), allowing the linker to
|
||||
* dead-code eliminate _vfprintf_r.
|
||||
* On picolibc (default for IDF >= 5 on RISC-V, IDF >= 6 everywhere) we
|
||||
* route output through a stack-allocated cookie FILE that forwards each
|
||||
* byte to the real target stream via fputc(). Picolibc's tinystdio
|
||||
* vfprintf walks the FILE::put callback one character at a time, so this
|
||||
* costs ~32 bytes of stack for the cookie struct vs. a 512-byte format
|
||||
* buffer. The buffered path overflows the loopTask stack on IDF 6.
|
||||
*
|
||||
* Saves ~11 KB of flash.
|
||||
* On newlib (IDF <= 5 on Xtensa) we keep the original snprintf-then-fwrite
|
||||
* path because that loopTask stack budget has plenty of headroom for the
|
||||
* 512-byte buffer; the picolibc-only crash above does not affect it.
|
||||
*
|
||||
* Saves ~11 KB of flash on newlib, ~2.8 KB on picolibc.
|
||||
*
|
||||
* To disable these wraps, set enable_full_printf: true in the esp32
|
||||
* advanced config section.
|
||||
@@ -30,10 +37,55 @@
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
|
||||
#ifndef __PICOLIBC__
|
||||
#include "esp_system.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::esp32 {}
|
||||
|
||||
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
extern "C" {
|
||||
|
||||
#ifdef __PICOLIBC__
|
||||
|
||||
#include <cstddef>
|
||||
#include <type_traits>
|
||||
|
||||
extern int __real_vfprintf(FILE *stream, const char *fmt, va_list ap);
|
||||
|
||||
namespace {
|
||||
|
||||
struct CookieFile {
|
||||
FILE base;
|
||||
FILE *target;
|
||||
};
|
||||
|
||||
// cookie_put() recovers CookieFile* from FILE* via reinterpret_cast, which is
|
||||
// only well-defined when FILE is the first member at offset 0 and CookieFile
|
||||
// is standard-layout.
|
||||
static_assert(offsetof(CookieFile, base) == 0, "FILE must be the first member of CookieFile");
|
||||
static_assert(std::is_standard_layout<CookieFile>::value, "CookieFile must be standard-layout");
|
||||
|
||||
int cookie_put(char c, FILE *stream) {
|
||||
auto *cookie = reinterpret_cast<CookieFile *>(stream);
|
||||
return fputc(static_cast<unsigned char>(c), cookie->target);
|
||||
}
|
||||
|
||||
const FILE COOKIE_FILE_TEMPLATE = FDEV_SETUP_STREAM(cookie_put, nullptr, nullptr, _FDEV_SETUP_WRITE);
|
||||
|
||||
} // namespace
|
||||
|
||||
int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) {
|
||||
CookieFile cookie;
|
||||
cookie.base = COOKIE_FILE_TEMPLATE;
|
||||
cookie.target = stream;
|
||||
return __real_vfprintf(&cookie.base, fmt, ap);
|
||||
}
|
||||
|
||||
int __wrap_vprintf(const char *fmt, va_list ap) { return __wrap_vfprintf(stdout, fmt, ap); }
|
||||
|
||||
#else // !__PICOLIBC__
|
||||
|
||||
static constexpr size_t PRINTF_BUFFER_SIZE = 512;
|
||||
|
||||
// These stubs are essentially dead code at runtime — ESPHome replaces the
|
||||
@@ -55,14 +107,18 @@ static int write_printf_buffer(FILE *stream, char *buf, int len) {
|
||||
return len;
|
||||
}
|
||||
|
||||
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
extern "C" {
|
||||
|
||||
int __wrap_vprintf(const char *fmt, va_list ap) {
|
||||
char buf[PRINTF_BUFFER_SIZE];
|
||||
return write_printf_buffer(stdout, buf, vsnprintf(buf, sizeof(buf), fmt, ap));
|
||||
}
|
||||
|
||||
int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) {
|
||||
char buf[PRINTF_BUFFER_SIZE];
|
||||
return write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap));
|
||||
}
|
||||
|
||||
#endif // __PICOLIBC__
|
||||
|
||||
int __wrap_printf(const char *fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
@@ -71,11 +127,6 @@ int __wrap_printf(const char *fmt, ...) {
|
||||
return len;
|
||||
}
|
||||
|
||||
int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) {
|
||||
char buf[PRINTF_BUFFER_SIZE];
|
||||
return write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap));
|
||||
}
|
||||
|
||||
int __wrap_fprintf(FILE *stream, const char *fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
|
||||
@@ -246,9 +246,10 @@ async def to_code(config):
|
||||
idf_ver = esp32.idf_version()
|
||||
os.environ["ESP_IDF_VERSION"] = f"{idf_ver.major}.{idf_ver.minor}"
|
||||
if idf_ver >= cv.Version(5, 5, 0):
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.4.0")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.1")
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.5.1")
|
||||
esp32.add_idf_component(name="espressif/wifi_remote_over_eppp", ref="0.3.2")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.5")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.6")
|
||||
else:
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
|
||||
|
||||
@@ -1,74 +1,16 @@
|
||||
#ifdef USE_HOST
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "preferences.h"
|
||||
|
||||
#include <csignal>
|
||||
#include <sched.h>
|
||||
#include <time.h>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace {
|
||||
volatile sig_atomic_t s_signal_received = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
void signal_handler(int signal) { s_signal_received = signal; }
|
||||
} // namespace
|
||||
|
||||
namespace esphome {
|
||||
|
||||
void HOT yield() { ::sched_yield(); }
|
||||
uint32_t IRAM_ATTR HOT millis() {
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_MONOTONIC, &spec);
|
||||
return static_cast<uint32_t>(spec.tv_sec * 1000ULL + spec.tv_nsec / 1000000);
|
||||
}
|
||||
uint64_t millis_64() {
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_MONOTONIC, &spec);
|
||||
return static_cast<uint64_t>(spec.tv_sec) * 1000ULL + static_cast<uint64_t>(spec.tv_nsec) / 1000000ULL;
|
||||
}
|
||||
void HOT delay(uint32_t ms) {
|
||||
struct timespec ts;
|
||||
ts.tv_sec = ms / 1000;
|
||||
ts.tv_nsec = (ms % 1000) * 1000000;
|
||||
int res;
|
||||
do {
|
||||
res = nanosleep(&ts, &ts);
|
||||
} while (res != 0 && errno == EINTR);
|
||||
}
|
||||
uint32_t IRAM_ATTR HOT micros() {
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_MONOTONIC, &spec);
|
||||
return static_cast<uint32_t>(spec.tv_sec * 1000000ULL + spec.tv_nsec / 1000);
|
||||
}
|
||||
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) {
|
||||
struct timespec ts;
|
||||
ts.tv_sec = us / 1000000U;
|
||||
ts.tv_nsec = (us % 1000000U) * 1000U;
|
||||
int res;
|
||||
do {
|
||||
res = nanosleep(&ts, &ts);
|
||||
} while (res != 0 && errno == EINTR);
|
||||
}
|
||||
void arch_restart() { exit(0); }
|
||||
void arch_init() {
|
||||
// pass
|
||||
}
|
||||
void HOT arch_feed_wdt() {
|
||||
// pass
|
||||
}
|
||||
|
||||
uint32_t arch_get_cpu_cycle_count() {
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_MONOTONIC, &spec);
|
||||
time_t seconds = spec.tv_sec;
|
||||
uint32_t us = spec.tv_nsec;
|
||||
return ((uint32_t) seconds) * 1000000000U + us;
|
||||
}
|
||||
uint32_t arch_get_cpu_freq_hz() { return 1000000000U; }
|
||||
|
||||
} // namespace esphome
|
||||
// HAL functions live in hal.cpp.
|
||||
|
||||
void setup();
|
||||
void loop();
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#ifdef USE_HOST
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <time.h>
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
|
||||
// Empty host namespace block to satisfy ci-custom's lint_namespace check.
|
||||
// HAL functions live in namespace esphome (root) — they are not part of the
|
||||
// host component's API.
|
||||
namespace esphome::host {} // namespace esphome::host
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// yield(), arch_init(), arch_feed_wdt(), arch_get_cpu_freq_hz() inlined in
|
||||
// core/hal/hal_host.h.
|
||||
|
||||
uint32_t IRAM_ATTR HOT millis() {
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_MONOTONIC, &spec);
|
||||
return static_cast<uint32_t>(spec.tv_sec * 1000ULL + spec.tv_nsec / 1000000);
|
||||
}
|
||||
uint64_t millis_64() {
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_MONOTONIC, &spec);
|
||||
return static_cast<uint64_t>(spec.tv_sec) * 1000ULL + static_cast<uint64_t>(spec.tv_nsec) / 1000000ULL;
|
||||
}
|
||||
void HOT delay(uint32_t ms) {
|
||||
struct timespec ts;
|
||||
ts.tv_sec = ms / 1000;
|
||||
ts.tv_nsec = (ms % 1000) * 1000000;
|
||||
int res;
|
||||
do {
|
||||
res = nanosleep(&ts, &ts);
|
||||
} while (res != 0 && errno == EINTR);
|
||||
}
|
||||
uint32_t IRAM_ATTR HOT micros() {
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_MONOTONIC, &spec);
|
||||
return static_cast<uint32_t>(spec.tv_sec * 1000000ULL + spec.tv_nsec / 1000);
|
||||
}
|
||||
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) {
|
||||
struct timespec ts;
|
||||
ts.tv_sec = us / 1000000U;
|
||||
ts.tv_nsec = (us % 1000000U) * 1000U;
|
||||
int res;
|
||||
do {
|
||||
res = nanosleep(&ts, &ts);
|
||||
} while (res != 0 && errno == EINTR);
|
||||
}
|
||||
void arch_restart() { exit(0); }
|
||||
|
||||
uint32_t arch_get_cpu_cycle_count() {
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_MONOTONIC, &spec);
|
||||
time_t seconds = spec.tv_sec;
|
||||
uint32_t ns = static_cast<uint32_t>(spec.tv_nsec);
|
||||
return static_cast<uint32_t>(seconds) * 1000000000U + ns;
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_HOST
|
||||
@@ -37,6 +37,7 @@ from .const import (
|
||||
CONF_UART_PORT,
|
||||
FAMILIES,
|
||||
FAMILY_BK7231N,
|
||||
FAMILY_BK7238,
|
||||
FAMILY_COMPONENT,
|
||||
FAMILY_FRIENDLY,
|
||||
FAMILY_RTL8710B,
|
||||
@@ -56,19 +57,22 @@ CODEOWNERS = ["@kuba2k2"]
|
||||
AUTO_LOAD = ["preferences"]
|
||||
IS_TARGET_PLATFORM = True
|
||||
|
||||
# BK7231N SDK options to disable unused features.
|
||||
# BLE 5.x BK SDK options to disable unused features.
|
||||
# Disabling BLE saves ~21KB RAM and ~200KB Flash because BLE init code is
|
||||
# called unconditionally by the SDK. ESPHome doesn't use BLE on LibreTiny.
|
||||
#
|
||||
# This only works on BK7231N (BLE 5.x). Other BK72XX chips using BLE 4.2
|
||||
# (BK7231T, BK7231Q, BK7251; BK7252 boards use the BK7251 family) have a bug
|
||||
# where the BLE library still links and references undefined symbols when
|
||||
# CFG_SUPPORT_BLE=0.
|
||||
# This only works on BLE 5.x BK chips (BK7231N, BK7238). Other BK72XX chips
|
||||
# using BLE 4.2 (BK7231T, BK7231Q, BK7251; BK7252 boards use the BK7251 family)
|
||||
# have a bug where the BLE library still links and references undefined symbols
|
||||
# when CFG_SUPPORT_BLE=0.
|
||||
#
|
||||
# On BK7238 the SDK also hangs at WiFi STA enable when BLE init runs, so
|
||||
# disabling it is required for reliable boot, not just an optimization.
|
||||
#
|
||||
# Other options like CFG_TX_EVM_TEST, CFG_RX_SENSITIVITY_TEST, CFG_SUPPORT_BKREG,
|
||||
# CFG_SUPPORT_OTA_HTTP, and CFG_USE_SPI_SLAVE were evaluated but provide no # NOLINT
|
||||
# measurable benefit - the linker already strips unreferenced code via -gc-sections.
|
||||
_BK7231N_SYS_CONFIG_OPTIONS = [
|
||||
_BLE5_BK_SYS_CONFIG_OPTIONS = [
|
||||
"CFG_SUPPORT_BLE=0",
|
||||
]
|
||||
|
||||
@@ -549,9 +553,9 @@ async def component_to_code(config):
|
||||
cg.add_platformio_option("custom_fw_version", __version__)
|
||||
|
||||
# Apply chip-specific SDK options to save RAM/Flash
|
||||
if config[CONF_FAMILY] == FAMILY_BK7231N:
|
||||
if config[CONF_FAMILY] in (FAMILY_BK7231N, FAMILY_BK7238):
|
||||
cg.add_platformio_option(
|
||||
"custom_options.sys_config#h", _BK7231N_SYS_CONFIG_OPTIONS
|
||||
"custom_options.sys_config#h", _BLE5_BK_SYS_CONFIG_OPTIONS
|
||||
)
|
||||
|
||||
# Tune lwIP for ESPHome's actual needs.
|
||||
|
||||
@@ -1,41 +1,6 @@
|
||||
#ifdef USE_RP2040
|
||||
|
||||
#include "core.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_RP2040_CRASH_HANDLER
|
||||
#include "crash_handler.h"
|
||||
#endif
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include "hardware/timer.h"
|
||||
#include "hardware/watchdog.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// yield(), delay(), micros(), millis(), millis_64() inlined in hal.h.
|
||||
void HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
|
||||
void arch_restart() {
|
||||
watchdog_reboot(0, 0, 10);
|
||||
while (1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
void arch_init() {
|
||||
#ifdef USE_RP2040_CRASH_HANDLER
|
||||
rp2040::crash_handler_read_and_clear();
|
||||
#endif
|
||||
#if USE_RP2040_WATCHDOG_TIMEOUT > 0
|
||||
watchdog_enable(USE_RP2040_WATCHDOG_TIMEOUT, false);
|
||||
#endif
|
||||
}
|
||||
|
||||
void HOT arch_feed_wdt() { watchdog_update(); }
|
||||
|
||||
uint32_t HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); }
|
||||
uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); }
|
||||
|
||||
} // namespace esphome
|
||||
// HAL functions live in hal.cpp. core.cpp is intentionally empty for
|
||||
// rp2040 — there is no extra component bootstrap to keep here.
|
||||
|
||||
#endif // USE_RP2040
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#ifdef USE_RP2040
|
||||
|
||||
#include "core.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#ifdef USE_RP2040_CRASH_HANDLER
|
||||
#include "crash_handler.h"
|
||||
#endif
|
||||
|
||||
#include "hardware/watchdog.h"
|
||||
|
||||
// Empty rp2040 namespace block to satisfy ci-custom's lint_namespace check.
|
||||
// HAL functions live in namespace esphome (root) — they are not part of the
|
||||
// rp2040 component's API.
|
||||
namespace esphome::rp2040 {} // namespace esphome::rp2040
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// yield(), delay(), micros(), millis(), millis_64(), delayMicroseconds(),
|
||||
// arch_feed_wdt(), arch_get_cpu_cycle_count() inlined in core/hal/hal_rp2040.h.
|
||||
void arch_restart() {
|
||||
watchdog_reboot(0, 0, 10);
|
||||
while (1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
void arch_init() {
|
||||
#ifdef USE_RP2040_CRASH_HANDLER
|
||||
rp2040::crash_handler_read_and_clear();
|
||||
#endif
|
||||
#if USE_RP2040_WATCHDOG_TIMEOUT > 0
|
||||
watchdog_enable(USE_RP2040_WATCHDOG_TIMEOUT, false);
|
||||
#endif
|
||||
}
|
||||
|
||||
uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); }
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_RP2040
|
||||
@@ -24,6 +24,7 @@ CONF_SENDSPIN_ID = "sendspin_id"
|
||||
|
||||
CONF_INITIAL_STATIC_DELAY = "initial_static_delay"
|
||||
CONF_FIXED_DELAY = "fixed_delay"
|
||||
CONF_DECODE_MEMORY = "decode_memory"
|
||||
|
||||
# sendspin-cpp library lives in the global `sendspin` namespace.
|
||||
sendspin_library_ns = cg.global_ns.namespace("sendspin")
|
||||
@@ -39,6 +40,18 @@ CODEC_FORMAT_UNSUPPORTED = SendspinCodecFormat.enum("UNSUPPORTED")
|
||||
AudioSupportedFormatObject = sendspin_library_ns.struct("AudioSupportedFormatObject")
|
||||
PlayerRoleConfig = sendspin_library_ns.struct("PlayerRoleConfig")
|
||||
|
||||
# MemoryLocation enum (from sendspin/types.h) controls SPIRAM-vs-internal-RAM placement
|
||||
# preference for the player role's transfer buffers.
|
||||
SendspinMemoryLocation = sendspin_library_ns.enum("MemoryLocation", is_class=True)
|
||||
|
||||
MEMORY_PSRAM = "psram"
|
||||
MEMORY_INTERNAL = "internal"
|
||||
MEMORY_LOCATIONS = [MEMORY_PSRAM, MEMORY_INTERNAL]
|
||||
MEMORY_LOCATION_ENUM = {
|
||||
MEMORY_PSRAM: SendspinMemoryLocation.PREFER_EXTERNAL,
|
||||
MEMORY_INTERNAL: SendspinMemoryLocation.PREFER_INTERNAL,
|
||||
}
|
||||
|
||||
# Trailing underscore avoids clashing with sendspin-cpp's global `sendspin` namespace.
|
||||
# Analysis tools strip the trailing underscore (same pattern as `template_`).
|
||||
sendspin_ns = cg.esphome_ns.namespace("sendspin_")
|
||||
@@ -193,7 +206,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
)
|
||||
|
||||
# sendspin-cpp library
|
||||
esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.3.1")
|
||||
esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.4.0")
|
||||
|
||||
cg.add_define("USE_SENDSPIN", True) # for MDNS
|
||||
|
||||
@@ -249,14 +262,23 @@ async def to_code(config: ConfigType) -> None:
|
||||
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
|
||||
)
|
||||
|
||||
player_config_struct = cg.StructInitializer(
|
||||
PlayerRoleConfig,
|
||||
# Library defaults: priority 18 (one above httpd_priority 17 so the decoder is not
|
||||
# starved by the HTTP server during the initial encoded-audio burst at stream start),
|
||||
# interpolation/decode buffer locations PREFER_EXTERNAL.
|
||||
player_struct_fields = [
|
||||
("audio_formats", audio_format_structs),
|
||||
("audio_buffer_capacity", player_cfg[CONF_BUFFER_SIZE]),
|
||||
("fixed_delay_us", player_cfg[CONF_FIXED_DELAY]),
|
||||
("initial_static_delay_ms", player_cfg[CONF_INITIAL_STATIC_DELAY]),
|
||||
("psram_stack", psram_stack),
|
||||
("priority", 2),
|
||||
]
|
||||
if (decode_memory := player_cfg.get(CONF_DECODE_MEMORY)) is not None:
|
||||
player_struct_fields.append(
|
||||
("decode_buffer_location", MEMORY_LOCATION_ENUM[decode_memory])
|
||||
)
|
||||
player_config_struct = cg.StructInitializer(
|
||||
PlayerRoleConfig,
|
||||
*player_struct_fields,
|
||||
)
|
||||
cg.add(var.set_player_config(player_config_struct))
|
||||
else:
|
||||
|
||||
@@ -13,9 +13,11 @@ from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
CONF_DECODE_MEMORY,
|
||||
CONF_FIXED_DELAY,
|
||||
CONF_INITIAL_STATIC_DELAY,
|
||||
CONF_SENDSPIN_ID,
|
||||
MEMORY_LOCATIONS,
|
||||
SendspinHub,
|
||||
_validate_task_stack_in_psram,
|
||||
register_player_config,
|
||||
@@ -57,6 +59,7 @@ def _register(config: ConfigType) -> ConfigType:
|
||||
CONF_INITIAL_STATIC_DELAY: config[CONF_INITIAL_STATIC_DELAY],
|
||||
CONF_FIXED_DELAY: config[CONF_FIXED_DELAY],
|
||||
CONF_TASK_STACK_IN_PSRAM: config.get(CONF_TASK_STACK_IN_PSRAM, False),
|
||||
CONF_DECODE_MEMORY: config.get(CONF_DECODE_MEMORY),
|
||||
}
|
||||
)
|
||||
return config
|
||||
@@ -82,6 +85,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_SAMPLE_RATE, default=48000): cv.int_range(
|
||||
min=16000, max=96000
|
||||
),
|
||||
cv.Optional(CONF_DECODE_MEMORY): cv.one_of(*MEMORY_LOCATIONS, lower=True),
|
||||
}
|
||||
),
|
||||
cv.only_on_esp32,
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#ifdef USE_ZEPHYR
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/drivers/watchdog.h>
|
||||
#include <zephyr/sys/reboot.h>
|
||||
#include <zephyr/random/random.h>
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -10,55 +8,7 @@
|
||||
|
||||
namespace esphome {
|
||||
|
||||
#ifdef CONFIG_WATCHDOG
|
||||
static int wdt_channel_id = -1; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0));
|
||||
#endif
|
||||
|
||||
void yield() { ::k_yield(); }
|
||||
uint32_t millis() { return static_cast<uint32_t>(millis_64()); }
|
||||
uint64_t millis_64() { return static_cast<uint64_t>(k_uptime_get()); }
|
||||
uint32_t micros() { return k_ticks_to_us_floor32(k_uptime_ticks()); }
|
||||
void delayMicroseconds(uint32_t us) { ::k_usleep(us); }
|
||||
void delay(uint32_t ms) { ::k_msleep(ms); }
|
||||
|
||||
void arch_init() {
|
||||
#ifdef CONFIG_WATCHDOG
|
||||
if (device_is_ready(WDT)) {
|
||||
static wdt_timeout_cfg wdt_config{};
|
||||
wdt_config.flags = WDT_FLAG_RESET_SOC;
|
||||
#ifdef USE_ZIGBEE
|
||||
// zboss thread use a lot of cpu cycles during start
|
||||
wdt_config.window.max = 10000;
|
||||
#else
|
||||
wdt_config.window.max = 2000;
|
||||
#endif
|
||||
wdt_channel_id = wdt_install_timeout(WDT, &wdt_config);
|
||||
if (wdt_channel_id >= 0) {
|
||||
uint8_t options = 0;
|
||||
#ifdef USE_DEBUG
|
||||
options |= WDT_OPT_PAUSE_HALTED_BY_DBG;
|
||||
#endif
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
options |= WDT_OPT_PAUSE_IN_SLEEP;
|
||||
#endif
|
||||
wdt_setup(WDT, options);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void arch_feed_wdt() {
|
||||
#ifdef CONFIG_WATCHDOG
|
||||
if (wdt_channel_id >= 0) {
|
||||
wdt_feed(WDT, wdt_channel_id);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void arch_restart() { sys_reboot(SYS_REBOOT_COLD); }
|
||||
uint32_t arch_get_cpu_cycle_count() { return k_cycle_get_32(); }
|
||||
uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); }
|
||||
// HAL functions live in hal.cpp.
|
||||
|
||||
Mutex::Mutex() {
|
||||
auto *mutex = new k_mutex();
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#ifdef USE_ZEPHYR
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
#include <zephyr/drivers/watchdog.h>
|
||||
#include <zephyr/sys/reboot.h>
|
||||
|
||||
// Empty zephyr namespace block to satisfy ci-custom's lint_namespace check.
|
||||
// HAL functions live in namespace esphome (root) — they are not part of the
|
||||
// zephyr component's API.
|
||||
namespace esphome::zephyr {} // namespace esphome::zephyr
|
||||
|
||||
namespace esphome {
|
||||
|
||||
#ifdef CONFIG_WATCHDOG
|
||||
static int wdt_channel_id = -1; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static const device *const WDT = DEVICE_DT_GET(DT_ALIAS(watchdog0));
|
||||
#endif
|
||||
|
||||
// yield(), delay(), micros(), millis(), millis_64(), delayMicroseconds(),
|
||||
// arch_get_cpu_cycle_count(), arch_get_cpu_freq_hz() inlined in
|
||||
// core/hal/hal_zephyr.h.
|
||||
|
||||
void arch_init() {
|
||||
#ifdef CONFIG_WATCHDOG
|
||||
if (device_is_ready(WDT)) {
|
||||
static wdt_timeout_cfg wdt_config{};
|
||||
wdt_config.flags = WDT_FLAG_RESET_SOC;
|
||||
#ifdef USE_ZIGBEE
|
||||
// zboss thread uses a lot of CPU cycles during startup
|
||||
wdt_config.window.max = 10000;
|
||||
#else
|
||||
wdt_config.window.max = 2000;
|
||||
#endif
|
||||
wdt_channel_id = wdt_install_timeout(WDT, &wdt_config);
|
||||
if (wdt_channel_id >= 0) {
|
||||
uint8_t options = 0;
|
||||
#ifdef USE_DEBUG
|
||||
options |= WDT_OPT_PAUSE_HALTED_BY_DBG;
|
||||
#endif
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
options |= WDT_OPT_PAUSE_IN_SLEEP;
|
||||
#endif
|
||||
wdt_setup(WDT, options);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void arch_feed_wdt() {
|
||||
#ifdef CONFIG_WATCHDOG
|
||||
if (wdt_channel_id >= 0) {
|
||||
wdt_feed(WDT, wdt_channel_id);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void arch_restart() { sys_reboot(SYS_REBOOT_COLD); }
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ZEPHYR
|
||||
@@ -9,6 +9,7 @@ from esphome.components.esp32 import (
|
||||
add_idf_component,
|
||||
add_idf_sdkconfig_option,
|
||||
add_partition,
|
||||
idf_version,
|
||||
require_vfs_select,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
@@ -186,6 +187,10 @@ async def _zigbee_add_sdkconfigs(config: ConfigType) -> None:
|
||||
# The pre-built Zigbee library uses esp_log_default_level which requires
|
||||
# dynamic log level control to be enabled
|
||||
add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", True)
|
||||
# The pre-built Zigbee library is compiled against newlib which requires newlib
|
||||
# reentrancy to be enabled with picolibc compatibility.
|
||||
if idf_version() >= cv.Version(6, 0, 0):
|
||||
add_idf_sdkconfig_option("CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY", True)
|
||||
|
||||
|
||||
async def attributes_to_code(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#ifdef USE_HOST
|
||||
|
||||
#include <cstdint>
|
||||
#include <sched.h>
|
||||
|
||||
#define IRAM_ATTR
|
||||
#define PROGMEM
|
||||
@@ -13,17 +14,18 @@ namespace esphome {
|
||||
/// Host has no ISR concept.
|
||||
__attribute__((always_inline)) inline bool in_isr_context() { return false; }
|
||||
|
||||
void yield();
|
||||
__attribute__((always_inline)) inline void yield() { ::sched_yield(); }
|
||||
|
||||
void delay(uint32_t ms);
|
||||
uint32_t micros();
|
||||
uint32_t millis();
|
||||
uint64_t millis_64();
|
||||
|
||||
void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming)
|
||||
void arch_feed_wdt();
|
||||
uint32_t arch_get_cpu_cycle_count();
|
||||
void arch_init();
|
||||
uint32_t arch_get_cpu_freq_hz();
|
||||
|
||||
__attribute__((always_inline)) inline void arch_init() {}
|
||||
__attribute__((always_inline)) inline void arch_feed_wdt() {}
|
||||
__attribute__((always_inline)) inline uint32_t arch_get_cpu_freq_hz() { return 1000000000U; }
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
|
||||
@@ -20,8 +20,17 @@ extern "C" unsigned long millis(void);
|
||||
// Forward decl from <pico/time.h>.
|
||||
extern "C" uint64_t time_us_64(void);
|
||||
|
||||
// Forward decls from pico-sdk / FreeRTOS port for the inline arch_*
|
||||
// wrappers below.
|
||||
extern "C" void watchdog_update(void);
|
||||
extern "C" unsigned long ulMainGetRunTimeCounterValue(void);
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// Forward decl from helpers.h.
|
||||
// NOLINTNEXTLINE(readability-redundant-declaration)
|
||||
void delay_microseconds_safe(uint32_t us);
|
||||
|
||||
/// Returns true when executing inside an interrupt handler.
|
||||
__attribute__((always_inline)) inline bool in_isr_context() {
|
||||
uint32_t ipsr;
|
||||
@@ -35,9 +44,13 @@ __attribute__((always_inline)) inline uint32_t micros() { return static_cast<uin
|
||||
__attribute__((always_inline)) inline uint32_t millis() { return micros_to_millis(::time_us_64()); }
|
||||
__attribute__((always_inline)) inline uint64_t millis_64() { return micros_to_millis<uint64_t>(::time_us_64()); }
|
||||
|
||||
void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming)
|
||||
void arch_feed_wdt();
|
||||
uint32_t arch_get_cpu_cycle_count();
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
__attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
|
||||
__attribute__((always_inline)) inline void arch_feed_wdt() { watchdog_update(); }
|
||||
__attribute__((always_inline)) inline uint32_t arch_get_cpu_cycle_count() {
|
||||
return static_cast<uint32_t>(ulMainGetRunTimeCounterValue());
|
||||
}
|
||||
|
||||
void arch_init();
|
||||
uint32_t arch_get_cpu_freq_hz();
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
|
||||
#define IRAM_ATTR
|
||||
#define PROGMEM
|
||||
|
||||
@@ -13,17 +15,19 @@ namespace esphome {
|
||||
/// Zephyr/nRF52: not currently consulted — wake path is platform-specific.
|
||||
__attribute__((always_inline)) inline bool in_isr_context() { return false; }
|
||||
|
||||
void yield();
|
||||
void delay(uint32_t ms);
|
||||
uint32_t micros();
|
||||
uint32_t millis();
|
||||
uint64_t millis_64();
|
||||
__attribute__((always_inline)) inline void yield() { ::k_yield(); }
|
||||
__attribute__((always_inline)) inline void delay(uint32_t ms) { ::k_msleep(ms); }
|
||||
__attribute__((always_inline)) inline uint32_t micros() { return k_ticks_to_us_floor32(k_uptime_ticks()); }
|
||||
__attribute__((always_inline)) inline uint64_t millis_64() { return static_cast<uint64_t>(k_uptime_get()); }
|
||||
__attribute__((always_inline)) inline uint32_t millis() { return static_cast<uint32_t>(millis_64()); }
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
__attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { ::k_usleep(us); }
|
||||
__attribute__((always_inline)) inline uint32_t arch_get_cpu_cycle_count() { return k_cycle_get_32(); }
|
||||
__attribute__((always_inline)) inline uint32_t arch_get_cpu_freq_hz() { return sys_clock_hw_cycles_per_sec(); }
|
||||
|
||||
void delayMicroseconds(uint32_t us); // NOLINT(readability-identifier-naming)
|
||||
void arch_feed_wdt();
|
||||
uint32_t arch_get_cpu_cycle_count();
|
||||
void arch_init();
|
||||
uint32_t arch_get_cpu_freq_hz();
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
|
||||
+41
-20
@@ -2045,7 +2045,8 @@ void delay_microseconds_safe(uint32_t us);
|
||||
* Returns `nullptr` in case no memory is available.
|
||||
*
|
||||
* By setting flags, it can be configured to:
|
||||
* - perform external allocation falling back to main memory if SPI RAM is full or unavailable
|
||||
* - perform external allocation falling back to internal memory if SPI RAM is full or unavailable (default)
|
||||
* - perform internal allocation falling back to external memory (with PREFER_INTERNAL)
|
||||
* - perform external allocation only
|
||||
* - perform internal allocation only
|
||||
*/
|
||||
@@ -2054,16 +2055,26 @@ template<class T> class RAMAllocator {
|
||||
using value_type = T;
|
||||
|
||||
enum Flags {
|
||||
NONE = 0, // Perform external allocation and fall back to internal memory
|
||||
ALLOC_EXTERNAL = 1 << 0, // Perform external allocation only.
|
||||
ALLOC_INTERNAL = 1 << 1, // Perform internal allocation only.
|
||||
ALLOW_FAILURE = 1 << 2, // Does nothing. Kept for compatibility.
|
||||
NONE = 0, // Perform external allocation and fall back to internal memory
|
||||
ALLOC_EXTERNAL = 1 << 0, // Perform external allocation only.
|
||||
ALLOC_INTERNAL = 1 << 1, // Perform internal allocation only.
|
||||
ALLOW_FAILURE = 1 << 2, // Does nothing. Kept for compatibility.
|
||||
PREFER_INTERNAL = 1 << 3, // Perform internal allocation and fall back to external memory
|
||||
};
|
||||
|
||||
constexpr RAMAllocator() = default;
|
||||
constexpr RAMAllocator(uint8_t flags)
|
||||
: flags_((flags & (ALLOC_INTERNAL | ALLOC_EXTERNAL)) != 0 ? (flags & (ALLOC_INTERNAL | ALLOC_EXTERNAL))
|
||||
: (ALLOC_INTERNAL | ALLOC_EXTERNAL)) {}
|
||||
constexpr RAMAllocator(uint8_t flags) {
|
||||
if (flags & PREFER_INTERNAL) {
|
||||
this->flags_ = ALLOC_INTERNAL | ALLOC_EXTERNAL | PREFER_INTERNAL;
|
||||
return;
|
||||
}
|
||||
const uint8_t alloc_bits = flags & (ALLOC_INTERNAL | ALLOC_EXTERNAL);
|
||||
if (alloc_bits != 0) {
|
||||
this->flags_ = alloc_bits;
|
||||
return;
|
||||
}
|
||||
this->flags_ = ALLOC_INTERNAL | ALLOC_EXTERNAL;
|
||||
}
|
||||
template<class U> constexpr RAMAllocator(const RAMAllocator<U> &other) : flags_{other.flags_} {}
|
||||
|
||||
T *allocate(size_t n) { return this->allocate(n, sizeof(T)); }
|
||||
@@ -2072,12 +2083,8 @@ template<class T> class RAMAllocator {
|
||||
size_t size = n * manual_size;
|
||||
T *ptr = nullptr;
|
||||
#ifdef USE_ESP32
|
||||
if (this->flags_ & Flags::ALLOC_EXTERNAL) {
|
||||
ptr = static_cast<T *>(heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
|
||||
}
|
||||
if (ptr == nullptr && this->flags_ & Flags::ALLOC_INTERNAL) {
|
||||
ptr = static_cast<T *>(heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT));
|
||||
}
|
||||
const auto caps = this->get_caps_();
|
||||
ptr = static_cast<T *>(heap_caps_malloc_prefer(size, 2, caps[0], caps[1]));
|
||||
#else
|
||||
// Ignore ALLOC_EXTERNAL/ALLOC_INTERNAL flags if external allocation is not supported
|
||||
ptr = static_cast<T *>(malloc(size)); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc)
|
||||
@@ -2091,12 +2098,8 @@ template<class T> class RAMAllocator {
|
||||
size_t size = n * manual_size;
|
||||
T *ptr = nullptr;
|
||||
#ifdef USE_ESP32
|
||||
if (this->flags_ & Flags::ALLOC_EXTERNAL) {
|
||||
ptr = static_cast<T *>(heap_caps_realloc(p, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
|
||||
}
|
||||
if (ptr == nullptr && this->flags_ & Flags::ALLOC_INTERNAL) {
|
||||
ptr = static_cast<T *>(heap_caps_realloc(p, size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT));
|
||||
}
|
||||
const auto caps = this->get_caps_();
|
||||
ptr = static_cast<T *>(heap_caps_realloc_prefer(p, size, 2, caps[0], caps[1]));
|
||||
#else
|
||||
// Ignore ALLOC_EXTERNAL/ALLOC_INTERNAL flags if external allocation is not supported
|
||||
ptr = static_cast<T *>(realloc(p, size)); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc)
|
||||
@@ -2147,6 +2150,24 @@ template<class T> class RAMAllocator {
|
||||
}
|
||||
|
||||
private:
|
||||
#ifdef USE_ESP32
|
||||
/// Returns {primary_caps, fallback_caps} for heap_caps_*_prefer based on the configured flags.
|
||||
/// PREFER_INTERNAL implies both regions are enabled (enforced by the constructor), so when it is set
|
||||
/// the primary is internal and the fallback is external. Otherwise the primary is whichever region
|
||||
/// is enabled (external preferred when both are enabled), and the fallback is the other region (or
|
||||
/// the same region when only one is enabled, making the second attempt a no-op).
|
||||
std::array<uint32_t, 2> get_caps_() const {
|
||||
constexpr uint32_t external_caps = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT;
|
||||
constexpr uint32_t internal_caps = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
|
||||
if (this->flags_ & PREFER_INTERNAL) {
|
||||
return {internal_caps, external_caps};
|
||||
}
|
||||
const uint32_t primary = (this->flags_ & ALLOC_EXTERNAL) ? external_caps : internal_caps;
|
||||
const uint32_t fallback = (this->flags_ & ALLOC_INTERNAL) ? internal_caps : external_caps;
|
||||
return {primary, fallback};
|
||||
}
|
||||
#endif
|
||||
|
||||
uint8_t flags_{ALLOC_INTERNAL | ALLOC_EXTERNAL};
|
||||
};
|
||||
|
||||
|
||||
@@ -20,15 +20,19 @@ dependencies:
|
||||
espressif/mdns:
|
||||
version: 1.11.0
|
||||
espressif/esp_wifi_remote:
|
||||
version: 1.4.0
|
||||
version: 1.5.1
|
||||
rules:
|
||||
- if: "target in [esp32h2, esp32p4]"
|
||||
espressif/wifi_remote_over_eppp:
|
||||
version: 0.3.2
|
||||
rules:
|
||||
- if: "target in [esp32h2, esp32p4]"
|
||||
espressif/eppp_link:
|
||||
version: 1.1.4
|
||||
version: 1.1.5
|
||||
rules:
|
||||
- if: "target in [esp32h2, esp32p4]"
|
||||
espressif/esp_hosted:
|
||||
version: 2.12.1
|
||||
version: 2.12.6
|
||||
rules:
|
||||
- if: "target in [esp32h2, esp32p4]"
|
||||
zorxx/multipart-parser:
|
||||
@@ -92,6 +96,6 @@ dependencies:
|
||||
esp32async/asynctcp:
|
||||
version: 3.4.91
|
||||
sendspin/sendspin-cpp:
|
||||
version: 0.3.1
|
||||
version: 0.4.0
|
||||
lvgl/lvgl:
|
||||
version: 9.5.0
|
||||
|
||||
@@ -6,8 +6,7 @@ what files have changed. It outputs JSON with the following structure:
|
||||
|
||||
{
|
||||
"integration_tests": true/false,
|
||||
"integration_tests_run_all": true/false,
|
||||
"integration_test_files": ["tests/integration/test_foo.py", ...],
|
||||
"integration_test_buckets": [{"name": "1/3", "tests": ["tests/integration/test_foo.py", ...]}, ...],
|
||||
"clang_tidy": true/false,
|
||||
"clang_format": true/false,
|
||||
"python_linters": true/false,
|
||||
@@ -81,6 +80,62 @@ CLANG_TIDY_SPLIT_THRESHOLD = 65
|
||||
# Isolated components count as 10x, groupable components count as 1x
|
||||
COMPONENT_TEST_BATCH_SIZE = 40
|
||||
|
||||
# Integration test bucketing: when more than the threshold tests are scheduled,
|
||||
# fan out across this many parallel jobs. Below the threshold, a single job runs.
|
||||
INTEGRATION_TESTS_SPLIT_THRESHOLD = 10
|
||||
INTEGRATION_TESTS_SPLIT_BUCKETS = 3
|
||||
|
||||
|
||||
def _split_list(items: list[str], n: int) -> list[list[str]]:
|
||||
"""Split a list into n roughly-equal contiguous parts (matches script/clang-tidy)."""
|
||||
k, m = divmod(len(items), n)
|
||||
return [items[i * k + min(i, m) : (i + 1) * k + min(i + 1, m)] for i in range(n)]
|
||||
|
||||
|
||||
def _all_integration_test_files() -> list[str]:
|
||||
"""Return all integration test file paths, sorted, relative to repo root."""
|
||||
return sorted(
|
||||
str(p.relative_to(root_path))
|
||||
for p in (Path(root_path) / "tests" / "integration").glob("test_*.py")
|
||||
)
|
||||
|
||||
|
||||
def _compute_integration_test_buckets(
|
||||
integration_run_all: bool,
|
||||
integration_test_files: list[str],
|
||||
) -> tuple[bool, list[dict[str, Any]]]:
|
||||
"""Compute (run_integration, buckets) from the determine_integration_tests result.
|
||||
|
||||
Pure function for unit testing — no I/O beyond `_all_integration_test_files`
|
||||
when `integration_run_all` is set.
|
||||
|
||||
`buckets` is a list of `{name, tests}` dicts where `tests` is a JSON-friendly
|
||||
list of file paths so the workflow can build a bash array via jq, avoiding
|
||||
shell word-splitting / glob hazards.
|
||||
"""
|
||||
if integration_run_all:
|
||||
files = _all_integration_test_files()
|
||||
else:
|
||||
files = sorted(integration_test_files)
|
||||
|
||||
# Empty list (e.g. run_all expansion with no files on disk) would otherwise
|
||||
# cause the workflow to invoke pytest with no path argument and collect
|
||||
# tests outside tests/integration/. Suppress the run instead.
|
||||
if not files:
|
||||
return False, []
|
||||
|
||||
if len(files) > INTEGRATION_TESTS_SPLIT_THRESHOLD:
|
||||
parts = [
|
||||
part for part in _split_list(files, INTEGRATION_TESTS_SPLIT_BUCKETS) if part
|
||||
]
|
||||
buckets = [
|
||||
{"name": f"{i + 1}/{len(parts)}", "tests": part}
|
||||
for i, part in enumerate(parts)
|
||||
]
|
||||
else:
|
||||
buckets = [{"name": "1/1", "tests": files}]
|
||||
return True, buckets
|
||||
|
||||
|
||||
class Platform(StrEnum):
|
||||
"""Platform identifiers for memory impact analysis."""
|
||||
@@ -812,7 +867,9 @@ def main() -> None:
|
||||
integration_run_all, integration_test_files = determine_integration_tests(
|
||||
args.branch
|
||||
)
|
||||
run_integration = integration_run_all or bool(integration_test_files)
|
||||
run_integration, integration_test_buckets = _compute_integration_test_buckets(
|
||||
integration_run_all, integration_test_files
|
||||
)
|
||||
run_clang_tidy = should_run_clang_tidy(args.branch)
|
||||
run_clang_format = should_run_clang_format(args.branch)
|
||||
run_python_linters = should_run_python_linters(args.branch)
|
||||
@@ -944,8 +1001,7 @@ def main() -> None:
|
||||
|
||||
output: dict[str, Any] = {
|
||||
"integration_tests": run_integration,
|
||||
"integration_tests_run_all": integration_run_all,
|
||||
"integration_test_files": integration_test_files,
|
||||
"integration_test_buckets": integration_test_buckets,
|
||||
"clang_tidy": run_clang_tidy,
|
||||
"clang_tidy_mode": clang_tidy_mode,
|
||||
"clang_format": run_clang_format,
|
||||
|
||||
@@ -7,3 +7,4 @@ media_source:
|
||||
initial_static_delay: 5ms
|
||||
static_delay_adjustable: true
|
||||
fixed_delay: 480us
|
||||
decode_memory: internal
|
||||
|
||||
@@ -122,10 +122,19 @@ def test_main_all_tests_should_run(
|
||||
"esphome/helpers.py",
|
||||
]
|
||||
|
||||
# Stable, deterministic stand-in for the tests/integration/ glob so the
|
||||
# bucket assertions don't drift with the real test count.
|
||||
fake_test_files = [f"tests/integration/test_{i:03d}.py" for i in range(15)]
|
||||
|
||||
# Run main function with mocked argv
|
||||
with (
|
||||
patch("sys.argv", ["determine-jobs.py"]),
|
||||
patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"_all_integration_test_files",
|
||||
return_value=fake_test_files,
|
||||
),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"get_changed_components",
|
||||
@@ -161,8 +170,24 @@ def test_main_all_tests_should_run(
|
||||
output = json.loads(captured.out)
|
||||
|
||||
assert output["integration_tests"] is True
|
||||
assert output["integration_tests_run_all"] is True
|
||||
assert output["integration_test_files"] == []
|
||||
# run_all=True expands to the full glob and pre-buckets into 3 parts.
|
||||
# Each bucket's `tests` is a JSON list of file paths.
|
||||
assert isinstance(output["integration_test_buckets"], list)
|
||||
assert len(output["integration_test_buckets"]) == 3
|
||||
assert [b["name"] for b in output["integration_test_buckets"]] == [
|
||||
"1/3",
|
||||
"2/3",
|
||||
"3/3",
|
||||
]
|
||||
for bucket in output["integration_test_buckets"]:
|
||||
assert isinstance(bucket["tests"], list)
|
||||
for path in bucket["tests"]:
|
||||
assert isinstance(path, str)
|
||||
bucket_files = [f for b in output["integration_test_buckets"] for f in b["tests"]]
|
||||
assert bucket_files == fake_test_files
|
||||
# Bucket sizes are balanced (max-min difference at most 1).
|
||||
sizes = [len(b["tests"]) for b in output["integration_test_buckets"]]
|
||||
assert max(sizes) - min(sizes) <= 1
|
||||
assert output["clang_tidy"] is True
|
||||
assert output["clang_tidy_mode"] in ["nosplit", "split"]
|
||||
assert output["clang_format"] is True
|
||||
@@ -247,8 +272,7 @@ def test_main_no_tests_should_run(
|
||||
output = json.loads(captured.out)
|
||||
|
||||
assert output["integration_tests"] is False
|
||||
assert output["integration_tests_run_all"] is False
|
||||
assert output["integration_test_files"] == []
|
||||
assert output["integration_test_buckets"] == []
|
||||
assert output["clang_tidy"] is False
|
||||
assert output["clang_tidy_mode"] == "disabled"
|
||||
assert output["clang_format"] is False
|
||||
@@ -332,8 +356,7 @@ def test_main_with_branch_argument(
|
||||
output = json.loads(captured.out)
|
||||
|
||||
assert output["integration_tests"] is False
|
||||
assert output["integration_tests_run_all"] is False
|
||||
assert output["integration_test_files"] == []
|
||||
assert output["integration_test_buckets"] == []
|
||||
assert output["clang_tidy"] is True
|
||||
assert output["clang_tidy_mode"] in ["nosplit", "split"]
|
||||
assert output["clang_format"] is False
|
||||
@@ -357,6 +380,59 @@ def test_main_with_branch_argument(
|
||||
assert output["cpp_unit_tests_components"] == ["mqtt"]
|
||||
|
||||
|
||||
def test_compute_integration_test_buckets_empty() -> None:
|
||||
"""No integration tests scheduled => (False, [])."""
|
||||
run, buckets = determine_jobs._compute_integration_test_buckets(False, [])
|
||||
assert run is False
|
||||
assert buckets == []
|
||||
|
||||
|
||||
def test_compute_integration_test_buckets_below_threshold() -> None:
|
||||
"""A small explicit list (<= threshold) => single 1/1 bucket with that list."""
|
||||
files = [f"tests/integration/test_{name}.py" for name in ("c", "a", "b")]
|
||||
run, buckets = determine_jobs._compute_integration_test_buckets(False, files)
|
||||
assert run is True
|
||||
assert buckets == [{"name": "1/1", "tests": sorted(files)}]
|
||||
|
||||
|
||||
def test_compute_integration_test_buckets_at_threshold_stays_single() -> None:
|
||||
"""Exactly INTEGRATION_TESTS_SPLIT_THRESHOLD files => still one bucket
|
||||
(the split kicks in only when count is strictly greater than threshold)."""
|
||||
files = [
|
||||
f"tests/integration/test_{i:02d}.py"
|
||||
for i in range(determine_jobs.INTEGRATION_TESTS_SPLIT_THRESHOLD)
|
||||
]
|
||||
run, buckets = determine_jobs._compute_integration_test_buckets(False, files)
|
||||
assert run is True
|
||||
assert len(buckets) == 1
|
||||
assert buckets[0]["name"] == "1/1"
|
||||
assert buckets[0]["tests"] == sorted(files)
|
||||
|
||||
|
||||
def test_compute_integration_test_buckets_just_over_threshold_splits() -> None:
|
||||
"""One file over the threshold triggers the 3-bucket fan-out, balanced."""
|
||||
n = determine_jobs.INTEGRATION_TESTS_SPLIT_THRESHOLD + 1
|
||||
files = [f"tests/integration/test_{i:02d}.py" for i in range(n)]
|
||||
run, buckets = determine_jobs._compute_integration_test_buckets(False, files)
|
||||
assert run is True
|
||||
assert [b["name"] for b in buckets] == ["1/3", "2/3", "3/3"]
|
||||
union = [path for b in buckets for path in b["tests"]]
|
||||
assert union == sorted(files)
|
||||
sizes = [len(b["tests"]) for b in buckets]
|
||||
assert max(sizes) - min(sizes) <= 1
|
||||
|
||||
|
||||
def test_compute_integration_test_buckets_run_all_with_empty_glob_disables_run() -> (
|
||||
None
|
||||
):
|
||||
"""run_all=True but glob returns no files => run suppressed (otherwise
|
||||
pytest would collect tests outside tests/integration/)."""
|
||||
with patch.object(determine_jobs, "_all_integration_test_files", return_value=[]):
|
||||
run, buckets = determine_jobs._compute_integration_test_buckets(True, [])
|
||||
assert run is False
|
||||
assert buckets == []
|
||||
|
||||
|
||||
def test_determine_integration_tests(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user