mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[ufm01] Improve startup with reset retry and passive polling fallback (#17567)
Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
This commit is contained in:
co-authored by
Cursor
Jonathan Swoboda
parent
2798ef4de2
commit
56ec21d950
@@ -4,13 +4,23 @@
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <array>
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::ufm01 {
|
||||
|
||||
static const char *const TAG = "ufm01";
|
||||
|
||||
static constexpr uint8_t COMMAND_ACK = 0xE5;
|
||||
static constexpr uint32_t COMMAND_ACK_TIMEOUT_MS = 200;
|
||||
static constexpr uint32_t COMMAND_ACK_TIMEOUT_MS = 500;
|
||||
static constexpr uint32_t STARTUP_DELAY_MS = 2000;
|
||||
static constexpr uint32_t POST_RESET_DELAY_MS = 2000;
|
||||
static constexpr uint32_t RESET_RETRY_DELAY_MS = 800;
|
||||
static constexpr uint32_t STARTUP_RETRY_MS = 3000;
|
||||
static constexpr uint32_t PASSIVE_POLL_INTERVAL_MS = 1000;
|
||||
static constexpr uint32_t ACTIVE_STALE_MS = 5000;
|
||||
static constexpr uint32_t PASSIVE_READ_TIMEOUT_MS = 1000;
|
||||
static constexpr uint32_t ACTIVE_FRAME_TIMEOUT_MS = 3000;
|
||||
|
||||
static constexpr float L_PER_M3 = 1000.0f;
|
||||
static constexpr float M3_PER_L = 1.0f / L_PER_M3;
|
||||
@@ -18,12 +28,14 @@ static constexpr float M3_PER_L = 1.0f / L_PER_M3;
|
||||
static constexpr std::array<uint8_t, 7> ACTIVE_MODE = {0xFE, 0xFE, 0x11, 0x5C, 0x00, 0x5C, 0x16};
|
||||
static constexpr std::array<uint8_t, 7> CLEAR_ACCUMULATED_FLOW = {0xFE, 0xFE, 0x11, 0x5A, 0xFD, 0x57, 0x16};
|
||||
static constexpr std::array<uint8_t, 7> RESET_DEVICE = {0xFE, 0xFE, 0x11, 0x5D, 0xCB, 0x28, 0x16};
|
||||
static constexpr std::array<uint8_t, 7> READ_SENSOR_DATA_NO_ID = {0xFE, 0xFE, 0x11, 0x5B, 0x0F, 0x6A, 0x16};
|
||||
|
||||
// Active-mode frame layout (datasheet Table 7)
|
||||
static constexpr size_t FRAME_CHECKSUM_INDEX = 30;
|
||||
static constexpr size_t FRAME_STOP_INDEX = 31;
|
||||
static constexpr uint8_t FRAME_START_BYTE_1 = 0x3C;
|
||||
static constexpr uint8_t FRAME_START_BYTE_2 = 0x32;
|
||||
static constexpr uint8_t PASSIVE_START_BYTE_2 = 0x64;
|
||||
static constexpr uint8_t FRAME_STOP_BYTE = 0x16;
|
||||
static constexpr uint8_t FRAME_INDEX_INSTANT_FLOW_FLAG = 15;
|
||||
static constexpr uint8_t FRAME_INDEX_RESERVED_SECTION = 21;
|
||||
@@ -55,7 +67,7 @@ static bool check_byte(const uint8_t data[FRAME_SIZE], size_t index, uint8_t exp
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool validate_data(uint8_t data[FRAME_SIZE]) {
|
||||
static bool validate_active_frame(const uint8_t data[FRAME_SIZE]) {
|
||||
uint8_t sum = 0;
|
||||
for (size_t i = 0; i < FRAME_CHECKSUM_INDEX; ++i)
|
||||
sum += data[i];
|
||||
@@ -68,13 +80,43 @@ static bool validate_data(uint8_t data[FRAME_SIZE]) {
|
||||
check_byte(data, FRAME_STOP_INDEX, FRAME_STOP_BYTE, "stop byte");
|
||||
}
|
||||
|
||||
static float read_accumulated_flow(uint8_t data[FRAME_SIZE]) {
|
||||
static bool validate_passive_frame(const uint8_t data[PASSIVE_FRAME_SIZE]) {
|
||||
if (data[0] != FRAME_START_BYTE_1 || data[1] != PASSIVE_START_BYTE_2 || data[22] != FRAME_STOP_BYTE)
|
||||
return false;
|
||||
uint8_t sum = 0;
|
||||
for (size_t i = 0; i < 21; ++i)
|
||||
sum += data[i];
|
||||
return data[21] == (sum & 0xFF);
|
||||
}
|
||||
|
||||
static void passive_no_id_to_active_frame(const uint8_t passive[PASSIVE_FRAME_SIZE], uint8_t active[FRAME_SIZE]) {
|
||||
std::memset(active, 0, FRAME_SIZE);
|
||||
active[0] = FRAME_START_BYTE_1;
|
||||
active[1] = FRAME_START_BYTE_2;
|
||||
active[7] = 0x01;
|
||||
active[8] = passive[2];
|
||||
for (size_t i = 0; i < 6; ++i)
|
||||
active[9 + i] = passive[3 + i];
|
||||
active[15] = passive[9];
|
||||
for (size_t i = 0; i < 5; ++i)
|
||||
active[16 + i] = passive[10 + i];
|
||||
active[21] = FRAME_FLAG_RESERVED_SECTION;
|
||||
active[24] = passive[15];
|
||||
for (size_t i = 0; i < 3; ++i)
|
||||
active[25 + i] = passive[16 + i];
|
||||
active[28] = passive[19];
|
||||
active[29] = passive[20];
|
||||
active[30] = passive[21];
|
||||
active[31] = FRAME_STOP_BYTE;
|
||||
}
|
||||
|
||||
static float read_accumulated_flow(const uint8_t data[FRAME_SIZE]) {
|
||||
return (data[FRAME_ACC_FLOW_FLAG_INDEX] == ACC_FLOW_M3_FLAG ? L_PER_M3 : 1.0f) *
|
||||
(to_float(data[14]) * 10000000.0f + to_float(data[13]) * 100000.0f + to_float(data[12]) * 1000.0f +
|
||||
to_float(data[11]) * 10.0f + to_float(data[10]) * 0.1f + to_float(data[9]) * 0.001f);
|
||||
}
|
||||
|
||||
static float read_flow(uint8_t data[FRAME_SIZE]) {
|
||||
static float read_flow(const uint8_t data[FRAME_SIZE]) {
|
||||
return (data[FRAME_FLOW_SIGN_INDEX] == FLOW_NEGATIVE_SIGN ? -1.0f : 1.0f) *
|
||||
(to_float(data[19]) * 10000.0f + to_float(data[18]) * 100.0f + to_float(data[17]) +
|
||||
to_float(data[16]) * 0.01f) *
|
||||
@@ -86,7 +128,7 @@ static void log_hex(const uint8_t *data, size_t len) {
|
||||
ESP_LOGD(TAG, "%s", format_hex_pretty_to(hex_buf, data, len, ' '));
|
||||
}
|
||||
|
||||
static float read_temperature(uint8_t data[FRAME_SIZE]) {
|
||||
static float read_temperature(const uint8_t data[FRAME_SIZE]) {
|
||||
// happens sometimes before getting a real reading
|
||||
if (data[27] == 0x00 && (data[26] == 0x00 || data[26] == 0x70) && data[25] == 0x00) {
|
||||
return NAN;
|
||||
@@ -106,19 +148,39 @@ static bool read_flow_rate_out_of_range(const uint8_t data[FRAME_SIZE]) {
|
||||
return data[FRAME_ST2_INDEX] & ST2_FLOW_RATE_OUT_OF_RANGE_MASK;
|
||||
}
|
||||
|
||||
bool UFM01Component::send_command_(const std::array<uint8_t, 7> &command) {
|
||||
void UFM01Component::flush_rx_() {
|
||||
while (this->available()) {
|
||||
uint8_t byte;
|
||||
this->read_byte(&byte);
|
||||
}
|
||||
this->read_index_ = 0;
|
||||
}
|
||||
|
||||
void UFM01Component::send_command_no_wait_(const std::array<uint8_t, 7> &command) {
|
||||
this->flush_rx_();
|
||||
this->write_array(command);
|
||||
this->flush();
|
||||
}
|
||||
|
||||
// Drains whatever is currently in the RX buffer, looking for a command ACK.
|
||||
bool UFM01Component::consume_ack_() {
|
||||
while (this->available()) {
|
||||
uint8_t byte;
|
||||
if (!this->read_byte(&byte))
|
||||
return false;
|
||||
if (byte == COMMAND_ACK)
|
||||
return true;
|
||||
ESP_LOGV(TAG, "Unexpected byte while waiting for command ACK: 0x%02X", byte);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UFM01Component::send_command_(const std::array<uint8_t, 7> &command) {
|
||||
this->send_command_no_wait_(command);
|
||||
const uint32_t start = millis();
|
||||
while (millis() - start < COMMAND_ACK_TIMEOUT_MS) {
|
||||
if (this->available()) {
|
||||
uint8_t byte;
|
||||
if (this->read_byte(&byte)) {
|
||||
if (byte == COMMAND_ACK)
|
||||
return true;
|
||||
ESP_LOGV(TAG, "Unexpected byte while waiting for command ACK: 0x%02X", byte);
|
||||
}
|
||||
}
|
||||
if (this->consume_ack_())
|
||||
return true;
|
||||
delay(1);
|
||||
}
|
||||
return false;
|
||||
@@ -130,14 +192,12 @@ bool UFM01Component::clear_accumulated_flow_() { return this->send_command_(CLEA
|
||||
|
||||
bool UFM01Component::set_active_mode_() { return this->send_command_(ACTIVE_MODE); }
|
||||
|
||||
float UFM01Component::get_setup_priority() const { return setup_priority::IO; }
|
||||
float UFM01Component::get_setup_priority() const { return setup_priority::LATE; }
|
||||
|
||||
void UFM01Component::setup() {
|
||||
ESP_LOGI(TAG, "Setting up UFM-01...");
|
||||
if (!this->set_active_mode_()) {
|
||||
ESP_LOGW(TAG, "Failed to set active mode (no ACK from device)");
|
||||
this->mark_failed();
|
||||
}
|
||||
this->startup_wait_ms_ = STARTUP_DELAY_MS;
|
||||
this->set_startup_phase_(StartupPhase::WAIT);
|
||||
}
|
||||
|
||||
void UFM01Component::dump_config() {
|
||||
@@ -154,12 +214,9 @@ void UFM01Component::dump_config() {
|
||||
LOG_BINARY_SENSOR(" ", "Flow Rate Out Of Range", this->flow_rate_out_of_range_binary_sensor_);
|
||||
#endif
|
||||
this->check_uart_settings(2400, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGW(TAG, "Setup failed: active mode not acknowledged by device");
|
||||
}
|
||||
}
|
||||
|
||||
void UFM01Component::on_data_(uint8_t data[FRAME_SIZE]) {
|
||||
void UFM01Component::on_active_frame_(uint8_t data[FRAME_SIZE]) {
|
||||
bool empty_tube = read_empty_tube(data);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
if (this->ufc_chip_error_binary_sensor_ != nullptr)
|
||||
@@ -189,10 +246,14 @@ void UFM01Component::on_data_(uint8_t data[FRAME_SIZE]) {
|
||||
this->temperature_sensor_->publish_state(read_temperature(data));
|
||||
}
|
||||
#endif
|
||||
this->last_valid_frame_ms_ = millis();
|
||||
this->status_clear_warning();
|
||||
this->status_clear_error();
|
||||
}
|
||||
|
||||
void UFM01Component::loop() {
|
||||
// Drain the UART buffer each loop, reading one byte at a time into the frame
|
||||
bool UFM01Component::process_active_stream_() {
|
||||
bool got_valid_frame = false;
|
||||
|
||||
while (this->available()) {
|
||||
if (!this->read_byte(&this->data_[this->read_index_])) {
|
||||
ESP_LOGW(TAG, "unable to read byte");
|
||||
@@ -201,23 +262,22 @@ void UFM01Component::loop() {
|
||||
}
|
||||
if ((this->read_index_ == 0 && this->data_[0] != FRAME_START_BYTE_1) ||
|
||||
(this->read_index_ == 1 && this->data_[1] != FRAME_START_BYTE_2)) {
|
||||
ESP_LOGW(TAG, "not start of data at %d (is 0x%02X)", this->read_index_, this->data_[this->read_index_]);
|
||||
ESP_LOGD(TAG, "not start of data at %d (is 0x%02X)", this->read_index_, this->data_[this->read_index_]);
|
||||
this->read_index_ = 0;
|
||||
continue;
|
||||
}
|
||||
if (++this->read_index_ < static_cast<int32_t>(FRAME_SIZE))
|
||||
continue;
|
||||
|
||||
// Full frame received
|
||||
if (validate_data(this->data_)) {
|
||||
this->on_data_(this->data_);
|
||||
if (validate_active_frame(this->data_)) {
|
||||
this->on_active_frame_(this->data_);
|
||||
this->read_index_ = 0;
|
||||
got_valid_frame = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Invalid frame: try to resync on the next start marker within the buffer
|
||||
log_hex(this->data_, sizeof(this->data_));
|
||||
ESP_LOGE(TAG, "unable to read data");
|
||||
ESP_LOGW(TAG, "unable to read data");
|
||||
for (int32_t i = 2;
|
||||
i < static_cast<int32_t>(FRAME_STOP_INDEX) && this->read_index_ == static_cast<int32_t>(FRAME_SIZE); ++i) {
|
||||
if ((this->data_[i] == FRAME_START_BYTE_1) && (this->data_[i + 1] == FRAME_START_BYTE_2)) {
|
||||
@@ -229,6 +289,190 @@ void UFM01Component::loop() {
|
||||
if (this->read_index_ == static_cast<int32_t>(FRAME_SIZE))
|
||||
this->read_index_ = 0;
|
||||
}
|
||||
|
||||
return got_valid_frame;
|
||||
}
|
||||
|
||||
void UFM01Component::set_startup_phase_(StartupPhase phase) {
|
||||
this->startup_phase_ = phase;
|
||||
this->phase_start_ms_ = millis();
|
||||
}
|
||||
|
||||
void UFM01Component::enter_active_stream_(const char *reason) {
|
||||
ESP_LOGI(TAG, "UFM-01 active stream %s", reason);
|
||||
this->operating_mode_ = OperatingMode::ACTIVE_STREAM;
|
||||
this->passive_read_pending_ = false;
|
||||
}
|
||||
|
||||
void UFM01Component::start_passive_read_() {
|
||||
this->send_command_no_wait_(READ_SENSOR_DATA_NO_ID);
|
||||
this->passive_index_ = 0;
|
||||
this->passive_start_ms_ = millis();
|
||||
}
|
||||
|
||||
// Accumulates the reply to a passive read request across loop iterations.
|
||||
PassiveReadResult UFM01Component::continue_passive_read_() {
|
||||
while (this->available() && this->passive_index_ < PASSIVE_FRAME_SIZE) {
|
||||
uint8_t byte;
|
||||
if (!this->read_byte(&byte))
|
||||
break;
|
||||
|
||||
if (this->passive_index_ == 0 && byte != FRAME_START_BYTE_1)
|
||||
continue;
|
||||
if (this->passive_index_ == 1 && byte != PASSIVE_START_BYTE_2) {
|
||||
// The mismatched byte may itself be the start of the real frame
|
||||
this->passive_index_ = (byte == FRAME_START_BYTE_1) ? 1 : 0;
|
||||
continue;
|
||||
}
|
||||
this->passive_frame_[this->passive_index_++] = byte;
|
||||
}
|
||||
|
||||
if (this->passive_index_ < PASSIVE_FRAME_SIZE) {
|
||||
if (millis() - this->passive_start_ms_ < PASSIVE_READ_TIMEOUT_MS)
|
||||
return PassiveReadResult::PENDING;
|
||||
ESP_LOGD(TAG, "passive read timeout (%zu/%zu bytes)", this->passive_index_, PASSIVE_FRAME_SIZE);
|
||||
return PassiveReadResult::FAILURE;
|
||||
}
|
||||
|
||||
if (!validate_passive_frame(this->passive_frame_)) {
|
||||
log_hex(this->passive_frame_, PASSIVE_FRAME_SIZE);
|
||||
ESP_LOGW(TAG, "invalid passive frame");
|
||||
return PassiveReadResult::FAILURE;
|
||||
}
|
||||
|
||||
uint8_t active_frame[FRAME_SIZE];
|
||||
passive_no_id_to_active_frame(this->passive_frame_, active_frame);
|
||||
this->on_active_frame_(active_frame);
|
||||
return PassiveReadResult::SUCCESS;
|
||||
}
|
||||
|
||||
void UFM01Component::loop_startup_() {
|
||||
const uint32_t elapsed = millis() - this->phase_start_ms_;
|
||||
|
||||
switch (this->startup_phase_) {
|
||||
case StartupPhase::WAIT:
|
||||
// Pick up an already-streaming device without resetting it
|
||||
if (this->process_active_stream_()) {
|
||||
this->enter_active_stream_("started");
|
||||
return;
|
||||
}
|
||||
if (elapsed < this->startup_wait_ms_)
|
||||
return;
|
||||
ESP_LOGD(TAG, "Running startup sequence");
|
||||
this->status_set_warning("initializing UFM-01");
|
||||
this->reset_retried_ = false;
|
||||
this->send_command_no_wait_(RESET_DEVICE);
|
||||
this->set_startup_phase_(StartupPhase::RESET_WAIT_ACK);
|
||||
return;
|
||||
|
||||
case StartupPhase::RESET_WAIT_ACK:
|
||||
if (this->consume_ack_()) {
|
||||
this->set_startup_phase_(StartupPhase::POST_RESET_WAIT);
|
||||
return;
|
||||
}
|
||||
if (elapsed < COMMAND_ACK_TIMEOUT_MS)
|
||||
return;
|
||||
if (!this->reset_retried_) {
|
||||
ESP_LOGW(TAG, "Reset not acknowledged, retrying in %" PRIu32 " ms", RESET_RETRY_DELAY_MS);
|
||||
this->set_startup_phase_(StartupPhase::RESET_RETRY_WAIT);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Reset failed during startup");
|
||||
this->set_startup_phase_(StartupPhase::POST_RESET_WAIT);
|
||||
}
|
||||
return;
|
||||
|
||||
case StartupPhase::RESET_RETRY_WAIT:
|
||||
if (elapsed < RESET_RETRY_DELAY_MS)
|
||||
return;
|
||||
this->reset_retried_ = true;
|
||||
this->send_command_no_wait_(RESET_DEVICE);
|
||||
this->set_startup_phase_(StartupPhase::RESET_WAIT_ACK);
|
||||
return;
|
||||
|
||||
case StartupPhase::POST_RESET_WAIT:
|
||||
if (elapsed < POST_RESET_DELAY_MS)
|
||||
return;
|
||||
this->send_command_no_wait_(ACTIVE_MODE);
|
||||
this->set_startup_phase_(StartupPhase::ACTIVE_WAIT_FRAME);
|
||||
return;
|
||||
|
||||
case StartupPhase::ACTIVE_WAIT_FRAME:
|
||||
// The command ACK (0xE5) is consumed by the frame parser as noise
|
||||
if (this->process_active_stream_()) {
|
||||
this->enter_active_stream_("started");
|
||||
return;
|
||||
}
|
||||
if (elapsed < ACTIVE_FRAME_TIMEOUT_MS)
|
||||
return;
|
||||
this->start_passive_read_();
|
||||
this->set_startup_phase_(StartupPhase::PASSIVE_WAIT_REPLY);
|
||||
return;
|
||||
|
||||
case StartupPhase::PASSIVE_WAIT_REPLY:
|
||||
switch (this->continue_passive_read_()) {
|
||||
case PassiveReadResult::PENDING:
|
||||
return;
|
||||
case PassiveReadResult::SUCCESS:
|
||||
ESP_LOGI(TAG, "UFM-01 using passive polling");
|
||||
this->operating_mode_ = OperatingMode::PASSIVE_POLL;
|
||||
this->passive_read_pending_ = false;
|
||||
this->last_poll_ms_ = millis();
|
||||
return;
|
||||
case PassiveReadResult::FAILURE:
|
||||
ESP_LOGW(TAG, "Startup failed, retrying in %" PRIu32 " ms", STARTUP_RETRY_MS);
|
||||
this->startup_wait_ms_ = STARTUP_RETRY_MS;
|
||||
this->set_startup_phase_(StartupPhase::WAIT);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UFM01Component::loop_active_stream_() {
|
||||
this->process_active_stream_();
|
||||
if (this->last_valid_frame_ms_ != 0 && millis() - this->last_valid_frame_ms_ > ACTIVE_STALE_MS) {
|
||||
ESP_LOGW(TAG, "Active stream stale, switching to passive polling");
|
||||
this->operating_mode_ = OperatingMode::PASSIVE_POLL;
|
||||
this->passive_read_pending_ = false;
|
||||
this->last_poll_ms_ = 0;
|
||||
this->status_set_warning("UFM-01 passive poll");
|
||||
}
|
||||
}
|
||||
|
||||
void UFM01Component::loop_passive_poll_() {
|
||||
if (this->passive_read_pending_) {
|
||||
const PassiveReadResult result = this->continue_passive_read_();
|
||||
if (result == PassiveReadResult::PENDING)
|
||||
return;
|
||||
this->passive_read_pending_ = false;
|
||||
if (result == PassiveReadResult::FAILURE)
|
||||
this->status_set_warning("UFM-01 passive poll failed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->process_active_stream_()) {
|
||||
this->enter_active_stream_("resumed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (millis() - this->last_poll_ms_ >= PASSIVE_POLL_INTERVAL_MS) {
|
||||
this->last_poll_ms_ = millis();
|
||||
this->start_passive_read_();
|
||||
this->passive_read_pending_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void UFM01Component::loop() {
|
||||
switch (this->operating_mode_) {
|
||||
case OperatingMode::STARTUP:
|
||||
this->loop_startup_();
|
||||
return;
|
||||
case OperatingMode::ACTIVE_STREAM:
|
||||
this->loop_active_stream_();
|
||||
return;
|
||||
case OperatingMode::PASSIVE_POLL:
|
||||
this->loop_passive_poll_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::ufm01
|
||||
|
||||
@@ -11,12 +11,39 @@
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
|
||||
// component API definition at https://www.sciosense.com/wp-content/uploads/2025/06/UFM-01-Datasheet-1.pdf
|
||||
|
||||
namespace esphome::ufm01 {
|
||||
|
||||
namespace testing {
|
||||
class TestableUFM01;
|
||||
} // namespace testing
|
||||
|
||||
static constexpr size_t FRAME_SIZE = 32;
|
||||
static constexpr size_t PASSIVE_FRAME_SIZE = 23;
|
||||
|
||||
enum class OperatingMode : uint8_t {
|
||||
STARTUP = 0,
|
||||
ACTIVE_STREAM = 1,
|
||||
PASSIVE_POLL = 2,
|
||||
};
|
||||
|
||||
enum class StartupPhase : uint8_t {
|
||||
WAIT = 0,
|
||||
RESET_WAIT_ACK = 1,
|
||||
RESET_RETRY_WAIT = 2,
|
||||
POST_RESET_WAIT = 3,
|
||||
ACTIVE_WAIT_FRAME = 4,
|
||||
PASSIVE_WAIT_REPLY = 5,
|
||||
};
|
||||
|
||||
enum class PassiveReadResult : uint8_t {
|
||||
PENDING = 0,
|
||||
SUCCESS = 1,
|
||||
FAILURE = 2,
|
||||
};
|
||||
|
||||
class UFM01Component : public uart::UARTDevice, public Component {
|
||||
#ifdef USE_SENSOR
|
||||
@@ -48,10 +75,37 @@ class UFM01Component : public uart::UARTDevice, public Component {
|
||||
|
||||
private:
|
||||
bool send_command_(const std::array<uint8_t, 7> &command);
|
||||
void send_command_no_wait_(const std::array<uint8_t, 7> &command);
|
||||
bool consume_ack_();
|
||||
void flush_rx_();
|
||||
bool process_active_stream_();
|
||||
void on_active_frame_(uint8_t data[FRAME_SIZE]);
|
||||
|
||||
void loop_startup_();
|
||||
void loop_active_stream_();
|
||||
void loop_passive_poll_();
|
||||
void set_startup_phase_(StartupPhase phase);
|
||||
void enter_active_stream_(const char *reason);
|
||||
void start_passive_read_();
|
||||
PassiveReadResult continue_passive_read_();
|
||||
|
||||
OperatingMode operating_mode_{OperatingMode::STARTUP};
|
||||
StartupPhase startup_phase_{StartupPhase::WAIT};
|
||||
uint32_t phase_start_ms_{0};
|
||||
uint32_t startup_wait_ms_{0};
|
||||
bool reset_retried_{false};
|
||||
uint32_t last_valid_frame_ms_{0};
|
||||
uint32_t last_poll_ms_{0};
|
||||
|
||||
bool passive_read_pending_{false};
|
||||
uint32_t passive_start_ms_{0};
|
||||
size_t passive_index_{0};
|
||||
uint8_t passive_frame_[PASSIVE_FRAME_SIZE];
|
||||
|
||||
int32_t read_index_ = 0;
|
||||
uint8_t data_[FRAME_SIZE];
|
||||
void on_data_(uint8_t data[FRAME_SIZE]);
|
||||
|
||||
friend class testing::TestableUFM01;
|
||||
};
|
||||
|
||||
} // namespace esphome::ufm01
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/uart/uart_component.h"
|
||||
#include "esphome/components/ufm01/ufm01.h"
|
||||
|
||||
namespace esphome::ufm01::testing {
|
||||
|
||||
static constexpr uint8_t FRAME_START_BYTE_1 = 0x3C;
|
||||
static constexpr uint8_t FRAME_START_BYTE_2 = 0x32;
|
||||
static constexpr uint8_t PASSIVE_START_BYTE_2 = 0x64;
|
||||
static constexpr uint8_t FRAME_STOP_BYTE = 0x16;
|
||||
static constexpr uint8_t FRAME_FLAG_INSTANT_FLOW = 0x0B;
|
||||
static constexpr uint8_t FRAME_FLAG_RESERVED_SECTION = 0x0C;
|
||||
static constexpr uint8_t FRAME_FLAG_TEMP = 0x0D;
|
||||
static constexpr uint8_t COMMAND_ACK = 0xE5;
|
||||
|
||||
// UART mock with a byte queue for read-side simulation.
|
||||
class QueuedMockUART : public uart::UARTComponent {
|
||||
public:
|
||||
std::deque<uint8_t> rx_queue;
|
||||
std::vector<uint8_t> written_data;
|
||||
|
||||
void enqueue(const std::vector<uint8_t> &data) {
|
||||
this->rx_queue.insert(this->rx_queue.end(), data.begin(), data.end());
|
||||
}
|
||||
|
||||
void enqueue(std::initializer_list<uint8_t> data) {
|
||||
for (uint8_t byte : data)
|
||||
this->rx_queue.push_back(byte);
|
||||
}
|
||||
|
||||
void clear_rx() { this->rx_queue.clear(); }
|
||||
|
||||
bool read_array(uint8_t *data, size_t len) override {
|
||||
if (this->rx_queue.size() < len)
|
||||
return false;
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
data[i] = this->rx_queue.front();
|
||||
this->rx_queue.pop_front();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool peek_byte(uint8_t *data) override {
|
||||
if (this->rx_queue.empty())
|
||||
return false;
|
||||
*data = this->rx_queue.front();
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t available() override { return this->rx_queue.size(); }
|
||||
|
||||
uart::UARTFlushResult flush() override { return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; }
|
||||
|
||||
void write_array(const uint8_t *data, size_t len) override { this->written_data.assign(data, data + len); }
|
||||
|
||||
void check_logger_conflict() override {}
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
void load_settings(bool dump_config) override {}
|
||||
#endif
|
||||
};
|
||||
|
||||
class TestableUFM01 : public UFM01Component {
|
||||
public:
|
||||
void set_mock_uart(QueuedMockUART *uart) { this->set_uart_parent(uart); }
|
||||
|
||||
bool process_active_stream() { return this->process_active_stream_(); }
|
||||
|
||||
PassiveReadResult continue_passive_read() { return this->continue_passive_read_(); }
|
||||
|
||||
bool consume_ack() { return this->consume_ack_(); }
|
||||
|
||||
void start_passive_read() { this->start_passive_read_(); }
|
||||
|
||||
void loop_startup() { this->loop_startup_(); }
|
||||
|
||||
OperatingMode operating_mode() const { return this->operating_mode_; }
|
||||
|
||||
StartupPhase startup_phase() const { return this->startup_phase_; }
|
||||
|
||||
int32_t read_index() const { return this->read_index_; }
|
||||
|
||||
size_t passive_index() const { return this->passive_index_; }
|
||||
|
||||
uint32_t last_valid_frame_ms() const { return this->last_valid_frame_ms_; }
|
||||
|
||||
void prepare_passive_read() {
|
||||
this->passive_index_ = 0;
|
||||
this->passive_start_ms_ = millis();
|
||||
}
|
||||
|
||||
void init_wait_phase() {
|
||||
this->operating_mode_ = OperatingMode::STARTUP;
|
||||
this->startup_phase_ = StartupPhase::WAIT;
|
||||
this->startup_wait_ms_ = 60000;
|
||||
this->phase_start_ms_ = millis();
|
||||
}
|
||||
|
||||
void reset_state() {
|
||||
this->read_index_ = 0;
|
||||
this->last_valid_frame_ms_ = 0;
|
||||
this->passive_index_ = 0;
|
||||
this->passive_read_pending_ = false;
|
||||
}
|
||||
};
|
||||
|
||||
inline std::array<uint8_t, FRAME_SIZE> make_active_frame() {
|
||||
std::array<uint8_t, FRAME_SIZE> frame{};
|
||||
frame[0] = FRAME_START_BYTE_1;
|
||||
frame[1] = FRAME_START_BYTE_2;
|
||||
frame[15] = FRAME_FLAG_INSTANT_FLOW;
|
||||
frame[21] = FRAME_FLAG_RESERVED_SECTION;
|
||||
frame[24] = FRAME_FLAG_TEMP;
|
||||
frame[31] = FRAME_STOP_BYTE;
|
||||
uint8_t sum = 0;
|
||||
for (size_t i = 0; i < 30; ++i)
|
||||
sum += frame[i];
|
||||
frame[30] = sum;
|
||||
return frame;
|
||||
}
|
||||
|
||||
inline std::array<uint8_t, PASSIVE_FRAME_SIZE> make_passive_frame() {
|
||||
std::array<uint8_t, PASSIVE_FRAME_SIZE> frame{};
|
||||
frame[0] = FRAME_START_BYTE_1;
|
||||
frame[1] = PASSIVE_START_BYTE_2;
|
||||
frame[9] = FRAME_FLAG_INSTANT_FLOW;
|
||||
frame[15] = FRAME_FLAG_TEMP;
|
||||
frame[22] = FRAME_STOP_BYTE;
|
||||
uint8_t sum = 0;
|
||||
for (size_t i = 0; i < 21; ++i)
|
||||
sum += frame[i];
|
||||
frame[21] = sum;
|
||||
return frame;
|
||||
}
|
||||
|
||||
class UFM01Test : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
this->mock_uart_.clear_rx();
|
||||
this->mock_uart_.written_data.clear();
|
||||
this->ufm01_.set_mock_uart(&this->mock_uart_);
|
||||
this->ufm01_.reset_state();
|
||||
}
|
||||
|
||||
QueuedMockUART mock_uart_;
|
||||
TestableUFM01 ufm01_;
|
||||
};
|
||||
|
||||
} // namespace esphome::ufm01::testing
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "common.h"
|
||||
|
||||
namespace esphome::ufm01::testing {
|
||||
|
||||
TEST_F(UFM01Test, ValidActiveFrameAccepted) {
|
||||
auto frame = make_active_frame();
|
||||
this->mock_uart_.enqueue(std::vector<uint8_t>(frame.begin(), frame.end()));
|
||||
|
||||
EXPECT_TRUE(this->ufm01_.process_active_stream());
|
||||
EXPECT_EQ(this->ufm01_.read_index(), 0);
|
||||
EXPECT_NE(this->ufm01_.last_valid_frame_ms(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(UFM01Test, GarbagePrefixThenValidActiveFrame) {
|
||||
this->mock_uart_.enqueue({0x00, 0xFF, 0xAA});
|
||||
auto frame = make_active_frame();
|
||||
this->mock_uart_.enqueue(std::vector<uint8_t>(frame.begin(), frame.end()));
|
||||
|
||||
EXPECT_TRUE(this->ufm01_.process_active_stream());
|
||||
EXPECT_EQ(this->ufm01_.read_index(), 0);
|
||||
}
|
||||
|
||||
TEST_F(UFM01Test, InvalidActiveFrameChecksumRejected) {
|
||||
auto frame = make_active_frame();
|
||||
frame[30] ^= 0xFF;
|
||||
this->mock_uart_.enqueue(std::vector<uint8_t>(frame.begin(), frame.end()));
|
||||
|
||||
EXPECT_FALSE(this->ufm01_.process_active_stream());
|
||||
EXPECT_EQ(this->ufm01_.read_index(), 0);
|
||||
EXPECT_EQ(this->ufm01_.last_valid_frame_ms(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(UFM01Test, ValidPassiveFrameReadSuccess) {
|
||||
auto frame = make_passive_frame();
|
||||
this->mock_uart_.enqueue(std::vector<uint8_t>(frame.begin(), frame.end()));
|
||||
this->ufm01_.prepare_passive_read();
|
||||
|
||||
EXPECT_EQ(this->ufm01_.continue_passive_read(), PassiveReadResult::SUCCESS);
|
||||
EXPECT_EQ(this->ufm01_.passive_index(), PASSIVE_FRAME_SIZE);
|
||||
EXPECT_NE(this->ufm01_.last_valid_frame_ms(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(UFM01Test, InvalidPassiveChecksumFails) {
|
||||
auto frame = make_passive_frame();
|
||||
frame[21] ^= 0xFF;
|
||||
this->mock_uart_.enqueue(std::vector<uint8_t>(frame.begin(), frame.end()));
|
||||
this->ufm01_.prepare_passive_read();
|
||||
|
||||
EXPECT_EQ(this->ufm01_.continue_passive_read(), PassiveReadResult::FAILURE);
|
||||
EXPECT_EQ(this->ufm01_.last_valid_frame_ms(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(UFM01Test, PassiveReadResyncsAfterGarbagePrefix) {
|
||||
auto frame = make_passive_frame();
|
||||
this->mock_uart_.enqueue({0x00, 0x01, 0x02});
|
||||
this->mock_uart_.enqueue(std::vector<uint8_t>(frame.begin(), frame.end()));
|
||||
this->ufm01_.prepare_passive_read();
|
||||
|
||||
EXPECT_EQ(this->ufm01_.continue_passive_read(), PassiveReadResult::SUCCESS);
|
||||
}
|
||||
|
||||
TEST_F(UFM01Test, PassiveReadResyncsOnSecondStartByte) {
|
||||
auto frame = make_passive_frame();
|
||||
this->mock_uart_.enqueue({FRAME_START_BYTE_1, 0x99});
|
||||
this->mock_uart_.enqueue(std::vector<uint8_t>(frame.begin(), frame.end()));
|
||||
this->ufm01_.prepare_passive_read();
|
||||
|
||||
EXPECT_EQ(this->ufm01_.continue_passive_read(), PassiveReadResult::SUCCESS);
|
||||
}
|
||||
|
||||
TEST_F(UFM01Test, PassiveReadPendingWhenPartial) {
|
||||
auto frame = make_passive_frame();
|
||||
this->mock_uart_.enqueue(std::vector<uint8_t>(frame.begin(), frame.begin() + 10));
|
||||
this->ufm01_.prepare_passive_read();
|
||||
|
||||
EXPECT_EQ(this->ufm01_.continue_passive_read(), PassiveReadResult::PENDING);
|
||||
EXPECT_LT(this->ufm01_.passive_index(), PASSIVE_FRAME_SIZE);
|
||||
|
||||
this->mock_uart_.enqueue(std::vector<uint8_t>(frame.begin() + 10, frame.end()));
|
||||
EXPECT_EQ(this->ufm01_.continue_passive_read(), PassiveReadResult::SUCCESS);
|
||||
}
|
||||
|
||||
} // namespace esphome::ufm01::testing
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "common.h"
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::ufm01::testing {
|
||||
|
||||
TEST(UFM01SetupPriority, IsLate) {
|
||||
TestableUFM01 ufm01;
|
||||
EXPECT_EQ(ufm01.get_setup_priority(), setup_priority::LATE);
|
||||
}
|
||||
|
||||
TEST_F(UFM01Test, ConsumeAckFindsByteAmongGarbage) {
|
||||
this->mock_uart_.enqueue({0x00, 0x01, COMMAND_ACK, 0x02});
|
||||
|
||||
EXPECT_TRUE(this->ufm01_.consume_ack());
|
||||
EXPECT_EQ(this->mock_uart_.available(), 1u);
|
||||
}
|
||||
|
||||
TEST_F(UFM01Test, ConsumeAckReturnsFalseWhenEmpty) { EXPECT_FALSE(this->ufm01_.consume_ack()); }
|
||||
|
||||
TEST_F(UFM01Test, StartupWaitDetectsActiveStream) {
|
||||
auto frame = make_active_frame();
|
||||
this->mock_uart_.enqueue(std::vector<uint8_t>(frame.begin(), frame.end()));
|
||||
this->ufm01_.init_wait_phase();
|
||||
|
||||
this->ufm01_.loop_startup();
|
||||
|
||||
EXPECT_EQ(this->ufm01_.operating_mode(), OperatingMode::ACTIVE_STREAM);
|
||||
EXPECT_EQ(this->ufm01_.startup_phase(), StartupPhase::WAIT);
|
||||
}
|
||||
|
||||
TEST_F(UFM01Test, StartPassiveReadSendsCommand) {
|
||||
this->ufm01_.start_passive_read();
|
||||
|
||||
ASSERT_EQ(this->mock_uart_.written_data.size(), 7u);
|
||||
EXPECT_EQ(this->mock_uart_.written_data[0], 0xFE);
|
||||
EXPECT_EQ(this->mock_uart_.written_data[1], 0xFE);
|
||||
EXPECT_EQ(this->mock_uart_.written_data[2], 0x11);
|
||||
EXPECT_EQ(this->mock_uart_.written_data[3], 0x5B);
|
||||
EXPECT_EQ(this->mock_uart_.written_data[6], FRAME_STOP_BYTE);
|
||||
}
|
||||
|
||||
} // namespace esphome::ufm01::testing
|
||||
Reference in New Issue
Block a user