mirror of
https://github.com/esphome/esphome.git
synced 2026-08-23 06:36:23 +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
@@ -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