ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
dfrobot_sen0609.cpp
Go to the documentation of this file.
1#include "dfrobot_sen0609.h"
2
4#include "esphome/core/hal.h"
5#include "esphome/core/log.h"
7
8#include <algorithm>
9#include <cmath>
10#include <cstdlib>
11#include <cstring>
12
14
15ESPHOME_LOG_TAG(TAG, "dfrobot_sen0609");
16
17static constexpr uint8_t MAX_ATTEMPTS = 3;
18// The module ignores a command that follows its last answer too closely
19static constexpr uint32_t COMMAND_GAP_MS = 100;
20// Gathers entity changes made close together into one flash write
21static constexpr uint32_t COALESCE_MS = 1000;
22static constexpr uint32_t DEFAULT_TIMEOUT_MS = 1000;
23static constexpr uint32_t LONG_TIMEOUT_MS = 2000;
24static constexpr uint32_t FACTORY_RESET_TIMEOUT_MS = 3000;
25// resetSystem has no answer; this is how long the module takes to boot again
26static constexpr uint32_t RESTART_MS = 1500;
27static constexpr uint32_t GIVE_UP_BACKOFF_MS = 30000;
28// Presence frames come every second (setUartOutput period); this long without one means the module lost them
29static constexpr uint32_t FRAME_TIMEOUT_MS = 10000;
30static constexpr uint8_t FRAME_FIELDS = 5;
31
32static constexpr uint8_t LINE_END[] = {'\r', '\n'};
33
34// Indexed by Param; empty for parameters that cannot be read
35PROGMEM_STRING_TABLE(ReadCommands, "", "", "getRange", "getTrigRange", "getSensitivity", "getLatency", "getInhibit",
36 "getThrFactor", "getMicroMotion", "getGpioMode 1", "", "", "getSWV", "getHWV");
37
38static_assert(PARAM_COUNT <= 16, "pending_writes_ and pending_reads_ hold one bit per parameter");
39static constexpr uint16_t param_bit(Param param) { return static_cast<uint16_t>(1U << param); }
40
41// Parameters kept in the module's flash only after saveConfig
42static constexpr uint16_t PERSISTENT_PARAMS =
43 param_bit(PARAM_RANGE) | param_bit(PARAM_TRIG_RANGE) | param_bit(PARAM_SENSITIVITY) | param_bit(PARAM_LATENCY) |
44 param_bit(PARAM_INHIBIT) | param_bit(PARAM_THR_FACTOR) | param_bit(PARAM_MICRO_MOTION) | param_bit(PARAM_GPIO_MODE);
45// The frames depend on these, so their writes are kept until they succeed
46static constexpr uint16_t FRAME_PARAMS = param_bit(PARAM_RUN_APP) | param_bit(PARAM_UART_OUTPUT);
47
48#ifdef USE_SWITCH
49static constexpr bool is_led(Param param) { return param == PARAM_RUN_LED || param == PARAM_OUT_LED; }
50static constexpr SwitchSetting led_switch(Param param) {
52}
53#endif
54
55#ifdef USE_NUMBER
56static_assert(NUMBER_COUNT <= 16, "changed_numbers_ holds one bit per number");
57static constexpr uint8_t NUMBER_PARAMS[NUMBER_COUNT] PROGMEM = {
60};
61#endif
62
63static bool starts_with(const char *str, ESPHOME_PGM_P prefix) {
64 return ESPHOME_strncmp_P(str, prefix, ESPHOME_strlen_P(prefix)) == 0;
65}
66
67static bool contains(const char *str, ESPHOME_PGM_P needle) {
68 const size_t len = ESPHOME_strlen_P(needle);
69 for (; *str != '\0'; str++) {
70 if (ESPHOME_strncmp_P(str, needle, len) == 0)
71 return true;
72 }
73 return false;
74}
75
76// Splits "a,b,c*" in place; returns the number of fields stored in `fields`
77static uint8_t split_fields(char *str, char **fields, uint8_t max_fields) {
78 uint8_t count = 0;
79 char *start = str;
80 for (;; str++) {
81 if (*str != ',' && *str != '*' && *str != '\0')
82 continue;
83 const bool last = *str != ',';
84 *str = '\0';
85 if (count < max_fields)
86 fields[count++] = start;
87 if (last)
88 return count;
89 start = str + 1;
90 }
91}
92
93// Answers that refuse a command whose effect is already in place
94static bool answer_means_done(Operation operation, const char *line) {
95 switch (operation) {
97 return contains(line, ESPHOME_PSTR("stopped already"));
99 return contains(line, ESPHOME_PSTR("started already"));
101 return starts_with(line, ESPHOME_PSTR("no parameter"));
102 default:
103 return false;
104 }
105}
106
107static bool parse_float(const char *str, float &value) {
108 char *end;
109 value = strtof(str, &end);
110 return end != str && *end == '\0';
111}
112
113static const LogString *operation_to_str(Operation operation) {
114 switch (operation) {
116 return LOG_STR("stop");
118 return LOG_STR("start");
120 return LOG_STR("restart");
122 return LOG_STR("factory reset");
124 return LOG_STR("write");
126 return LOG_STR("save");
128 return LOG_STR("read");
129 default:
130 return LOG_STR("none");
131 }
132}
133
134DFRobotSEN0609Component::DFRobotSEN0609Component(RunMode mode) : mode_(mode) {
135#ifdef USE_NUMBER
136 this->number_values_.fill(NAN);
137#endif
138}
139
140void DFRobotSEN0609Component::dump_config() {
141 ESP_LOGCONFIG(
142 TAG,
143 "DFRobot SEN0609:\n"
144 " Mode: %s",
145 this->mode_ == RunMode::RUN_MODE_PRESENCE ? LOG_STR_LITERAL("presence") : LOG_STR_LITERAL("speed and distance"));
146#ifdef USE_BINARY_SENSOR
147 LOG_BINARY_SENSOR(" ", "HasTarget", this->has_target_binary_sensor_);
148#endif
149#ifdef USE_SENSOR
150 LOG_SENSOR(" ", "TargetDistance", this->target_distance_sensor_);
151 LOG_SENSOR(" ", "TargetSpeed", this->target_speed_sensor_);
152 LOG_SENSOR(" ", "TargetEnergy", this->target_energy_sensor_);
153#endif
154#ifdef USE_TEXT_SENSOR
155 LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_);
156 LOG_TEXT_SENSOR(" ", "HardwareVersion", this->hardware_version_text_sensor_);
157#endif
158#ifdef USE_NUMBER
159 for (number::Number *n : this->numbers_) {
160 LOG_NUMBER(" ", "Number", n);
161 }
162#endif
163#ifdef USE_SWITCH
164 for (switch_::Switch *s : this->switches_) {
165 LOG_SWITCH(" ", "Switch", s);
166 }
167#endif
168#ifdef USE_SELECT
169 LOG_SELECT(" ", "OutPinLevel", this->out_pin_level_select_);
170#endif
171#ifdef USE_BUTTON
172 LOG_BUTTON(" ", "FactoryReset", this->factory_reset_button_);
173 LOG_BUTTON(" ", "Query", this->query_button_);
174 LOG_BUTTON(" ", "Restart", this->restart_button_);
175#endif
176}
177
178void DFRobotSEN0609Component::setup() {
179#ifdef USE_SWITCH
180 // The module forgets the LED states at power off
181 for (uint8_t i = SWITCH_RUN_LED; i <= SWITCH_OUT_LED; i++) {
182 switch_::Switch *sw = this->switches_[i];
183 if (sw == nullptr)
184 continue;
185 const bool on = sw->get_initial_state_with_restore_mode().value_or(true);
186 this->publish_switch_(static_cast<SwitchSetting>(i), on != sw->is_inverted());
187 }
188#endif
189 this->queue_configuration_();
190}
191
192void DFRobotSEN0609Component::loop() {
193 uint8_t buf[MAX_LINE_LENGTH];
194 size_t avail = this->available();
195 while (avail > 0) {
196 const size_t to_read = std::min(avail, sizeof(buf));
197 if (!this->read_array(buf, to_read)) {
198 if (!this->read_failed_) {
199 ESP_LOGW(TAG, "UART read failed");
200 }
201 this->read_failed_ = true;
202 break;
203 }
204 this->read_failed_ = false;
205 avail -= to_read;
206 for (size_t i = 0; i < to_read; i++)
207 this->readline_(buf[i]);
208 }
209 this->run_sequencer_();
210}
211
212void DFRobotSEN0609Component::readline_(uint8_t c) {
213 if (c == '\r')
214 return;
215 if (c == '\n') {
216 if (!this->line_overflow_) {
217 this->line_[this->line_pos_] = '\0';
218 this->handle_line_(this->line_);
219 }
220 this->line_overflow_ = false;
221 this->line_pos_ = 0;
222 return;
223 }
224 if (this->line_overflow_)
225 return;
226 if (this->line_pos_ >= MAX_LINE_LENGTH - 1) {
227 ESP_LOGW(TAG, "Line too long, dropped");
228 this->line_overflow_ = true;
229 return;
230 }
231 this->line_[this->line_pos_++] = static_cast<char>(c);
232 // The prompt has no line end, and the echo of the next command follows it on the same line
233 if (c == '>') {
234 this->line_[this->line_pos_] = '\0';
235 if (ESPHOME_strcmp_P(this->line_, ESPHOME_PSTR("DFRobot:/>")) == 0)
236 this->line_pos_ = 0;
237 }
238}
239
240void DFRobotSEN0609Component::handle_line_(char *line) {
241 if (*line == '\0')
242 return;
243 ESP_LOGV(TAG, "RX: %s", line);
244 if (line[0] == '$') {
245 this->handle_frame_(line);
246 return;
247 }
248 // Version lines name their value, so they are used even when they arrive late
249 const bool software = starts_with(line, ESPHOME_PSTR("SoftwareVersion:"));
250 if (software || starts_with(line, ESPHOME_PSTR("HardwareVersion:"))) {
251 if (this->operation_ == Operation::OPERATION_READ &&
252 this->param_ == (software ? PARAM_SW_VERSION : PARAM_HW_VERSION))
253 this->version_received_ = true;
254#ifdef USE_TEXT_SENSOR
255 text_sensor::TextSensor *sens = software ? this->version_text_sensor_ : this->hardware_version_text_sensor_;
256 if (sens != nullptr)
257 sens->publish_state(line + 16);
258#endif
259 return;
260 }
261 if (this->operation_ == Operation::OPERATION_NONE) {
262 ESP_LOGD(TAG, "Ignored line, no command pending: %s", line);
263 return;
264 }
265 if (strcmp(line, this->command_) == 0) {
266 this->echo_seen_ = true;
267 return;
268 }
269 // The module echoes every command first; lines before the echo belong to an earlier command
270 if (!this->echo_seen_) {
271 ESP_LOGD(TAG, "Ignored line from an earlier command: %s", line);
272 return;
273 }
274
275 if (starts_with(line, ESPHOME_PSTR("Done")) || answer_means_done(this->operation_, line)) {
276 this->finish_operation_();
277 } else if (starts_with(line, ESPHOME_PSTR("Response "))) {
278 this->parse_response_(line + 9);
279 } else if (contains(line, ESPHOME_PSTR("not stopped"))) {
280 // Stop the sensor before the next attempt
281 this->stopped_ = false;
282 this->fail_operation_();
283 } else if (starts_with(line, ESPHOME_PSTR("Error")) || contains(line, ESPHOME_PSTR("not recognized"))) {
284 ESP_LOGW(TAG, "Command '%s' rejected: %s", this->command_, line);
285 this->fail_operation_();
286 } else {
287 ESP_LOGD(TAG, "Unexpected answer to '%s': %s", this->command_, line);
288 }
289}
290
291void DFRobotSEN0609Component::handle_frame_(char *line) {
292 // $DFHPD,<presence>, , , *
293 if (starts_with(line, ESPHOME_PSTR("$DFHPD,"))) {
294 const char presence = line[7];
295 if (presence != '0' && presence != '1') {
296 this->log_bad_frame_(LOG_STR("Malformed presence frame"), line);
297 return;
298 }
299 this->last_frame_ms_ = App.get_loop_component_start_time();
300#ifdef USE_BINARY_SENSOR
301 if (this->has_target_binary_sensor_ != nullptr)
302 this->has_target_binary_sensor_->publish_state(presence == '1');
303#endif
304 return;
305 }
306 // $DFDMD,<targets>,<reserved>,<distance m>,<speed m/s>,<energy>,...*
307 if (!starts_with(line, ESPHOME_PSTR("$DFDMD,"))) {
308 this->log_bad_frame_(LOG_STR("Unknown frame"), line);
309 return;
310 }
311 char *fields[FRAME_FIELDS];
312 float distance;
313 float speed;
314 float energy;
315 if (split_fields(line + 7, fields, FRAME_FIELDS) < FRAME_FIELDS) {
316 this->log_bad_frame_(LOG_STR("Malformed target frame"), line);
317 return;
318 }
319 const optional<uint8_t> targets = parse_number<uint8_t>(fields[0]);
320 if (!targets.has_value()) {
321 this->log_bad_frame_(LOG_STR("Malformed target frame"), line);
322 return;
323 }
324 if (*targets == 0) {
325#ifdef USE_BINARY_SENSOR
326 if (this->has_target_binary_sensor_ != nullptr)
327 this->has_target_binary_sensor_->publish_state(false);
328#endif
329#ifdef USE_SENSOR
330 this->publish_target_(NAN, NAN, NAN);
331#endif
332 return;
333 }
334 if (!parse_float(fields[2], distance) || !parse_float(fields[3], speed) || !parse_float(fields[4], energy)) {
335 this->log_bad_frame_(LOG_STR("Malformed target frame"), line);
336 return;
337 }
338#ifdef USE_BINARY_SENSOR
339 if (this->has_target_binary_sensor_ != nullptr)
340 this->has_target_binary_sensor_->publish_state(true);
341#endif
342#ifdef USE_SENSOR
343 this->publish_target_(distance, speed, energy);
344#endif
345}
346
347void DFRobotSEN0609Component::log_bad_frame_(const LogString *problem, const char *line) {
348 // The module sends frames several times a second, so only the first bad one is logged
349 if (this->bad_frame_logged_)
350 return;
351 this->bad_frame_logged_ = true;
352 ESP_LOGW(TAG, "%s: %s", LOG_STR_ARG(problem), line);
353}
354
355#ifdef USE_SENSOR
356static void publish_deduplicated(sensor::Sensor *sens, Deduplicator<float> &dedup, float value) {
357 if (sens == nullptr)
358 return;
359 if (std::isnan(value) ? dedup.next_unknown() : dedup.next(value))
360 sens->publish_state(value);
361}
362
363void DFRobotSEN0609Component::publish_target_(float distance, float speed, float energy) {
364 publish_deduplicated(this->target_distance_sensor_, this->distance_dedup_, distance);
365 publish_deduplicated(this->target_speed_sensor_, this->speed_dedup_, speed);
366 publish_deduplicated(this->target_energy_sensor_, this->energy_dedup_, energy);
367}
368#endif
369
370void DFRobotSEN0609Component::parse_response_(const char *args) {
371 this->response_count_ = 0;
372 while (this->response_count_ < MAX_RESPONSE_VALUES) {
373 char *end;
374 const float value = strtof(args, &end);
375 // strtof also takes "nan" and "inf", which no setting can hold
376 if (end == args || !std::isfinite(value))
377 break;
378 this->response_[this->response_count_++] = value;
379 args = end;
380 }
381}
382
383void DFRobotSEN0609Component::queue_configuration_() {
384 // The mode, the UART output and the LEDs are applied at every boot instead of being saved
385 uint16_t writes = param_bit(PARAM_RUN_APP);
386 if (this->mode_ == RunMode::RUN_MODE_PRESENCE)
387 writes |= param_bit(PARAM_UART_OUTPUT);
388#ifdef USE_SWITCH
389 if (this->switches_[SWITCH_RUN_LED] != nullptr)
390 writes |= param_bit(PARAM_RUN_LED);
391 if (this->switches_[SWITCH_OUT_LED] != nullptr)
392 writes |= param_bit(PARAM_OUT_LED);
393#endif
394 this->pending_writes_ |= writes;
395 this->pending_reads_ |= this->wanted_reads_();
396}
397
398uint16_t DFRobotSEN0609Component::wanted_reads_() const {
399 uint16_t reads = 0;
400#ifdef USE_TEXT_SENSOR
401 if (this->version_text_sensor_ != nullptr)
402 reads |= param_bit(PARAM_SW_VERSION);
403 if (this->hardware_version_text_sensor_ != nullptr)
404 reads |= param_bit(PARAM_HW_VERSION);
405#endif
406#ifdef USE_NUMBER
407 for (uint8_t i = 0; i < NUMBER_COUNT; i++) {
408 if (this->numbers_[i] != nullptr)
409 reads |= param_bit(static_cast<Param>(progmem_read_byte(&NUMBER_PARAMS[i])));
410 }
411#endif
412#ifdef USE_SWITCH
413 if (this->switches_[SWITCH_MICRO_MOTION] != nullptr)
414 reads |= param_bit(PARAM_MICRO_MOTION);
415#endif
416#ifdef USE_SELECT
417 if (this->out_pin_level_select_ != nullptr)
418 reads |= param_bit(PARAM_GPIO_MODE);
419#endif
420 return reads;
421}
422
423void DFRobotSEN0609Component::check_frames_(uint32_t now) {
424 // Speed mode frames are not watched: whether the module sends them without a target is not known
425 if (this->mode_ != RunMode::RUN_MODE_PRESENCE || now - this->last_frame_ms_ < FRAME_TIMEOUT_MS)
426 return;
427 // The module most likely restarted on its own and lost the configuration applied at boot
428 ESP_LOGW(TAG, "No presence frames, applying the configuration again");
429 this->status_set_warning();
430#ifdef USE_BINARY_SENSOR
431 if (this->has_target_binary_sensor_ != nullptr)
432 this->has_target_binary_sensor_->invalidate_state();
433#endif
434 this->last_frame_ms_ = now;
435 this->queue_configuration_();
436}
437
438void DFRobotSEN0609Component::request_write_(Param param) {
440 this->pending_writes_ |= param_bit(param);
441 this->last_change_ms_ = now;
442 if (this->operation_ != Operation::OPERATION_NONE || this->stopped_)
443 return;
444 // Start a new sequence a little later, so changes made together end in one flash write.
445 // A longer hold, such as the back-off from a module that does not answer, is not cut short.
446 const uint32_t elapsed = now - this->hold_start_ms_;
447 if (elapsed >= this->hold_ms_ || this->hold_ms_ - elapsed < COALESCE_MS)
448 this->hold_(COALESCE_MS);
449}
450
451void DFRobotSEN0609Component::restart() {
452 this->restart_pending_ = true;
453 this->hold_(COMMAND_GAP_MS);
454}
455
456void DFRobotSEN0609Component::factory_reset() {
457 this->factory_reset_pending_ = true;
458 this->hold_(COMMAND_GAP_MS);
459}
460
461void DFRobotSEN0609Component::query_params() {
462 this->pending_reads_ |= this->wanted_reads_();
463 this->hold_(COMMAND_GAP_MS);
464}
465
466#ifdef USE_NUMBER
467void DFRobotSEN0609Component::control_number(NumberSetting setting, float value) {
468 // The module needs min_range <= trigger_range <= max_range; a value not read yet is NAN and passes
469 const auto &values = this->number_values_;
470 const float min_range = setting == NUMBER_MIN_RANGE ? value : values[NUMBER_MIN_RANGE];
471 const float max_range = setting == NUMBER_MAX_RANGE ? value : values[NUMBER_MAX_RANGE];
472 const float trigger_range = setting == NUMBER_TRIGGER_RANGE ? value : values[NUMBER_TRIGGER_RANGE];
473 if (setting <= NUMBER_TRIGGER_RANGE &&
474 (min_range > max_range || trigger_range < min_range || trigger_range > max_range)) {
475 ESP_LOGW(TAG, "Ranges must keep minimum <= trigger <= maximum, not written");
476 if (!std::isnan(this->number_values_[setting]))
477 this->publish_number_(setting, this->number_values_[setting]);
478 return;
479 }
480 this->changed_numbers_ |= 1U << setting;
481 this->publish_number_(setting, value);
482 this->request_write_(static_cast<Param>(progmem_read_byte(&NUMBER_PARAMS[setting])));
483}
484
485void DFRobotSEN0609Component::forget_number_changes_(Param param, bool unknown) {
486 for (uint8_t i = 0; i < NUMBER_COUNT; i++) {
487 if (progmem_read_byte(&NUMBER_PARAMS[i]) != param)
488 continue;
489 this->changed_numbers_ &= ~(1U << i);
490 if (unknown)
491 this->publish_number_(static_cast<NumberSetting>(i), NAN);
492 }
493}
494
495void DFRobotSEN0609Component::publish_number_(NumberSetting setting, float value) {
496 this->number_values_[setting] = value;
497 if (this->numbers_[setting] != nullptr)
498 this->numbers_[setting]->publish_state(value);
499}
500#endif
501
502#ifdef USE_SWITCH
503void DFRobotSEN0609Component::control_switch(SwitchSetting setting, bool state) {
504 this->publish_switch_(setting, state);
505 switch (setting) {
507 this->request_write_(PARAM_MICRO_MOTION);
508 break;
509 case SWITCH_RUN_LED:
510 this->request_write_(PARAM_RUN_LED);
511 break;
512 default:
513 this->request_write_(PARAM_OUT_LED);
514 break;
515 }
516}
517
518void DFRobotSEN0609Component::publish_switch_(SwitchSetting setting, bool state) {
519 this->switch_states_[setting] = state;
520 if (this->switches_[setting] != nullptr)
521 this->switches_[setting]->publish_state(state);
522}
523#endif
524
525#ifdef USE_SELECT
526void DFRobotSEN0609Component::control_out_pin_level(size_t index) {
527 this->out_pin_level_ = static_cast<uint8_t>(index);
528 this->out_pin_level_select_->publish_state(index);
529 this->request_write_(PARAM_GPIO_MODE);
530}
531#endif
532
533void DFRobotSEN0609Component::hold_(uint32_t duration_ms) {
534 this->hold_start_ms_ = App.get_loop_component_start_time();
535 this->hold_ms_ = duration_ms;
536}
537
538void DFRobotSEN0609Component::run_sequencer_() {
540 if (this->operation_ != Operation::OPERATION_NONE) {
541 uint32_t timeout = DEFAULT_TIMEOUT_MS;
542 switch (this->operation_) {
543 case Operation::OPERATION_RESTART:
544 timeout = RESTART_MS;
545 break;
546 case Operation::OPERATION_FACTORY_RESET:
547 timeout = FACTORY_RESET_TIMEOUT_MS;
548 break;
549 case Operation::OPERATION_STOP:
550 case Operation::OPERATION_SAVE:
551 timeout = LONG_TIMEOUT_MS;
552 break;
553 default:
554 break;
555 }
556 if (now - this->sent_ms_ < timeout)
557 return;
558 if (this->operation_ == Operation::OPERATION_RESTART) {
559 this->finish_operation_();
560 } else {
561 ESP_LOGD(TAG, "No answer to '%s'", this->command_);
562 this->fail_operation_();
563 }
564 return;
565 }
566 if (now - this->hold_start_ms_ < this->hold_ms_)
567 return;
568
569 const bool work_pending =
570 this->factory_reset_pending_ || this->save_pending_ || this->pending_writes_ != 0 || this->pending_reads_ != 0;
571 Operation operation;
572 Param param = PARAM_COUNT;
573 if (this->restart_pending_) {
574 // The reboot drops a write the module took but did not save; writes not sent yet are kept for after it
575 operation = this->stopped_ && this->save_pending_ ? Operation::OPERATION_SAVE : Operation::OPERATION_RESTART;
576 } else if (!work_pending) {
577 if (!this->stopped_) {
578 this->check_frames_(now);
579 // Work given up on is tried again in a later sequence, so the sensor runs in between
580 if ((this->deferred_writes_ != 0 || this->deferred_save_) && now - this->deferred_ms_ >= GIVE_UP_BACKOFF_MS) {
581 this->pending_writes_ |= this->deferred_writes_;
582 this->save_pending_ = this->save_pending_ || this->deferred_save_;
583 this->deferred_writes_ = 0;
584 this->deferred_save_ = false;
585 }
586 return;
587 }
588 operation = Operation::OPERATION_START;
589 } else if (!this->stopped_) {
590 operation = Operation::OPERATION_STOP;
591 } else if (this->factory_reset_pending_) {
592 operation = Operation::OPERATION_FACTORY_RESET;
593 } else if (this->pending_writes_ != 0) {
594 operation = Operation::OPERATION_WRITE;
595 param = static_cast<Param>(__builtin_ctz(this->pending_writes_));
596 } else if (this->save_pending_) {
597 // Changes made within a second of each other share one flash write, also while a sequence runs
598 if (now - this->last_change_ms_ < COALESCE_MS)
599 return;
600 operation = Operation::OPERATION_SAVE;
601 } else {
602 operation = Operation::OPERATION_READ;
603 param = static_cast<Param>(__builtin_ctz(this->pending_reads_));
604 }
605
606 const uint16_t bit = param_bit(param);
607 bool built = this->build_command_(operation, param);
608 if (!built && operation == Operation::OPERATION_WRITE) {
609 // The value this write is paired with is unknown: read it first, once, and keep the write pending
610 if ((this->paired_reads_ & bit) == 0)
611 this->pending_reads_ |= this->wanted_reads_() & bit;
612 if ((this->pending_reads_ & bit) != 0) {
613 this->paired_reads_ |= bit;
614 operation = Operation::OPERATION_READ;
615 built = this->build_command_(operation, param);
616 }
617 }
618 if (!built) {
619 if (operation == Operation::OPERATION_WRITE) {
620 ESP_LOGW(TAG, "Setting not written: the value it is paired with could not be read");
621 this->pending_writes_ &= ~bit;
622 this->paired_reads_ &= ~bit;
623#ifdef USE_NUMBER
624 // Both values come from the read that failed, so what the module holds is unknown
625 this->forget_number_changes_(param, true);
626#endif
627 } else {
628 ESP_LOGE(TAG, "No command for %s of parameter %u", LOG_STR_ARG(operation_to_str(operation)),
629 static_cast<unsigned>(param));
630 this->pending_reads_ &= ~bit;
631 }
632 this->sequence_failed_ = true;
633 this->status_set_warning();
634 return;
635 }
636 // Cleared now, so a change made while the write is on its way is written again
637 if (operation == Operation::OPERATION_WRITE) {
638 this->pending_writes_ &= ~bit;
639 this->paired_reads_ &= ~bit;
640#ifdef USE_NUMBER
641 this->forget_number_changes_(param, false);
642#endif
643 }
644 this->operation_ = operation;
645 this->param_ = param;
646 this->response_count_ = 0;
647 this->echo_seen_ = false;
648 this->version_received_ = false;
649 this->sent_ms_ = now;
650 ESP_LOGV(TAG, "TX: %s", this->command_);
651 this->write_str(this->command_);
652 this->write_array(LINE_END, sizeof(LINE_END));
653}
654
655bool DFRobotSEN0609Component::build_command_(Operation operation, Param param) {
656 char *buf = this->command_;
657 constexpr size_t len = MAX_COMMAND_LENGTH;
658 switch (operation) {
659 case Operation::OPERATION_STOP:
660 ESPHOME_strncpy_P(buf, ESPHOME_PSTR("sensorStop"), len);
661 return true;
662 case Operation::OPERATION_START:
663 ESPHOME_strncpy_P(buf, ESPHOME_PSTR("sensorStart"), len);
664 return true;
665 case Operation::OPERATION_RESTART:
666 ESPHOME_strncpy_P(buf, ESPHOME_PSTR("resetSystem"), len);
667 return true;
668 case Operation::OPERATION_FACTORY_RESET:
669 ESPHOME_strncpy_P(buf, ESPHOME_PSTR("resetCfg"), len);
670 return true;
671 case Operation::OPERATION_SAVE:
672 ESPHOME_strncpy_P(buf, ESPHOME_PSTR("saveConfig"), len);
673 return true;
674 case Operation::OPERATION_READ:
675 ESPHOME_strncpy_P(buf, reinterpret_cast<ESPHOME_PGM_P>(ReadCommands::get_progmem_str(param, 0)), len);
676 return buf[0] != '\0';
677 default:
678 break;
679 }
680
681 switch (param) {
682 case PARAM_RUN_APP:
683 ESPHOME_snprintf_P(buf, len, ESPHOME_PSTR("setRunApp %u"), static_cast<unsigned>(this->mode_));
684 return true;
686 ESPHOME_strncpy_P(buf, ESPHOME_PSTR("setUartOutput 1 1 1 1.0"), len);
687 return true;
688#ifdef USE_SWITCH
690 ESPHOME_snprintf_P(buf, len, ESPHOME_PSTR("setMicroMotion %u"),
691 static_cast<unsigned>(this->switch_states_[SWITCH_MICRO_MOTION]));
692 return true;
693 case PARAM_RUN_LED:
694 case PARAM_OUT_LED: {
695 // 0 turns the LED on
696 const bool on = this->switch_states_[led_switch(param)];
697 this->sent_led_on_ = on;
698 ESPHOME_snprintf_P(buf, len, ESPHOME_PSTR("setLedMode %u %u"), param == PARAM_RUN_LED ? 1U : 2U, on ? 0U : 1U);
699 return true;
700 }
701#endif
702#ifdef USE_SELECT
703 case PARAM_GPIO_MODE:
704 ESPHOME_snprintf_P(buf, len, ESPHOME_PSTR("setGpioMode 1 %u"), static_cast<unsigned>(this->out_pin_level_));
705 return true;
706#endif
707 default:
708 break;
709 }
710
711#ifdef USE_NUMBER
712 const auto &values = this->number_values_;
713 float first = NAN;
714 float second = 0.0f;
715 ESPHOME_PGM_P format = nullptr;
716 switch (param) {
717 case PARAM_RANGE:
718 first = values[NUMBER_MIN_RANGE];
719 second = values[NUMBER_MAX_RANGE];
720 format = ESPHOME_PSTR("setRange %.2f %.2f");
721 break;
722 case PARAM_TRIG_RANGE:
723 first = values[NUMBER_TRIGGER_RANGE];
724 format = ESPHOME_PSTR("setTrigRange %.2f");
725 break;
727 first = values[NUMBER_HOLD_SENSITIVITY];
729 format = ESPHOME_PSTR("setSensitivity %.0f %.0f");
730 break;
731 case PARAM_LATENCY:
732 first = values[NUMBER_ON_LATENCY];
733 second = values[NUMBER_OFF_LATENCY];
734 format = ESPHOME_PSTR("setLatency %.2f %.1f");
735 break;
736 case PARAM_INHIBIT:
737 first = values[NUMBER_INHIBIT_TIME];
738 format = ESPHOME_PSTR("setInhibit %.2f");
739 break;
740 case PARAM_THR_FACTOR:
741 first = values[NUMBER_THRESHOLD_FACTOR];
742 format = ESPHOME_PSTR("setThrFactor %.0f");
743 break;
744 default:
745 return false;
746 }
747 // Commands that take two values need both read from the module first
748 if (std::isnan(first) || std::isnan(second))
749 return false;
750 ESPHOME_snprintf_P(buf, len, format, first, second);
751 return true;
752#else
753 return false;
754#endif
755}
756
757bool DFRobotSEN0609Component::apply_read_(Param param) {
758 const float *r = this->response_;
759 const uint8_t count = this->response_count_;
760 // A change still waiting to be written is newer than what the module holds
761 const bool write_pending = (this->pending_writes_ & param_bit(param)) != 0;
762#ifdef USE_NUMBER
763 // Checked for each number: publishing runs automations, which may change the next one
764 auto read_number = [this](NumberSetting setting, float value) {
765 if ((this->changed_numbers_ & (1U << setting)) == 0)
766 this->publish_number_(setting, value);
767 };
768#endif
769 switch (param) {
770#ifdef USE_NUMBER
771 case PARAM_RANGE:
772 if (count < 2)
773 return false;
774 read_number(NUMBER_MIN_RANGE, r[0]);
775 read_number(NUMBER_MAX_RANGE, r[1]);
776 return true;
777 case PARAM_TRIG_RANGE:
778 if (count < 1)
779 return false;
780 read_number(NUMBER_TRIGGER_RANGE, r[0]);
781 return true;
783 if (count < 2)
784 return false;
785 read_number(NUMBER_HOLD_SENSITIVITY, r[0]);
786 read_number(NUMBER_TRIGGER_SENSITIVITY, r[1]);
787 return true;
788 case PARAM_LATENCY:
789 if (count < 2)
790 return false;
791 read_number(NUMBER_ON_LATENCY, r[0]);
792 read_number(NUMBER_OFF_LATENCY, r[1]);
793 return true;
794 case PARAM_INHIBIT:
795 if (count < 1)
796 return false;
797 read_number(NUMBER_INHIBIT_TIME, r[0]);
798 return true;
799 case PARAM_THR_FACTOR:
800 if (count < 1)
801 return false;
802 read_number(NUMBER_THRESHOLD_FACTOR, r[0]);
803 return true;
804#endif
805#ifdef USE_SWITCH
807 if (count < 1)
808 return false;
809 if (!write_pending)
810 this->publish_switch_(SWITCH_MICRO_MOTION, r[0] != 0.0f);
811 return true;
812#endif
813#ifdef USE_SELECT
814 case PARAM_GPIO_MODE:
815 // Response <pin> <level>
816 if (count < 2 || (r[1] != 0.0f && r[1] != 1.0f))
817 return false;
818 if (!write_pending) {
819 this->out_pin_level_ = static_cast<uint8_t>(r[1]);
820 this->out_pin_level_select_->publish_state(this->out_pin_level_);
821 }
822 return true;
823#endif
824 case PARAM_SW_VERSION:
825 case PARAM_HW_VERSION:
826 // The versions are published as their lines arrive
827 return this->version_received_;
828 default:
829 return true;
830 }
831}
832
833void DFRobotSEN0609Component::finish_operation_() {
834 switch (this->operation_) {
835 case Operation::OPERATION_STOP:
836 this->stopped_ = true;
837 break;
838 case Operation::OPERATION_START:
839 this->stopped_ = false;
840 this->last_frame_ms_ = App.get_loop_component_start_time();
841 if (!this->sequence_failed_)
842 this->status_clear_warning();
843 this->sequence_failed_ = false;
844 break;
845 case Operation::OPERATION_RESTART:
846 // The module starts sensing on its own after a reboot
847 this->restart_pending_ = false;
848 this->stopped_ = false;
849 this->queue_configuration_();
850 break;
851 case Operation::OPERATION_FACTORY_RESET:
852 this->factory_reset_pending_ = false;
853 this->queue_configuration_();
854 break;
855 case Operation::OPERATION_WRITE:
856 this->deferred_writes_ &= ~param_bit(this->param_);
857 if (PERSISTENT_PARAMS & param_bit(this->param_))
858 this->save_pending_ = true;
859#ifdef USE_SWITCH
860 if (is_led(this->param_)) {
861 const uint8_t bit = 1U << led_switch(this->param_);
862 this->led_known_ |= bit;
863 this->led_on_ = this->sent_led_on_ ? (this->led_on_ | bit) : (this->led_on_ & ~bit);
864 }
865#endif
866 break;
867 case Operation::OPERATION_SAVE:
868 this->save_pending_ = false;
869 this->deferred_save_ = false;
870 break;
871 case Operation::OPERATION_READ:
872 if (!this->apply_read_(this->param_)) {
873 ESP_LOGW(TAG, "Malformed answer to '%s'", this->command_);
874 this->fail_operation_();
875 return;
876 }
877 this->pending_reads_ &= ~param_bit(this->param_);
878 this->paired_reads_ &= ~param_bit(this->param_);
879 break;
880 default:
881 return;
882 }
883 if (this->is_aux_operation_()) {
884 this->aux_attempts_ = 0;
885 } else {
886 this->attempts_ = 0;
887 }
888 this->operation_ = Operation::OPERATION_NONE;
889 this->hold_(COMMAND_GAP_MS);
890}
891
892void DFRobotSEN0609Component::fail_operation_() {
893 const bool aux = this->is_aux_operation_();
894 uint8_t &attempts = aux ? this->aux_attempts_ : this->attempts_;
895 const Operation operation = this->operation_;
896 this->operation_ = Operation::OPERATION_NONE;
897 this->hold_(COMMAND_GAP_MS);
898 const bool keep_write = operation == Operation::OPERATION_WRITE && (FRAME_PARAMS & param_bit(this->param_)) != 0;
899 const bool retry = ++attempts < MAX_ATTEMPTS;
900 if (operation == Operation::OPERATION_WRITE && (retry || keep_write))
901 this->pending_writes_ |= param_bit(this->param_);
902 if (retry)
903 return;
904
905 attempts = 0;
906 this->status_set_warning();
907 if (aux || keep_write) {
908 // Without stop, start or the configuration the sensor cannot run as set up; try again later
909 ESP_LOGW(TAG, "Command '%s' failed, trying again later", this->command_);
910 this->hold_ms_ = GIVE_UP_BACKOFF_MS;
911 return;
912 }
913 ESP_LOGW(TAG, "Giving up on %s '%s'", LOG_STR_ARG(operation_to_str(operation)), this->command_);
914 this->sequence_failed_ = true;
915 switch (operation) {
916 case Operation::OPERATION_RESTART:
917 this->restart_pending_ = false;
918 break;
919 case Operation::OPERATION_FACTORY_RESET:
920 this->factory_reset_pending_ = false;
921 break;
922 case Operation::OPERATION_WRITE:
923#ifdef USE_SWITCH
924 if (is_led(this->param_)) {
925 // A newer change is written anyway
926 if ((this->pending_writes_ & param_bit(this->param_)) != 0)
927 break;
928 // The LEDs cannot be read: show the state the module last accepted, or write this one again later
929 const uint8_t bit = 1U << led_switch(this->param_);
930 if ((this->led_known_ & bit) != 0) {
931 this->publish_switch_(led_switch(this->param_), (this->led_on_ & bit) != 0);
932 } else {
933 this->deferred_writes_ |= param_bit(this->param_);
934 this->deferred_ms_ = App.get_loop_component_start_time();
935 }
936 break;
937 }
938#endif
939 // The entity shows a value the module never got; show the module's value again
940 this->pending_reads_ |= this->wanted_reads_() & param_bit(this->param_);
941 break;
942 case Operation::OPERATION_SAVE:
943 // The entities show the changes as kept, but unsaved they are lost at power off: save in a later sequence
944 this->save_pending_ = false;
945 this->deferred_save_ = true;
946 this->deferred_ms_ = App.get_loop_component_start_time();
947 break;
948 case Operation::OPERATION_READ:
949 this->pending_reads_ &= ~param_bit(this->param_);
950 break;
951 default:
952 break;
953 }
954}
955
956} // namespace esphome::dfrobot_sen0609
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.
Helper class to deduplicate items in a series of values.
Definition helpers.h:1969
bool next(T value)
Feeds the next item in the series to the deduplicator and returns false if this is a duplicate.
Definition helpers.h:1972
bool next_unknown()
Returns true if the deduplicator's value was previously known.
Definition helpers.h:1982
Base-class for all sensors.
Definition sensor.h:47
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:65
void publish_state(const std::string &state)
Definition text_sensor.h:41
uint8_t second
int speed
Definition fan.h:3
bool state
Definition fan.h:2
unsigned count
Definition gif.cpp:51
const char * format
constexpr float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM
Definition bme680.cpp:26
NumberSetting
Settings shown as number entities.
Param
Module parameters; a write or read of each is one command. Pending work runs in this order.
ESPHOME_LOG_TAG(TAG, "dfrobot_sen0609")
SwitchSetting
Settings shown as switch entities.
PROGMEM_STRING_TABLE(ReadCommands, "", "", "getRange", "getTrigRange", "getSensitivity", "getLatency", "getInhibit", "getThrFactor", "getMicroMotion", "getGpioMode 1", "", "", "getSWV", "getHWV")
const void size_t len
Definition hal.h:68
optional< T > parse_number(const char *str)
Parse an unsigned decimal number from a null-terminated string.
Definition helpers.h:1276
optional< float > parse_float(const char *str)
Parse a decimal floating-point number; plain decimals skip the libc parser.
Definition helpers.cpp:326
Application App
Global storage of Application pointer - only one Application can exist.
uint8_t progmem_read_byte(const uint8_t *addr)
Definition hal.h:51
static void uint32_t
uint8_t end[39]
Definition sun_gtil2.cpp:17