mirror of
https://github.com/esphome/esphome.git
synced 2026-09-15 17:18:40 +00:00
Merge remote-tracking branch 'upstream/dev' into integration
This commit is contained in:
@@ -117,8 +117,9 @@ FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card)
|
||||
|
||||
|
||||
async def setup_display_core_(var, config):
|
||||
if CONF_ROTATION in config:
|
||||
cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]]))
|
||||
if rotation := config.get(CONF_ROTATION, 0):
|
||||
# Default initialised value for rotation is 0
|
||||
cg.add(var.set_rotation(DISPLAY_ROTATIONS[rotation]))
|
||||
|
||||
if (auto_clear := config.get(CONF_AUTO_CLEAR_ENABLED)) is not None:
|
||||
# Default to true if pages or lambda is specified. Ideally this would be done during validation, but
|
||||
|
||||
@@ -175,9 +175,7 @@ async def to_code(config):
|
||||
*model.get_constructor_args(config),
|
||||
)
|
||||
|
||||
# Rotation is handled by setting the transform
|
||||
display_config = {k: v for k, v in config.items() if k != CONF_ROTATION}
|
||||
await display.register_display(var, display_config)
|
||||
await display.register_display(var, config)
|
||||
await spi.register_spi_device(var, config, write_only=True)
|
||||
|
||||
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
|
||||
@@ -201,16 +199,6 @@ async def to_code(config):
|
||||
transform[CONF_SWAP_XY] = False
|
||||
else:
|
||||
transform = {x: model.get_default(x, False) for x in TRANSFORM_OPTIONS}
|
||||
rotation = config[CONF_ROTATION]
|
||||
if rotation == 180:
|
||||
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
|
||||
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
|
||||
elif rotation == 90:
|
||||
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
|
||||
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
|
||||
elif rotation == 270:
|
||||
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
|
||||
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
|
||||
transform_str = "|".join(
|
||||
{
|
||||
str(getattr(Transform, x.upper()))
|
||||
|
||||
@@ -97,6 +97,23 @@ bool EPaperBase::reset() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void EPaperBase::update_effective_transform_() {
|
||||
switch (this->rotation_) {
|
||||
case DISPLAY_ROTATION_90_DEGREES:
|
||||
this->effective_transform_ = this->transform_ ^ (SWAP_XY | MIRROR_X);
|
||||
break;
|
||||
case DISPLAY_ROTATION_180_DEGREES:
|
||||
this->effective_transform_ = this->transform_ ^ (MIRROR_Y | MIRROR_X);
|
||||
break;
|
||||
case DISPLAY_ROTATION_270_DEGREES:
|
||||
this->effective_transform_ = this->transform_ ^ (SWAP_XY | MIRROR_Y);
|
||||
break;
|
||||
default:
|
||||
this->effective_transform_ = this->transform_;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void EPaperBase::update() {
|
||||
if (this->state_ != EPaperState::IDLE) {
|
||||
ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_());
|
||||
@@ -280,11 +297,11 @@ bool EPaperBase::initialise(bool partial) {
|
||||
bool EPaperBase::rotate_coordinates_(int &x, int &y) {
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
return false;
|
||||
if (this->transform_ & SWAP_XY)
|
||||
if (this->effective_transform_ & SWAP_XY)
|
||||
std::swap(x, y);
|
||||
if (this->transform_ & MIRROR_X)
|
||||
if (this->effective_transform_ & MIRROR_X)
|
||||
x = this->width_ - x - 1;
|
||||
if (this->transform_ & MIRROR_Y)
|
||||
if (this->effective_transform_ & MIRROR_Y)
|
||||
y = this->height_ - y - 1;
|
||||
if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0)
|
||||
return false;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/split_buffer/split_buffer.h"
|
||||
#include "esphome/core/component.h"
|
||||
@@ -51,7 +51,14 @@ class EPaperBase : public Display,
|
||||
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
|
||||
void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; }
|
||||
void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; }
|
||||
void set_transform(uint8_t transform) { this->transform_ = transform; }
|
||||
void set_transform(uint8_t transform) {
|
||||
this->transform_ = transform;
|
||||
this->update_effective_transform_();
|
||||
}
|
||||
void set_rotation(DisplayRotation rotation) override {
|
||||
Display::set_rotation(rotation);
|
||||
this->update_effective_transform_();
|
||||
}
|
||||
void set_full_update_every(uint8_t full_update_every) { this->full_update_every_ = full_update_every; }
|
||||
void dump_config() override;
|
||||
|
||||
@@ -106,8 +113,8 @@ class EPaperBase : public Display,
|
||||
protected:
|
||||
int get_height_internal() override { return this->height_; };
|
||||
int get_width_internal() override { return this->width_; };
|
||||
int get_width() override { return this->transform_ & SWAP_XY ? this->height_ : this->width_; }
|
||||
int get_height() override { return this->transform_ & SWAP_XY ? this->width_ : this->height_; }
|
||||
int get_width() override { return this->effective_transform_ & SWAP_XY ? this->height_ : this->width_; }
|
||||
int get_height() override { return this->effective_transform_ & SWAP_XY ? this->width_ : this->height_; }
|
||||
void draw_pixel_at(int x, int y, Color color) override;
|
||||
void process_state_();
|
||||
|
||||
@@ -119,6 +126,7 @@ class EPaperBase : public Display,
|
||||
void send_init_sequence_(const uint8_t *sequence, size_t length);
|
||||
void wait_for_idle_(bool should_wait);
|
||||
bool init_buffer_(size_t buffer_length);
|
||||
void update_effective_transform_();
|
||||
bool rotate_coordinates_(int &x, int &y);
|
||||
|
||||
/**
|
||||
@@ -171,6 +179,7 @@ class EPaperBase : public Display,
|
||||
uint32_t delay_until_{}; // timestamp until which to delay processing
|
||||
uint16_t next_delay_{}; // milliseconds to delay before next state
|
||||
uint8_t transform_{};
|
||||
uint8_t effective_transform_{};
|
||||
uint8_t update_count_{};
|
||||
// these values represent the bounds of the updated buffer. Note that x_high and y_high
|
||||
// point to the pixel past the last one updated, i.e. may range up to width/height.
|
||||
|
||||
@@ -1,7 +1,195 @@
|
||||
#include <array>
|
||||
#include <numeric>
|
||||
#include "mitsubishi_cn105.h"
|
||||
|
||||
namespace esphome::mitsubishi_cn105 {
|
||||
|
||||
static const char *const TAG = "mitsubishi_cn105.driver";
|
||||
|
||||
static constexpr uint32_t WRITE_TIMEOUT_MS = 2000;
|
||||
|
||||
static constexpr size_t HEADER_LEN = 5;
|
||||
static constexpr uint8_t PREAMBLE = 0xFC;
|
||||
static constexpr uint8_t HEADER_BYTE_1 = 0x01;
|
||||
static constexpr uint8_t HEADER_BYTE_2 = 0x30;
|
||||
|
||||
static constexpr uint8_t PACKET_TYPE_CONNECT_REQUEST = 0x5A;
|
||||
static constexpr uint8_t PACKET_TYPE_CONNECT_RESPONSE = 0x7A;
|
||||
static constexpr std::array<uint8_t, 2> CONNECT_REQUEST_PAYLOAD = {{0xCA, 0x01}};
|
||||
|
||||
static constexpr uint8_t checksum(const uint8_t *bytes, size_t length) {
|
||||
return static_cast<uint8_t>(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0}));
|
||||
}
|
||||
|
||||
template<std::size_t PayloadSize>
|
||||
static constexpr auto make_packet(uint8_t type, const std::array<uint8_t, PayloadSize> &payload) {
|
||||
const size_t full_len = PayloadSize + HEADER_LEN + 1;
|
||||
std::array<uint8_t, full_len> packet{PREAMBLE, type, HEADER_BYTE_1, HEADER_BYTE_2, static_cast<uint8_t>(PayloadSize)};
|
||||
std::copy_n(payload.begin(), PayloadSize, packet.begin() + HEADER_LEN);
|
||||
packet.back() = checksum(packet.data(), packet.size() - 1);
|
||||
return packet;
|
||||
}
|
||||
|
||||
static constexpr auto CONNECT_PACKET = make_packet(PACKET_TYPE_CONNECT_REQUEST, CONNECT_REQUEST_PAYLOAD);
|
||||
|
||||
void MitsubishiCN105::initialize() { this->set_state_(State::CONNECTING); }
|
||||
|
||||
void MitsubishiCN105::update() {
|
||||
if (const auto start = this->write_timeout_start_ms_; start && (get_loop_time_ms() - *start) >= WRITE_TIMEOUT_MS) {
|
||||
this->write_timeout_start_ms_.reset();
|
||||
this->read_pos_ = 0;
|
||||
this->set_state_(State::READ_TIMEOUT);
|
||||
return;
|
||||
}
|
||||
|
||||
this->read_incoming_bytes_();
|
||||
}
|
||||
|
||||
void MitsubishiCN105::set_state_(State new_state) {
|
||||
if (should_transition(this->state_, new_state)) {
|
||||
ESP_LOGV(TAG, "Did transition: %s -> %s", LOG_STR_ARG(state_to_string(this->state_)),
|
||||
LOG_STR_ARG(state_to_string(new_state)));
|
||||
this->state_ = new_state;
|
||||
this->did_transition_(new_state);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Ignoring unexpected transition %s -> %s", LOG_STR_ARG(state_to_string(this->state_)),
|
||||
LOG_STR_ARG(state_to_string(new_state)));
|
||||
}
|
||||
}
|
||||
|
||||
bool MitsubishiCN105::should_transition(State from, State to) {
|
||||
switch (to) {
|
||||
case State::CONNECTING:
|
||||
return from == State::NOT_CONNECTED || from == State::READ_TIMEOUT;
|
||||
|
||||
case State::CONNECTED:
|
||||
case State::READ_TIMEOUT:
|
||||
return from == State::CONNECTING;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void MitsubishiCN105::did_transition_(State to) {
|
||||
switch (to) {
|
||||
case State::CONNECTING:
|
||||
this->send_packet_(CONNECT_PACKET);
|
||||
break;
|
||||
|
||||
case State::CONNECTED:
|
||||
this->write_timeout_start_ms_.reset();
|
||||
// TODO: read AC status after connected, next PR
|
||||
break;
|
||||
|
||||
case State::READ_TIMEOUT:
|
||||
this->set_state_(State::CONNECTING);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MitsubishiCN105::send_packet_(const uint8_t *packet, size_t len) {
|
||||
dump_buffer_vv("TX", packet, len);
|
||||
this->device_.write_array(packet, len);
|
||||
this->write_timeout_start_ms_ = get_loop_time_ms();
|
||||
}
|
||||
|
||||
void MitsubishiCN105::read_incoming_bytes_() {
|
||||
uint8_t watchdog = 64;
|
||||
while (this->device_.available() > 0 && watchdog-- > 0) {
|
||||
uint8_t &value = this->read_buffer_[this->read_pos_];
|
||||
if (!this->device_.read_byte(&value)) {
|
||||
ESP_LOGW(TAG, "UART read failed while data available");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (++this->read_pos_) {
|
||||
case 1:
|
||||
if (value != PREAMBLE) {
|
||||
this->reset_read_position_and_dump_buffer_("RX ignoring preamble");
|
||||
}
|
||||
continue;
|
||||
|
||||
case 2:
|
||||
continue;
|
||||
|
||||
case 3:
|
||||
if (value != HEADER_BYTE_1) {
|
||||
this->reset_read_position_and_dump_buffer_("RX invalid: header 1 mismatch");
|
||||
}
|
||||
continue;
|
||||
|
||||
case 4:
|
||||
if (value != HEADER_BYTE_2) {
|
||||
this->reset_read_position_and_dump_buffer_("RX invalid: header 2 mismatch");
|
||||
}
|
||||
continue;
|
||||
|
||||
case HEADER_LEN:
|
||||
static_assert(READ_BUFFER_SIZE > HEADER_LEN);
|
||||
if (this->read_buffer_[HEADER_LEN - 1] >= READ_BUFFER_SIZE - HEADER_LEN) {
|
||||
this->reset_read_position_and_dump_buffer_("RX invalid: payload too large");
|
||||
}
|
||||
continue;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const size_t len_without_checksum = HEADER_LEN + static_cast<size_t>(this->read_buffer_[HEADER_LEN - 1]);
|
||||
if (this->read_pos_ <= len_without_checksum) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (checksum(this->read_buffer_, len_without_checksum) != value) {
|
||||
this->reset_read_position_and_dump_buffer_("RX invalid: checksum mismatch");
|
||||
continue;
|
||||
}
|
||||
|
||||
this->process_rx_packet_(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN, len_without_checksum - HEADER_LEN);
|
||||
this->reset_read_position_and_dump_buffer_("RX");
|
||||
}
|
||||
}
|
||||
|
||||
void MitsubishiCN105::process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len) {
|
||||
switch (type) {
|
||||
case PACKET_TYPE_CONNECT_RESPONSE:
|
||||
this->set_state_(State::CONNECTED);
|
||||
break;
|
||||
|
||||
default:
|
||||
ESP_LOGVV(TAG, "RX unknown packet type 0x%02X", type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void MitsubishiCN105::reset_read_position_and_dump_buffer_(const char *prefix) {
|
||||
dump_buffer_vv(prefix, this->read_buffer_, this->read_pos_);
|
||||
this->read_pos_ = 0;
|
||||
}
|
||||
|
||||
void MitsubishiCN105::dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char buf[format_hex_pretty_size(READ_BUFFER_SIZE)];
|
||||
ESP_LOGVV(TAG, "%s (%zu): %s", prefix, len, format_hex_pretty_to(buf, data, len));
|
||||
#endif
|
||||
}
|
||||
|
||||
const LogString *MitsubishiCN105::state_to_string(State state) {
|
||||
switch (state) {
|
||||
case State::NOT_CONNECTED:
|
||||
return LOG_STR("Not connected");
|
||||
case State::CONNECTING:
|
||||
return LOG_STR("Connecting");
|
||||
case State::CONNECTED:
|
||||
return LOG_STR("Connected");
|
||||
case State::READ_TIMEOUT:
|
||||
return LOG_STR("ReadTimeout");
|
||||
}
|
||||
return LOG_STR("Unknown");
|
||||
}
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105
|
||||
|
||||
@@ -1,19 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
namespace esphome::mitsubishi_cn105 {
|
||||
|
||||
uint32_t get_loop_time_ms();
|
||||
|
||||
class MitsubishiCN105 {
|
||||
public:
|
||||
explicit MitsubishiCN105(uart::UARTDevice &device) : device_(device) {}
|
||||
|
||||
void initialize();
|
||||
void update();
|
||||
|
||||
uint32_t get_update_interval() const { return this->update_interval_ms_; }
|
||||
void set_update_interval(uint32_t interval_ms) { this->update_interval_ms_ = interval_ms; }
|
||||
|
||||
protected:
|
||||
enum class State : uint8_t { NOT_CONNECTED, CONNECTING, CONNECTED, READ_TIMEOUT };
|
||||
|
||||
void set_state_(State new_state);
|
||||
void did_transition_(State to);
|
||||
void read_incoming_bytes_();
|
||||
void process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len);
|
||||
void reset_read_position_and_dump_buffer_(const char *prefix);
|
||||
void send_packet_(const uint8_t *packet, size_t len);
|
||||
template<typename T> void send_packet_(const T &packet) { this->send_packet_(packet.data(), packet.size()); }
|
||||
static bool should_transition(State from, State to);
|
||||
static const LogString *state_to_string(State state);
|
||||
static void dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len);
|
||||
|
||||
uart::UARTDevice &device_;
|
||||
uint32_t update_interval_ms_{1000};
|
||||
std::optional<uint32_t> write_timeout_start_ms_;
|
||||
State state_{State::NOT_CONNECTED};
|
||||
|
||||
private:
|
||||
static constexpr size_t READ_BUFFER_SIZE = 32;
|
||||
uint8_t read_buffer_[READ_BUFFER_SIZE];
|
||||
uint8_t read_pos_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#include <cinttypes>
|
||||
#include "mitsubishi_cn105_climate.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -14,9 +15,9 @@ void MitsubishiCN105Climate::dump_config() {
|
||||
LOG_STR_ARG(parity_to_str(this->parent_->get_parity())), this->parent_->get_stop_bits());
|
||||
}
|
||||
|
||||
void MitsubishiCN105Climate::setup() {}
|
||||
void MitsubishiCN105Climate::setup() { this->hp_.initialize(); }
|
||||
|
||||
void MitsubishiCN105Climate::loop() {}
|
||||
void MitsubishiCN105Climate::loop() { this->hp_.update(); }
|
||||
|
||||
climate::ClimateTraits MitsubishiCN105Climate::traits() {
|
||||
climate::ClimateTraits traits;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::mitsubishi_cn105 {
|
||||
|
||||
uint32_t __attribute__((weak)) get_loop_time_ms() { return App.get_loop_component_start_time(); };
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105
|
||||
@@ -248,6 +248,9 @@ void RuntimeImage::release_buffer_() {
|
||||
this->height_ = 0;
|
||||
this->buffer_width_ = 0;
|
||||
this->buffer_height_ = 0;
|
||||
#ifdef USE_LVGL
|
||||
memset(&this->dsc_, 0, sizeof(this->dsc_));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -133,6 +133,6 @@ def test_code_generation(
|
||||
assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp
|
||||
assert "p4_nano->set_lane_bit_rate(1500.0f);" in main_cpp
|
||||
assert "p4_nano->set_rotation(display::DISPLAY_ROTATION_90_DEGREES);" in main_cpp
|
||||
assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" in main_cpp
|
||||
assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" not in main_cpp
|
||||
assert "custom_id->set_rotation(display::DISPLAY_ROTATION_180_DEGREES);" in main_cpp
|
||||
# assert "backlight_id = new light::LightState(mipi_dsi_dsibacklight_id);" in main_cpp
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
#include "../common.h"
|
||||
|
||||
namespace esphome::mitsubishi_cn105::testing {
|
||||
|
||||
struct TestContext {
|
||||
MockUARTComponent uart;
|
||||
uart::UARTDevice device{&uart};
|
||||
TestableMitsubishiCN105 sut{device};
|
||||
|
||||
TestContext() { this->sut.set_current_time(0); }
|
||||
};
|
||||
|
||||
TEST(MitsubishiCN105Tests, InitSendsConnectPacket) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.sut.set_current_time(123);
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::NOT_CONNECTED);
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
EXPECT_FALSE(ctx.sut.write_timeout_start_ms_.has_value());
|
||||
|
||||
ctx.sut.initialize();
|
||||
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x5A, 0x01, 0x30, 0x02, 0xCA, 0x01, 0xA8));
|
||||
EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{123});
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, SuccessfullyConnects) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.sut.initialize();
|
||||
ctx.uart.tx.clear(); // Remove first connect packet bytes
|
||||
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_TRUE(ctx.sut.write_timeout_start_ms_.has_value());
|
||||
|
||||
// Connect response
|
||||
ctx.uart.push_rx({0xFC, 0x7A, 0x01, 0x30, 0x00, 0x55});
|
||||
|
||||
ctx.sut.update();
|
||||
|
||||
// All bytes from UART should be consumed and state = CONNECTED
|
||||
EXPECT_TRUE(ctx.uart.rx.empty());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTED);
|
||||
EXPECT_FALSE(ctx.sut.write_timeout_start_ms_.has_value());
|
||||
|
||||
// Nothing should be send to UART
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, NoResponseTriggersReconnect) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.sut.initialize();
|
||||
ctx.uart.tx.clear(); // Remove first connect packet bytes
|
||||
|
||||
// No response (no RX data), no retry yet
|
||||
ctx.sut.update();
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{0});
|
||||
|
||||
// Still no response after 1999ms, no retry yet
|
||||
ctx.sut.set_current_time(1999);
|
||||
ctx.sut.update();
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{0});
|
||||
|
||||
// Stop waiting after 2s and retry connect
|
||||
ctx.sut.set_current_time(2000);
|
||||
ctx.sut.update();
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x5A, 0x01, 0x30, 0x02, 0xCA, 0x01, 0xA8));
|
||||
EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{2000});
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, RxWatchdogLimitsProcessingPerUpdate) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.sut.initialize();
|
||||
ctx.uart.tx.clear(); // Remove first connect packet bytes
|
||||
|
||||
// RX noise/unexpected traffic
|
||||
ctx.uart.push_rx({0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
|
||||
0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C,
|
||||
0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A,
|
||||
0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
|
||||
0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46});
|
||||
|
||||
// Make sure we have enough bytes in buffer.
|
||||
ASSERT_GT(ctx.uart.rx.size(), 64);
|
||||
|
||||
// No valid response, no state change expected
|
||||
ctx.sut.update();
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
|
||||
// Watchdog interrupts reading (max. 64 bytes at once) so we do not spend the whole loop draining UART
|
||||
EXPECT_FALSE(ctx.uart.rx.empty());
|
||||
|
||||
// Next update will read remaining bytes, no state change expected
|
||||
ctx.sut.update();
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
EXPECT_TRUE(ctx.uart.rx.empty());
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, ParserHandlesMixedRxStream) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.sut.initialize();
|
||||
ctx.uart.tx.clear(); // Remove first connect packet bytes
|
||||
|
||||
// Mixed RX stream with partial, malformed, and oversized frames to test parser robustness
|
||||
ctx.uart.push_rx({// ─────────────────────────────
|
||||
// Noise (no 0xFC) -> should be ignored via preamble reset
|
||||
// ────────────────────────────
|
||||
0x01, 0x02, 0x03, 0x04, 0x05,
|
||||
|
||||
// ─────────────────────────────
|
||||
// Partial frame (declares payload len=5, but we cut it short)
|
||||
// Later bytes will eventually force checksum mismatch and reset
|
||||
// ─────────────────────────────
|
||||
0xFC, 0x62, 0x01, 0x30, 0x05, 0xAA, 0xBB,
|
||||
|
||||
// ─────────────────────────────
|
||||
// Invalid header (header byte 3 should be 0x01, header byte 4 should be 0x30)
|
||||
// Should reset quickly on header mismatch
|
||||
// ─────────────────────────────
|
||||
0xFC, 0x62, 0xFF, 0xFF, 0x02, 0x01, 0x02, 0x00,
|
||||
|
||||
// ─────────────────────────────
|
||||
// Oversized length field (rejected by payload-too-large check at HEADER_LEN)
|
||||
// ─────────────────────────────
|
||||
0xFC, 0x62, 0x01, 0x30, 0xFE, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A,
|
||||
0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
|
||||
|
||||
// ─────────────────────────────
|
||||
// Valid unknown-type frame (type=0x62), should be parsed successfully then ignored
|
||||
// Frame: FC 62 01 30 02 AA BB 30
|
||||
// ─────────────────────────────
|
||||
0xFC, 0x62, 0x01, 0x30, 0x02, 0xAA, 0xBB, 0x30,
|
||||
|
||||
// ─────────────────────────────
|
||||
// Invalid checksum (should be rejected at checksum check)
|
||||
// ─────────────────────────────
|
||||
0xFC, 0x62, 0x01, 0x30, 0x02, 0x10, 0x20, 0xFF,
|
||||
|
||||
// ─────────────────────────────
|
||||
// Back-to-back VALID frames (unknown type=0x62) to stress boundary handling.
|
||||
// Frame A: FC 62 01 30 01 02 6C
|
||||
// Frame B: FC 62 01 30 01 03 6B
|
||||
// ─────────────────────────────
|
||||
0xFC, 0x62, 0x01, 0x30, 0x01, 0x02, 0x6C, 0xFC, 0x62, 0x01, 0x30, 0x01, 0x03, 0x6B,
|
||||
|
||||
// ─────────────────────────────
|
||||
// Trailing noise
|
||||
// ─────────────────────────────
|
||||
0x55, 0x66, 0x77, 0x88});
|
||||
|
||||
// Drain RX - no valid response, no state change expected
|
||||
int iterations = 0;
|
||||
while (!ctx.uart.rx.empty() && iterations++ < 10) {
|
||||
ctx.sut.update();
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
}
|
||||
|
||||
EXPECT_TRUE(ctx.uart.rx.empty());
|
||||
}
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105::testing
|
||||
@@ -0,0 +1,7 @@
|
||||
#include "common.h"
|
||||
|
||||
namespace esphome::mitsubishi_cn105 {
|
||||
|
||||
uint32_t get_loop_time_ms() { return testing::TestableMitsubishiCN105::test_loop_time_ms; };
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105
|
||||
@@ -0,0 +1,53 @@
|
||||
#pragma once
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#include <vector>
|
||||
#include "esphome/components/uart/uart_component.h"
|
||||
#include "esphome/components/mitsubishi_cn105/mitsubishi_cn105.h"
|
||||
|
||||
namespace esphome::mitsubishi_cn105::testing {
|
||||
|
||||
class MockUARTComponent : public uart::UARTComponent {
|
||||
public:
|
||||
std::vector<uint8_t> tx;
|
||||
std::vector<uint8_t> rx;
|
||||
|
||||
void push_rx(std::initializer_list<uint8_t> data) { this->rx.insert(this->rx.end(), data.begin(), data.end()); }
|
||||
|
||||
// UARTComponent
|
||||
void write_array(const uint8_t *data, size_t len) override { this->tx.insert(this->tx.end(), data, data + len); }
|
||||
|
||||
bool read_array(uint8_t *data, size_t len) override {
|
||||
if (this->rx.size() < len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::copy(this->rx.begin(), this->rx.begin() + len, data);
|
||||
this->rx.erase(this->rx.begin(), this->rx.begin() + len);
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t available() override { return this->rx.size(); }
|
||||
|
||||
MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override));
|
||||
MOCK_METHOD(uart::UARTFlushResult, flush, (), (override));
|
||||
MOCK_METHOD(void, check_logger_conflict, (), (override));
|
||||
};
|
||||
|
||||
class TestableMitsubishiCN105 : public MitsubishiCN105 {
|
||||
public:
|
||||
using MitsubishiCN105::MitsubishiCN105;
|
||||
using MitsubishiCN105::State;
|
||||
using MitsubishiCN105::state_;
|
||||
using MitsubishiCN105::write_timeout_start_ms_;
|
||||
|
||||
static inline uint32_t test_loop_time_ms = 0;
|
||||
|
||||
void set_current_time(uint32_t ms) { test_loop_time_ms = ms; }
|
||||
};
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105::testing
|
||||
Reference in New Issue
Block a user