mirror of
https://github.com/esphome/esphome.git
synced 2026-09-04 20:16:01 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a0e5ffce8 | ||
|
|
713b3b2bc9 | ||
|
|
d6179b6d56 | ||
|
|
73a95c411d | ||
|
|
de7af3865b | ||
|
|
338e498960 | ||
|
|
15e1ac2500 | ||
|
|
3402ee8bb1 | ||
|
|
61d2632851 | ||
|
|
e9d940aca4 |
@@ -64,6 +64,22 @@ static const char *const TAG = "ld2420";
|
||||
|
||||
// Local const's
|
||||
static constexpr uint16_t REFRESH_RATE_MS = 1000;
|
||||
static constexpr uint32_t CMD_ACK_TIMEOUT_MS = 1000;
|
||||
static constexpr uint8_t CMD_MAX_RETRIES = 3;
|
||||
|
||||
// Startup state machine timing. The module starts transmitting ~3.5 s after a
|
||||
// power cycle; the first listen window is roughly three times that to be
|
||||
// safe, and the shorter retry window still stays above the boot silence in
|
||||
// case the module reset itself between attempts.
|
||||
static constexpr uint32_t STARTUP_LISTEN_TIMEOUT_MS = 10000;
|
||||
static constexpr uint32_t STARTUP_RETRY_LISTEN_MS = 5000;
|
||||
static constexpr uint32_t STARTUP_LISTEN_SETTLE_MS = 500;
|
||||
static constexpr uint8_t STARTUP_SEQUENCE_MAX_RETRIES = 3;
|
||||
// Minimum reply data lengths for the startup reads: the limits read returns
|
||||
// three values and each gate read returns two, four bytes each plus the four
|
||||
// status bytes counted in the reply length field
|
||||
static constexpr uint8_t REPLY_MIN_LEN_LIMITS = 16;
|
||||
static constexpr uint8_t REPLY_MIN_LEN_GATE = 12;
|
||||
|
||||
// Command sets
|
||||
static constexpr uint16_t CMD_DISABLE_CONF = 0x00FE;
|
||||
@@ -184,13 +200,14 @@ static int32_t get_firmware_int(const char *version_string) {
|
||||
return result;
|
||||
}
|
||||
|
||||
float LD2420Component::get_setup_priority() const { return setup_priority::BUS; }
|
||||
|
||||
void LD2420Component::dump_config() {
|
||||
// Setup no longer blocks, so the config dump usually runs before the
|
||||
// version is read; do not present the "v0.0.0" placeholder as real
|
||||
const int32_t firmware = ld2420::get_firmware_int(this->firmware_ver_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"LD2420:\n"
|
||||
" Firmware version: %7s",
|
||||
this->firmware_ver_);
|
||||
firmware > 0 ? this->firmware_ver_ : "unknown");
|
||||
#ifdef USE_NUMBER
|
||||
ESP_LOGCONFIG(TAG, "Number:");
|
||||
LOG_NUMBER(" ", "Gate Timeout:", this->gate_timeout_number_);
|
||||
@@ -212,60 +229,318 @@ void LD2420Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Select:");
|
||||
LOG_SELECT(" ", "Operating Mode", this->operating_selector_);
|
||||
#endif
|
||||
if (ld2420::get_firmware_int(this->firmware_ver_) < CALIBRATE_VERSION_MIN) {
|
||||
if (firmware > 0 && firmware < CALIBRATE_VERSION_MIN) {
|
||||
ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->firmware_ver_);
|
||||
}
|
||||
}
|
||||
|
||||
void LD2420Component::setup() {
|
||||
if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed();
|
||||
void LD2420Component::setup() { this->begin_startup_(); }
|
||||
|
||||
void LD2420Component::begin_startup_() {
|
||||
// Default to energy mode so the stream parser can frame data from a module
|
||||
// that kept streaming across a soft restart, before the mode is negotiated.
|
||||
this->system_mode_ = CMD_SYSTEM_MODE_ENERGY;
|
||||
this->startup_sequence_retries_ = 0;
|
||||
this->config_read_complete_ = false;
|
||||
this->begin_listen_();
|
||||
}
|
||||
|
||||
void LD2420Component::begin_listen_() {
|
||||
this->phase_start_ms_ = millis();
|
||||
this->startup_state_ = StartupState::STARTUP_STATE_LISTEN_SETTLE;
|
||||
}
|
||||
|
||||
void LD2420Component::drain_rx_() {
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
size_t avail;
|
||||
while ((avail = this->available()) > 0) {
|
||||
if (!this->read_array(buf, std::min(avail, sizeof(buf)))) {
|
||||
ESP_LOGV(TAG, "Failed to drain the receive buffer");
|
||||
break;
|
||||
}
|
||||
}
|
||||
this->buffer_pos_ = 0;
|
||||
}
|
||||
|
||||
// Builds the command frame for the current startup state; returns false when
|
||||
// the state has no associated command
|
||||
bool LD2420Component::build_startup_frame_(CmdFrameT &frame) {
|
||||
switch (this->startup_state_) {
|
||||
case StartupState::STARTUP_STATE_ENTER_CONFIG:
|
||||
this->build_config_mode_frame_(frame, true);
|
||||
return true;
|
||||
case StartupState::STARTUP_STATE_READ_LIMITS:
|
||||
this->build_min_max_timeout_frame_(frame);
|
||||
return true;
|
||||
case StartupState::STARTUP_STATE_READ_VERSION:
|
||||
this->build_version_frame_(frame);
|
||||
return true;
|
||||
case StartupState::STARTUP_STATE_READ_GATES:
|
||||
this->build_gate_threshold_frame_(frame, this->startup_gate_);
|
||||
return true;
|
||||
case StartupState::STARTUP_STATE_SET_MODE:
|
||||
this->build_system_mode_frame_(frame, this->startup_target_mode_);
|
||||
return true;
|
||||
case StartupState::STARTUP_STATE_EXIT_CONFIG:
|
||||
this->build_config_mode_frame_(frame, false);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void LD2420Component::send_startup_cmd_() {
|
||||
CmdFrameT frame;
|
||||
if (!this->build_startup_frame_(frame)) {
|
||||
// Programming error: a command state without a frame would otherwise look
|
||||
// exactly like a module timeout
|
||||
ESP_LOGE(TAG, "No command frame for startup state %u", (unsigned) this->startup_state_);
|
||||
return;
|
||||
}
|
||||
this->get_min_max_distances_timeout_();
|
||||
#ifdef USE_NUMBER
|
||||
this->init_gate_config_numbers();
|
||||
#endif
|
||||
this->get_firmware_version_();
|
||||
const char *pfw = this->firmware_ver_;
|
||||
std::string fw_str(pfw);
|
||||
// Discard anything still buffered (including a late reply to a previous
|
||||
// send of the same command) so a stale ack cannot be matched to this one.
|
||||
// READ_LIMITS and all gate reads share the same command byte, so a late
|
||||
// reply accepted for the wrong request would shift every following gate's
|
||||
// thresholds by one.
|
||||
this->drain_rx_();
|
||||
this->startup_cmd_ = (uint8_t) frame.command;
|
||||
this->cmd_reply_.ack = false;
|
||||
this->cmd_reply_.error = 0;
|
||||
// A short reply acks without filling every data word; zero them so stale
|
||||
// values from the previous command cannot be stored as this command's data
|
||||
memset(this->cmd_reply_.data, 0, sizeof(this->cmd_reply_.data));
|
||||
this->write_cmd_frame_(frame);
|
||||
this->phase_start_ms_ = millis();
|
||||
}
|
||||
|
||||
for (auto &listener : this->listeners_) {
|
||||
listener->on_fw_version(fw_str);
|
||||
void LD2420Component::start_startup_cmd_(StartupState state) {
|
||||
this->startup_state_ = state;
|
||||
this->startup_cmd_attempts_ = 1;
|
||||
this->send_startup_cmd_();
|
||||
}
|
||||
|
||||
// Common ack handling for the startup commands: returns true once the reply to
|
||||
// the current startup frame arrived; resends on timeout, and after too many
|
||||
// failed sends either restarts the whole sequence or gives up with a warning.
|
||||
// A reply shorter than min_data_len is treated like silence so a truncated
|
||||
// read cannot be stored as zeroed configuration.
|
||||
bool LD2420Component::startup_ack_check_(uint8_t min_data_len) {
|
||||
if (this->cmd_reply_.ack && this->cmd_reply_.command == this->startup_cmd_ &&
|
||||
this->cmd_reply_.length >= min_data_len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) {
|
||||
delay_microseconds_safe(125);
|
||||
this->get_gate_threshold_(gate);
|
||||
if (this->cmd_reply_.error > 0) {
|
||||
// The module explicitly rejected the command; log why instead of letting
|
||||
// it look like silence. The normal retry cadence still applies.
|
||||
this->handle_cmd_error(this->cmd_reply_.error);
|
||||
this->cmd_reply_.error = 0;
|
||||
}
|
||||
if (millis() - this->phase_start_ms_ <= CMD_ACK_TIMEOUT_MS) {
|
||||
return false;
|
||||
}
|
||||
if (this->startup_cmd_attempts_ < CMD_MAX_RETRIES) {
|
||||
this->startup_cmd_attempts_++;
|
||||
ESP_LOGV(TAG, "No reply to startup command %2X; resending", this->startup_cmd_);
|
||||
this->send_startup_cmd_();
|
||||
return false;
|
||||
}
|
||||
this->abort_startup_cmd_();
|
||||
if (this->startup_sequence_retries_ < STARTUP_SEQUENCE_MAX_RETRIES) {
|
||||
this->startup_sequence_retries_++;
|
||||
ESP_LOGW(TAG, "Module setup attempt %u failed; retrying", this->startup_sequence_retries_);
|
||||
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->begin_listen_();
|
||||
return false;
|
||||
}
|
||||
this->abandon_startup_();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Gives up on configuration but keeps parsing the stream; a module that is
|
||||
// still streaming keeps publishing sensor data even without a config read.
|
||||
void LD2420Component::abandon_startup_() {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
if (ld2420::get_firmware_int(this->firmware_ver_) == 0) {
|
||||
// Old firmware streams text frames that are only parsed in simple mode;
|
||||
// without a version read the mode was never negotiated, so such a module
|
||||
// will not publish sensor data either.
|
||||
ESP_LOGE(TAG, "Firmware version and operating mode were never read");
|
||||
} else if (this->startup_state_ == StartupState::STARTUP_STATE_SET_MODE) {
|
||||
ESP_LOGE(TAG, "Operating mode write was not acknowledged; sensor data may not be parsed");
|
||||
}
|
||||
// Keep the editable config in sync with what was actually read so a later
|
||||
// Apply Config cannot write values that were never read from the module
|
||||
memcpy(&this->new_config, &this->current_config, sizeof(this->current_config));
|
||||
if (ld2420::get_firmware_int(this->firmware_ver_) < CALIBRATE_VERSION_MIN) {
|
||||
this->set_operating_mode(OP_SIMPLE_MODE_STRING);
|
||||
#ifdef USE_SELECT
|
||||
if (this->operating_selector_ != nullptr) {
|
||||
this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING);
|
||||
}
|
||||
#endif
|
||||
this->set_mode_(CMD_SYSTEM_MODE_SIMPLE);
|
||||
ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->firmware_ver_);
|
||||
} else {
|
||||
this->set_mode_(CMD_SYSTEM_MODE_ENERGY);
|
||||
#ifdef USE_SELECT
|
||||
if (this->operating_selector_ != nullptr) {
|
||||
this->operating_selector_->publish_state(OP_NORMAL_MODE_STRING);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#ifdef USE_NUMBER
|
||||
// Publish whatever was read before giving up so the number entities show
|
||||
// values next to the warning status instead of staying unknown forever
|
||||
this->init_gate_config_numbers();
|
||||
#endif
|
||||
this->set_system_mode(this->system_mode_);
|
||||
this->set_config_mode(false);
|
||||
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->startup_state_ = StartupState::STARTUP_STATE_RUNNING;
|
||||
}
|
||||
|
||||
void LD2420Component::abort_startup_cmd_() {
|
||||
// If the module already acknowledged config mode it stops streaming until
|
||||
// config mode is exited, so send the exit command blind before abandoning
|
||||
// the sequence; otherwise the stream never resumes and neither passive
|
||||
// parsing nor the next listen phase would ever see data. This is also sent
|
||||
// when config mode was never acknowledged: the ack may merely have been
|
||||
// lost, and the frame is harmless to a module that is not in config mode.
|
||||
CmdFrameT frame;
|
||||
this->build_config_mode_frame_(frame, false);
|
||||
this->write_cmd_frame_(frame);
|
||||
}
|
||||
|
||||
void LD2420Component::loop_startup_(bool got_data) {
|
||||
switch (this->startup_state_) {
|
||||
case StartupState::STARTUP_STATE_LISTEN_SETTLE:
|
||||
// Bytes can already be in flight when the listen phase starts: the tail
|
||||
// of a frame the module was transmitting when it was told to restart,
|
||||
// stale data buffered before setup, or the ack to the blind config mode
|
||||
// exit. Discard everything received during this settle window so only
|
||||
// data the module sends afterwards counts as proof that it is up and
|
||||
// streaming. The state runs at least one drain pass even when the main
|
||||
// loop stalls past the whole window, so bytes that arrived before the
|
||||
// listen phase can never be mistaken for fresh data.
|
||||
this->drain_rx_();
|
||||
if (millis() - this->phase_start_ms_ >= STARTUP_LISTEN_SETTLE_MS) {
|
||||
this->phase_start_ms_ = millis();
|
||||
this->startup_state_ = StartupState::STARTUP_STATE_LISTEN;
|
||||
}
|
||||
return;
|
||||
|
||||
case StartupState::STARTUP_STATE_LISTEN:
|
||||
// The module locks up until power cycled if it receives data before it
|
||||
// has sent its first frame after powering on, so wait until it has
|
||||
// provably transmitted before sending anything. (A full-frame check
|
||||
// cannot serve as that proof: old-firmware text frames are only
|
||||
// recognized once the operating mode is known, which requires the very
|
||||
// handshake this phase gates.) A module stuck in some other state
|
||||
// stays quiet, so fall through after the listen window.
|
||||
if (!got_data) {
|
||||
const uint32_t listen_timeout_ms =
|
||||
this->startup_sequence_retries_ == 0 ? STARTUP_LISTEN_TIMEOUT_MS : STARTUP_RETRY_LISTEN_MS;
|
||||
if (millis() - this->phase_start_ms_ < listen_timeout_ms) {
|
||||
return;
|
||||
}
|
||||
ESP_LOGW(TAG, "No data received from the module; attempting configuration anyway");
|
||||
}
|
||||
// Drop any partial frame so the ack parser starts clean
|
||||
this->drain_rx_();
|
||||
this->start_startup_cmd_(StartupState::STARTUP_STATE_ENTER_CONFIG);
|
||||
return;
|
||||
|
||||
case StartupState::STARTUP_STATE_ENTER_CONFIG:
|
||||
if (!this->startup_ack_check_()) {
|
||||
return;
|
||||
}
|
||||
this->start_startup_cmd_(StartupState::STARTUP_STATE_READ_LIMITS);
|
||||
return;
|
||||
|
||||
case StartupState::STARTUP_STATE_READ_LIMITS:
|
||||
if (!this->startup_ack_check_(REPLY_MIN_LEN_LIMITS)) {
|
||||
return;
|
||||
}
|
||||
this->current_config.min_gate = (uint16_t) this->cmd_reply_.data[0];
|
||||
this->current_config.max_gate = (uint16_t) this->cmd_reply_.data[1];
|
||||
this->current_config.timeout = (uint16_t) this->cmd_reply_.data[2];
|
||||
this->start_startup_cmd_(StartupState::STARTUP_STATE_READ_VERSION);
|
||||
return;
|
||||
|
||||
case StartupState::STARTUP_STATE_READ_VERSION: {
|
||||
if (!this->startup_ack_check_()) {
|
||||
return;
|
||||
}
|
||||
std::string fw_str(this->firmware_ver_);
|
||||
for (auto &listener : this->listeners_) {
|
||||
listener->on_fw_version(fw_str);
|
||||
}
|
||||
this->startup_gate_ = 0;
|
||||
this->start_startup_cmd_(StartupState::STARTUP_STATE_READ_GATES);
|
||||
return;
|
||||
}
|
||||
|
||||
case StartupState::STARTUP_STATE_READ_GATES:
|
||||
if (!this->startup_ack_check_(REPLY_MIN_LEN_GATE)) {
|
||||
return;
|
||||
}
|
||||
this->current_config.move_thresh[this->startup_gate_] = this->cmd_reply_.data[0];
|
||||
this->current_config.still_thresh[this->startup_gate_] = this->cmd_reply_.data[1];
|
||||
if (++this->startup_gate_ < TOTAL_GATES) {
|
||||
this->start_startup_cmd_(StartupState::STARTUP_STATE_READ_GATES);
|
||||
return;
|
||||
}
|
||||
this->config_read_complete_ = true;
|
||||
memcpy(&this->new_config, &this->current_config, sizeof(this->current_config));
|
||||
if (ld2420::get_firmware_int(this->firmware_ver_) < CALIBRATE_VERSION_MIN) {
|
||||
this->set_operating_mode(OP_SIMPLE_MODE_STRING);
|
||||
#ifdef USE_SELECT
|
||||
if (this->operating_selector_ != nullptr) {
|
||||
this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING);
|
||||
}
|
||||
#endif
|
||||
this->startup_target_mode_ = CMD_SYSTEM_MODE_SIMPLE;
|
||||
ESP_LOGW(TAG, "Firmware version %s and older supports Simple Mode only", this->firmware_ver_);
|
||||
} else {
|
||||
this->startup_target_mode_ = CMD_SYSTEM_MODE_ENERGY;
|
||||
#ifdef USE_SELECT
|
||||
if (this->operating_selector_ != nullptr) {
|
||||
this->operating_selector_->publish_state(OP_NORMAL_MODE_STRING);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#ifdef USE_NUMBER
|
||||
this->init_gate_config_numbers();
|
||||
#endif
|
||||
this->start_startup_cmd_(StartupState::STARTUP_STATE_SET_MODE);
|
||||
return;
|
||||
|
||||
case StartupState::STARTUP_STATE_SET_MODE:
|
||||
if (!this->startup_ack_check_()) {
|
||||
return;
|
||||
}
|
||||
// Switch the parser only after the module acknowledged the mode write,
|
||||
// so both sides stay in the same mode when the write is never acked
|
||||
this->set_mode_(this->startup_target_mode_);
|
||||
this->start_startup_cmd_(StartupState::STARTUP_STATE_EXIT_CONFIG);
|
||||
return;
|
||||
|
||||
case StartupState::STARTUP_STATE_EXIT_CONFIG:
|
||||
if (!this->startup_ack_check_()) {
|
||||
return;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
this->startup_state_ = StartupState::STARTUP_STATE_RUNNING;
|
||||
ESP_LOGI(TAG, "Module setup complete; firmware %s", this->firmware_ver_);
|
||||
return;
|
||||
|
||||
case StartupState::STARTUP_STATE_RUNNING:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Common precondition for the button actions: the startup handshake must have
|
||||
// finished, and actions that write configuration additionally require that
|
||||
// every limit and gate threshold was actually read (setup may have given up
|
||||
// partway through; writing the unread config to the module's NVM would wipe
|
||||
// its stored thresholds).
|
||||
bool LD2420Component::action_allowed_(bool needs_config) {
|
||||
if (this->startup_state_ != StartupState::STARTUP_STATE_RUNNING) {
|
||||
ESP_LOGW(TAG, "Module is still starting up; ignoring");
|
||||
return false;
|
||||
}
|
||||
if (needs_config && !this->config_read_complete_) {
|
||||
ESP_LOGW(TAG, "Module configuration was never fully read; ignoring");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void LD2420Component::apply_config_action() {
|
||||
if (!this->action_allowed_(true)) {
|
||||
return;
|
||||
}
|
||||
const uint8_t checksum = calc_checksum(&this->new_config, sizeof(this->new_config));
|
||||
if (checksum == calc_checksum(&this->current_config, sizeof(this->current_config))) {
|
||||
ESP_LOGD(TAG, "No configuration change detected");
|
||||
@@ -274,31 +549,44 @@ void LD2420Component::apply_config_action() {
|
||||
ESP_LOGD(TAG, "Reconfiguring");
|
||||
if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed();
|
||||
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL);
|
||||
return;
|
||||
}
|
||||
this->set_min_max_distances_timeout(this->new_config.max_gate, this->new_config.min_gate, this->new_config.timeout);
|
||||
uint8_t error = this->set_min_max_distances_timeout(this->new_config.max_gate, this->new_config.min_gate,
|
||||
this->new_config.timeout);
|
||||
for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) {
|
||||
delay_microseconds_safe(125);
|
||||
this->set_gate_threshold(gate);
|
||||
error |= this->set_gate_threshold(gate);
|
||||
}
|
||||
if (error == LD2420_ERROR_NONE) {
|
||||
// Only adopt the new values as current once every write was acknowledged
|
||||
memcpy(¤t_config, &new_config, sizeof(new_config));
|
||||
}
|
||||
memcpy(¤t_config, &new_config, sizeof(new_config));
|
||||
#ifdef USE_NUMBER
|
||||
this->init_gate_config_numbers();
|
||||
#endif
|
||||
this->set_system_mode(this->system_mode_);
|
||||
this->set_config_mode(false); // Disable config mode to save new values in LD2420 nvm
|
||||
// Disable config mode to save the new values in the LD2420 nvm
|
||||
if (this->set_config_mode(false) == LD2420_ERROR_NONE && error == LD2420_ERROR_NONE) {
|
||||
this->status_clear_warning();
|
||||
} else {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
this->set_operating_mode(OP_NORMAL_MODE_STRING);
|
||||
}
|
||||
|
||||
void LD2420Component::factory_reset_action() {
|
||||
if (!this->action_allowed_(true)) {
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Setting factory defaults");
|
||||
if (this->set_config_mode(true) == LD2420_ERROR_TIMEOUT) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed();
|
||||
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL);
|
||||
return;
|
||||
}
|
||||
this->set_min_max_distances_timeout(FACTORY_MAX_GATE, FACTORY_MIN_GATE, FACTORY_TIMEOUT);
|
||||
uint8_t error = this->set_min_max_distances_timeout(FACTORY_MAX_GATE, FACTORY_MIN_GATE, FACTORY_TIMEOUT);
|
||||
#ifdef USE_NUMBER
|
||||
this->gate_timeout_number_->state = FACTORY_TIMEOUT;
|
||||
this->min_gate_distance_number_->state = FACTORY_MIN_GATE;
|
||||
@@ -308,11 +596,18 @@ void LD2420Component::factory_reset_action() {
|
||||
this->new_config.move_thresh[gate] = FACTORY_MOVE_THRESH[gate];
|
||||
this->new_config.still_thresh[gate] = FACTORY_STILL_THRESH[gate];
|
||||
delay_microseconds_safe(125);
|
||||
this->set_gate_threshold(gate);
|
||||
error |= this->set_gate_threshold(gate);
|
||||
}
|
||||
if (error == LD2420_ERROR_NONE) {
|
||||
memcpy(&this->current_config, &this->new_config, sizeof(this->new_config));
|
||||
}
|
||||
memcpy(&this->current_config, &this->new_config, sizeof(this->new_config));
|
||||
this->set_system_mode(this->system_mode_);
|
||||
this->set_config_mode(false);
|
||||
if (this->set_config_mode(false) == LD2420_ERROR_NONE && error == LD2420_ERROR_NONE) {
|
||||
this->status_clear_warning();
|
||||
} else {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
#ifdef USE_NUMBER
|
||||
this->init_gate_config_numbers();
|
||||
this->refresh_gate_config_numbers();
|
||||
@@ -320,16 +615,21 @@ void LD2420Component::factory_reset_action() {
|
||||
}
|
||||
|
||||
void LD2420Component::restart_module_action() {
|
||||
if (!this->action_allowed_(false)) {
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Restarting");
|
||||
this->send_module_restart();
|
||||
this->set_timeout(250, [this]() {
|
||||
this->set_config_mode(true);
|
||||
this->set_system_mode(this->system_mode_);
|
||||
this->set_config_mode(false);
|
||||
});
|
||||
// The module is silent while it boots and locks up if it receives data
|
||||
// before it has sent its first frame, so re-run the listen-first startup
|
||||
// sequence instead of transmitting into the boot window.
|
||||
this->begin_startup_();
|
||||
}
|
||||
|
||||
void LD2420Component::revert_config_action() {
|
||||
if (!this->action_allowed_(false)) {
|
||||
return;
|
||||
}
|
||||
memcpy(&this->new_config, &this->current_config, sizeof(this->current_config));
|
||||
#ifdef USE_NUMBER
|
||||
this->init_gate_config_numbers();
|
||||
@@ -342,7 +642,10 @@ void LD2420Component::loop() {
|
||||
if (this->cmd_active_) {
|
||||
return;
|
||||
}
|
||||
this->read_batch_(this->buffer_data_);
|
||||
const bool got_data = this->read_batch_(this->buffer_data_);
|
||||
if (this->startup_state_ != StartupState::STARTUP_STATE_RUNNING) {
|
||||
this->loop_startup_(got_data);
|
||||
}
|
||||
}
|
||||
|
||||
void LD2420Component::update_radar_data(uint16_t const *gate_energy, uint8_t sample_number) {
|
||||
@@ -549,9 +852,10 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) {
|
||||
}
|
||||
}
|
||||
|
||||
void LD2420Component::read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer) {
|
||||
bool LD2420Component::read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer) {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
const bool got_data = avail > 0;
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
@@ -564,6 +868,7 @@ void LD2420Component::read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer) {
|
||||
this->readline_(buf[i], buffer.data(), buffer.size());
|
||||
}
|
||||
}
|
||||
return got_data;
|
||||
}
|
||||
|
||||
void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
|
||||
@@ -633,37 +938,39 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) {
|
||||
}
|
||||
}
|
||||
|
||||
void LD2420Component::write_cmd_frame_(const CmdFrameT &frame) {
|
||||
uint8_t cmd_buffer[MAX_LINE_LENGTH];
|
||||
uint16_t length = 0;
|
||||
const uint16_t frame_data_bytes = frame.data_length + 2; // Always add two bytes for the cmd size
|
||||
|
||||
memcpy(&cmd_buffer[length], &frame.header, sizeof(frame.header));
|
||||
length += sizeof(frame.header);
|
||||
|
||||
memcpy(&cmd_buffer[length], &frame_data_bytes, sizeof(frame.data_length));
|
||||
length += sizeof(frame.data_length);
|
||||
|
||||
memcpy(&cmd_buffer[length], &frame.command, sizeof(frame.command));
|
||||
length += sizeof(frame.command);
|
||||
|
||||
memcpy(&cmd_buffer[length], frame.data, frame.data_length);
|
||||
length += frame.data_length;
|
||||
|
||||
memcpy(&cmd_buffer[length], &frame.footer, sizeof(frame.footer));
|
||||
length += sizeof(frame.footer);
|
||||
this->write_array(cmd_buffer, length);
|
||||
}
|
||||
|
||||
int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
|
||||
uint32_t start_millis = millis();
|
||||
uint8_t error = 0;
|
||||
uint8_t ack_buffer[MAX_LINE_LENGTH];
|
||||
uint8_t cmd_buffer[MAX_LINE_LENGTH];
|
||||
this->cmd_reply_.ack = false;
|
||||
if (frame.command != CMD_RESTART) {
|
||||
this->cmd_active_ = true;
|
||||
} // Restart does not reply, thus no ack state required
|
||||
uint8_t retry = 3;
|
||||
uint8_t retry = CMD_MAX_RETRIES;
|
||||
while (retry) {
|
||||
frame.length = 0;
|
||||
uint16_t frame_data_bytes = frame.data_length + 2; // Always add two bytes for the cmd size
|
||||
|
||||
memcpy(&cmd_buffer[frame.length], &frame.header, sizeof(frame.header));
|
||||
frame.length += sizeof(frame.header);
|
||||
|
||||
memcpy(&cmd_buffer[frame.length], &frame_data_bytes, sizeof(frame.data_length));
|
||||
frame.length += sizeof(frame.data_length);
|
||||
|
||||
memcpy(&cmd_buffer[frame.length], &frame.command, sizeof(frame.command));
|
||||
frame.length += sizeof(frame.command);
|
||||
|
||||
for (uint16_t index = 0; index < frame.data_length; index++) {
|
||||
memcpy(&cmd_buffer[frame.length], &frame.data[index], sizeof(frame.data[index]));
|
||||
frame.length += sizeof(frame.data[index]);
|
||||
}
|
||||
|
||||
memcpy(cmd_buffer + frame.length, &frame.footer, sizeof(frame.footer));
|
||||
frame.length += sizeof(frame.footer);
|
||||
this->write_array(cmd_buffer, frame.length);
|
||||
this->write_cmd_frame_(frame);
|
||||
|
||||
error = 0;
|
||||
if (frame.command == CMD_RESTART) {
|
||||
@@ -676,7 +983,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
|
||||
}
|
||||
delay_microseconds_safe(1450);
|
||||
// Wait on an Rx from the LD2420 for up to 3 1 second loops, otherwise it could trigger a WDT.
|
||||
if ((millis() - start_millis) > 1000) {
|
||||
if ((millis() - start_millis) > CMD_ACK_TIMEOUT_MS) {
|
||||
start_millis = millis();
|
||||
error = LD2420_ERROR_TIMEOUT;
|
||||
retry--;
|
||||
@@ -690,19 +997,26 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
|
||||
this->handle_cmd_error(this->cmd_reply_.error);
|
||||
}
|
||||
}
|
||||
// On ack the reply parser already cleared this; clear it here as well so an
|
||||
// exhausted retry loop cannot leave loop() skipping all processing forever.
|
||||
this->cmd_active_ = false;
|
||||
return error;
|
||||
}
|
||||
|
||||
void LD2420Component::build_config_mode_frame_(CmdFrameT &frame, bool enable) {
|
||||
frame.data_length = 0;
|
||||
frame.header = CMD_FRAME_HEADER;
|
||||
frame.command = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
|
||||
if (enable) {
|
||||
memcpy(&frame.data[0], &CMD_PROTOCOL_VER, sizeof(CMD_PROTOCOL_VER));
|
||||
frame.data_length += sizeof(CMD_PROTOCOL_VER);
|
||||
}
|
||||
frame.footer = CMD_FRAME_FOOTER;
|
||||
}
|
||||
|
||||
uint8_t LD2420Component::set_config_mode(bool enable) {
|
||||
CmdFrameT cmd_frame;
|
||||
cmd_frame.data_length = 0;
|
||||
cmd_frame.header = CMD_FRAME_HEADER;
|
||||
cmd_frame.command = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
|
||||
if (enable) {
|
||||
memcpy(&cmd_frame.data[0], &CMD_PROTOCOL_VER, sizeof(CMD_PROTOCOL_VER));
|
||||
cmd_frame.data_length += sizeof(CMD_PROTOCOL_VER);
|
||||
}
|
||||
cmd_frame.footer = CMD_FRAME_FOOTER;
|
||||
this->build_config_mode_frame_(cmd_frame, enable);
|
||||
ESP_LOGV(TAG, "Sending set config %s command: %2X", enable ? "enable" : "disable", cmd_frame.command);
|
||||
return this->send_cmd_from_array(cmd_frame);
|
||||
}
|
||||
@@ -720,18 +1034,6 @@ void LD2420Component::ld2420_restart() {
|
||||
this->send_cmd_from_array(cmd_frame);
|
||||
}
|
||||
|
||||
void LD2420Component::get_reg_value_(uint16_t reg) {
|
||||
CmdFrameT cmd_frame;
|
||||
cmd_frame.data_length = 0;
|
||||
cmd_frame.header = CMD_FRAME_HEADER;
|
||||
cmd_frame.command = CMD_READ_REGISTER;
|
||||
cmd_frame.data[1] = reg;
|
||||
cmd_frame.data_length += 2;
|
||||
cmd_frame.footer = CMD_FRAME_FOOTER;
|
||||
ESP_LOGV(TAG, "Sending read register %4X command: %2X", reg, cmd_frame.command);
|
||||
this->send_cmd_from_array(cmd_frame);
|
||||
}
|
||||
|
||||
void LD2420Component::set_reg_value(uint16_t reg, uint16_t value) {
|
||||
CmdFrameT cmd_frame;
|
||||
cmd_frame.data_length = 0;
|
||||
@@ -755,84 +1057,69 @@ void LD2420Component::handle_cmd_error(uint16_t error) {
|
||||
}
|
||||
}
|
||||
|
||||
int LD2420Component::get_gate_threshold_(uint8_t gate) {
|
||||
uint8_t error;
|
||||
CmdFrameT cmd_frame;
|
||||
cmd_frame.data_length = 0;
|
||||
cmd_frame.header = CMD_FRAME_HEADER;
|
||||
cmd_frame.command = CMD_READ_ABD_PARAM;
|
||||
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_GATE_MOVE_THRESH[gate], sizeof(CMD_GATE_MOVE_THRESH[gate]));
|
||||
cmd_frame.data_length += 2;
|
||||
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_GATE_STILL_THRESH[gate], sizeof(CMD_GATE_STILL_THRESH[gate]));
|
||||
cmd_frame.data_length += 2;
|
||||
cmd_frame.footer = CMD_FRAME_FOOTER;
|
||||
ESP_LOGV(TAG, "Sending read gate %d high/low threshold command: %2X", gate, cmd_frame.command);
|
||||
error = this->send_cmd_from_array(cmd_frame);
|
||||
if (error == 0) {
|
||||
this->current_config.move_thresh[gate] = cmd_reply_.data[0];
|
||||
this->current_config.still_thresh[gate] = cmd_reply_.data[1];
|
||||
}
|
||||
return error;
|
||||
void LD2420Component::build_gate_threshold_frame_(CmdFrameT &frame, uint8_t gate) {
|
||||
frame.data_length = 0;
|
||||
frame.header = CMD_FRAME_HEADER;
|
||||
frame.command = CMD_READ_ABD_PARAM;
|
||||
memcpy(&frame.data[frame.data_length], &CMD_GATE_MOVE_THRESH[gate], sizeof(CMD_GATE_MOVE_THRESH[gate]));
|
||||
frame.data_length += 2;
|
||||
memcpy(&frame.data[frame.data_length], &CMD_GATE_STILL_THRESH[gate], sizeof(CMD_GATE_STILL_THRESH[gate]));
|
||||
frame.data_length += 2;
|
||||
frame.footer = CMD_FRAME_FOOTER;
|
||||
ESP_LOGV(TAG, "Sending read gate %d high/low threshold command: %2X", gate, frame.command);
|
||||
}
|
||||
|
||||
int LD2420Component::get_min_max_distances_timeout_() {
|
||||
uint8_t error;
|
||||
CmdFrameT cmd_frame;
|
||||
cmd_frame.data_length = 0;
|
||||
cmd_frame.header = CMD_FRAME_HEADER;
|
||||
cmd_frame.command = CMD_READ_ABD_PARAM;
|
||||
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_MIN_GATE_REG,
|
||||
void LD2420Component::build_min_max_timeout_frame_(CmdFrameT &frame) {
|
||||
frame.data_length = 0;
|
||||
frame.header = CMD_FRAME_HEADER;
|
||||
frame.command = CMD_READ_ABD_PARAM;
|
||||
memcpy(&frame.data[frame.data_length], &CMD_MIN_GATE_REG,
|
||||
sizeof(CMD_MIN_GATE_REG)); // Register: global min detect gate number
|
||||
cmd_frame.data_length += sizeof(CMD_MIN_GATE_REG);
|
||||
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_MAX_GATE_REG,
|
||||
frame.data_length += sizeof(CMD_MIN_GATE_REG);
|
||||
memcpy(&frame.data[frame.data_length], &CMD_MAX_GATE_REG,
|
||||
sizeof(CMD_MAX_GATE_REG)); // Register: global max detect gate number
|
||||
cmd_frame.data_length += sizeof(CMD_MAX_GATE_REG);
|
||||
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_TIMEOUT_REG,
|
||||
frame.data_length += sizeof(CMD_MAX_GATE_REG);
|
||||
memcpy(&frame.data[frame.data_length], &CMD_TIMEOUT_REG,
|
||||
sizeof(CMD_TIMEOUT_REG)); // Register: global delay time
|
||||
cmd_frame.data_length += sizeof(CMD_TIMEOUT_REG);
|
||||
cmd_frame.footer = CMD_FRAME_FOOTER;
|
||||
ESP_LOGV(TAG, "Sending read gate min max and timeout command: %2X", cmd_frame.command);
|
||||
error = this->send_cmd_from_array(cmd_frame);
|
||||
if (error == 0) {
|
||||
this->current_config.min_gate = (uint16_t) cmd_reply_.data[0];
|
||||
this->current_config.max_gate = (uint16_t) cmd_reply_.data[1];
|
||||
this->current_config.timeout = (uint16_t) cmd_reply_.data[2];
|
||||
}
|
||||
return error;
|
||||
frame.data_length += sizeof(CMD_TIMEOUT_REG);
|
||||
frame.footer = CMD_FRAME_FOOTER;
|
||||
ESP_LOGV(TAG, "Sending read gate min max and timeout command: %2X", frame.command);
|
||||
}
|
||||
|
||||
void LD2420Component::build_system_mode_frame_(CmdFrameT &frame, uint16_t mode) {
|
||||
uint16_t unknown_parm = 0x0000;
|
||||
frame.data_length = 0;
|
||||
frame.header = CMD_FRAME_HEADER;
|
||||
frame.command = CMD_WRITE_SYS_PARAM;
|
||||
memcpy(&frame.data[frame.data_length], &CMD_SYSTEM_MODE, sizeof(CMD_SYSTEM_MODE));
|
||||
frame.data_length += sizeof(CMD_SYSTEM_MODE);
|
||||
memcpy(&frame.data[frame.data_length], &mode, sizeof(mode));
|
||||
frame.data_length += sizeof(mode);
|
||||
memcpy(&frame.data[frame.data_length], &unknown_parm, sizeof(unknown_parm));
|
||||
frame.data_length += sizeof(unknown_parm);
|
||||
frame.footer = CMD_FRAME_FOOTER;
|
||||
ESP_LOGV(TAG, "Sending write system mode command: %2X", frame.command);
|
||||
}
|
||||
|
||||
void LD2420Component::set_system_mode(uint16_t mode) {
|
||||
CmdFrameT cmd_frame;
|
||||
uint16_t unknown_parm = 0x0000;
|
||||
cmd_frame.data_length = 0;
|
||||
cmd_frame.header = CMD_FRAME_HEADER;
|
||||
cmd_frame.command = CMD_WRITE_SYS_PARAM;
|
||||
memcpy(&cmd_frame.data[cmd_frame.data_length], &CMD_SYSTEM_MODE, sizeof(CMD_SYSTEM_MODE));
|
||||
cmd_frame.data_length += sizeof(CMD_SYSTEM_MODE);
|
||||
memcpy(&cmd_frame.data[cmd_frame.data_length], &mode, sizeof(mode));
|
||||
cmd_frame.data_length += sizeof(mode);
|
||||
memcpy(&cmd_frame.data[cmd_frame.data_length], &unknown_parm, sizeof(unknown_parm));
|
||||
cmd_frame.data_length += sizeof(unknown_parm);
|
||||
cmd_frame.footer = CMD_FRAME_FOOTER;
|
||||
ESP_LOGV(TAG, "Sending write system mode command: %2X", cmd_frame.command);
|
||||
this->build_system_mode_frame_(cmd_frame, mode);
|
||||
if (this->send_cmd_from_array(cmd_frame) == 0) {
|
||||
this->set_mode_(mode);
|
||||
}
|
||||
}
|
||||
|
||||
void LD2420Component::get_firmware_version_() {
|
||||
CmdFrameT cmd_frame;
|
||||
cmd_frame.data_length = 0;
|
||||
cmd_frame.header = CMD_FRAME_HEADER;
|
||||
cmd_frame.command = CMD_READ_VERSION;
|
||||
cmd_frame.footer = CMD_FRAME_FOOTER;
|
||||
|
||||
ESP_LOGV(TAG, "Sending read firmware version command: %2X", cmd_frame.command);
|
||||
this->send_cmd_from_array(cmd_frame);
|
||||
void LD2420Component::build_version_frame_(CmdFrameT &frame) {
|
||||
frame.data_length = 0;
|
||||
frame.header = CMD_FRAME_HEADER;
|
||||
frame.command = CMD_READ_VERSION;
|
||||
frame.footer = CMD_FRAME_FOOTER;
|
||||
ESP_LOGV(TAG, "Sending read firmware version command: %2X", frame.command);
|
||||
}
|
||||
|
||||
void LD2420Component::set_min_max_distances_timeout(uint32_t max_gate_distance, uint32_t min_gate_distance, // NOLINT
|
||||
uint32_t timeout) {
|
||||
uint8_t LD2420Component::set_min_max_distances_timeout(uint32_t max_gate_distance,
|
||||
uint32_t min_gate_distance, // NOLINT
|
||||
uint32_t timeout) {
|
||||
// Header H, Length L, Register R, Value V, Footer F
|
||||
// |Min Gate |Max Gate |Timeout |
|
||||
// HH HH HH HH LL LL CC CC RR RR VV VV VV VV RR RR VV VV VV VV RR RR VV VV VV VV FF FF FF FF
|
||||
@@ -861,10 +1148,10 @@ void LD2420Component::set_min_max_distances_timeout(uint32_t max_gate_distance,
|
||||
cmd_frame.footer = CMD_FRAME_FOOTER;
|
||||
|
||||
ESP_LOGV(TAG, "Sending write gate min max and timeout command: %2X", cmd_frame.command);
|
||||
this->send_cmd_from_array(cmd_frame);
|
||||
return this->send_cmd_from_array(cmd_frame);
|
||||
}
|
||||
|
||||
void LD2420Component::set_gate_threshold(uint8_t gate) {
|
||||
uint8_t LD2420Component::set_gate_threshold(uint8_t gate) {
|
||||
// Header H, Length L, Command C, Register R, Value V, Footer F
|
||||
// HH HH HH HH LL LL CC CC RR RR VV VV VV VV RR RR VV VV VV VV FF FF FF FF
|
||||
// FD FC FB FA 14 00 07 00 10 00 00 FF 00 00 00 01 00 0F 00 00 04 03 02 01
|
||||
@@ -887,7 +1174,7 @@ void LD2420Component::set_gate_threshold(uint8_t gate) {
|
||||
cmd_frame.data_length += sizeof(this->new_config.still_thresh[gate]);
|
||||
cmd_frame.footer = CMD_FRAME_FOOTER;
|
||||
ESP_LOGV(TAG, "Sending set gate %4X sensitivity command: %2X", gate, cmd_frame.command);
|
||||
this->send_cmd_from_array(cmd_frame);
|
||||
return this->send_cmd_from_array(cmd_frame);
|
||||
}
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
|
||||
@@ -45,15 +45,14 @@ class LD2420Component final : public Component, public uart::UARTDevice {
|
||||
struct CmdFrameT {
|
||||
uint32_t header{0};
|
||||
uint32_t footer{0};
|
||||
uint16_t length{0};
|
||||
uint16_t command{0};
|
||||
uint16_t data_length{0};
|
||||
uint8_t data[18];
|
||||
};
|
||||
|
||||
struct RegConfigT {
|
||||
uint32_t move_thresh[TOTAL_GATES];
|
||||
uint32_t still_thresh[TOTAL_GATES];
|
||||
uint32_t move_thresh[TOTAL_GATES]{};
|
||||
uint32_t still_thresh[TOTAL_GATES]{};
|
||||
uint16_t min_gate{0};
|
||||
uint16_t max_gate{0};
|
||||
uint16_t timeout{0};
|
||||
@@ -105,7 +104,6 @@ class LD2420Component final : public Component, public uart::UARTDevice {
|
||||
void apply_config_action();
|
||||
void factory_reset_action();
|
||||
void revert_config_action();
|
||||
float get_setup_priority() const override;
|
||||
int send_cmd_from_array(CmdFrameT cmd_frame);
|
||||
void report_gate_data();
|
||||
void handle_cmd_error(uint16_t error);
|
||||
@@ -113,8 +111,8 @@ class LD2420Component final : public Component, public uart::UARTDevice {
|
||||
void auto_calibrate_sensitivity();
|
||||
void update_radar_data(uint16_t const *gate_energy, uint8_t sample_number);
|
||||
uint8_t set_config_mode(bool enable);
|
||||
void set_min_max_distances_timeout(uint32_t max_gate_distance, uint32_t min_gate_distance, uint32_t timeout);
|
||||
void set_gate_threshold(uint8_t gate);
|
||||
uint8_t set_min_max_distances_timeout(uint32_t max_gate_distance, uint32_t min_gate_distance, uint32_t timeout);
|
||||
uint8_t set_gate_threshold(uint8_t gate);
|
||||
void set_reg_value(uint16_t reg, uint16_t value);
|
||||
void set_system_mode(uint16_t mode);
|
||||
void ld2420_restart();
|
||||
@@ -154,10 +152,40 @@ class LD2420Component final : public Component, public uart::UARTDevice {
|
||||
volatile bool ack;
|
||||
};
|
||||
|
||||
void get_firmware_version_();
|
||||
int get_gate_threshold_(uint8_t gate);
|
||||
void get_reg_value_(uint16_t reg);
|
||||
int get_min_max_distances_timeout_();
|
||||
// Startup runs as a non-blocking state machine driven from loop(). The module
|
||||
// locks up until power cycled if it receives any data before it has sent its
|
||||
// first frame after powering on, so the state machine listens for data from
|
||||
// the module before transmitting anything.
|
||||
enum class StartupState : uint8_t {
|
||||
STARTUP_STATE_LISTEN_SETTLE = 0,
|
||||
STARTUP_STATE_LISTEN,
|
||||
STARTUP_STATE_ENTER_CONFIG,
|
||||
STARTUP_STATE_READ_LIMITS,
|
||||
STARTUP_STATE_READ_VERSION,
|
||||
STARTUP_STATE_READ_GATES,
|
||||
STARTUP_STATE_SET_MODE,
|
||||
STARTUP_STATE_EXIT_CONFIG,
|
||||
STARTUP_STATE_RUNNING,
|
||||
};
|
||||
|
||||
void begin_startup_();
|
||||
void begin_listen_();
|
||||
void loop_startup_(bool got_data);
|
||||
void start_startup_cmd_(StartupState state);
|
||||
void send_startup_cmd_();
|
||||
void abort_startup_cmd_();
|
||||
void abandon_startup_();
|
||||
bool startup_ack_check_(uint8_t min_data_len = 0);
|
||||
bool action_allowed_(bool needs_config);
|
||||
void drain_rx_();
|
||||
void write_cmd_frame_(const CmdFrameT &frame);
|
||||
bool build_startup_frame_(CmdFrameT &frame);
|
||||
void build_config_mode_frame_(CmdFrameT &frame, bool enable);
|
||||
void build_min_max_timeout_frame_(CmdFrameT &frame);
|
||||
void build_gate_threshold_frame_(CmdFrameT &frame, uint8_t gate);
|
||||
void build_version_frame_(CmdFrameT &frame);
|
||||
void build_system_mode_frame_(CmdFrameT &frame, uint16_t mode);
|
||||
|
||||
uint16_t get_mode_() { return this->system_mode_; };
|
||||
void set_mode_(uint16_t mode) { this->system_mode_ = mode; };
|
||||
bool get_presence_() { return this->presence_; };
|
||||
@@ -168,7 +196,7 @@ class LD2420Component final : public Component, public uart::UARTDevice {
|
||||
void handle_energy_mode_(uint8_t *buffer, int len);
|
||||
void handle_ack_data_(uint8_t *buffer, int len);
|
||||
void readline_(int rx_data, uint8_t *buffer, int len);
|
||||
void read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer);
|
||||
bool read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer);
|
||||
void set_calibration_(bool state) { this->calibration_ = state; };
|
||||
bool get_calibration_() { return this->calibration_; };
|
||||
|
||||
@@ -184,9 +212,17 @@ class LD2420Component final : public Component, public uart::UARTDevice {
|
||||
#endif
|
||||
|
||||
uint16_t distance_{0};
|
||||
uint16_t system_mode_;
|
||||
uint16_t system_mode_{0}; // Set to the energy mode default in begin_startup_()
|
||||
uint16_t startup_target_mode_{0}; // Mode the startup handshake writes; applied to system_mode_ once acked
|
||||
uint16_t gate_energy_[TOTAL_GATES];
|
||||
uint8_t buffer_pos_{0}; // where to resume processing/populating buffer
|
||||
uint32_t phase_start_ms_{0};
|
||||
StartupState startup_state_{StartupState::STARTUP_STATE_LISTEN_SETTLE};
|
||||
uint8_t startup_cmd_{0}; // Command byte of the in-flight startup command, for ack matching
|
||||
uint8_t startup_cmd_attempts_{0};
|
||||
uint8_t startup_sequence_retries_{0};
|
||||
uint8_t startup_gate_{0};
|
||||
bool config_read_complete_{false}; // All limits and gate thresholds were read from the module
|
||||
uint8_t buffer_pos_{0}; // where to resume processing/populating buffer
|
||||
uint8_t buffer_data_[MAX_LINE_LENGTH];
|
||||
char firmware_ver_[8]{"v0.0.0"};
|
||||
bool cmd_active_{false};
|
||||
|
||||
@@ -50,19 +50,10 @@ uart_mock:
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Catch-all response: match any command footer (04 03 02 01).
|
||||
# Returns a generic ACK with cmd=0xFF (CMD_ENABLE_CONF case in switch).
|
||||
# All commands get unblocked via cmd_reply_.ack = true.
|
||||
# Data fields stay zeroed (min_gate=0, max_gate=0, timeout=0, thresholds=0).
|
||||
#
|
||||
# Response layout:
|
||||
# [0-3] FD FC FB FA = header
|
||||
# [4-5] 04 00 = length 4
|
||||
# [6] FF = cmd (handled as CMD_ENABLE_CONF)
|
||||
# [7] 01 = status (ACK)
|
||||
# [8-9] 00 00 = error = 0
|
||||
# [10-13] 04 03 02 01 = footer
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
# Config mode enable: CMD_ENABLE_CONF (0x00FF)
|
||||
# TX = FD FC FB FA 04 00 FF 00 02 00 04 03 02 01
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xFF, 0x00, 0x02, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
@@ -72,8 +63,58 @@ uart_mock:
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# System mode write: CMD_WRITE_SYS_PARAM (0x0012), mode = energy (0x0004)
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x08, 0x00, 0x12, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0x12, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Config mode disable: CMD_DISABLE_CONF (0x00FE)
|
||||
- expect_tx: [0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0xFE, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFE, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Catch-all for the remaining commands, which are all CMD_READ_ABD_PARAM
|
||||
# (0x0008) reads: min/max/timeout limits and the 16 gate threshold reads.
|
||||
# The reply carries three zeroed uint32 values (data length 16 = 4 + 12),
|
||||
# so limits and thresholds all read as 0.
|
||||
#
|
||||
# Response layout:
|
||||
# [0-3] FD FC FB FA = header
|
||||
# [4-5] 10 00 = length 16
|
||||
# [6] 08 = cmd (CMD_READ_ABD_PARAM)
|
||||
# [7] 01 = status (ACK)
|
||||
# [8-9] 00 00 = error = 0
|
||||
# [10-21] 00 x12 = three zeroed uint32 data values
|
||||
# [22-25] 04 03 02 01 = footer
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x10, 0x00,
|
||||
0x08, 0x01,
|
||||
0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
injections:
|
||||
# Phase 1 (t=100ms): Valid LD2420 energy mode data frame - happy path
|
||||
# Phase 1 (t=700ms): Valid LD2420 energy mode data frame - happy path
|
||||
# Delay=700ms keeps it outside the component's 500ms listen settle window,
|
||||
# which is measured from boot and ignores earlier reception; this frame is
|
||||
# both the happy path data and the wake-up that starts the setup handshake.
|
||||
# Buffer is clean (buffer_pos_=0). This frame should parse correctly.
|
||||
# Presence: 1 (target detected), Distance: 100cm, Gate energies: all 0
|
||||
#
|
||||
@@ -84,7 +125,7 @@ uart_mock:
|
||||
# [7-8] 64 00 = distance 100 (uint16_t LE)
|
||||
# [9-40] 00 00 x16 = 16 gate energies (uint16_t LE each)
|
||||
# [41-44] F8 F7 F6 F5 = energy frame footer
|
||||
- delay: 100ms
|
||||
- delay: 700ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
@@ -98,13 +139,15 @@ uart_mock:
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Phase 2 (t=300ms): Garbage bytes
|
||||
# Phase 2 (t=1600ms): Garbage bytes
|
||||
# LD2420's readline_ does NOT check frame headers at position 0 (unlike LD2412),
|
||||
# so these bytes accumulate in the buffer. buffer_pos_ goes from 0 to 7.
|
||||
- delay: 200ms
|
||||
# Delay=900ms leaves time for the setup handshake (triggered by Phase 1,
|
||||
# the first data seen from the module) to finish first.
|
||||
- delay: 900ms
|
||||
inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22]
|
||||
|
||||
# Phase 3 (t=400ms): Truncated energy frame WITH footer (13 bytes)
|
||||
# Phase 3 (t=1700ms): Truncated energy frame WITH footer (13 bytes)
|
||||
# This tests PR #14458 bug #3: missing length validation in handle_energy_mode_.
|
||||
# The 7 garbage bytes from Phase 2 are still in the buffer (buffer_pos_=7).
|
||||
# These 13 bytes are appended at positions 7-19 (buffer_pos_=20).
|
||||
@@ -126,7 +169,7 @@ uart_mock:
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Phase 4 (t=600ms): Overflow - inject 50 bytes of 0xFF (MAX_LINE_LENGTH=50)
|
||||
# Phase 4 (t=1900ms): Overflow - inject 50 bytes of 0xFF (MAX_LINE_LENGTH=50)
|
||||
# After Phase 3, buffer_pos_=0 (reset after energy footer detection).
|
||||
# 49 bytes fill positions 0-48 (buffer_pos_=49), 50th byte triggers overflow.
|
||||
# Logs "Max command length exceeded; ignoring", buffer_pos_=0.
|
||||
@@ -140,11 +183,11 @@ uart_mock:
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
]
|
||||
|
||||
# Phase 5 (t=1500ms): Valid frame after overflow - recovery test
|
||||
# Phase 5 (t=2300ms): Valid frame after overflow - recovery test
|
||||
# Buffer was reset by overflow. This valid frame should parse correctly.
|
||||
# Presence: 1 (target), Distance: 50cm
|
||||
# Delay=900ms ensures >1000ms gap from Phase 1 for REFRESH_RATE_MS throttle.
|
||||
- delay: 900ms
|
||||
# Delay=400ms ensures >1000ms gap from Phase 1 for REFRESH_RATE_MS throttle.
|
||||
- delay: 400ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2420-retry-test
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy ld2420's DEPENDENCIES = ["uart"]
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
# Exercises the per-command retry path: the module ignores the first config
|
||||
# mode enable command and only answers the resend, so the startup handshake
|
||||
# must time out once, resend, and then complete normally.
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 115200
|
||||
auto_start: true
|
||||
|
||||
injections:
|
||||
# Wake-up frame (t=700ms): energy frame (presence=1, distance=100).
|
||||
# Delay=700ms keeps it outside the component's 500ms listen settle window.
|
||||
- delay: 700ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x23, 0x00,
|
||||
0x01,
|
||||
0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# The config mode enable command is matched by an empty responder so the
|
||||
# catch-all cannot answer it (responders match on the TX suffix and every
|
||||
# command ends with the frame footer); the on_tx hook below acks it from
|
||||
# the second attempt on, so the first attempt is genuine silence.
|
||||
responses:
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xFF, 0x00, 0x02, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx: []
|
||||
|
||||
# Version response: returns "v2.0.0" → 200 >= 154 → energy mode
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x0C, 0x00,
|
||||
0x00, 0x01,
|
||||
0x00, 0x00,
|
||||
0x06, 0x00,
|
||||
0x76, 0x32, 0x2E, 0x30, 0x2E, 0x30,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# System mode write: CMD_WRITE_SYS_PARAM (0x0012), mode = energy (0x0004)
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x08, 0x00, 0x12, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0x12, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Config mode disable: CMD_DISABLE_CONF (0x00FE)
|
||||
- expect_tx: [0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0xFE, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFE, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Catch-all for the CMD_READ_ABD_PARAM (0x0008) reads: limits and the 16
|
||||
# gate threshold reads. Three zeroed uint32 data values.
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x10, 0x00,
|
||||
0x08, 0x01,
|
||||
0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Ignore the first config mode enable command; ack every one after it
|
||||
on_tx:
|
||||
- lambda: |-
|
||||
static int enable_count = 0;
|
||||
if (data.size() == 14 && data[6] == 0xFF) {
|
||||
enable_count++;
|
||||
if (enable_count >= 2) {
|
||||
id(mock_uart).inject_to_rx_buffer(std::vector<uint8_t>{
|
||||
0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xFF, 0x01, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01});
|
||||
}
|
||||
}
|
||||
|
||||
ld2420:
|
||||
id: ld2420_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
sensor:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
moving_distance:
|
||||
name: "Moving Distance"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
has_target:
|
||||
name: "Has Target"
|
||||
filters:
|
||||
- settle: 50ms
|
||||
@@ -0,0 +1,168 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2420-boot-test
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy ld2420's DEPENDENCIES = ["uart"]
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
# Simulates a cold boot where the LD2420 module boots slower than the ESP:
|
||||
# the module is silent for 2 seconds and then sends its first energy frame.
|
||||
# The module locks up until power cycled if it receives any data before it
|
||||
# has sent its first frame, so the component must stay quiet for the full
|
||||
# 2 seconds and only start its setup handshake after the first frame.
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 115200
|
||||
auto_start: true
|
||||
|
||||
injections:
|
||||
# Module boot finished (t=2000ms): first energy frame
|
||||
# (presence=1, distance=100). Any TX from the component before this
|
||||
# point would have locked up real hardware.
|
||||
- delay: 2000ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x23, 0x00,
|
||||
0x01,
|
||||
0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Repeat frame (t=3100ms): distance=100 again. If the API client happens
|
||||
# to subscribe after the first frame, the first published state is
|
||||
# swallowed as the entity's initial state; repeating the value makes the
|
||||
# test's first collected state deterministic. Delay=1100ms keeps >1000ms
|
||||
# publish throttle gap from the first frame.
|
||||
- delay: 1100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x23, 0x00,
|
||||
0x01,
|
||||
0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
# Post-setup frame (t=4200ms): distance=50 proves streaming still works
|
||||
# after the setup handshake. Delay=1100ms keeps >1000ms publish throttle
|
||||
# gap from the repeat frame.
|
||||
- delay: 1100ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x23, 0x00,
|
||||
0x01,
|
||||
0x32, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
responses:
|
||||
# Version response: returns "v2.0.0" → 200 >= 154 → energy mode
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x0C, 0x00,
|
||||
0x00, 0x01,
|
||||
0x00, 0x00,
|
||||
0x06, 0x00,
|
||||
0x76, 0x32, 0x2E, 0x30, 0x2E, 0x30,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Config mode enable: CMD_ENABLE_CONF (0x00FF)
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xFF, 0x00, 0x02, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFF, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# System mode write: CMD_WRITE_SYS_PARAM (0x0012), mode = energy (0x0004)
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x08, 0x00, 0x12, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0x12, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Config mode disable: CMD_DISABLE_CONF (0x00FE)
|
||||
- expect_tx: [0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0xFE, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFE, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Catch-all for the CMD_READ_ABD_PARAM (0x0008) reads: limits and the 16
|
||||
# gate threshold reads. Three zeroed uint32 data values.
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x10, 0x00,
|
||||
0x08, 0x01,
|
||||
0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
ld2420:
|
||||
id: ld2420_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
sensor:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
moving_distance:
|
||||
name: "Moving Distance"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
has_target:
|
||||
name: "Has Target"
|
||||
filters:
|
||||
- settle: 50ms
|
||||
@@ -0,0 +1,128 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2420-giveup-test
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy ld2420's DEPENDENCIES = ["uart"]
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
# Exercises the sequence retry and give-up path: the module streams energy
|
||||
# frames and answers every command except the firmware version read. The
|
||||
# startup handshake must retry the whole sequence, eventually give up with a
|
||||
# warning instead of marking the component failed, and keep parsing the
|
||||
# stream afterwards. Runs for roughly 16 seconds of retry cadence.
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 115200
|
||||
auto_start: true
|
||||
|
||||
# Module streams a valid energy frame (presence=1, distance=100) continuously
|
||||
periodic_rx:
|
||||
- interval: 250ms
|
||||
data:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x23, 0x00,
|
||||
0x01,
|
||||
0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
injections:
|
||||
# Post-give-up parser probe (t=22s): 50 bytes of 0xFF overflow the frame
|
||||
# buffer, which the parser answers with a "Max command length exceeded"
|
||||
# warning. The give-up happens around t=16s, so seeing that warning after
|
||||
# the give-up proves the stream parser is still running in the degraded
|
||||
# state. (A distinct sensor value cannot serve as the probe: the 1s
|
||||
# publish throttle races the constant periodic stream, and the API
|
||||
# deduplicates repeated identical states.)
|
||||
- delay: 22000ms
|
||||
inject_rx:
|
||||
[
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
]
|
||||
|
||||
responses:
|
||||
# Version read: matched so the catch-all cannot answer it, but never
|
||||
# replied to; this is the command the handshake gives up on
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx: []
|
||||
|
||||
# Config mode enable: CMD_ENABLE_CONF (0x00FF)
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xFF, 0x00, 0x02, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFF, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Config mode disable: CMD_DISABLE_CONF (0x00FE), sent blind before each
|
||||
# sequence retry and on the final give-up
|
||||
- expect_tx: [0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0xFE, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFE, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Catch-all for the CMD_READ_ABD_PARAM (0x0008) reads
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x10, 0x00,
|
||||
0x08, 0x01,
|
||||
0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
ld2420:
|
||||
id: ld2420_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
sensor:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
moving_distance:
|
||||
name: "Moving Distance"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
has_target:
|
||||
name: "Has Target"
|
||||
filters:
|
||||
- settle: 50ms
|
||||
@@ -0,0 +1,167 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2420-restart-test
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy ld2420's DEPENDENCIES = ["uart"]
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
# Exercises the module restart path. The restart command hits the module while
|
||||
# it is mid transmission, so a few tail bytes of the in-flight frame arrive
|
||||
# right after the restart. The module is then silent for 2 seconds while it
|
||||
# boots, and it locks up until power cycled if it receives any data in that
|
||||
# window. The component must not treat the tail bytes as proof the module is
|
||||
# up, and must only re-run its setup handshake after the module's first
|
||||
# post-boot frame.
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 115200
|
||||
auto_start: true
|
||||
|
||||
injections:
|
||||
# Initial wake-up frame (t=700ms): energy frame (presence=1, distance=100).
|
||||
# Delay=700ms keeps it outside the component's 500ms listen settle window,
|
||||
# which is measured from boot and ignores earlier reception.
|
||||
- delay: 700ms
|
||||
inject_rx:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x23, 0x00,
|
||||
0x01,
|
||||
0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
responses:
|
||||
# Version response: returns "v2.0.0" → 200 >= 154 → energy mode
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x0C, 0x00,
|
||||
0x00, 0x01,
|
||||
0x00, 0x00,
|
||||
0x06, 0x00,
|
||||
0x76, 0x32, 0x2E, 0x30, 0x2E, 0x30,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Config mode enable: CMD_ENABLE_CONF (0x00FF)
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xFF, 0x00, 0x02, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFF, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# System mode write: CMD_WRITE_SYS_PARAM (0x0012), mode = energy (0x0004)
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x08, 0x00, 0x12, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0x12, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Config mode disable: CMD_DISABLE_CONF (0x00FE)
|
||||
- expect_tx: [0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0xFE, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFE, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# The module restart command (CMD_RESTART, 0x0068) gets no reply; a real
|
||||
# module goes silent and reboots. Matching it here prevents the catch-all
|
||||
# below from answering it.
|
||||
- expect_tx: [0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0x68, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx: []
|
||||
|
||||
# Catch-all for the CMD_READ_ABD_PARAM (0x0008) reads: limits and the 16
|
||||
# gate threshold reads. Three zeroed uint32 data values.
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x10, 0x00,
|
||||
0x08, 0x01,
|
||||
0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Restart Module"
|
||||
on_press:
|
||||
- lambda: 'id(ld2420_dev).restart_module_action();'
|
||||
# Tail of the energy frame the module was transmitting when the restart
|
||||
# command hit it; must not count as proof the module is up
|
||||
- uart_mock.inject_rx:
|
||||
id: mock_uart
|
||||
data: [0x00, 0x00, 0x00, 0xF8, 0xF7, 0xF6, 0xF5]
|
||||
# The module's first frame after its ~2s boot (presence=1, distance=100)
|
||||
- uart_mock.inject_rx:
|
||||
id: mock_uart
|
||||
delay: 2000ms
|
||||
data:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x23, 0x00,
|
||||
0x01,
|
||||
0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
ld2420:
|
||||
id: ld2420_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
sensor:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
moving_distance:
|
||||
name: "Moving Distance"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
has_target:
|
||||
name: "Has Target"
|
||||
filters:
|
||||
- settle: 50ms
|
||||
@@ -22,10 +22,24 @@ uart_mock:
|
||||
baud_rate: 115200
|
||||
auto_start: false
|
||||
responses:
|
||||
# Catch-all response only (no version-specific response).
|
||||
# Without a version response, firmware_ver_ stays at default "v0.0.0".
|
||||
# get_firmware_int("v0.0.0") = 0 < 154 → simple mode (CMD_SYSTEM_MODE_SIMPLE).
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
# Version response with an old firmware version "v1.5.3".
|
||||
# get_firmware_int("v1.5.3") = 153 < 154 → simple mode (CMD_SYSTEM_MODE_SIMPLE).
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x0C, 0x00,
|
||||
0x00, 0x01,
|
||||
0x00, 0x00,
|
||||
0x06, 0x00,
|
||||
0x76, 0x31, 0x2E, 0x35, 0x2E, 0x33,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Config mode enable: CMD_ENABLE_CONF (0x00FF)
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xFF, 0x00, 0x02, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
@@ -35,11 +49,50 @@ uart_mock:
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# System mode write: CMD_WRITE_SYS_PARAM (0x0012), mode = simple (0x0064)
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x08, 0x00, 0x12, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0x12, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Config mode disable: CMD_DISABLE_CONF (0x00FE)
|
||||
- expect_tx: [0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0xFE, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFE, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Catch-all for the CMD_READ_ABD_PARAM (0x0008) reads: limits and the 16
|
||||
# gate threshold reads. Three zeroed uint32 data values.
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x10, 0x00,
|
||||
0x08, 0x01,
|
||||
0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
injections:
|
||||
# Phase 1 (t=100ms): Valid simple mode text frame - happy path
|
||||
# "ON Range 0100\r\n" → presence=true, distance=100
|
||||
# Simple mode frames end with \r\n (0x0D 0x0A), triggering handle_simple_mode_.
|
||||
- delay: 100ms
|
||||
# Phase 0 (t=700ms): Wake-up frame. The component listens for data from the
|
||||
# module before transmitting anything, so this frame starts the setup
|
||||
# handshake. It is not parsed as simple mode because the component's system
|
||||
# mode is only switched to simple after the firmware version is read.
|
||||
# Delay=700ms keeps it outside the component's 500ms listen settle window,
|
||||
# which is measured from boot and ignores earlier reception.
|
||||
- delay: 700ms
|
||||
inject_rx:
|
||||
[
|
||||
0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20,
|
||||
@@ -47,12 +100,24 @@ uart_mock:
|
||||
0x0D, 0x0A,
|
||||
]
|
||||
|
||||
# Phase 2 (t=300ms): Garbage bytes
|
||||
# Phase 1 (t=1600ms): Valid simple mode text frame - happy path
|
||||
# "ON Range 0100\r\n" → presence=true, distance=100
|
||||
# Simple mode frames end with \r\n (0x0D 0x0A), triggering handle_simple_mode_.
|
||||
# Delay=900ms leaves time for the setup handshake to finish first.
|
||||
- delay: 900ms
|
||||
inject_rx:
|
||||
[
|
||||
0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20,
|
||||
0x30, 0x31, 0x30, 0x30,
|
||||
0x0D, 0x0A,
|
||||
]
|
||||
|
||||
# Phase 2 (t=1800ms): Garbage bytes
|
||||
# LD2420's readline_ stores all bytes regardless of header. buffer_pos_ = 7.
|
||||
- delay: 200ms
|
||||
inject_rx: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x11, 0x22]
|
||||
|
||||
# Phase 3 (t=500ms): Overflow - inject 50 bytes of 0xFF (MAX_LINE_LENGTH=50)
|
||||
# Phase 3 (t=2000ms): Overflow - inject 50 bytes of 0xFF (MAX_LINE_LENGTH=50)
|
||||
# buffer_pos_ starts at 7 (from Phase 2 garbage).
|
||||
# Positions 7-48 fill (42 bytes), byte 43 triggers overflow (buffer_pos_=49).
|
||||
# After overflow: buffer_pos_=0, remaining 7 bytes fill positions 0-6.
|
||||
@@ -67,13 +132,13 @@ uart_mock:
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
]
|
||||
|
||||
# Phase 4 (t=1400ms): Recovery after overflow
|
||||
# Phase 4 (t=2700ms): Recovery after overflow
|
||||
# buffer_pos_ = 7 (from overflow remainder). These 15 bytes fill positions 7-21.
|
||||
# At position 21 (0x0A), \r\n detected → handle_simple_mode_(buffer, 22).
|
||||
# Parser skips 0xFF bytes at positions 0-6, finds "ON" at positions 7-8,
|
||||
# parses digits "0050" → distance=50.
|
||||
# Delay=900ms ensures >1000ms gap from Phase 1 for REFRESH_RATE_MS throttle.
|
||||
- delay: 900ms
|
||||
# Delay=700ms ensures >1000ms gap from Phase 1 for REFRESH_RATE_MS throttle.
|
||||
- delay: 700ms
|
||||
inject_rx:
|
||||
[
|
||||
0x4F, 0x4E, 0x20, 0x52, 0x61, 0x6E, 0x67, 0x65, 0x20,
|
||||
@@ -81,7 +146,7 @@ uart_mock:
|
||||
0x0D, 0x0A,
|
||||
]
|
||||
|
||||
# Phase 5 (t=2500ms): 16-digit distance - tests PR #14458 bug #1
|
||||
# Phase 5 (t=3800ms): 16-digit distance - tests PR #14458 bug #1
|
||||
# "ON Range 0000000000000000\r\n" has 16 digit characters.
|
||||
# handle_simple_mode_ outbuf is 16 bytes, can hold 15 digits (index 0-14).
|
||||
#
|
||||
@@ -100,7 +165,7 @@ uart_mock:
|
||||
0x0D, 0x0A,
|
||||
]
|
||||
|
||||
# Phase 6 (t=3700ms): Post-bug-trigger recovery
|
||||
# Phase 6 (t=5000ms): Post-bug-trigger recovery
|
||||
# If Phase 5 didn't hang, this frame should parse correctly.
|
||||
# "ON Range 0025\r\n" → distance=25
|
||||
# Delay=1200ms ensures >1000ms gap from Phase 5 for throttle.
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
esphome:
|
||||
name: uart-mock-ld2420-warm-test
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy ld2420's DEPENDENCIES = ["uart"]
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
# Simulates a warm restart: the ESP rebooted but the LD2420 module stayed
|
||||
# powered and keeps streaming energy frames from the moment the firmware
|
||||
# starts. The component must not transmit anything until it has seen data
|
||||
# from the module, then run its setup handshake against the live stream.
|
||||
uart_mock:
|
||||
id: mock_uart
|
||||
baud_rate: 115200
|
||||
auto_start: true
|
||||
|
||||
# Module streams valid energy frames continuously. Two alternating frames
|
||||
# are used (presence=1/distance=100 and presence=0/distance=75) so states
|
||||
# keep changing: with a constant frame the API deduplicates the repeated
|
||||
# identical states, and a client that subscribes after the first publish
|
||||
# would swallow the only transition as the initial state and never see an
|
||||
# update.
|
||||
periodic_rx:
|
||||
- interval: 250ms
|
||||
data:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x23, 0x00,
|
||||
0x01,
|
||||
0x64, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
- interval: 1050ms
|
||||
data:
|
||||
[
|
||||
0xF4, 0xF3, 0xF2, 0xF1,
|
||||
0x23, 0x00,
|
||||
0x00,
|
||||
0x4B, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xF8, 0xF7, 0xF6, 0xF5,
|
||||
]
|
||||
|
||||
responses:
|
||||
# Version response: returns "v2.0.0" → 200 >= 154 → energy mode
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x0C, 0x00,
|
||||
0x00, 0x01,
|
||||
0x00, 0x00,
|
||||
0x06, 0x00,
|
||||
0x76, 0x32, 0x2E, 0x30, 0x2E, 0x30,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Config mode enable: CMD_ENABLE_CONF (0x00FF)
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x04, 0x00, 0xFF, 0x00, 0x02, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFF, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# System mode write: CMD_WRITE_SYS_PARAM (0x0012), mode = energy (0x0004)
|
||||
- expect_tx:
|
||||
[0xFD, 0xFC, 0xFB, 0xFA, 0x08, 0x00, 0x12, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0x12, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Config mode disable: CMD_DISABLE_CONF (0x00FE)
|
||||
- expect_tx: [0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0xFE, 0x00, 0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x04, 0x00,
|
||||
0xFE, 0x01,
|
||||
0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
# Catch-all for the CMD_READ_ABD_PARAM (0x0008) reads: limits and the 16
|
||||
# gate threshold reads. Three zeroed uint32 data values.
|
||||
- expect_tx: [0x04, 0x03, 0x02, 0x01]
|
||||
inject_rx:
|
||||
[
|
||||
0xFD, 0xFC, 0xFB, 0xFA,
|
||||
0x10, 0x00,
|
||||
0x08, 0x01,
|
||||
0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x04, 0x03, 0x02, 0x01,
|
||||
]
|
||||
|
||||
ld2420:
|
||||
id: ld2420_dev
|
||||
uart_id: mock_uart
|
||||
|
||||
sensor:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
moving_distance:
|
||||
name: "Moving Distance"
|
||||
filters:
|
||||
- timeout:
|
||||
timeout: 50ms
|
||||
value: last
|
||||
- throttle_with_priority: 50ms
|
||||
|
||||
binary_sensor:
|
||||
- platform: ld2420
|
||||
ld2420_id: ld2420_dev
|
||||
has_target:
|
||||
name: "Has Target"
|
||||
filters:
|
||||
- settle: 50ms
|
||||
@@ -15,6 +15,36 @@ test_uart_mock_ld2420_simple (simple mode):
|
||||
3. Buffer overflow recovery
|
||||
4. 16-digit distance triggers infinite loop pre-fix (PR #14458 bug #1)
|
||||
5. Post-bug-trigger recovery proves the parser survived
|
||||
|
||||
test_uart_mock_ld2420_warm_restart (module streaming at boot):
|
||||
Simulates a warm restart where the module stayed powered and streams energy
|
||||
frames from the moment the firmware starts. Asserts the component never
|
||||
transmits before receiving data from the module, completes setup against
|
||||
the live stream, and publishes sensor data.
|
||||
|
||||
test_uart_mock_ld2420_delayed_boot (module boots slower than the ESP):
|
||||
Simulates a cold boot where the module is silent for 2 seconds. The module
|
||||
locks up until power cycled if it receives data before sending its first
|
||||
frame, so the component must stay quiet until the module talks, then
|
||||
complete setup and keep parsing the stream.
|
||||
|
||||
test_uart_mock_ld2420_restart_button (module restart action):
|
||||
Presses the restart button after setup. The restart hits the module mid
|
||||
transmission, so a few tail bytes of the in-flight frame arrive right after
|
||||
the restart command, then the module is silent for 2 seconds while it
|
||||
boots. The component must not treat the tail bytes as proof the module is
|
||||
up and must only re-run its handshake after the module's first post-boot
|
||||
frame; transmitting into the boot window locks up real hardware.
|
||||
|
||||
test_uart_mock_ld2420_cmd_retry (per-command resend):
|
||||
The module ignores the first config mode enable command and only answers
|
||||
the resend. The handshake must time out once, resend, and complete.
|
||||
|
||||
test_uart_mock_ld2420_give_up (sequence retry and give-up):
|
||||
The module streams and answers everything except the firmware version
|
||||
read. The handshake must retry the whole sequence, eventually give up with
|
||||
a warning instead of marking the component failed, and keep publishing
|
||||
sensor data from the stream afterwards.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -25,7 +55,12 @@ from pathlib import Path
|
||||
from aioesphomeapi import ButtonInfo
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper, SensorStateCollector, find_entity
|
||||
from .state_utils import (
|
||||
InitialStateHelper,
|
||||
SensorStateCollector,
|
||||
find_entity,
|
||||
require_entity,
|
||||
)
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@@ -160,6 +195,400 @@ async def test_uart_mock_ld2420(
|
||||
)
|
||||
|
||||
|
||||
SETUP_COMPLETE_LOG = "Module setup complete; firmware v2.0.0"
|
||||
|
||||
|
||||
class _LogWatcher:
|
||||
"""Resolves futures when watched substrings appear in device log lines.
|
||||
|
||||
Use as the run_compiled line_callback. watch() returns a future that
|
||||
resolves once a line containing all given substrings has been seen `count`
|
||||
times; `after` gates matching on another future being done, and `until`
|
||||
stops matching once another future is done. collect() gathers every line
|
||||
containing any of the given substrings into `self.collected`.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._loop = asyncio.get_running_loop()
|
||||
self._watches: list[dict] = []
|
||||
self._collect_substrings: tuple[str, ...] = ()
|
||||
self.collected: list[str] = []
|
||||
|
||||
def watch(
|
||||
self,
|
||||
substrings: str | list[str],
|
||||
*,
|
||||
count: int = 1,
|
||||
after: asyncio.Future | None = None,
|
||||
until: asyncio.Future | None = None,
|
||||
) -> asyncio.Future:
|
||||
subs = [substrings] if isinstance(substrings, str) else substrings
|
||||
watch = {
|
||||
"subs": subs,
|
||||
"count": count,
|
||||
"after": after,
|
||||
"until": until,
|
||||
"future": self._loop.create_future(),
|
||||
"seen": 0,
|
||||
}
|
||||
self._watches.append(watch)
|
||||
return watch["future"]
|
||||
|
||||
def collect(self, *substrings: str) -> None:
|
||||
self._collect_substrings = substrings
|
||||
|
||||
def __call__(self, line: str) -> None:
|
||||
for watch in self._watches:
|
||||
if watch["future"].done():
|
||||
continue
|
||||
if watch["after"] is not None and not watch["after"].done():
|
||||
continue
|
||||
if watch["until"] is not None and watch["until"].done():
|
||||
continue
|
||||
if all(s in line for s in watch["subs"]):
|
||||
watch["seen"] += 1
|
||||
if watch["seen"] >= watch["count"]:
|
||||
watch["future"].set_result(True)
|
||||
if any(s in line for s in self._collect_substrings):
|
||||
self.collected.append(line)
|
||||
|
||||
|
||||
async def _wait_or_fail(awaitable, timeout: float, message) -> None:
|
||||
"""Await with a timeout, translating TimeoutError into pytest.fail.
|
||||
|
||||
`message` may be a string or a zero-argument callable evaluated at
|
||||
failure time (for messages that embed the current collector state).
|
||||
"""
|
||||
try:
|
||||
await asyncio.wait_for(awaitable, timeout=timeout)
|
||||
except TimeoutError:
|
||||
pytest.fail(message() if callable(message) else message)
|
||||
|
||||
|
||||
async def _subscribe_and_wait(client, collector: SensorStateCollector | None = None):
|
||||
"""List entities, subscribe states, and wait for the initial state flood."""
|
||||
entities, _ = await client.list_entities_services()
|
||||
if collector is not None:
|
||||
collector.build_key_mapping(entities)
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
on_state = collector.on_state if collector is not None else (lambda s: None)
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
await _wait_or_fail(
|
||||
initial_state_helper.wait_for_initial_states(),
|
||||
11.0,
|
||||
"Timeout waiting for initial states",
|
||||
)
|
||||
return entities
|
||||
|
||||
|
||||
async def _run_listen_first_test(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
*,
|
||||
post_setup_distance: float | None = None,
|
||||
strict_first: bool = True,
|
||||
) -> None:
|
||||
"""Shared body for the listen-first startup tests.
|
||||
|
||||
Asserts the component never transmits before the module has sent data
|
||||
(real hardware locks up until power cycled if it does), that the setup
|
||||
handshake completes, and that sensor data publishes. When
|
||||
post_setup_distance is given, additionally waits for that value to prove
|
||||
streaming still works after the handshake. strict_first asserts on the
|
||||
first collected state; pass False for fixtures whose stream alternates
|
||||
values, where the first collected state depends on subscribe timing.
|
||||
"""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
setup_complete = loop.create_future()
|
||||
rx_seen = False
|
||||
tx_before_rx = False
|
||||
failure_lines: list[str] = []
|
||||
|
||||
def line_callback(line: str) -> None:
|
||||
nonlocal rx_seen, tx_before_rx
|
||||
if "uart_mock" in line:
|
||||
if "RX inject" in line or "Injecting" in line:
|
||||
rx_seen = True
|
||||
elif "TX " in line and not rx_seen:
|
||||
tx_before_rx = True
|
||||
if SETUP_COMPLETE_LOG in line and not setup_complete.done():
|
||||
setup_complete.set_result(True)
|
||||
if (
|
||||
"marked FAILED" in line
|
||||
or "was marked as failed" in line
|
||||
or "Communication failed" in line
|
||||
or "No data received from the module" in line
|
||||
):
|
||||
failure_lines.append(line)
|
||||
|
||||
collector = SensorStateCollector(
|
||||
sensor_names=["moving_distance"],
|
||||
binary_sensor_names=["has_target"],
|
||||
)
|
||||
|
||||
post_setup_received = None
|
||||
if post_setup_distance is not None:
|
||||
post_setup_received = collector.add_waiter(
|
||||
lambda: (
|
||||
pytest.approx(post_setup_distance)
|
||||
in collector.sensor_states["moving_distance"]
|
||||
)
|
||||
)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=line_callback),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
await _subscribe_and_wait(client, collector)
|
||||
|
||||
# Setup handshake must complete once the module has talked
|
||||
await _wait_or_fail(
|
||||
setup_complete,
|
||||
10.0,
|
||||
"Timeout waiting for 'Module setup complete' log line. "
|
||||
"The startup state machine did not finish its handshake.",
|
||||
)
|
||||
|
||||
# Sensor data must flow from the stream
|
||||
await _wait_or_fail(
|
||||
collector.wait_for_all(timeout=5.0),
|
||||
6.0,
|
||||
lambda: (
|
||||
f"Timeout waiting for sensor data. Received:\n"
|
||||
f" sensor_states: {collector.sensor_states}\n"
|
||||
f" binary_states: {collector.binary_states}"
|
||||
),
|
||||
)
|
||||
|
||||
if strict_first:
|
||||
assert collector.sensor_states["moving_distance"][0] == pytest.approx(100.0)
|
||||
assert collector.binary_states["has_target"][0] is True
|
||||
else:
|
||||
assert pytest.approx(100.0) in collector.sensor_states["moving_distance"]
|
||||
assert True in collector.binary_states["has_target"]
|
||||
|
||||
if post_setup_received is not None:
|
||||
await _wait_or_fail(
|
||||
post_setup_received,
|
||||
5.0,
|
||||
lambda: (
|
||||
f"Timeout waiting for post-setup frame "
|
||||
f"(distance={post_setup_distance}). Received:\n"
|
||||
f" moving_distance: {collector.sensor_states['moving_distance']}"
|
||||
),
|
||||
)
|
||||
|
||||
# The component must never transmit before the module has talked;
|
||||
# real hardware locks up until power cycled if it does.
|
||||
assert not tx_before_rx, (
|
||||
"Component transmitted on the UART before receiving any data "
|
||||
"from the module; this locks up real LD2420 hardware"
|
||||
)
|
||||
|
||||
assert not failure_lines, f"Unexpected failure log lines: {failure_lines}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_ld2420_warm_restart(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Module streams from boot; component must listen first, then set up."""
|
||||
await _run_listen_first_test(
|
||||
yaml_config, run_compiled, api_client_connected, strict_first=False
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_ld2420_delayed_boot(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Module silent for 2 s; component must not transmit into the boot window."""
|
||||
await _run_listen_first_test(
|
||||
yaml_config, run_compiled, api_client_connected, post_setup_distance=50.0
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_ld2420_cmd_retry(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""First config command gets no reply; the resend must recover."""
|
||||
watcher = _LogWatcher()
|
||||
resend_seen = watcher.watch("No reply to startup command")
|
||||
setup_complete = watcher.watch(SETUP_COMPLETE_LOG)
|
||||
watcher.collect(
|
||||
"marked FAILED",
|
||||
"was marked as failed",
|
||||
"Communication failed",
|
||||
"Module setup attempt",
|
||||
)
|
||||
|
||||
collector = SensorStateCollector(
|
||||
sensor_names=["moving_distance"],
|
||||
binary_sensor_names=["has_target"],
|
||||
)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=watcher),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
await _subscribe_and_wait(client, collector)
|
||||
|
||||
# The first enable command is ignored, so a resend must happen
|
||||
await _wait_or_fail(
|
||||
resend_seen, 10.0, "Timeout waiting for the startup command resend log line"
|
||||
)
|
||||
|
||||
# The resend gets an ack and the handshake completes normally
|
||||
await _wait_or_fail(
|
||||
setup_complete,
|
||||
10.0,
|
||||
"Timeout waiting for 'Module setup complete' after the resend",
|
||||
)
|
||||
|
||||
await _wait_or_fail(
|
||||
collector.wait_for_all(timeout=5.0),
|
||||
6.0,
|
||||
lambda: (
|
||||
f"Timeout waiting for sensor data. Received:\n"
|
||||
f" sensor_states: {collector.sensor_states}"
|
||||
),
|
||||
)
|
||||
|
||||
assert collector.sensor_states["moving_distance"][0] == pytest.approx(100.0)
|
||||
|
||||
# A single command resend must not burn a whole sequence retry or
|
||||
# produce any failure log line
|
||||
assert not watcher.collected, (
|
||||
f"Unexpected failure log lines: {watcher.collected}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_ld2420_give_up(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Version read never answers; retries then give-up, stream keeps working."""
|
||||
watcher = _LogWatcher()
|
||||
sequence_retry_seen = watcher.watch("Module setup attempt 1 failed; retrying")
|
||||
give_up_seen = watcher.watch("Firmware version and operating mode were never read")
|
||||
# The overflow probe injected at t=22s (after the give-up) makes the
|
||||
# parser log this warning only if it is still running
|
||||
parser_alive_after_give_up = watcher.watch(
|
||||
"Max command length exceeded", after=give_up_seen
|
||||
)
|
||||
watcher.collect("marked FAILED", "was marked as failed")
|
||||
|
||||
collector = SensorStateCollector(
|
||||
sensor_names=["moving_distance"],
|
||||
binary_sensor_names=["has_target"],
|
||||
)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=watcher),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
await _subscribe_and_wait(client, collector)
|
||||
|
||||
# The version read times out three times, then the sequence retries
|
||||
await _wait_or_fail(
|
||||
sequence_retry_seen, 15.0, "Timeout waiting for the sequence retry log line"
|
||||
)
|
||||
|
||||
# After all sequence retries the component gives up with a warning
|
||||
await _wait_or_fail(
|
||||
give_up_seen, 30.0, "Timeout waiting for the give-up log line"
|
||||
)
|
||||
|
||||
# The stream must still be parsed after giving up
|
||||
await _wait_or_fail(
|
||||
parser_alive_after_give_up,
|
||||
20.0,
|
||||
"No parser activity after the give-up; the stream parser "
|
||||
"must keep running in the degraded state",
|
||||
)
|
||||
|
||||
# The stream published sensor data while the handshake was failing
|
||||
assert pytest.approx(100.0) in collector.sensor_states["moving_distance"], (
|
||||
f"Expected the stream to publish distance=100, "
|
||||
f"got: {collector.sensor_states['moving_distance']}"
|
||||
)
|
||||
|
||||
# The whole point of the degraded state: the component keeps running
|
||||
assert not watcher.collected, (
|
||||
f"Component was marked failed: {watcher.collected}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_ld2420_restart_button(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Restart action must not transmit into the module's boot window."""
|
||||
watcher = _LogWatcher()
|
||||
first_setup_complete = watcher.watch(SETUP_COMPLETE_LOG)
|
||||
second_setup_complete = watcher.watch(SETUP_COMPLETE_LOG, count=2)
|
||||
restart_seen = watcher.watch(["[ld2420", "Restarting"])
|
||||
# The module's first frame after its simulated 2 s boot
|
||||
module_frame_after_restart = watcher.watch("RX inject 45 bytes", after=restart_seen)
|
||||
# Config mode enable transmitted before the module's first post-boot
|
||||
# frame; on real hardware this locks the module up
|
||||
tx_into_boot_window = watcher.watch(
|
||||
["uart_mock", "TX ", "FF:00:02:00"],
|
||||
after=restart_seen,
|
||||
until=module_frame_after_restart,
|
||||
)
|
||||
watcher.collect("marked FAILED", "was marked as failed", "Communication failed")
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=watcher),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities = await _subscribe_and_wait(client)
|
||||
|
||||
# Wait for the initial startup handshake to finish
|
||||
await _wait_or_fail(
|
||||
first_setup_complete,
|
||||
10.0,
|
||||
"Timeout waiting for the initial 'Module setup complete'",
|
||||
)
|
||||
|
||||
# Restart the module; the button automation also injects the in-flight
|
||||
# frame tail immediately and the module's first frame 2 s later
|
||||
restart_btn = require_entity(entities, "restart_module", ButtonInfo)
|
||||
client.button_command(restart_btn.key)
|
||||
|
||||
# The handshake must complete again after the module comes back
|
||||
await _wait_or_fail(
|
||||
second_setup_complete,
|
||||
15.0,
|
||||
"Timeout waiting for 'Module setup complete' after the restart. "
|
||||
"The component did not recover from the module restart.",
|
||||
)
|
||||
|
||||
assert not tx_into_boot_window.done(), (
|
||||
"Component transmitted the config handshake into the module's "
|
||||
"boot window after a restart; the in-flight frame tail bytes must "
|
||||
"not count as proof the module is up"
|
||||
)
|
||||
|
||||
assert not watcher.collected, (
|
||||
f"Unexpected failure log lines: {watcher.collected}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_ld2420_simple(
|
||||
yaml_config: str,
|
||||
|
||||
Reference in New Issue
Block a user