ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
espectre.cpp
Go to the documentation of this file.
1#include "espectre.h"
2
3#ifdef USE_ESPECTRE
4
5#include <cinttypes>
6#include <cmath>
7#include <cstring>
9#include "esphome/core/log.h"
10#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_ROAMING_SUPPRESSION)
12#endif
13
14namespace esphome::espectre {
15
16ESPHOME_LOG_TAG(TAG, "espectre");
17
18struct TrafficModeOption {
19 const char *name;
20 ::espectre::TrafficGeneratorMode mode;
21};
22// Option names match the traffic_generator_mode YAML values.
23static constexpr TrafficModeOption TRAFFIC_MODE_OPTIONS[] = {
24 {"ping", ::espectre::TrafficGeneratorMode::PING},
25 {"dns", ::espectre::TrafficGeneratorMode::DNS},
26 {"dns_tcp", ::espectre::TrafficGeneratorMode::DNS_TCP},
27 {"wifi_raw", ::espectre::TrafficGeneratorMode::WIFI_RAW},
28 {"external", ::espectre::TrafficGeneratorMode::EXTERNAL_HOST},
29};
30
31static const char *traffic_mode_name(::espectre::TrafficGeneratorMode mode) {
32 for (const auto &option : TRAFFIC_MODE_OPTIONS) {
33 if (option.mode == mode)
34 return option.name;
35 }
36 return nullptr;
37}
38
39static const char *csi_capture_profile_name(::espectre::CsiCapturePolicy profile) {
40 switch (profile) {
41 case ::espectre::CsiCapturePolicy::LLTF:
42 return LOG_STR_LITERAL("lltf");
43 case ::espectre::CsiCapturePolicy::HT_VHT:
44 return LOG_STR_LITERAL("ht_vht");
45 default:
46 return LOG_STR_LITERAL("auto");
47 }
48}
49
50#ifdef USE_SELECT
52 for (const auto &option : TRAFFIC_MODE_OPTIONS) {
53 if (strcmp(option.name, name) == 0) {
54 this->pending_traffic_mode_ = option.mode;
55 return;
56 }
57 }
58}
59#endif
60
61static int log_level(::espectre::LogLevel level) {
62 switch (level) {
63 case ::espectre::LogLevel::ERROR:
64 return ESPHOME_LOG_LEVEL_ERROR;
65 case ::espectre::LogLevel::WARNING:
66 return ESPHOME_LOG_LEVEL_WARN;
67 case ::espectre::LogLevel::INFO:
68 return ESPHOME_LOG_LEVEL_INFO;
69 case ::espectre::LogLevel::DEBUG:
70 return ESPHOME_LOG_LEVEL_DEBUG;
71 default:
72 return ESPHOME_LOG_LEVEL_VERBOSE;
73 }
74}
75
77 // ESPHome owns scan results, including scans requested by the SDK's CSI recovery.
78 this->runtime_.config().wifi_scan_results_managed_externally = true;
79#ifdef USE_SELECT
81#endif
82 this->start_runtime_();
83}
84
86#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_ROAMING_SUPPRESSION)
87 // A sensor stays in one place, and each roaming scan takes the radio off-channel for
88 // seconds, emptying the CSI window. Losing the access point still reconnects normally.
89 if (wifi::global_wifi_component != nullptr) {
91 this->roaming_suppressed_ = true;
92 }
93#endif
94 ::espectre::set_log_sink({
95 .context = nullptr,
96 .enabled = [](void *, ::espectre::LogLevel level, const char *) { return log_level(level) <= ESPHOME_LOG_LEVEL; },
97 .write = [](void *, ::espectre::LogLevel level, const char *tag, int line, const char *format,
98 va_list args) { esp_log_vprintf_(log_level(level), tag, line, format, args); },
99 });
100 if (!this->runtime_.setup(this)) {
101 ESP_LOGE(TAG, "Runtime setup failed");
102 this->stop_();
103 this->schedule_restart_();
104 return;
105 }
106 this->running_ = true;
107 this->status_clear_error();
108#ifdef USE_SELECT
109 this->publish_traffic_mode_();
110#endif
111}
112
114 // A fault can be transient, such as a Wi-Fi stall, so retry instead of failing for good.
115 this->runtime_fault_ = false;
116 this->status_set_error(LOG_STR("Runtime stopped"));
117 ESP_LOGW(TAG, "Restarting the runtime in %" PRIu32 " s", RESTART_DELAY_MS / 1000);
118 this->restart_pending_ = true;
120}
121
122#ifdef USE_SELECT
124 if (this->traffic_mode_select_ == nullptr)
125 return;
126 // Keyed by the YAML mode, so changing it in YAML discards a mode saved from the select.
127 const auto yaml_mode = this->runtime_.config().traffic_generator_mode;
128 this->traffic_mode_pref_ =
129 this->traffic_mode_select_->make_entity_preference<uint8_t>(static_cast<uint32_t>(yaml_mode) + 1);
130 uint8_t saved;
131 if (!this->traffic_mode_pref_.load(&saved))
132 return;
133 const auto mode = static_cast<::espectre::TrafficGeneratorMode>(saved);
134 const char *name = traffic_mode_name(mode);
135 if (name != nullptr && this->traffic_mode_select_->has_option(name))
136 this->runtime_.config().traffic_generator_mode = mode;
137}
138
140 if (!this->pending_traffic_mode_.has_value())
141 return;
142 const auto mode = *this->pending_traffic_mode_;
143 this->pending_traffic_mode_.reset();
144 if (this->runtime_.set_traffic_generator_mode(mode)) {
145 const auto saved = static_cast<uint8_t>(mode);
146 this->traffic_mode_pref_.save(&saved);
147 } else {
148 ESP_LOGW(TAG, "Traffic generator mode %s was rejected", traffic_mode_name(mode));
149 }
150 this->publish_traffic_mode_();
151}
152
154 if (this->traffic_mode_select_ == nullptr)
155 return;
156 const char *name = traffic_mode_name(this->runtime_.config().traffic_generator_mode);
157 if (name != nullptr)
159}
160#endif
161
163 if (this->runtime_fault_) {
164 this->stop_();
165 this->schedule_restart_();
166 return;
167 }
168 if (!this->running_) {
169 // Without a backend this only reaps a traffic worker that outlived the last runtime.
170 this->runtime_.loop();
171 if (this->restart_pending_ &&
172 App.get_loop_component_start_time() - this->restart_requested_ms_ >= RESTART_DELAY_MS) {
173 this->restart_pending_ = false;
174 this->start_runtime_();
175 }
176 return;
177 }
178 if (this->recalibrate_pending_) {
179 this->recalibrate_pending_ = false;
180 if (!this->runtime_.trigger_recalibration()) {
181 ESP_LOGW(TAG, "Recalibration is not available");
182 }
183 }
184#ifdef USE_SELECT
186#endif
187 this->runtime_.loop();
188 if (this->runtime_fault_)
189 return;
190
191 // Read once after the SDK finishes dispatching callbacks, including readiness changes.
192 const auto &snapshot = this->runtime_.snapshot();
193#ifdef USE_BINARY_SENSOR
194 if (this->calibrating_binary_sensor_ != nullptr &&
195 (!this->calibrating_published_ || this->calibrating_state_ != snapshot.calibrating)) {
196 this->calibrating_state_ = snapshot.calibrating;
197 this->calibrating_published_ = true;
198 this->calibrating_binary_sensor_->publish_state(snapshot.calibrating);
199 }
200#endif
201 if (!snapshot.ready_to_publish) {
202 if (this->ready_)
203 this->invalidate_sensing_();
204 this->movement_pending_ = false;
205 return;
206 }
207
208#ifdef USE_BINARY_SENSOR
209 const bool motion = snapshot.motion_state == ::espectre::MotionState::MOTION;
210 if (this->motion_binary_sensor_ != nullptr && (!this->ready_ || this->motion_state_ != motion)) {
211 this->motion_state_ = motion;
212 this->motion_binary_sensor_->publish_state(motion);
213 }
214#endif
215#ifdef USE_SENSOR
216 if (this->movement_sensor_ != nullptr && (this->movement_pending_ || !this->ready_))
217 this->movement_sensor_->publish_state(snapshot.movement_metric);
218#endif
219 this->movement_pending_ = false;
220 this->ready_ = true;
221}
222
224#ifdef USE_BINARY_SENSOR
225 if (this->motion_binary_sensor_ != nullptr)
226 this->motion_binary_sensor_->invalidate_state();
227#endif
228#ifdef USE_SENSOR
229 if (this->movement_sensor_ != nullptr)
230 this->movement_sensor_->publish_state(NAN);
231#endif
232 this->ready_ = false;
233}
234
235void ESPectreComponent::on_calibration_finished(const ::espectre::RuntimeSnapshot &snapshot, bool success) {
236 if (success) {
237 this->calibrated_ = true;
238 this->status_clear_warning();
239 ESP_LOGI(TAG, "Calibration complete");
240 return;
241 }
242 // After a successful calibration, a failed one keeps that calibrated threshold.
243 if (!this->calibrated_)
244 this->status_set_warning(LOG_STR("Calibration failed"));
245 ESP_LOGW(TAG, "Calibration failed; retaining the previous threshold");
246}
247
249 ESP_LOGE(TAG, "Runtime fault: %s", message);
250 this->runtime_fault_ = true;
251}
252
254#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_ROAMING_SUPPRESSION)
255 if (this->roaming_suppressed_ && wifi::global_wifi_component != nullptr) {
257 }
258#endif
259 this->roaming_suppressed_ = false;
260 this->running_ = false;
261 // The next runtime starts from the default threshold and publishes its calibration again.
262 this->calibrated_ = false;
263 this->calibrating_published_ = false;
264 this->runtime_.shutdown();
265 ::espectre::clear_log_sink();
266 this->invalidate_sensing_();
267#ifdef USE_BINARY_SENSOR
268 if (this->calibrating_binary_sensor_ != nullptr)
269 this->calibrating_binary_sensor_->invalidate_state();
270#endif
271}
272
274 this->restart_pending_ = false;
275 this->stop_();
276}
277
279 const auto &config = this->runtime_.config();
280 ESP_LOGCONFIG(TAG,
281 "ESPectre:\n"
282 " Detection algorithm: %s\n"
283 " CSI capture profile: %s\n"
284 " Traffic generator mode: %s\n"
285 " Motion on/off hits: %u/%u",
286 config.detection_algorithm == ::espectre::DetectionAlgorithm::LIGHTWEIGHT
287 ? LOG_STR_LITERAL("lightweight")
288 : LOG_STR_LITERAL("high_accuracy"),
289 csi_capture_profile_name(config.csi_capture_policy), traffic_mode_name(config.traffic_generator_mode),
290 config.motion_on_hits, config.motion_off_hits);
291#ifdef USE_BINARY_SENSOR
292 LOG_BINARY_SENSOR(" ", "Motion", this->motion_binary_sensor_);
293 LOG_BINARY_SENSOR(" ", "Calibrating", this->calibrating_binary_sensor_);
294#endif
295#ifdef USE_SENSOR
296 LOG_SENSOR(" ", "Movement score", this->movement_sensor_);
297#endif
298#ifdef USE_SELECT
299 LOG_SELECT(" ", "Traffic generator mode", this->traffic_mode_select_);
300#endif
301}
302
303} // namespace esphome::espectre
304
305#endif // USE_ESPECTRE
BedjetMode mode
BedJet operating mode.
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void status_clear_error()
Definition component.h:297
void status_clear_warning()
Definition component.h:291
ESPPreferenceObject make_entity_preference(uint32_t version=0)
Create a preference object for storing this entity's state/settings.
SUB_BINARY_SENSOR(motion) SUB_BINARY_SENSOR(calibrating) SUB_SENSOR(movement) const void on_runtime_fault(const char *message) override
Latest one-second runtime diagnostics sample, or nullptr before the runtime starts.
Definition espectre.cpp:248
void request_traffic_generator_mode(::espectre::TrafficGeneratorMode mode)
Definition espectre.h:56
optional<::espectre::TrafficGeneratorMode > pending_traffic_mode_
Definition espectre.h:93
ESPPreferenceObject traffic_mode_pref_
Definition espectre.h:92
static constexpr uint32_t RESTART_DELAY_MS
Definition espectre.h:86
::espectre::RuntimeFrontendController runtime_
Definition espectre.h:88
select::Select * traffic_mode_select_
Definition espectre.h:91
bool has_option(const std::string &option) const
Return whether this select component contains the provided option.
Definition select.h:51
void publish_state(const std::string &state)
Definition select.h:36
void release_roaming_suppression()
Release a roaming suppression request.
void request_roaming_suppression()
Request that post-connect roaming scans be suppressed.
const LogString * message
Definition component.cpp:35
const char * format
ESPHOME_LOG_TAG(TAG, "espectre")
WiFiComponent * global_wifi_component
Application App
Global storage of Application pointer - only one Application can exist.
void HOT esp_log_vprintf_(int level, const char *tag, int line, const char *format, va_list args)
Definition log.cpp:51
static void uint32_t