[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:
Petter Ljungqvist
2026-08-10 12:31:31 -04:00
committed by GitHub
co-authored by Cursor Jonathan Swoboda
parent 2798ef4de2
commit 56ec21d950
5 changed files with 612 additions and 32 deletions
+275 -31
View File
@@ -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
+55 -1
View File
@@ -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
+156
View File
@@ -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