mirror of
https://github.com/esphome/esphome.git
synced 2026-09-24 05:24:14 +00:00
Merge branch 'dev' into partition-table-ota
This commit is contained in:
@@ -27,9 +27,9 @@ jobs:
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Auto Label PR
|
||||
|
||||
@@ -8,4 +8,4 @@ on:
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@3c4e8446aa1029f1c346a482034b3ee1489077ca # 2026.4.0
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@025a1e6255610c498ed590403b7e510b69e474df # 2026.4.1
|
||||
|
||||
@@ -223,7 +223,7 @@ jobs:
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
owner: esphome
|
||||
repositories: home-assistant-addon
|
||||
@@ -258,7 +258,7 @@ jobs:
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
owner: esphome
|
||||
repositories: esphome-schema
|
||||
@@ -289,7 +289,7 @@ jobs:
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
owner: esphome
|
||||
repositories: version-notifier
|
||||
|
||||
@@ -729,6 +729,9 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"dev": cv.Version(5, 5, 4),
|
||||
}
|
||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(
|
||||
6, 0, 1
|
||||
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
|
||||
cv.Version(
|
||||
6, 0, 0
|
||||
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
|
||||
|
||||
@@ -3,98 +3,12 @@
|
||||
#include "core.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/time_64.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "preferences.h"
|
||||
#include <Arduino.h>
|
||||
#include <core_esp8266_features.h>
|
||||
|
||||
extern "C" {
|
||||
#include <user_interface.h>
|
||||
}
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// yield(), micros(), millis_64() inlined in hal.h.
|
||||
// Fast accumulator replacement for Arduino's millis() (~3.3 μs via 4× 64-bit
|
||||
// multiplies on the LX106). Tracks a running ms counter from 32-bit
|
||||
// system_get_time() deltas using pure 32-bit ops. Installed as __wrap_millis
|
||||
// (via -Wl,--wrap=millis) so Arduino libs and IRAM_ATTR ISR handlers (e.g.
|
||||
// Wiegand, ZyAura) also get the fast version. xt_rsil(15) guards the static
|
||||
// state against ISR re-entry; the critical section is bounded (≤10 while-loop
|
||||
// iterations, ~100 ns on the common path, or a constant-time /1000 ~2.5 μs on
|
||||
// the rare path — well under WiFi's ~10 μs ISR latency budget). NMIs (level
|
||||
// >15) are not masked, but the ESP8266 SDK's NMI handlers don't call millis().
|
||||
//
|
||||
// system_get_time() wraps every ~71.6 min; unsigned (now_us - last_us) handles
|
||||
// one wrap. The main loop calls millis() at 60+ Hz, so delta stays tiny — a
|
||||
// >71 min block would trip the watchdog long before it could matter here.
|
||||
static constexpr uint32_t MILLIS_RARE_PATH_THRESHOLD_US = 10000;
|
||||
static constexpr uint32_t US_PER_MS = 1000;
|
||||
|
||||
uint32_t IRAM_ATTR HOT millis() {
|
||||
// Struct packs the three statics so the compiler loads one base address
|
||||
// instead of three separate literal pool entries (saves ~8 bytes IRAM).
|
||||
static struct {
|
||||
uint32_t cache;
|
||||
uint32_t remainder;
|
||||
uint32_t last_us;
|
||||
} state = {0, 0, 0};
|
||||
uint32_t ps = xt_rsil(15);
|
||||
uint32_t now_us = system_get_time();
|
||||
uint32_t delta = now_us - state.last_us;
|
||||
state.last_us = now_us;
|
||||
state.remainder += delta;
|
||||
if (state.remainder >= MILLIS_RARE_PATH_THRESHOLD_US) {
|
||||
// Rare path: large gap (WiFi scan, boot, long block). Constant-time
|
||||
// conversion keeps the critical section bounded.
|
||||
uint32_t ms = state.remainder / US_PER_MS;
|
||||
state.cache += ms;
|
||||
// Reuse ms instead of `remainder %= US_PER_MS` — `%` would compile to a
|
||||
// second __umodsi3 call on the LX106 (no hardware divide).
|
||||
state.remainder -= ms * US_PER_MS;
|
||||
} else {
|
||||
// Common path: small gap. At most ~10 iterations since remainder was
|
||||
// < threshold (10 ms) on entry and delta adds at most one more threshold
|
||||
// before exiting this branch.
|
||||
while (state.remainder >= US_PER_MS) {
|
||||
state.cache++;
|
||||
state.remainder -= US_PER_MS;
|
||||
}
|
||||
}
|
||||
uint32_t result = state.cache;
|
||||
xt_wsr_ps(ps);
|
||||
return result;
|
||||
}
|
||||
// Poll-based delay that avoids ::delay() — Arduino's __delay has an intra-object
|
||||
// call to the original millis() that --wrap can't intercept, so calling ::delay()
|
||||
// would keep the slow Arduino millis body alive in IRAM. optimistic_yield still
|
||||
// enters esp_schedule()/esp_suspend_within_cont() via yield(), so SDK tasks and
|
||||
// WiFi run correctly. Theoretically less power-efficient than Arduino's
|
||||
// os_timer-based delay() for long waits, but nearly all ESPHome delays are short
|
||||
// (sensor/I²C/SPI settling in the 1–100 ms range) where the difference is
|
||||
// negligible.
|
||||
void HOT delay(uint32_t ms) {
|
||||
if (ms == 0) {
|
||||
optimistic_yield(1000);
|
||||
return;
|
||||
}
|
||||
uint32_t start = millis();
|
||||
while (millis() - start < ms) {
|
||||
optimistic_yield(1000);
|
||||
}
|
||||
}
|
||||
// delayMicroseconds(), arch_feed_wdt(), and progmem_read_*() are inlined in hal/hal_esp8266.h.
|
||||
void arch_restart() {
|
||||
system_restart();
|
||||
// restart() doesn't always end execution
|
||||
while (true) { // NOLINT(clang-diagnostic-unreachable-code)
|
||||
yield();
|
||||
}
|
||||
}
|
||||
void arch_init() {}
|
||||
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); }
|
||||
uint32_t arch_get_cpu_freq_hz() { return F_CPU; }
|
||||
// HAL functions live in hal.cpp. This file keeps only the ESP8266-specific
|
||||
// firmware bootstrap (Tasmota OTA magic bytes, optional GPIO pre-init).
|
||||
|
||||
void force_link_symbols() {
|
||||
// Tasmota uses magic bytes in the binary to check if an OTA firmware is compatible
|
||||
@@ -131,12 +45,4 @@ extern "C" void resetPins() { // NOLINT
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
// Linker wrap: redirect all ::millis() calls (Arduino libs, ISRs) to our accumulator.
|
||||
// Requires -Wl,--wrap=millis in build flags (added by __init__.py).
|
||||
// NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
extern "C" uint32_t IRAM_ATTR __wrap_millis() { return esphome::millis(); }
|
||||
// Note: Arduino's init() registers a 60-second overflow timer for micros64().
|
||||
// We leave it running — wrapping init() as a no-op would break micros64()'s
|
||||
// overflow tracking, and the timer's cost is negligible (~3 μs per 60 s).
|
||||
|
||||
#endif // USE_ESP8266
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <core_esp8266_features.h>
|
||||
|
||||
extern "C" {
|
||||
#include <user_interface.h>
|
||||
}
|
||||
|
||||
// Empty esp8266 namespace block to satisfy ci-custom's lint_namespace check.
|
||||
// HAL functions live in namespace esphome (root) — they are not part of the
|
||||
// esp8266 component's API.
|
||||
namespace esphome::esp8266 {} // namespace esphome::esp8266
|
||||
|
||||
namespace esphome {
|
||||
|
||||
// yield(), micros(), millis_64(), delayMicroseconds(), arch_feed_wdt(),
|
||||
// progmem_read_*() are inlined in core/hal/hal_esp8266.h.
|
||||
//
|
||||
// Fast accumulator replacement for Arduino's millis() (~3.3 μs via 4× 64-bit
|
||||
// multiplies on the LX106). Tracks a running ms counter from 32-bit
|
||||
// system_get_time() deltas using pure 32-bit ops. Installed as __wrap_millis
|
||||
// (via -Wl,--wrap=millis) so Arduino libs and IRAM_ATTR ISR handlers (e.g.
|
||||
// Wiegand, ZyAura) also get the fast version. xt_rsil(15) guards the static
|
||||
// state against ISR re-entry; the critical section is bounded (≤10 while-loop
|
||||
// iterations, ~100 ns on the common path, or a constant-time /1000 ~2.5 μs on
|
||||
// the rare path — well under WiFi's ~10 μs ISR latency budget). NMIs (level
|
||||
// >15) are not masked, but the ESP8266 SDK's NMI handlers don't call millis().
|
||||
//
|
||||
// system_get_time() wraps every ~71.6 min; unsigned (now_us - last_us) handles
|
||||
// one wrap. The main loop calls millis() at 60+ Hz, so delta stays tiny — a
|
||||
// >71 min block would trip the watchdog long before it could matter here.
|
||||
static constexpr uint32_t MILLIS_RARE_PATH_THRESHOLD_US = 10000;
|
||||
static constexpr uint32_t US_PER_MS = 1000;
|
||||
|
||||
uint32_t IRAM_ATTR HOT millis() {
|
||||
// Struct packs the three statics so the compiler loads one base address
|
||||
// instead of three separate literal pool entries (saves ~8 bytes IRAM).
|
||||
static struct {
|
||||
uint32_t cache;
|
||||
uint32_t remainder;
|
||||
uint32_t last_us;
|
||||
} state = {0, 0, 0};
|
||||
uint32_t ps = xt_rsil(15);
|
||||
uint32_t now_us = system_get_time();
|
||||
uint32_t delta = now_us - state.last_us;
|
||||
state.last_us = now_us;
|
||||
state.remainder += delta;
|
||||
if (state.remainder >= MILLIS_RARE_PATH_THRESHOLD_US) {
|
||||
// Rare path: large gap (WiFi scan, boot, long block). Constant-time
|
||||
// conversion keeps the critical section bounded.
|
||||
uint32_t ms = state.remainder / US_PER_MS;
|
||||
state.cache += ms;
|
||||
// Reuse ms instead of `remainder %= US_PER_MS` — `%` would compile to a
|
||||
// second __umodsi3 call on the LX106 (no hardware divide).
|
||||
state.remainder -= ms * US_PER_MS;
|
||||
} else {
|
||||
// Common path: small gap. At most ~10 iterations since remainder was
|
||||
// < threshold (10 ms) on entry and delta adds at most one more threshold
|
||||
// before exiting this branch.
|
||||
while (state.remainder >= US_PER_MS) {
|
||||
state.cache++;
|
||||
state.remainder -= US_PER_MS;
|
||||
}
|
||||
}
|
||||
uint32_t result = state.cache;
|
||||
xt_wsr_ps(ps);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Poll-based delay that avoids ::delay() — Arduino's __delay has an intra-object
|
||||
// call to the original millis() that --wrap can't intercept, so calling ::delay()
|
||||
// would keep the slow Arduino millis body alive in IRAM. optimistic_yield still
|
||||
// enters esp_schedule()/esp_suspend_within_cont() via yield(), so SDK tasks and
|
||||
// WiFi run correctly. Theoretically less power-efficient than Arduino's
|
||||
// os_timer-based delay() for long waits, but nearly all ESPHome delays are short
|
||||
// (sensor/I²C/SPI settling in the 1–100 ms range) where the difference is
|
||||
// negligible.
|
||||
void HOT delay(uint32_t ms) {
|
||||
if (ms == 0) {
|
||||
optimistic_yield(1000);
|
||||
return;
|
||||
}
|
||||
uint32_t start = millis();
|
||||
while (millis() - start < ms) {
|
||||
optimistic_yield(1000);
|
||||
}
|
||||
}
|
||||
|
||||
void arch_restart() {
|
||||
system_restart();
|
||||
// restart() doesn't always end execution
|
||||
while (true) { // NOLINT(clang-diagnostic-unreachable-code)
|
||||
yield();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
// Linker wrap: redirect all ::millis() calls (Arduino libs, ISRs) to our accumulator.
|
||||
// Requires -Wl,--wrap=millis in build flags (added by __init__.py).
|
||||
// NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
|
||||
extern "C" uint32_t IRAM_ATTR __wrap_millis() { return esphome::millis(); }
|
||||
// Note: Arduino's init() registers a 60-second overflow timer for micros64().
|
||||
// We leave it running — wrapping init() as a no-op would break micros64()'s
|
||||
// overflow tracking, and the timer's cost is negligible (~3 μs per 60 s).
|
||||
|
||||
#endif // USE_ESP8266
|
||||
@@ -2,6 +2,7 @@ from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ALLOW_OTHER_USES,
|
||||
CONF_ID,
|
||||
CONF_INPUT,
|
||||
CONF_INTERRUPT,
|
||||
@@ -30,10 +31,29 @@ MCP23XXX_INTERRUPT_MODES = {
|
||||
"FALLING": MCP23XXXInterruptMode.MCP23XXX_FALLING,
|
||||
}
|
||||
|
||||
|
||||
def _validate_interrupt_pin(value):
|
||||
# The MCP component owns INT polarity (active-low, hardcoded falling-edge ISR)
|
||||
# and installs a single ISR per GPIO, so neither inversion nor sharing is supported.
|
||||
value = pins.internal_gpio_input_pin_schema(value)
|
||||
if value.get(CONF_INVERTED):
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_INVERTED}: true' is not supported on '{CONF_INTERRUPT_PIN}'; "
|
||||
"the MCP23xxx INT line is fixed active-low"
|
||||
)
|
||||
if value.get(CONF_ALLOW_OTHER_USES):
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_ALLOW_OTHER_USES}: true' is not supported on '{CONF_INTERRUPT_PIN}'; "
|
||||
"sharing the interrupt pin between multiple MCP23xxx (or other components) "
|
||||
"is not implemented. Remove the interrupt_pin to fall back to polling."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
MCP23XXX_CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_OPEN_DRAIN_INTERRUPT, default=False): cv.boolean,
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_INTERRUPT_PIN): _validate_interrupt_pin,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import esphome.codegen as cg
|
||||
|
||||
st7789v_ns = cg.esphome_ns.namespace("st7789v")
|
||||
|
||||
DEPRECATED_COMPONENT = """
|
||||
The 'st7789v' component is deprecated and no new functionality will be added to it.
|
||||
PRs should target the newer and more performant 'mipi_spi' component.
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display, power_supply, spi
|
||||
@@ -26,6 +28,8 @@ CODEOWNERS = ["@kbx81"]
|
||||
|
||||
DEPENDENCIES = ["spi"]
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ST7789V = st7789v_ns.class_(
|
||||
"ST7789V", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer
|
||||
)
|
||||
@@ -175,6 +179,9 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
LOGGER.warning(
|
||||
"The 'st7789v' component is deprecated, it is recommended to use 'mipi_spi' instead."
|
||||
)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import spi
|
||||
@@ -5,6 +7,8 @@ from esphome.components.const import CONF_CRC_ENABLE, CONF_ON_PACKET
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BUSY_PIN, CONF_DATA, CONF_FREQUENCY, CONF_ID
|
||||
from esphome.core import ID, TimePeriod
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType, TemplateArgsType
|
||||
|
||||
MULTI_CONF = True
|
||||
CODEOWNERS = ["@swoboda1337"]
|
||||
@@ -15,6 +19,7 @@ CONF_SX126X_ID = "sx126x_id"
|
||||
CONF_BANDWIDTH = "bandwidth"
|
||||
CONF_BITRATE = "bitrate"
|
||||
CONF_CODING_RATE = "coding_rate"
|
||||
CONF_COLD = "cold"
|
||||
CONF_CRC_INVERTED = "crc_inverted"
|
||||
CONF_CRC_SIZE = "crc_size"
|
||||
CONF_CRC_POLYNOMIAL = "crc_polynomial"
|
||||
@@ -144,7 +149,7 @@ SetModeStandbyAction = sx126x_ns.class_(
|
||||
)
|
||||
|
||||
|
||||
def validate_raw_data(value):
|
||||
def validate_raw_data(value: Any) -> bytes | list[int]:
|
||||
if isinstance(value, str):
|
||||
return value.encode("utf-8")
|
||||
if isinstance(value, list):
|
||||
@@ -154,7 +159,7 @@ def validate_raw_data(value):
|
||||
)
|
||||
|
||||
|
||||
def validate_config(config):
|
||||
def validate_config(config: ConfigType) -> ConfigType:
|
||||
lora_bws = [
|
||||
"7_8kHz",
|
||||
"10_4kHz",
|
||||
@@ -235,7 +240,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
@@ -307,24 +312,50 @@ NO_ARGS_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
NO_ARGS_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
@automation.register_action(
|
||||
"sx126x.set_mode_sleep",
|
||||
SetModeSleepAction,
|
||||
NO_ARGS_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
@automation.register_action(
|
||||
"sx126x.set_mode_standby",
|
||||
SetModeStandbyAction,
|
||||
NO_ARGS_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def no_args_action_to_code(config, action_id, template_arg, args):
|
||||
async def no_args_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
SET_MODE_SLEEP_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(SX126x),
|
||||
cv.Optional(CONF_COLD, default=False): cv.templatable(cv.boolean),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"sx126x.set_mode_sleep",
|
||||
SetModeSleepAction,
|
||||
SET_MODE_SLEEP_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def set_mode_sleep_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
template_ = await cg.templatable(config[CONF_COLD], args, bool)
|
||||
cg.add(var.set_cold(template_))
|
||||
return var
|
||||
|
||||
|
||||
SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(SX126x),
|
||||
@@ -340,7 +371,12 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
SEND_PACKET_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def send_packet_action_to_code(config, action_id, template_arg, args):
|
||||
async def send_packet_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
data = config[CONF_DATA]
|
||||
|
||||
@@ -56,7 +56,8 @@ template<typename... Ts> class SetModeRxAction : public Action<Ts...>, public Pa
|
||||
|
||||
template<typename... Ts> class SetModeSleepAction : public Action<Ts...>, public Parented<SX126x> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->set_mode_sleep(); }
|
||||
TEMPLATABLE_VALUE(bool, cold)
|
||||
void play(const Ts &...x) override { this->parent_->set_mode_sleep(this->cold_.value(x...)); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetModeStandbyAction : public Action<Ts...>, public Parented<SX126x> {
|
||||
|
||||
@@ -459,9 +459,10 @@ void SX126x::set_mode_tx() {
|
||||
this->write_opcode_(RADIO_SET_TX, buf, 3);
|
||||
}
|
||||
|
||||
void SX126x::set_mode_sleep() {
|
||||
void SX126x::set_mode_sleep(bool cold) {
|
||||
// 0x04 = warm start (config retained), 0x00 = cold start (config lost, lowest power)
|
||||
uint8_t buf[1];
|
||||
buf[0] = 0x05;
|
||||
buf[0] = cold ? 0x00 : 0x04;
|
||||
this->write_opcode_(RADIO_SET_SLEEP, buf, 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ class SX126x : public Component,
|
||||
void set_mode_rx();
|
||||
void set_mode_tx();
|
||||
void set_mode_standby(SX126xStandbyMode mode);
|
||||
void set_mode_sleep();
|
||||
void set_mode_sleep(bool cold = false);
|
||||
void set_modulation(uint8_t modulation) { this->modulation_ = modulation; }
|
||||
void set_pa_power(int8_t power) { this->pa_power_ = power; }
|
||||
void set_pa_ramp(uint8_t ramp) { this->pa_ramp_ = ramp; }
|
||||
|
||||
@@ -655,7 +655,15 @@ class WarnIfComponentBlockingGuard {
|
||||
// Inlined: the fast path is just millis() + subtract + compare
|
||||
inline uint32_t HOT finish() {
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
this->component_->runtime_stats_.record_time(micros() - this->started_us_);
|
||||
uint32_t elapsed_us = micros() - this->started_us_;
|
||||
// component_ is nullptr for self-keyed scheduler items (set_timeout/set_interval(self, ...))
|
||||
if (this->component_ != nullptr) {
|
||||
this->component_->runtime_stats_.record_time(elapsed_us);
|
||||
} else {
|
||||
// Still accumulate into the global counter so Application::loop() can subtract
|
||||
// this time from before_loop_tasks_ wall time.
|
||||
ComponentRuntimeStats::global_recorded_us += elapsed_us;
|
||||
}
|
||||
#endif
|
||||
uint32_t curr_time = MillisInternal::get();
|
||||
#ifndef USE_BENCHMARK
|
||||
|
||||
+3
-4
@@ -31,11 +31,10 @@
|
||||
namespace esphome {
|
||||
|
||||
// Cross-platform declarations. delayMicroseconds(), arch_feed_wdt(),
|
||||
// arch_get_cpu_cycle_count() vary per platform (some inline, some
|
||||
// out-of-line) so they live in hal/hal_<platform>.h.
|
||||
// arch_get_cpu_cycle_count(), arch_init(), arch_get_cpu_freq_hz() vary
|
||||
// per platform (some inline, some out-of-line) so they live in
|
||||
// hal/hal_<platform>.h.
|
||||
void __attribute__((noreturn)) arch_restart();
|
||||
void arch_init();
|
||||
uint32_t arch_get_cpu_freq_hz();
|
||||
|
||||
#ifndef USE_ESP8266
|
||||
// All non-ESP8266 platforms: PROGMEM is a no-op, so these are direct dereferences.
|
||||
|
||||
@@ -42,6 +42,9 @@ __attribute__((always_inline)) inline void delayMicroseconds(uint32_t us) { dela
|
||||
__attribute__((always_inline)) inline void arch_feed_wdt() { esp_task_wdt_reset(); }
|
||||
__attribute__((always_inline)) inline uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
|
||||
|
||||
void arch_init();
|
||||
uint32_t arch_get_cpu_freq_hz();
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
#include <c_types.h>
|
||||
#include <core_esp8266_features.h>
|
||||
#include <cstdint>
|
||||
#include <pgmspace.h>
|
||||
|
||||
@@ -59,8 +60,11 @@ __attribute__((always_inline)) inline uint16_t progmem_read_uint16(const uint16_
|
||||
// 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() { system_soft_wdt_feed(); }
|
||||
|
||||
uint32_t arch_get_cpu_cycle_count();
|
||||
__attribute__((always_inline)) inline void arch_init() {}
|
||||
// esp_get_cycle_count() declared in <core_esp8266_features.h>; F_CPU is a
|
||||
// compiler-driven macro from the ESP8266 Arduino board defs (-DF_CPU=...).
|
||||
__attribute__((always_inline)) inline uint32_t arch_get_cpu_cycle_count() { return esp_get_cycle_count(); }
|
||||
__attribute__((always_inline)) inline uint32_t arch_get_cpu_freq_hz() { return F_CPU; }
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ 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();
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
|
||||
@@ -91,6 +91,8 @@ __attribute__((always_inline)) inline uint64_t millis_64() { return Millis64Impl
|
||||
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
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ __attribute__((always_inline)) inline uint64_t millis_64() { return micros_to_mi
|
||||
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
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ 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();
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
|
||||
@@ -14,6 +14,37 @@ from esphome.util import run_external_process
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _strip_win_long_path_prefix(path: str) -> str:
|
||||
r"""Strip the Windows extended-length path prefix from ``path``.
|
||||
|
||||
Handles both forms documented at
|
||||
https://learn.microsoft.com/windows/win32/fileio/naming-a-file:
|
||||
|
||||
* ``\\?\C:\path\to\file`` -> ``C:\path\to\file``
|
||||
* ``\\?\UNC\server\share\path`` -> ``\\server\share\path``
|
||||
|
||||
The NSIS-installed ``esphome.exe`` launcher on Windows starts Python with
|
||||
``sys.executable`` already prefixed with ``\\?\``. That prefix propagates
|
||||
into PlatformIO's ``$PYTHONEXE`` (PlatformIO reads ``PYTHONEXEPATH`` from
|
||||
the environment, falling back to ``os.path.normpath(sys.executable)``)
|
||||
and ends up baked into SCons-emitted command lines for build steps such
|
||||
as the esp8266 ``elf2bin`` invocation. ``cmd.exe`` does not understand
|
||||
the ``\\?\`` prefix, so the build fails with
|
||||
"The system cannot find the path specified." Stripping the prefix early
|
||||
keeps the path shell-quotable.
|
||||
|
||||
No-op on non-Windows platforms.
|
||||
"""
|
||||
if sys.platform != "win32":
|
||||
return path
|
||||
if path.startswith("\\\\?\\UNC\\"):
|
||||
# \\?\UNC\server\share\... -> \\server\share\...
|
||||
return "\\\\" + path[len("\\\\?\\UNC\\") :]
|
||||
if path.startswith("\\\\?\\"):
|
||||
return path[len("\\\\?\\") :]
|
||||
return path
|
||||
|
||||
|
||||
def run_platformio_cli(*args, **kwargs) -> str | int:
|
||||
os.environ["PLATFORMIO_FORCE_COLOR"] = "true"
|
||||
os.environ["PLATFORMIO_BUILD_DIR"] = str(CORE.relative_pioenvs_path().absolute())
|
||||
@@ -24,7 +55,18 @@ def run_platformio_cli(*args, **kwargs) -> str | int:
|
||||
os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning")
|
||||
# Increase uv retry count to handle transient network errors (default is 3)
|
||||
os.environ.setdefault("UV_HTTP_RETRIES", "10")
|
||||
cmd = [sys.executable, "-m", "esphome.platformio_runner"] + list(args)
|
||||
# Strip the Windows extended-length path prefix from sys.executable so it
|
||||
# doesn't propagate into PlatformIO's $PYTHONEXE and break SCons-emitted
|
||||
# command lines run through cmd.exe.
|
||||
python_exe = _strip_win_long_path_prefix(sys.executable)
|
||||
if python_exe != sys.executable:
|
||||
# Only override PYTHONEXEPATH when we actually stripped a prefix.
|
||||
# PlatformIO's get_pythonexe_path() reads this and falls back to
|
||||
# sys.executable otherwise; setting it unconditionally would clobber
|
||||
# a user-provided value (or the unmodified path on platforms that
|
||||
# don't need the strip).
|
||||
os.environ["PYTHONEXEPATH"] = python_exe
|
||||
cmd = [python_exe, "-m", "esphome.platformio_runner"] + list(args)
|
||||
|
||||
return run_external_process(*cmd, **kwargs)
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ platformio==6.1.19
|
||||
esptool==5.2.0
|
||||
click==8.3.3
|
||||
esphome-dashboard==20260425.0
|
||||
aioesphomeapi==44.22.0
|
||||
aioesphomeapi==44.23.0
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.19.1 # dashboard_import
|
||||
|
||||
@@ -311,6 +311,105 @@ def test_run_platformio_cli_sets_environment_variables(
|
||||
assert "arg" in args
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("platform", "input_path", "expected"),
|
||||
[
|
||||
# win32: drive-letter extended-length prefix is stripped
|
||||
(
|
||||
"win32",
|
||||
"\\\\?\\C:\\Users\\jesse\\AppData\\Local\\ESPHome Builder\\python\\python.exe",
|
||||
"C:\\Users\\jesse\\AppData\\Local\\ESPHome Builder\\python\\python.exe",
|
||||
),
|
||||
# win32: UNC extended-length prefix is translated to a regular UNC path
|
||||
(
|
||||
"win32",
|
||||
"\\\\?\\UNC\\server\\share\\python.exe",
|
||||
"\\\\server\\share\\python.exe",
|
||||
),
|
||||
# win32: paths without the prefix are returned unchanged
|
||||
(
|
||||
"win32",
|
||||
"C:\\Users\\jesse\\AppData\\Local\\ESPHome Builder\\python\\python.exe",
|
||||
"C:\\Users\\jesse\\AppData\\Local\\ESPHome Builder\\python\\python.exe",
|
||||
),
|
||||
# non-win32: prefix is left alone (no-op)
|
||||
("linux", "\\\\?\\C:\\python.exe", "\\\\?\\C:\\python.exe"),
|
||||
("darwin", "/usr/bin/python3", "/usr/bin/python3"),
|
||||
],
|
||||
)
|
||||
def test_strip_win_long_path_prefix(
|
||||
platform: str, input_path: str, expected: str
|
||||
) -> None:
|
||||
r"""``\\?\`` and ``\\?\UNC\`` prefixes are stripped only on win32."""
|
||||
with patch("esphome.platformio_api.sys.platform", platform):
|
||||
assert platformio_api._strip_win_long_path_prefix(input_path) == expected
|
||||
|
||||
|
||||
def test_run_platformio_cli_strips_win_long_path_prefix(
|
||||
setup_core: Path, mock_run_external_process: Mock
|
||||
) -> None:
|
||||
r"""Windows ``\\?\`` prefix on sys.executable does not leak into the subprocess.
|
||||
|
||||
The NSIS-installed esphome.exe launcher starts Python with
|
||||
``sys.executable`` already prefixed by the extended-length path marker.
|
||||
That prefix would otherwise propagate into PlatformIO's ``PYTHONEXE`` and
|
||||
break SCons-emitted command lines run through ``cmd.exe``.
|
||||
"""
|
||||
CORE.build_path = str(setup_core / "build" / "test")
|
||||
prefixed_exe = (
|
||||
"\\\\?\\C:\\Users\\jesse\\AppData\\Local\\ESPHome Builder\\python\\python.exe"
|
||||
)
|
||||
stripped_exe = (
|
||||
"C:\\Users\\jesse\\AppData\\Local\\ESPHome Builder\\python\\python.exe"
|
||||
)
|
||||
|
||||
with (
|
||||
patch.dict(os.environ, {}, clear=False),
|
||||
patch("esphome.platformio_api.sys.platform", "win32"),
|
||||
patch("esphome.platformio_api.sys.executable", prefixed_exe),
|
||||
):
|
||||
# Pop any pre-existing PYTHONEXEPATH so the assertion below reflects
|
||||
# what run_platformio_cli set, not whatever the test runner's
|
||||
# environment happened to contain.
|
||||
os.environ.pop("PYTHONEXEPATH", None)
|
||||
mock_run_external_process.return_value = 0
|
||||
platformio_api.run_platformio_cli("test", "arg")
|
||||
|
||||
# The subprocess is invoked with the stripped executable path.
|
||||
mock_run_external_process.assert_called_once()
|
||||
args = mock_run_external_process.call_args[0]
|
||||
assert args[0] == stripped_exe
|
||||
# PYTHONEXEPATH is exported with the stripped path so PlatformIO's
|
||||
# get_pythonexe_path() picks it up in the subprocess.
|
||||
assert os.environ["PYTHONEXEPATH"] == stripped_exe
|
||||
|
||||
|
||||
def test_run_platformio_cli_does_not_set_pythonexepath_without_strip(
|
||||
setup_core: Path, mock_run_external_process: Mock
|
||||
) -> None:
|
||||
r"""PYTHONEXEPATH is not touched when sys.executable has no ``\\?\`` prefix.
|
||||
|
||||
Setting it unconditionally would clobber a user-provided value (or
|
||||
interfere with non-Windows tooling that has no prefix to strip).
|
||||
"""
|
||||
CORE.build_path = str(setup_core / "build" / "test")
|
||||
plain_exe = "/usr/bin/python3"
|
||||
|
||||
with (
|
||||
patch.dict(os.environ, {}, clear=False),
|
||||
patch("esphome.platformio_api.sys.platform", "linux"),
|
||||
patch("esphome.platformio_api.sys.executable", plain_exe),
|
||||
):
|
||||
os.environ.pop("PYTHONEXEPATH", None)
|
||||
mock_run_external_process.return_value = 0
|
||||
platformio_api.run_platformio_cli("test", "arg")
|
||||
|
||||
mock_run_external_process.assert_called_once()
|
||||
args = mock_run_external_process.call_args[0]
|
||||
assert args[0] == plain_exe
|
||||
assert "PYTHONEXEPATH" not in os.environ
|
||||
|
||||
|
||||
def test_run_platformio_cli_run_builds_command(
|
||||
setup_core: Path, mock_run_platformio_cli: Mock
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user