ESPHome 2026.5.0b1
Loading...
Searching...
No Matches
wake_host.cpp
Go to the documentation of this file.
2
3#ifdef USE_HOST
4
5#include "esphome/core/hal.h"
6#include "esphome/core/log.h"
7#include "esphome/core/wake.h"
8
9#include <arpa/inet.h>
10#include <cerrno>
11#include <fcntl.h>
12#include <netinet/in.h>
13#include <sys/select.h>
14#include <sys/socket.h>
15#include <unistd.h>
16#include <vector>
17
18namespace esphome {
19
20// === Wake-requested flag storage ===
21// Host is always ESPHOME_THREAD_MULTI_ATOMICS.
22// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
23std::atomic<uint8_t> g_wake_requested{0};
24
25static const char *const TAG = "wake";
26
27namespace internal {
28// File-scope state — referenced inline by wake_drain_notifications() and
29// wake_fd_ready() in wake_host.h, and by the bodies in this file.
30// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
32// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
33fd_set g_read_fds{};
34} // namespace internal
35
36namespace {
37// File-local state owned entirely by the select() loop.
38// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
39std::vector<int> s_socket_fds;
40int s_max_fd = -1;
41bool s_socket_fds_changed = false;
42fd_set s_base_read_fds{};
43// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
44} // namespace
45
46bool wake_register_fd(int fd) {
47 // WARNING: not thread-safe — must be called only from the main loop.
48 if (fd < 0)
49 return false;
50
51 if (fd >= FD_SETSIZE) {
52 ESP_LOGE(TAG, "fd %d exceeds FD_SETSIZE %d", fd, FD_SETSIZE);
53 return false;
54 }
55
56 s_socket_fds.push_back(fd);
57 s_socket_fds_changed = true;
58 if (fd > s_max_fd) {
59 s_max_fd = fd;
60 }
61
62 return true;
63}
64
65void wake_unregister_fd(int fd) {
66 // WARNING: not thread-safe — must be called only from the main loop.
67 if (fd < 0)
68 return;
69
70 for (size_t i = 0; i < s_socket_fds.size(); i++) {
71 if (s_socket_fds[i] != fd)
72 continue;
73
74 // Swap with last element and pop — O(1) removal since order doesn't matter.
75 if (i < s_socket_fds.size() - 1)
76 s_socket_fds[i] = s_socket_fds.back();
77 s_socket_fds.pop_back();
78 s_socket_fds_changed = true;
79 // Only recalculate max_fd if we removed the current max.
80 if (fd == s_max_fd) {
81 s_max_fd = -1;
82 for (int sock_fd : s_socket_fds) {
83 if (sock_fd > s_max_fd)
84 s_max_fd = sock_fd;
85 }
86 }
87 return;
88 }
89}
90
91namespace internal {
92void wakeable_delay(uint32_t ms) {
93 // Fallback select() path for the host platform (and any future platform
94 // without fast select). select() is the host equivalent of FreeRTOS task
95 // notify / esp_delay / WFE used on the embedded targets.
96 if (!s_socket_fds.empty()) [[likely]] {
97 // Update fd_set if socket list has changed.
98 if (s_socket_fds_changed) [[unlikely]] {
99 FD_ZERO(&s_base_read_fds);
100 // fd bounds are validated in wake_register_fd().
101 for (int fd : s_socket_fds) {
102 FD_SET(fd, &s_base_read_fds);
103 }
104 s_socket_fds_changed = false;
105 }
106
107 // Copy base fd_set before each select.
108 g_read_fds = s_base_read_fds;
109
110 // Convert ms to timeval.
111 struct timeval tv;
112 tv.tv_sec = ms / 1000;
113 tv.tv_usec = (ms - tv.tv_sec * 1000) * 1000;
114
115 // Call select with timeout.
116 int ret = ::select(s_max_fd + 1, &g_read_fds, nullptr, nullptr, &tv);
117
118 // Process select() result:
119 // ret > 0: socket(s) have data ready - normal and expected
120 // ret == 0: timeout occurred - normal and expected
121 if (ret >= 0) [[likely]] {
122 // Yield if zero timeout since select(0) only polls without yielding.
123 if (ms == 0) [[unlikely]] {
124 yield();
125 }
126 return;
127 }
128 // ret < 0: error (EINTR is normal, anything else is unexpected).
129 const int err = errno;
130 if (err == EINTR) {
131 return;
132 }
133 // select() error - log and fall through to delay().
134 ESP_LOGW(TAG, "select() failed with errno %d", err);
135 }
136 // No sockets registered or select() failed - use regular delay.
137 delay(ms);
138}
139} // namespace internal
140
142 // Set flag before sending so the consumer's gate check on the next loop()
143 // entry observes the wake regardless of select() scheduling.
144 wake_request_set();
146 const char dummy = 1;
147 ::send(internal::g_wake_socket_fd, &dummy, 1, 0);
148 }
149}
150
152 // Create UDP socket for wake notifications.
153 internal::g_wake_socket_fd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
155 ESP_LOGW(TAG, "Wake socket create failed: %d", errno);
156 return;
157 }
158
159 // Bind to loopback with auto-assigned port.
160 struct sockaddr_in addr = {};
161 addr.sin_family = AF_INET;
162 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
163 addr.sin_port = 0; // Auto-assign port
164
165 if (::bind(internal::g_wake_socket_fd, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
166 ESP_LOGW(TAG, "Wake socket bind failed: %d", errno);
169 return;
170 }
171
172 // Get the assigned address and connect to it.
173 // Connecting a UDP socket allows using send() instead of sendto() for better performance.
174 struct sockaddr_in wake_addr;
175 socklen_t len = sizeof(wake_addr);
176 if (::getsockname(internal::g_wake_socket_fd, (struct sockaddr *) &wake_addr, &len) < 0) {
177 ESP_LOGW(TAG, "Wake socket address failed: %d", errno);
180 return;
181 }
182
183 // Connect to self (loopback) — allows using send() instead of sendto().
184 // After connect(), no need to store wake_addr — the socket remembers it.
185 if (::connect(internal::g_wake_socket_fd, (struct sockaddr *) &wake_addr, sizeof(wake_addr)) < 0) {
186 ESP_LOGW(TAG, "Wake socket connect failed: %d", errno);
189 return;
190 }
191
192 // Set non-blocking mode.
193 int flags = ::fcntl(internal::g_wake_socket_fd, F_GETFL, 0);
194 ::fcntl(internal::g_wake_socket_fd, F_SETFL, flags | O_NONBLOCK);
195
196 // Register with the select() loop.
198 ESP_LOGW(TAG, "Wake socket register failed");
201 return;
202 }
203}
204
205} // namespace esphome
206
207#endif // USE_HOST
void yield(void)
uint16_t flags
uint32_t socklen_t
Definition headers.h:99
void ESPHOME_ALWAYS_INLINE wakeable_delay(uint32_t ms)
Host wakeable_delay uses select() over the registered fds — defined in wake_host.cpp.
const char *const TAG
Definition spi.cpp:7
std::atomic< uint8_t > g_wake_requested
Definition wake.h:49
void wake_loop_threadsafe()
Non-ISR: always inline.
std::string size_t len
bool wake_register_fd(int fd)
Register a socket file descriptor with the host select() loop.
Definition wake_host.cpp:46
void wake_unregister_fd(int fd)
Unregister a socket file descriptor. Not thread-safe — main loop only.
Definition wake_host.cpp:65
void HOT delay(uint32_t ms)
Definition hal.cpp:82
void wake_setup()
One-time setup of the loopback wake socket. Called from Application::setup().
static void uint32_t
struct in_addr sin_addr
Definition headers.h:67
sa_family_t sin_family
Definition headers.h:65
in_port_t sin_port
Definition headers.h:66
Platform-specific main loop wake primitives.