mirror of
https://github.com/esphome/esphome.git
synced 2026-09-11 23:37:34 +00:00
Merge branch 'extend_ultra_low_latency_select_libretiny' into integration
This commit is contained in:
@@ -6,8 +6,9 @@
|
||||
|
||||
- [ ] Bugfix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Developer breaking change (an API change that could break external components)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) — [policy](https://developers.esphome.io/contributing/code/#what-constitutes-a-c-breaking-change)
|
||||
- [ ] Developer breaking change (an API change that could break external components) — [policy](https://developers.esphome.io/contributing/code/#what-is-considered-public-c-api)
|
||||
- [ ] Undocumented C++ API change (removal or change of undocumented public methods that lambda users may depend on) — [policy](https://developers.esphome.io/contributing/code/#c-user-expectations)
|
||||
- [ ] Code quality improvements to existing code or addition of tests
|
||||
- [ ] Other
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ module.exports = {
|
||||
'new-feature',
|
||||
'breaking-change',
|
||||
'developer-breaking-change',
|
||||
'undocumented-api-change',
|
||||
'code-quality',
|
||||
'deprecated-component'
|
||||
],
|
||||
|
||||
@@ -238,6 +238,7 @@ async function detectPRTemplateCheckboxes(context) {
|
||||
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
|
||||
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
|
||||
{ pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' },
|
||||
{ pattern: /- \[x\] Undocumented C\+\+ API change \(removal or change of undocumented public methods that lambda users may depend on\)/i, label: 'undocumented-api-change' },
|
||||
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
|
||||
];
|
||||
|
||||
|
||||
@@ -84,32 +84,30 @@ SprinklerValveOperator::SprinklerValveOperator(SprinklerValve *valve, Sprinkler
|
||||
: controller_(controller), valve_(valve) {}
|
||||
|
||||
void SprinklerValveOperator::loop() {
|
||||
// Use wrapping subtraction so 32-bit millis() rollover is handled correctly:
|
||||
// (now - start) yields the true elapsed time even across the 49.7-day boundary.
|
||||
uint32_t now = App.get_loop_component_start_time();
|
||||
if (now >= this->start_millis_) { // dummy check
|
||||
switch (this->state_) {
|
||||
case STARTING:
|
||||
if (now > (this->start_millis_ + this->start_delay_)) {
|
||||
this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state
|
||||
}
|
||||
break;
|
||||
switch (this->state_) {
|
||||
case STARTING:
|
||||
if ((now - *this->start_millis_) > this->start_delay_) {
|
||||
this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state
|
||||
}
|
||||
break;
|
||||
|
||||
case ACTIVE:
|
||||
if (now > (this->start_millis_ + this->start_delay_ + this->run_duration_)) {
|
||||
this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down
|
||||
}
|
||||
break;
|
||||
case ACTIVE:
|
||||
if ((now - *this->start_millis_) > (this->start_delay_ + this->run_duration_)) {
|
||||
this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down
|
||||
}
|
||||
break;
|
||||
|
||||
case STOPPING:
|
||||
if (now > (this->stop_millis_ + this->stop_delay_)) {
|
||||
this->kill_(); // stop_delay_has been exceeded, ensure all valves are off
|
||||
}
|
||||
break;
|
||||
case STOPPING:
|
||||
if ((now - *this->stop_millis_) > this->stop_delay_) {
|
||||
this->kill_(); // stop_delay_has been exceeded, ensure all valves are off
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else { // perhaps millis() rolled over...or something else is horribly wrong!
|
||||
this->stop(); // bail out (TODO: handle this highly unlikely situation better...)
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,11 +122,11 @@ void SprinklerValveOperator::set_valve(SprinklerValve *valve) {
|
||||
if (this->state_ != IDLE) { // Only kill if not already idle
|
||||
this->kill_(); // ensure everything is off before we let go!
|
||||
}
|
||||
this->state_ = IDLE; // reset state
|
||||
this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it
|
||||
this->start_millis_ = 0; // reset because (new) valve has not been started yet
|
||||
this->stop_millis_ = 0; // reset because (new) valve has not been started yet
|
||||
this->valve_ = valve; // finally, set the pointer to the new valve
|
||||
this->state_ = IDLE; // reset state
|
||||
this->run_duration_ = 0; // reset to ensure the valve isn't started without updating it
|
||||
this->start_millis_.reset(); // reset because (new) valve has not been started yet
|
||||
this->stop_millis_.reset(); // reset because (new) valve has not been started yet
|
||||
this->valve_ = valve; // finally, set the pointer to the new valve
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +160,7 @@ void SprinklerValveOperator::start() {
|
||||
} else {
|
||||
this->run_(); // there is no start_delay_, so just start the pump and valve
|
||||
}
|
||||
this->stop_millis_ = 0;
|
||||
this->stop_millis_.reset();
|
||||
this->start_millis_ = millis(); // save the time the start request was made
|
||||
}
|
||||
|
||||
@@ -189,22 +187,25 @@ void SprinklerValveOperator::stop() {
|
||||
uint32_t SprinklerValveOperator::run_duration() { return this->run_duration_ / 1000; }
|
||||
|
||||
uint32_t SprinklerValveOperator::time_remaining() {
|
||||
if (this->start_millis_ == 0) {
|
||||
if (!this->start_millis_.has_value()) {
|
||||
return this->run_duration(); // hasn't been started yet
|
||||
}
|
||||
|
||||
if (this->stop_millis_) {
|
||||
if (this->stop_millis_ - this->start_millis_ >= this->start_delay_ + this->run_duration_) {
|
||||
if (this->stop_millis_.has_value()) {
|
||||
uint32_t elapsed = *this->stop_millis_ - *this->start_millis_;
|
||||
if (elapsed >= this->start_delay_ + this->run_duration_) {
|
||||
return 0; // valve was active for more than its configured duration, so we are done
|
||||
} else {
|
||||
// we're stopped; return time remaining
|
||||
return (this->run_duration_ - (this->stop_millis_ - this->start_millis_)) / 1000;
|
||||
}
|
||||
if (elapsed <= this->start_delay_) {
|
||||
return this->run_duration_ / 1000; // stopped during start delay, full run duration remains
|
||||
}
|
||||
return (this->run_duration_ - (elapsed - this->start_delay_)) / 1000;
|
||||
}
|
||||
|
||||
auto completed_millis = this->start_millis_ + this->start_delay_ + this->run_duration_;
|
||||
if (completed_millis > millis()) {
|
||||
return (completed_millis - millis()) / 1000; // running now
|
||||
uint32_t elapsed = millis() - *this->start_millis_;
|
||||
uint32_t total_duration = this->start_delay_ + this->run_duration_;
|
||||
if (elapsed < total_duration) {
|
||||
return (total_duration - elapsed) / 1000; // running now
|
||||
}
|
||||
return 0; // run completed
|
||||
}
|
||||
@@ -593,7 +594,7 @@ void Sprinkler::set_repeat(optional<uint32_t> repeat) {
|
||||
if (this->repeat_number_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (this->repeat_number_->state == repeat.value()) {
|
||||
if (this->repeat_number_->state == repeat.value_or(0)) {
|
||||
return;
|
||||
}
|
||||
auto call = this->repeat_number_->make_call();
|
||||
@@ -793,7 +794,7 @@ void Sprinkler::start_single_valve(const optional<size_t> valve_number, optional
|
||||
void Sprinkler::queue_valve(optional<size_t> valve_number, optional<uint32_t> run_duration) {
|
||||
if (valve_number.has_value()) {
|
||||
if (this->is_a_valid_valve(valve_number.value()) && (this->queued_valves_.size() < this->max_queue_size_)) {
|
||||
SprinklerQueueItem item{valve_number.value(), run_duration.value()};
|
||||
SprinklerQueueItem item{valve_number.value(), run_duration.value_or(0)};
|
||||
this->queued_valves_.insert(this->queued_valves_.begin(), item);
|
||||
ESP_LOGD(TAG, "Valve %zu placed into queue with run duration of %" PRIu32 " seconds", valve_number.value_or(0),
|
||||
run_duration.value_or(0));
|
||||
@@ -1080,7 +1081,7 @@ uint32_t Sprinkler::total_cycle_time_enabled_incomplete_valves() {
|
||||
}
|
||||
}
|
||||
|
||||
if (incomplete_valve_count >= enabled_valve_count) {
|
||||
if (incomplete_valve_count > 0 && incomplete_valve_count >= enabled_valve_count) {
|
||||
incomplete_valve_count--;
|
||||
}
|
||||
if (incomplete_valve_count) {
|
||||
|
||||
@@ -141,8 +141,8 @@ class SprinklerValveOperator {
|
||||
uint32_t start_delay_{0};
|
||||
uint32_t stop_delay_{0};
|
||||
uint32_t run_duration_{0};
|
||||
uint64_t start_millis_{0};
|
||||
uint64_t stop_millis_{0};
|
||||
optional<uint32_t> start_millis_{};
|
||||
optional<uint32_t> stop_millis_{};
|
||||
Sprinkler *controller_{nullptr};
|
||||
SprinklerValve *valve_{nullptr};
|
||||
SprinklerState state_{IDLE};
|
||||
|
||||
@@ -556,7 +556,7 @@ void Application::enable_pending_loops_() {
|
||||
}
|
||||
|
||||
void Application::before_loop_tasks_(uint32_t loop_start_time) {
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32)
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT)
|
||||
// Drain wake notifications first to clear socket for next wake
|
||||
this->drain_wake_notifications_();
|
||||
#endif
|
||||
@@ -633,8 +633,9 @@ void Application::unregister_socket_fd(int fd) {
|
||||
continue;
|
||||
|
||||
// Swap with last element and pop - O(1) removal since order doesn't matter.
|
||||
// No need to unhook the netconn callback on ESP32 — all LwIP sockets share
|
||||
// the same static event_callback, and the socket will be closed by the caller.
|
||||
// No need to unhook the netconn callback on fast select platforms — all LwIP
|
||||
// sockets share the same static event_callback, and the socket will be closed
|
||||
// by the caller.
|
||||
if (i < this->socket_fds_.size() - 1)
|
||||
this->socket_fds_[i] = this->socket_fds_.back();
|
||||
this->socket_fds_.pop_back();
|
||||
|
||||
@@ -628,9 +628,10 @@ class Application {
|
||||
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
/// Fast path for Socket::ready() via friendship - skips negative fd check.
|
||||
/// Main loop only — on ESP32, reads rcvevent via lwip_socket_dbg_get_socket()
|
||||
/// which has no refcount; safe only because the main loop owns socket lifetime
|
||||
/// (creates, reads, and closes sockets on the same thread).
|
||||
/// Main loop only — with USE_LWIP_FAST_SELECT, reads rcvevent via
|
||||
/// lwip_socket_dbg_get_socket(), which has no refcount; safe only because
|
||||
/// the main loop owns socket lifetime (creates, reads, and closes sockets
|
||||
/// on the same thread).
|
||||
#ifdef USE_LWIP_FAST_SELECT
|
||||
bool is_socket_ready_(int fd) const { return esphome_lwip_socket_has_data(fd); }
|
||||
#else
|
||||
@@ -828,7 +829,7 @@ class Application {
|
||||
/// Global storage of Application pointer - only one Application can exist.
|
||||
extern Application App; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32)
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT)
|
||||
// Inline implementations for hot-path functions
|
||||
// drain_wake_notifications_() is called on every loop iteration
|
||||
|
||||
@@ -850,6 +851,6 @@ inline void Application::drain_wake_notifications_() {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_ESP32)
|
||||
#endif // defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE) && !defined(USE_LWIP_FAST_SELECT)
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
// 1. lwip/priv/sockets_priv.h conflicts with C++ compilation units
|
||||
// 2. The netconn callback is a C function pointer
|
||||
//
|
||||
// defines.h is force-included by the build system (-include flag), providing USE_LWIP_FAST_SELECT etc.
|
||||
// USE_ESP32 and USE_LIBRETINY platform flags (-D) control compilation of this file.
|
||||
// See the guard at the bottom of the header comment for details.
|
||||
//
|
||||
// Thread safety analysis
|
||||
// ======================
|
||||
@@ -104,9 +105,9 @@
|
||||
// critical sections). Multiple concurrent xTaskNotifyGive calls are safe —
|
||||
// the notification count simply increments.
|
||||
|
||||
// USE_ESP32 and USE_LIBRETINY are build flags (-D), always available to .c files.
|
||||
// USE_LWIP_FAST_SELECT is in the generated defines.h (force-included for .cpp but
|
||||
// may not reach .c files on all build systems), so we use platform flags here.
|
||||
// USE_ESP32 and USE_LIBRETINY are compiler -D flags, so they are always visible in this .c file.
|
||||
// Feature macros like USE_LWIP_FAST_SELECT may come from generated headers that are not included here,
|
||||
// so this implementation is enabled based on platform flags instead of USE_LWIP_FAST_SELECT.
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
|
||||
// LwIP headers must come first — they define netconn_callback, struct lwip_sock, etc.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import pytest
|
||||
|
||||
from esphome.components import socket
|
||||
from esphome.const import (
|
||||
KEY_CORE,
|
||||
@@ -93,9 +95,15 @@ def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() -
|
||||
assert udp_consumers == initial_udp
|
||||
|
||||
|
||||
def test_require_wake_loop_threadsafe__esp32_no_udp_socket() -> None:
|
||||
"""Test that ESP32 uses task notifications instead of UDP socket."""
|
||||
_setup_platform(PLATFORM_ESP32)
|
||||
@pytest.mark.parametrize(
|
||||
"platform",
|
||||
[PLATFORM_ESP32, PLATFORM_BK72XX, PLATFORM_RTL87XX, PLATFORM_LN882X],
|
||||
)
|
||||
def test_require_wake_loop_threadsafe__fast_select_no_udp_socket(
|
||||
platform: str,
|
||||
) -> None:
|
||||
"""Test that fast select platforms use task notifications instead of UDP socket."""
|
||||
_setup_platform(platform)
|
||||
CORE.config = {"wifi": True}
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
@@ -103,13 +111,13 @@ def test_require_wake_loop_threadsafe__esp32_no_udp_socket() -> None:
|
||||
assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True
|
||||
assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
|
||||
|
||||
# Verify no UDP socket was consumed (ESP32 uses FreeRTOS task notifications)
|
||||
# Verify no UDP socket was consumed (fast select platforms use FreeRTOS task notifications)
|
||||
udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
|
||||
assert "socket.wake_loop_threadsafe" not in udp_consumers
|
||||
|
||||
|
||||
def test_require_wake_loop_threadsafe__non_esp32_consumes_udp_socket() -> None:
|
||||
"""Test that non-ESP32 platforms consume a UDP socket for wake notifications."""
|
||||
def test_require_wake_loop_threadsafe__non_fast_select_consumes_udp_socket() -> None:
|
||||
"""Test that platforms without fast select consume a UDP socket for wake notifications."""
|
||||
_setup_platform(PLATFORM_ESP8266)
|
||||
CORE.config = {"wifi": True}
|
||||
socket.require_wake_loop_threadsafe()
|
||||
@@ -117,48 +125,3 @@ def test_require_wake_loop_threadsafe__non_esp32_consumes_udp_socket() -> None:
|
||||
# Verify UDP socket was consumed
|
||||
udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
|
||||
assert udp_consumers.get("socket.wake_loop_threadsafe") == 1
|
||||
|
||||
|
||||
def test_require_wake_loop_threadsafe__bk72xx_no_udp_socket() -> None:
|
||||
"""Test that BK72xx (LibreTiny) uses task notifications instead of UDP socket."""
|
||||
_setup_platform(PLATFORM_BK72XX)
|
||||
CORE.config = {"wifi": True}
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
# Verify the define was added
|
||||
assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True
|
||||
assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
|
||||
|
||||
# Verify no UDP socket was consumed (LibreTiny uses FreeRTOS task notifications)
|
||||
udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
|
||||
assert "socket.wake_loop_threadsafe" not in udp_consumers
|
||||
|
||||
|
||||
def test_require_wake_loop_threadsafe__rtl87xx_no_udp_socket() -> None:
|
||||
"""Test that RTL87xx (LibreTiny) uses task notifications instead of UDP socket."""
|
||||
_setup_platform(PLATFORM_RTL87XX)
|
||||
CORE.config = {"wifi": True}
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
# Verify the define was added
|
||||
assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True
|
||||
assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
|
||||
|
||||
# Verify no UDP socket was consumed (LibreTiny uses FreeRTOS task notifications)
|
||||
udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
|
||||
assert "socket.wake_loop_threadsafe" not in udp_consumers
|
||||
|
||||
|
||||
def test_require_wake_loop_threadsafe__ln882x_no_udp_socket() -> None:
|
||||
"""Test that LN882H (LibreTiny) uses task notifications instead of UDP socket."""
|
||||
_setup_platform(PLATFORM_LN882X)
|
||||
CORE.config = {"wifi": True}
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
# Verify the define was added
|
||||
assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True
|
||||
assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
|
||||
|
||||
# Verify no UDP socket was consumed (LibreTiny uses FreeRTOS task notifications)
|
||||
udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
|
||||
assert "socket.wake_loop_threadsafe" not in udp_consumers
|
||||
|
||||
Reference in New Issue
Block a user