mirror of
https://github.com/esphome/esphome.git
synced 2026-10-25 15:35:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
470ed7cc06 | ||
|
|
195c8285d7 | ||
|
|
e5b5dd52ef | ||
|
|
014e6938c3 | ||
|
|
67a56da25c | ||
|
|
bf36f7532d | ||
|
|
10c32a5cc7 | ||
|
|
69f6429728 | ||
|
|
4ae6ec545c | ||
|
|
263de44633 | ||
|
|
bab99748f0 |
No files matched your search
@@ -216,10 +216,9 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
the helper's arguments instead of writing a loop. 7-byte BCD RTC calendars use `time::decode_bcd_calendar<LAYOUT>`
|
||||
and `encode_bcd_calendar<LAYOUT>` with `EPSON_BCD_CALENDAR`, `DS_BCD_CALENDAR` or `PCF_BCD_CALENDAR`.
|
||||
* **Bus helpers:** UART drains with `read_available(on_byte)` and `discard_available()`, text lines with
|
||||
`LineReader<N>`, fixed-length binary frames with `FrameReader<N, HEADER...>`; I2C has `update_register`,
|
||||
`write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id` and `read_bytes_16_le`.
|
||||
`if (!this->status_comm_result(ok)) return;` sets or clears the warning. A child sensor that skips repeated
|
||||
values is `SUB_SENSOR_WITH_DEDUP(name, type)`.
|
||||
`LineReader<N>`; I2C has `update_register`, `write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id`
|
||||
and `read_bytes_16_le`. `if (!this->status_comm_result(ok)) return;` sets or clears the warning.
|
||||
A child sensor that skips repeated values is `SUB_SENSOR_WITH_DEDUP(name, type)`.
|
||||
* **An unset float is `NAN`**, not `optional<float>`; the optional costs 4 to 8 bytes of padding per member.
|
||||
* **`PollingComponent` first runs `update()`** after a random share of the interval; call `this->update()` at the
|
||||
end of `setup()` when the entities need a state at boot.
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#include "dfrobot_sen0465_uart.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace esphome::dfrobot_sen0465_uart {
|
||||
|
||||
using dfrobot_sen0465_base::FRAME_HEAD;
|
||||
using dfrobot_sen0465_base::FRAME_SIZE;
|
||||
using dfrobot_sen0465_base::RESPONSE_TIMEOUT_ID;
|
||||
|
||||
@@ -15,21 +18,30 @@ void DFRobotSEN0465UARTComponent::setup() {
|
||||
}
|
||||
|
||||
void DFRobotSEN0465UARTComponent::loop() {
|
||||
// The tail of a reading the probe sent on its own may precede the answer; the reader skips to the frame head.
|
||||
const uint8_t *frame = nullptr;
|
||||
this->read_available<FRAME_SIZE>([&](uint8_t c) { return (frame = this->reader_.feed(c)) == nullptr; });
|
||||
if (frame == nullptr)
|
||||
// The tail of a reading the probe sent on its own may precede the answer; start at the next frame head.
|
||||
uint8_t byte;
|
||||
while (this->rx_count_ == 0 && this->peek_byte(&byte) && byte != FRAME_HEAD)
|
||||
this->read_byte(&byte);
|
||||
const size_t available = this->available();
|
||||
if (available == 0)
|
||||
return;
|
||||
const size_t len = std::min<size_t>(available, FRAME_SIZE - this->rx_count_);
|
||||
if (!this->read_array(this->rx_buffer_ + this->rx_count_, len))
|
||||
return;
|
||||
this->rx_count_ += len;
|
||||
if (this->rx_count_ < FRAME_SIZE)
|
||||
return;
|
||||
|
||||
this->rx_count_ = 0;
|
||||
this->cancel_timeout(RESPONSE_TIMEOUT_ID);
|
||||
this->disable_loop();
|
||||
this->handle_response_(frame);
|
||||
this->handle_response_(this->rx_buffer_);
|
||||
}
|
||||
|
||||
bool DFRobotSEN0465UARTComponent::write_frame(const uint8_t *frame) {
|
||||
// Drop anything left over, such as the answer to the mode change or readings sent before passive mode was set
|
||||
this->discard_available();
|
||||
this->reader_.reset();
|
||||
this->rx_count_ = 0;
|
||||
this->write_array(frame, FRAME_SIZE);
|
||||
return true;
|
||||
}
|
||||
@@ -38,7 +50,7 @@ void DFRobotSEN0465UARTComponent::read_response() {
|
||||
this->enable_loop();
|
||||
this->set_timeout(RESPONSE_TIMEOUT_ID, RESPONSE_TIMEOUT_MS, [this]() {
|
||||
this->disable_loop();
|
||||
this->reader_.reset();
|
||||
this->rx_count_ = 0;
|
||||
this->handle_no_response_();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "esphome/components/dfrobot_sen0465_base/dfrobot_sen0465_base.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/frame_reader.h"
|
||||
|
||||
namespace esphome::dfrobot_sen0465_uart {
|
||||
|
||||
@@ -18,7 +17,8 @@ class DFRobotSEN0465UARTComponent final : public dfrobot_sen0465_base::DFRobotSE
|
||||
bool write_frame(const uint8_t *frame) override;
|
||||
void read_response() override;
|
||||
|
||||
FrameReader<dfrobot_sen0465_base::FRAME_SIZE, dfrobot_sen0465_base::FRAME_HEAD> reader_;
|
||||
uint8_t rx_buffer_[dfrobot_sen0465_base::FRAME_SIZE];
|
||||
uint8_t rx_count_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::dfrobot_sen0465_uart
|
||||
@@ -181,6 +181,16 @@ def get_all_display_metadata() -> dict[str, DisplayMetaData]:
|
||||
return {id_.id: meta for id_, meta in entries}
|
||||
|
||||
|
||||
def config_has_writer(config: ConfigType) -> bool:
|
||||
"""Return True when the display config draws anything."""
|
||||
return (
|
||||
config.get(CONF_AUTO_CLEAR_ENABLED) is True
|
||||
or config.get(CONF_PAGES) is not None
|
||||
or config.get(CONF_LAMBDA) is not None
|
||||
or config.get(CONF_SHOW_TEST_CARD) is True
|
||||
)
|
||||
|
||||
|
||||
def get_display_metadata(display_id: ID) -> DisplayMetaData:
|
||||
"""Get display metadata by ID object
|
||||
|
||||
@@ -210,10 +220,7 @@ def get_display_metadata(display_id: ID) -> DisplayMetaData:
|
||||
height=dimensions[1],
|
||||
has_hardware_rotation=False,
|
||||
byte_order=disp_config.get(CONF_BYTE_ORDER, cv.UNDEFINED),
|
||||
has_writer=disp_config.get(CONF_AUTO_CLEAR_ENABLED) is True
|
||||
or disp_config.get(CONF_PAGES) is not None
|
||||
or disp_config.get(CONF_LAMBDA) is not None
|
||||
or disp_config.get(CONF_SHOW_TEST_CARD) is True,
|
||||
has_writer=config_has_writer(disp_config),
|
||||
rotation=disp_config.get(CONF_ROTATION, 0),
|
||||
draw_rounding=disp_config.get(CONF_DRAW_ROUNDING, 0),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#include "init_sequence.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::display {
|
||||
|
||||
InitSequenceStep InitSequenceReader::next(InitSequenceEntry &entry, uint8_t max_count) {
|
||||
const size_t remaining = this->length_ - this->index_;
|
||||
if (remaining == 0)
|
||||
return INIT_SEQUENCE_STEP_END;
|
||||
const uint8_t *pos = this->sequence_ + this->index_;
|
||||
if (remaining >= 2) {
|
||||
entry.value = progmem_read_byte(pos);
|
||||
entry.count = progmem_read_byte(pos + 1);
|
||||
entry.args = pos + 2;
|
||||
if (entry.count == INIT_SEQUENCE_DELAY_FLAG) {
|
||||
this->index_ += 2;
|
||||
return INIT_SEQUENCE_STEP_DELAY;
|
||||
}
|
||||
if (entry.count <= max_count && remaining - 2 >= entry.count) {
|
||||
this->index_ += 2 + entry.count;
|
||||
return INIT_SEQUENCE_STEP_COMMAND;
|
||||
}
|
||||
}
|
||||
this->index_ = this->length_;
|
||||
return INIT_SEQUENCE_STEP_MALFORMED;
|
||||
}
|
||||
|
||||
} // namespace esphome::display
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::display {
|
||||
|
||||
/// Count byte that marks a delay entry: the byte before it is the delay in ms.
|
||||
static constexpr uint8_t INIT_SEQUENCE_DELAY_FLAG = 0xFF;
|
||||
|
||||
/// Unscoped so `while (auto step = reader.next(entry))` stops at END.
|
||||
enum InitSequenceStep : uint8_t {
|
||||
INIT_SEQUENCE_STEP_END = 0,
|
||||
INIT_SEQUENCE_STEP_COMMAND,
|
||||
INIT_SEQUENCE_STEP_DELAY,
|
||||
INIT_SEQUENCE_STEP_MALFORMED,
|
||||
};
|
||||
|
||||
/// `value` is the command or the delay in ms; `args` points into the (possibly PROGMEM) sequence.
|
||||
struct InitSequenceEntry {
|
||||
const uint8_t *args;
|
||||
uint8_t value;
|
||||
uint8_t count;
|
||||
};
|
||||
|
||||
/// Walks a mipi.flatten_sequence table (`cmd, count, args...` or `ms, 0xFF`) with progmem reads; resumable.
|
||||
class InitSequenceReader {
|
||||
public:
|
||||
InitSequenceReader(const uint8_t *sequence, size_t length) : sequence_(sequence), length_(length) {}
|
||||
|
||||
/// Fill `entry` with the next entry; a command with more than `max_count` args is MALFORMED.
|
||||
InitSequenceStep next(InitSequenceEntry &entry, uint8_t max_count = 0xFE);
|
||||
|
||||
protected:
|
||||
const uint8_t *sequence_;
|
||||
size_t length_;
|
||||
size_t index_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::display
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "ds1603l.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -8,28 +10,35 @@ namespace esphome::ds1603l {
|
||||
ESPHOME_LOG_TAG(TAG, "ds1603l.sensor");
|
||||
|
||||
void DS1603L::loop() {
|
||||
this->read_available([this](uint8_t byte) {
|
||||
const uint8_t *frame = this->reader_.feed(byte);
|
||||
if (frame == nullptr) {
|
||||
if (this->reader_.length() == 0) {
|
||||
ESP_LOGV(TAG, "Skipping byte 0x%02X while looking for header", byte);
|
||||
}
|
||||
return;
|
||||
// Assemble frames one byte at a time so a stream that starts mid-frame can realign
|
||||
uint8_t byte;
|
||||
while (this->available() > 0 && this->read_byte(&byte)) {
|
||||
if (this->rx_count_ == 0 && byte != HEADER_BYTE) {
|
||||
ESP_LOGV(TAG, "Skipping byte 0x%02X while looking for header", byte);
|
||||
continue;
|
||||
}
|
||||
if (!this->parse_data_(frame)) {
|
||||
|
||||
this->rx_buffer_[this->rx_count_++] = byte;
|
||||
if (this->rx_count_ < FRAME_SIZE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this->parse_data_()) {
|
||||
this->rx_count_ = 0;
|
||||
} else {
|
||||
// The header byte was part of the payload of a misaligned frame, so realign instead of dropping everything
|
||||
this->reader_.resync();
|
||||
this->resync_();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void DS1603L::dump_config() { LOG_SENSOR("", "DS1603L", this); }
|
||||
|
||||
bool DS1603L::parse_data_(const uint8_t *frame) {
|
||||
uint8_t header = frame[0];
|
||||
uint8_t data_h = frame[1];
|
||||
uint8_t data_l = frame[2];
|
||||
uint8_t checksum = frame[3];
|
||||
bool DS1603L::parse_data_() {
|
||||
uint8_t header = this->rx_buffer_[0];
|
||||
uint8_t data_h = this->rx_buffer_[1];
|
||||
uint8_t data_l = this->rx_buffer_[2];
|
||||
uint8_t checksum = this->rx_buffer_[3];
|
||||
|
||||
uint8_t computed_checksum = (header + data_h + data_l) & 0xFF;
|
||||
|
||||
@@ -44,4 +53,16 @@ bool DS1603L::parse_data_(const uint8_t *frame) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void DS1603L::resync_() {
|
||||
// Drop the byte that was treated as the header, then look for the next candidate header in what is left
|
||||
size_t start = 1;
|
||||
while (start < this->rx_count_ && this->rx_buffer_[start] != HEADER_BYTE) {
|
||||
start++;
|
||||
}
|
||||
this->rx_count_ -= start;
|
||||
if (this->rx_count_ > 0) {
|
||||
memmove(this->rx_buffer_, this->rx_buffer_ + start, this->rx_count_);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::ds1603l
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/frame_reader.h"
|
||||
|
||||
namespace esphome::ds1603l {
|
||||
|
||||
@@ -19,10 +18,13 @@ class DS1603L final : public sensor::Sensor, public Component, public uart::UART
|
||||
static constexpr uint8_t HEADER_BYTE = 0xFF;
|
||||
static constexpr size_t FRAME_SIZE = 4;
|
||||
|
||||
// Validates the checksum of the frame and publishes it. Returns false if the frame is invalid.
|
||||
bool parse_data_(const uint8_t *frame);
|
||||
// Validates the checksum of the frame in rx_buffer_ and publishes it. Returns false if the frame is invalid.
|
||||
bool parse_data_();
|
||||
// Drops the first buffered byte and realigns the buffer on the next possible header byte.
|
||||
void resync_();
|
||||
|
||||
FrameReader<FRAME_SIZE, HEADER_BYTE> reader_;
|
||||
uint8_t rx_buffer_[FRAME_SIZE]; // Buffer for the frame being assembled
|
||||
size_t rx_count_{0}; // Number of bytes currently in rx_buffer_
|
||||
};
|
||||
|
||||
} // namespace esphome::ds1603l
|
||||
@@ -15,7 +15,6 @@ from esphome.components.mipi import (
|
||||
import esphome.config_validation as cv
|
||||
from esphome.config_validation import update_interval
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_BUSY_PIN,
|
||||
CONF_CS_PIN,
|
||||
CONF_DATA_RATE,
|
||||
@@ -189,10 +188,7 @@ def customise_schema(config):
|
||||
height,
|
||||
has_hardware_rotation=True,
|
||||
byte_order=cv.UNDEFINED,
|
||||
has_writer=config.get(CONF_AUTO_CLEAR_ENABLED) is True
|
||||
or config.get(CONF_PAGES) is not None
|
||||
or config.get(CONF_LAMBDA) is not None
|
||||
or config.get(CONF_SHOW_TEST_CARD) is True,
|
||||
has_writer=display.config_has_writer(config),
|
||||
rotation=config.get(CONF_ROTATION, 0),
|
||||
draw_rounding=0,
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "epaper_spi.h"
|
||||
#include "esphome/components/display/init_sequence.h"
|
||||
#include <cinttypes>
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -315,26 +316,18 @@ void EPaperBase::start_data_() {
|
||||
void EPaperBase::on_safe_shutdown() { this->deep_sleep(); }
|
||||
|
||||
void EPaperBase::send_init_sequence_(const uint8_t *sequence, size_t length) {
|
||||
size_t index = 0;
|
||||
|
||||
while (index != length) {
|
||||
if (length - index < 2) {
|
||||
display::InitSequenceReader reader(sequence, length);
|
||||
display::InitSequenceEntry entry;
|
||||
while (auto step = reader.next(entry)) {
|
||||
if (step == display::INIT_SEQUENCE_STEP_MALFORMED) {
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
const uint8_t cmd = sequence[index++];
|
||||
if (const uint8_t x = sequence[index++]; x == DELAY_FLAG) {
|
||||
ESP_LOGV(TAG, "Delay %dms", cmd);
|
||||
delay(cmd);
|
||||
if (step == display::INIT_SEQUENCE_STEP_DELAY) {
|
||||
ESP_LOGV(TAG, "Delay %dms", entry.value);
|
||||
delay(entry.value);
|
||||
} else {
|
||||
const uint8_t num_args = x & 0x7F;
|
||||
if (length - index < num_args) {
|
||||
ESP_LOGE(TAG, "Malformed init sequence, cmd = %X, num_args = %u", cmd, num_args);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->cmd_data(cmd, sequence + index, num_args);
|
||||
index += num_args;
|
||||
this->cmd_data(entry.value, entry.args, entry.count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ static constexpr uint32_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to
|
||||
static constexpr size_t MAX_TRANSFER_SIZE = 128;
|
||||
// Longer busy waits are the waveform or a stuck pin, and are polled at the normal loop pace
|
||||
static constexpr uint32_t FAST_BUSY_POLL_MS = 50;
|
||||
static constexpr uint8_t DELAY_FLAG = 0xFF;
|
||||
|
||||
class EPaperBase : public Display,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||
|
||||
@@ -38,7 +38,6 @@ from esphome.components.mipi import (
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_COLOR_ORDER,
|
||||
CONF_DIMENSIONS,
|
||||
CONF_ENABLE_PIN,
|
||||
@@ -167,8 +166,7 @@ def _config_schema(config: ConfigType) -> ConfigType:
|
||||
height,
|
||||
has_hardware_rotation=False,
|
||||
byte_order=config[CONF_BYTE_ORDER],
|
||||
has_writer=requires_buffer(config)
|
||||
or config.get(CONF_AUTO_CLEAR_ENABLED) is True,
|
||||
has_writer=display.config_has_writer(config),
|
||||
rotation=config.get(CONF_ROTATION, 0),
|
||||
draw_rounding=config.get(CONF_DRAW_ROUNDING, 0),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifdef USE_ESP32_VARIANT_ESP32P4
|
||||
#include <utility>
|
||||
#include "mipi_dsi.h"
|
||||
#include "esphome/components/display/init_sequence.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -123,47 +124,38 @@ void MipiDsi::setup() {
|
||||
this->smark_failed(LOG_STR("esp_lcd_init failed"), err);
|
||||
return;
|
||||
}
|
||||
size_t index = 0;
|
||||
const uint8_t *seq = this->init_sequence_;
|
||||
const size_t len = this->init_sequence_len_;
|
||||
while (index != len) {
|
||||
if (len - index < 2) {
|
||||
display::InitSequenceReader reader(this->init_sequence_, this->init_sequence_len_);
|
||||
display::InitSequenceEntry entry;
|
||||
while (auto step = reader.next(entry)) {
|
||||
if (step == display::INIT_SEQUENCE_STEP_MALFORMED) {
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
uint8_t cmd = seq[index++];
|
||||
uint8_t x = seq[index++];
|
||||
if (x == DELAY_FLAG) {
|
||||
ESP_LOGD(TAG, "Delay %dms", cmd);
|
||||
delay(cmd);
|
||||
} else {
|
||||
uint8_t num_args = x & 0x7F;
|
||||
if (len - index < num_args) {
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
if (cmd == SLEEP_OUT) {
|
||||
// are we ready, boots?
|
||||
int duration = when - millis();
|
||||
if (duration > 0) {
|
||||
delay(duration);
|
||||
}
|
||||
}
|
||||
const auto *ptr = seq + index;
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MIPI_DSI_MAX_CMD_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGVV(TAG, "Command %02X, length %d, byte(s) %s", cmd, num_args,
|
||||
format_hex_pretty_to(hex_buf, ptr, num_args, '.'));
|
||||
err = esp_lcd_panel_io_tx_param(this->io_handle_, cmd, ptr, num_args);
|
||||
if (err != ESP_OK) {
|
||||
this->smark_failed(LOG_STR("lcd_panel_io_tx_param failed"), err);
|
||||
return;
|
||||
}
|
||||
index += num_args;
|
||||
if (cmd == SLEEP_OUT)
|
||||
delay(10);
|
||||
if (step == display::INIT_SEQUENCE_STEP_DELAY) {
|
||||
ESP_LOGD(TAG, "Delay %dms", entry.value);
|
||||
delay(entry.value);
|
||||
continue;
|
||||
}
|
||||
const uint8_t cmd = entry.value;
|
||||
if (cmd == SLEEP_OUT) {
|
||||
// are we ready, boots?
|
||||
int duration = when - millis();
|
||||
if (duration > 0) {
|
||||
delay(duration);
|
||||
}
|
||||
}
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MIPI_DSI_MAX_CMD_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGVV(TAG, "Command %02X, length %d, byte(s) %s", cmd, entry.count,
|
||||
format_hex_pretty_to(hex_buf, entry.args, entry.count, '.'));
|
||||
err = esp_lcd_panel_io_tx_param(this->io_handle_, cmd, entry.args, entry.count);
|
||||
if (err != ESP_OK) {
|
||||
this->smark_failed(LOG_STR("lcd_panel_io_tx_param failed"), err);
|
||||
return;
|
||||
}
|
||||
if (cmd == SLEEP_OUT)
|
||||
delay(10);
|
||||
}
|
||||
this->io_lock_ = xSemaphoreCreateBinary();
|
||||
esp_lcd_dpi_panel_event_callbacks_t cbs = {
|
||||
|
||||
@@ -31,7 +31,6 @@ const uint8_t INVERT_OFF = 0x20;
|
||||
const uint8_t INVERT_ON = 0x21;
|
||||
const uint8_t DISPLAY_ON = 0x29;
|
||||
const uint8_t CMD2_BKSEL = 0xFF;
|
||||
const uint8_t DELAY_FLAG = 0xFF;
|
||||
const uint8_t MADCTL_BGR = 0x08;
|
||||
const uint8_t MADCTL_MX = 0x40;
|
||||
const uint8_t MADCTL_MY = 0x80;
|
||||
|
||||
@@ -50,7 +50,6 @@ from esphome.components.spi import (
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_BLUE,
|
||||
CONF_COLOR_ORDER,
|
||||
CONF_CS_PIN,
|
||||
@@ -243,8 +242,7 @@ def _config_schema(config: ConfigType) -> ConfigType:
|
||||
height,
|
||||
has_hardware_rotation=True,
|
||||
byte_order=config[CONF_BYTE_ORDER],
|
||||
has_writer=requires_buffer(config)
|
||||
or config.get(CONF_AUTO_CLEAR_ENABLED) is True,
|
||||
has_writer=display.config_has_writer(config),
|
||||
rotation=config.get(CONF_ROTATION, 0),
|
||||
draw_rounding=config.get(CONF_DRAW_ROUNDING, 0),
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S31)
|
||||
#include "mipi_rgb.h"
|
||||
#include "esphome/components/display/init_sequence.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -13,8 +14,6 @@
|
||||
|
||||
namespace esphome::mipi_rgb {
|
||||
|
||||
static const uint8_t DELAY_FLAG = 0xFF;
|
||||
|
||||
// Maximum bytes to log for init commands (truncated if larger)
|
||||
static constexpr size_t MIPI_RGB_MAX_CMD_LOG_BYTES = 64;
|
||||
static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
|
||||
@@ -75,44 +74,30 @@ void MipiRgbSpi::write_data_(uint8_t value) {
|
||||
this->disable();
|
||||
}
|
||||
|
||||
/**
|
||||
* this relies upon the init sequence being well-formed, which is guaranteed by the Python init code.
|
||||
*/
|
||||
|
||||
void MipiRgbSpi::write_init_sequence_() {
|
||||
size_t index = 0;
|
||||
const uint8_t *seq = this->init_sequence_;
|
||||
const size_t len = this->init_sequence_len_;
|
||||
while (index != len) {
|
||||
if (len - index < 2) {
|
||||
display::InitSequenceReader reader(this->init_sequence_, this->init_sequence_len_);
|
||||
display::InitSequenceEntry entry;
|
||||
while (auto step = reader.next(entry)) {
|
||||
if (step == display::INIT_SEQUENCE_STEP_MALFORMED) {
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
uint8_t cmd = seq[index++];
|
||||
uint8_t x = seq[index++];
|
||||
if (x == DELAY_FLAG) {
|
||||
ESP_LOGD(TAG, "Delay %dms", cmd);
|
||||
delay(cmd);
|
||||
} else {
|
||||
uint8_t num_args = x & 0x7F;
|
||||
if (len - index < num_args) {
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
if (cmd == SLEEP_OUT) {
|
||||
delay(120); // NOLINT
|
||||
}
|
||||
const auto *ptr = seq + index;
|
||||
char hex_buf[format_hex_pretty_size(MIPI_RGB_MAX_CMD_LOG_BYTES)];
|
||||
ESP_LOGD(TAG, "Write command %02X, length %d, byte(s) %s", cmd, num_args,
|
||||
format_hex_pretty_to(hex_buf, ptr, num_args, '.'));
|
||||
index += num_args;
|
||||
this->write_command_(cmd);
|
||||
while (num_args-- != 0)
|
||||
this->write_data_(*ptr++);
|
||||
if (cmd == SLEEP_OUT)
|
||||
delay(10);
|
||||
if (step == display::INIT_SEQUENCE_STEP_DELAY) {
|
||||
ESP_LOGD(TAG, "Delay %dms", entry.value);
|
||||
delay(entry.value);
|
||||
continue;
|
||||
}
|
||||
const uint8_t cmd = entry.value;
|
||||
if (cmd == SLEEP_OUT)
|
||||
delay(120); // NOLINT
|
||||
char hex_buf[format_hex_pretty_size(MIPI_RGB_MAX_CMD_LOG_BYTES)];
|
||||
ESP_LOGD(TAG, "Write command %02X, length %d, byte(s) %s", cmd, entry.count,
|
||||
format_hex_pretty_to(hex_buf, entry.args, entry.count, '.'));
|
||||
this->write_command_(cmd);
|
||||
for (uint8_t i = 0; i < entry.count; i++)
|
||||
this->write_data_(entry.args[i]);
|
||||
if (cmd == SLEEP_OUT)
|
||||
delay(10);
|
||||
}
|
||||
// this->spi_teardown(); // SPI not needed after this
|
||||
delay(10);
|
||||
|
||||
@@ -33,7 +33,6 @@ from esphome.components.spi import CONF_SPI_MODE, TYPE_OCTAL, TYPE_QUAD, TYPE_SI
|
||||
import esphome.config_validation as cv
|
||||
from esphome.config_validation import ALLOW_EXTRA
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_BRIGHTNESS,
|
||||
CONF_BUFFER_SIZE,
|
||||
CONF_COLOR_ORDER,
|
||||
@@ -73,6 +72,10 @@ Model = mipi_spi_ns.enum("Model")
|
||||
PixelMode = mipi_spi_ns.enum("PixelMode")
|
||||
BusType = mipi_spi_ns.enum("BusType")
|
||||
|
||||
# Matches INIT_SEQUENCE_MAX_ARGS, the flash copy buffer in mipi_spi.h
|
||||
INIT_SEQUENCE_MAX_ARGS = 128
|
||||
|
||||
|
||||
COLOR_ORDERS = {
|
||||
MODE_RGB: ColorOrder.COLOR_ORDER_RGB,
|
||||
MODE_BGR: ColorOrder.COLOR_ORDER_BGR,
|
||||
@@ -216,7 +219,15 @@ def model_schema(config: ConfigType) -> cv.All | cv.Schema:
|
||||
bus_mode, lower=True
|
||||
),
|
||||
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
|
||||
iseqconf: cv.ensure_list(map_sequence),
|
||||
iseqconf: cv.ensure_list(
|
||||
cv.All(
|
||||
map_sequence,
|
||||
cv.Length(
|
||||
max=INIT_SEQUENCE_MAX_ARGS + 1,
|
||||
msg=f"An init sequence command can have at most {INIT_SEQUENCE_MAX_ARGS} data bytes",
|
||||
),
|
||||
)
|
||||
),
|
||||
cv.Optional(CONF_BUFFER_SIZE): cv.All(
|
||||
cv.percentage, cv.Range(0.12, 1.0)
|
||||
),
|
||||
@@ -290,8 +301,7 @@ def customise_schema(config: ConfigType) -> ConfigType:
|
||||
height,
|
||||
has_hardware_transform,
|
||||
byte_order=config[CONF_BYTE_ORDER],
|
||||
has_writer=requires_buffer(config)
|
||||
or config.get(CONF_AUTO_CLEAR_ENABLED) is True,
|
||||
has_writer=display.config_has_writer(config),
|
||||
rotation=config.get(CONF_ROTATION, 0),
|
||||
draw_rounding=config.get(CONF_DRAW_ROUNDING, 0),
|
||||
)
|
||||
|
||||
@@ -7,12 +7,16 @@
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
#include "esphome/components/display/init_sequence.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::mipi_spi {
|
||||
|
||||
/// Largest argument count in one init command; display.py rejects longer ones.
|
||||
static constexpr size_t INIT_SEQUENCE_MAX_ARGS = 128;
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "display.mipi_spi");
|
||||
|
||||
// Maximum bytes to log for commands (truncated if larger)
|
||||
@@ -48,7 +52,6 @@ static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horiz
|
||||
static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
|
||||
static constexpr uint16_t MADCTL_FLIP_FLAG = 0x100; // controller uses axis flip bits
|
||||
|
||||
static constexpr uint8_t DELAY_FLAG = 0xFF;
|
||||
// store a 16 bit value in a buffer, big endian.
|
||||
static inline void put16_be(uint8_t *buf, uint16_t value) {
|
||||
buf[0] = value >> 8;
|
||||
@@ -163,36 +166,23 @@ class MipiSpi : public display::Display,
|
||||
|
||||
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
|
||||
auto when = millis() + 120;
|
||||
size_t index = 0;
|
||||
const uint8_t *seq = this->init_sequence_;
|
||||
const size_t len = this->init_sequence_len_;
|
||||
while (index != len) {
|
||||
if (len - index < 2) {
|
||||
esph_log_e(TAG, "Malformed init sequence");
|
||||
this->mark_failed();
|
||||
display::InitSequenceReader reader(this->init_sequence_, this->init_sequence_len_);
|
||||
display::InitSequenceEntry entry;
|
||||
// The sequence is in flash, which SPI DMA (and ESP8266 byte loads) cannot read
|
||||
uint8_t args[INIT_SEQUENCE_MAX_ARGS];
|
||||
while (auto step = reader.next(entry, sizeof(args))) {
|
||||
if (step == display::INIT_SEQUENCE_STEP_MALFORMED) {
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
uint8_t cmd = progmem_read_byte(seq + index++);
|
||||
uint8_t x = progmem_read_byte(seq + index++);
|
||||
if (x == DELAY_FLAG) {
|
||||
if (cmd == 0) {
|
||||
cmd = clamp_at_least((int) (when - millis()), 0);
|
||||
}
|
||||
esph_log_d(TAG, "Delay %dms", cmd);
|
||||
delay(cmd);
|
||||
} else {
|
||||
uint8_t num_args = x & 0x7F;
|
||||
if (len - index < num_args) {
|
||||
esph_log_e(TAG, "Malformed init sequence");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// The sequence is in flash, which SPI DMA (and ESP8266 byte loads) cannot read
|
||||
uint8_t args[0x80];
|
||||
progmem_memcpy(args, seq + index, num_args);
|
||||
this->write_command_(cmd, args, num_args);
|
||||
index += num_args;
|
||||
if (step == display::INIT_SEQUENCE_STEP_DELAY) {
|
||||
const uint8_t ms = entry.value != 0 ? entry.value : clamp_at_least((int) (when - millis()), 0);
|
||||
esph_log_d(TAG, "Delay %dms", ms);
|
||||
delay(ms);
|
||||
continue;
|
||||
}
|
||||
progmem_memcpy(args, entry.args, entry.count);
|
||||
this->write_command_(entry.value, args, entry.count);
|
||||
}
|
||||
this->reset_params_();
|
||||
// Marks init as done, so later commands log at verbose level instead of debug
|
||||
|
||||
@@ -1,27 +1,33 @@
|
||||
#include "sm300d2.h"
|
||||
#include "esphome/core/frame_reader.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::sm300d2 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "sm300d2");
|
||||
static constexpr uint8_t SM300D2_RESPONSE_LENGTH = 17;
|
||||
static const uint8_t SM300D2_RESPONSE_LENGTH = 17;
|
||||
|
||||
void SM300D2Sensor::update() {
|
||||
// Takes the first frame already received; only one cut off at the end of the buffer is waited for
|
||||
FrameReader<SM300D2_RESPONSE_LENGTH, 0x3C, 0x02> reader;
|
||||
const uint8_t *response = nullptr;
|
||||
this->read_available<1>([&](uint8_t c) { return (response = reader.feed(c)) == nullptr; });
|
||||
for (uint8_t c; response == nullptr && reader.length() > 0 && this->read_byte(&c);)
|
||||
response = reader.feed(c);
|
||||
uint8_t response[SM300D2_RESPONSE_LENGTH];
|
||||
uint8_t peeked;
|
||||
|
||||
if (response == nullptr) {
|
||||
while (this->available() > 0 && this->peek_byte(&peeked) && peeked != 0x3C)
|
||||
this->read();
|
||||
|
||||
bool read_success = read_array(response, SM300D2_RESPONSE_LENGTH);
|
||||
|
||||
if (!read_success) {
|
||||
ESP_LOGW(TAG, "Reading data from SM300D2 failed!");
|
||||
status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
if (response[0] != 0x3C || response[1] != 0x02) {
|
||||
ESP_LOGW(TAG, "Invalid preamble for SM300D2 response!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t calculated_checksum = sum_bytes(response, SM300D2_RESPONSE_LENGTH - 1);
|
||||
uint16_t calculated_checksum = this->sm300d2_checksum_(response);
|
||||
// Occasionally the checksum has a +/- 0x80 offset. Negative temperatures are
|
||||
// responsible for some of these. The rest are unknown/undocumented.
|
||||
if ((calculated_checksum != response[SM300D2_RESPONSE_LENGTH - 1]) &&
|
||||
@@ -77,6 +83,8 @@ void SM300D2Sensor::update() {
|
||||
this->humidity_sensor_->publish_state(humidity);
|
||||
}
|
||||
|
||||
uint16_t SM300D2Sensor::sm300d2_checksum_(uint8_t *ptr) { return sum_bytes(ptr, SM300D2_RESPONSE_LENGTH - 1); }
|
||||
|
||||
void SM300D2Sensor::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "SM300D2:");
|
||||
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
|
||||
|
||||
@@ -20,6 +20,8 @@ class SM300D2Sensor final : public PollingComponent, public uart::UARTDevice {
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
uint16_t sm300d2_checksum_(uint8_t *ptr);
|
||||
|
||||
sensor::Sensor *co2_sensor_{nullptr};
|
||||
sensor::Sensor *formaldehyde_sensor_{nullptr};
|
||||
sensor::Sensor *tvoc_sensor_{nullptr};
|
||||
|
||||
@@ -4,7 +4,6 @@ import pkgutil
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display, spi
|
||||
from esphome.components.display import CONF_SHOW_TEST_CARD
|
||||
from esphome.components.mipi import (
|
||||
DriverChip,
|
||||
flatten_sequence,
|
||||
@@ -13,16 +12,13 @@ from esphome.components.mipi import (
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_CS_PIN,
|
||||
CONF_DATA_RATE,
|
||||
CONF_DC_PIN,
|
||||
CONF_HEIGHT,
|
||||
CONF_ID,
|
||||
CONF_INIT_SEQUENCE,
|
||||
CONF_LAMBDA,
|
||||
CONF_MODEL,
|
||||
CONF_PAGES,
|
||||
CONF_RESET_PIN,
|
||||
CONF_ROTATION,
|
||||
CONF_WIDTH,
|
||||
@@ -182,10 +178,7 @@ def customise_schema(config: ConfigType) -> ConfigType:
|
||||
config[CONF_HEIGHT],
|
||||
has_hardware_rotation=False,
|
||||
byte_order=cv.UNDEFINED,
|
||||
has_writer=config.get(CONF_AUTO_CLEAR_ENABLED) is True
|
||||
or config.get(CONF_PAGES) is not None
|
||||
or config.get(CONF_LAMBDA) is not None
|
||||
or config.get(CONF_SHOW_TEST_CARD) is True,
|
||||
has_writer=display.config_has_writer(config),
|
||||
rotation=config.get(CONF_ROTATION, 0),
|
||||
)
|
||||
return config
|
||||
|
||||
@@ -56,33 +56,26 @@ void ST7305::reset_() {
|
||||
}
|
||||
|
||||
void ST7305::send_init_sequence_() {
|
||||
while (this->init_index_ < this->init_sequence_length_) {
|
||||
if (this->init_sequence_length_ - this->init_index_ < 2) {
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
const uint8_t command = progmem_read_byte(&this->init_sequence_[this->init_index_++]);
|
||||
const uint8_t count = progmem_read_byte(&this->init_sequence_[this->init_index_++]);
|
||||
if (count == 0xFF) {
|
||||
// Resume the sequence after the delay instead of blocking the main loop.
|
||||
this->set_timeout(STARTUP_TIMEOUT_ID, command, [this]() { this->send_init_sequence_(); });
|
||||
return;
|
||||
}
|
||||
if (this->init_sequence_length_ - this->init_index_ < count) {
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
display::InitSequenceEntry entry;
|
||||
display::InitSequenceStep step;
|
||||
while ((step = this->init_reader_.next(entry)) == display::INIT_SEQUENCE_STEP_COMMAND) {
|
||||
this->enable();
|
||||
this->send_command_(command);
|
||||
if (count != 0) {
|
||||
this->send_command_(entry.value);
|
||||
if (entry.count != 0) {
|
||||
this->dc_pin_->digital_write(true);
|
||||
for (uint8_t i = 0; i < count; i++)
|
||||
this->write_byte(progmem_read_byte(&this->init_sequence_[this->init_index_ + i]));
|
||||
for (uint8_t i = 0; i < entry.count; i++)
|
||||
this->write_byte(progmem_read_byte(entry.args + i));
|
||||
}
|
||||
this->disable();
|
||||
this->init_index_ += count;
|
||||
}
|
||||
this->finish_init_();
|
||||
if (step == display::INIT_SEQUENCE_STEP_DELAY) {
|
||||
// Resume the sequence after the delay instead of blocking the main loop.
|
||||
this->set_timeout(STARTUP_TIMEOUT_ID, entry.value, [this]() { this->send_init_sequence_(); });
|
||||
} else if (step == display::INIT_SEQUENCE_STEP_MALFORMED) {
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
} else {
|
||||
this->finish_init_();
|
||||
}
|
||||
}
|
||||
|
||||
void ST7305::finish_init_() {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/display/init_sequence.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
@@ -35,8 +36,7 @@ class ST7305 : public display::DisplayBuffer,
|
||||
row_start_(row_start),
|
||||
row_end_(row_end),
|
||||
buffer_length_(static_cast<size_t>(width) * static_cast<size_t>(height) / 8),
|
||||
init_sequence_(init_sequence),
|
||||
init_sequence_length_(init_sequence_length) {}
|
||||
init_reader_(init_sequence, init_sequence_length) {}
|
||||
|
||||
void setup() override;
|
||||
void update() override;
|
||||
@@ -113,9 +113,7 @@ class ST7305 : public display::DisplayBuffer,
|
||||
uint8_t row_start_;
|
||||
uint8_t row_end_;
|
||||
size_t buffer_length_;
|
||||
const uint8_t *init_sequence_;
|
||||
size_t init_sequence_length_;
|
||||
size_t init_index_{0};
|
||||
display::InitSequenceReader init_reader_;
|
||||
bool ready_{false};
|
||||
|
||||
int x_low_{0};
|
||||
|
||||
@@ -44,6 +44,8 @@ static constexpr std::array<uint8_t, 7> GET_SOFTWARE_VERSION = {0xFE, 0xFE, 0x11
|
||||
static constexpr size_t FRAME_DEVICE_ID_INDEX = 2;
|
||||
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 PASSIVE_START_BYTE_2_WITH_ID = 0x96;
|
||||
static constexpr uint8_t FRAME_STOP_BYTE = 0x16;
|
||||
@@ -118,8 +120,9 @@ static bool check_byte(const uint8_t data[FRAME_SIZE], size_t index, uint8_t exp
|
||||
|
||||
static bool validate_active_frame(const uint8_t data[FRAME_SIZE]) {
|
||||
const uint8_t sum = sum_bytes(data, FRAME_CHECKSUM_INDEX);
|
||||
// FrameReader already matched the start bytes
|
||||
return check_byte(data, FRAME_INDEX_INSTANT_FLOW_FLAG, FRAME_FLAG_INSTANT_FLOW, "instant flow flag") &&
|
||||
return check_byte(data, 0, FRAME_START_BYTE_1, "start byte 1") &&
|
||||
check_byte(data, 1, FRAME_START_BYTE_2, "start byte 2") &&
|
||||
check_byte(data, FRAME_INDEX_INSTANT_FLOW_FLAG, FRAME_FLAG_INSTANT_FLOW, "instant flow flag") &&
|
||||
check_byte(data, FRAME_INDEX_RESERVED_SECTION, FRAME_FLAG_RESERVED_SECTION, "reserved section flag") &&
|
||||
check_byte(data, FRAME_INDEX_TEMP_FLAG, FRAME_FLAG_TEMP, "temperature flag") &&
|
||||
check_byte(data, FRAME_CHECKSUM_INDEX, sum, "checksum") &&
|
||||
@@ -212,7 +215,7 @@ static bool read_flow_rate_out_of_range(const uint8_t data[FRAME_SIZE]) {
|
||||
|
||||
void UFM01Component::flush_rx_() {
|
||||
this->discard_available();
|
||||
this->active_reader_.reset();
|
||||
this->read_index_ = 0;
|
||||
}
|
||||
|
||||
void UFM01Component::send_command_no_wait_(const std::array<uint8_t, 7> &command) {
|
||||
@@ -420,7 +423,7 @@ bool UFM01Component::service_software_version_(uint32_t now) {
|
||||
}
|
||||
#endif // USE_UFM01_SOFTWARE_VERSION
|
||||
|
||||
void UFM01Component::on_active_frame_(const uint8_t data[FRAME_SIZE], uint32_t now) {
|
||||
void UFM01Component::on_active_frame_(uint8_t data[FRAME_SIZE], uint32_t now) {
|
||||
#ifdef USE_UFM01_METER_ID
|
||||
this->publish_meter_id_from_frame_(data);
|
||||
#endif
|
||||
@@ -458,26 +461,40 @@ void UFM01Component::on_active_frame_(const uint8_t data[FRAME_SIZE], uint32_t n
|
||||
bool UFM01Component::process_active_stream_(uint32_t now) {
|
||||
bool got_valid_frame = false;
|
||||
|
||||
const bool read_ok = this->read_available([this, now, &got_valid_frame](uint8_t byte) {
|
||||
const uint8_t *frame = this->active_reader_.feed(byte);
|
||||
if (frame == nullptr) {
|
||||
if (this->active_reader_.length() == 0) {
|
||||
ESP_LOGD(TAG, "not start of data (is 0x%02X)", byte);
|
||||
}
|
||||
return;
|
||||
while (this->available()) {
|
||||
if (!this->read_byte(&this->data_[this->read_index_])) {
|
||||
ESP_LOGW(TAG, "unable to read byte");
|
||||
this->read_index_ = 0;
|
||||
continue;
|
||||
}
|
||||
if (validate_active_frame(frame)) {
|
||||
this->on_active_frame_(frame, now);
|
||||
if ((this->read_index_ == 0 && this->data_[0] != FRAME_START_BYTE_1) ||
|
||||
(this->read_index_ == 1 && this->data_[1] != FRAME_START_BYTE_2)) {
|
||||
ESP_LOGD(TAG, "not start of data at %" PRId32 " (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;
|
||||
|
||||
if (validate_active_frame(this->data_)) {
|
||||
this->on_active_frame_(this->data_, now);
|
||||
this->read_index_ = 0;
|
||||
got_valid_frame = true;
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
log_hex(frame, FRAME_SIZE);
|
||||
|
||||
log_hex(this->data_, sizeof(this->data_));
|
||||
ESP_LOGW(TAG, "unable to read data");
|
||||
this->active_reader_.resync();
|
||||
});
|
||||
if (!read_ok) {
|
||||
ESP_LOGW(TAG, "unable to read byte");
|
||||
this->active_reader_.reset();
|
||||
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)) {
|
||||
for (int32_t j = i; j < static_cast<int32_t>(FRAME_SIZE); ++j)
|
||||
this->data_[j - i] = this->data_[j];
|
||||
this->read_index_ = static_cast<int32_t>(FRAME_SIZE) - i;
|
||||
}
|
||||
}
|
||||
if (this->read_index_ == static_cast<int32_t>(FRAME_SIZE))
|
||||
this->read_index_ = 0;
|
||||
}
|
||||
|
||||
return got_valid_frame;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/frame_reader.h"
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#endif
|
||||
@@ -26,8 +25,6 @@ class TestableUFM01;
|
||||
} // namespace testing
|
||||
|
||||
static constexpr size_t FRAME_SIZE = 32;
|
||||
static constexpr uint8_t FRAME_START_BYTE_1 = 0x3C;
|
||||
static constexpr uint8_t FRAME_START_BYTE_2 = 0x32;
|
||||
static constexpr size_t PASSIVE_FRAME_SIZE = 23;
|
||||
static constexpr size_t PASSIVE_FRAME_WITH_ID_SIZE = 39;
|
||||
#ifdef USE_UFM01_METER_ID
|
||||
@@ -111,7 +108,7 @@ class UFM01Component : public uart::UARTDevice, public Component {
|
||||
bool consume_ack_();
|
||||
void flush_rx_();
|
||||
bool process_active_stream_(uint32_t now);
|
||||
void on_active_frame_(const uint8_t data[FRAME_SIZE], uint32_t now);
|
||||
void on_active_frame_(uint8_t data[FRAME_SIZE], uint32_t now);
|
||||
#ifdef USE_UFM01_METER_ID
|
||||
void publish_meter_id_from_frame_(const uint8_t data[FRAME_SIZE]);
|
||||
#endif
|
||||
@@ -148,6 +145,7 @@ class UFM01Component : public uart::UARTDevice, public Component {
|
||||
#ifdef USE_UFM01_CLEAR_ACCUMULATED_FLOW_ACTION
|
||||
uint32_t pending_clear_start_ms_{0};
|
||||
#endif
|
||||
int32_t read_index_{0};
|
||||
|
||||
OperatingMode operating_mode_{OperatingMode::STARTUP};
|
||||
StartupPhase startup_phase_{StartupPhase::WAIT};
|
||||
@@ -172,7 +170,7 @@ class UFM01Component : public uart::UARTDevice, public Component {
|
||||
#endif
|
||||
|
||||
uint8_t passive_frame_[PASSIVE_FRAME_MAX_SIZE];
|
||||
FrameReader<FRAME_SIZE, FRAME_START_BYTE_1, FRAME_START_BYTE_2> active_reader_;
|
||||
uint8_t data_[FRAME_SIZE];
|
||||
|
||||
friend class testing::TestableUFM01;
|
||||
};
|
||||
|
||||
@@ -4,38 +4,70 @@
|
||||
|
||||
namespace esphome::wts01 {
|
||||
|
||||
constexpr uint8_t HEADER_1 = 0x55;
|
||||
constexpr uint8_t HEADER_2 = 0x01;
|
||||
constexpr uint8_t HEADER_3 = 0x01;
|
||||
constexpr uint8_t HEADER_4 = 0x04;
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "wts01");
|
||||
|
||||
void WTS01Sensor::loop() {
|
||||
this->read_available([this](uint8_t c) {
|
||||
if (const uint8_t *packet = this->reader_.feed(c)) {
|
||||
this->process_packet_(packet);
|
||||
}
|
||||
});
|
||||
this->read_available([this](uint8_t c) { this->handle_char_(c); });
|
||||
}
|
||||
|
||||
void WTS01Sensor::dump_config() { LOG_SENSOR("", "WTS01 Sensor", this); }
|
||||
|
||||
void WTS01Sensor::process_packet_(const uint8_t *packet) {
|
||||
void WTS01Sensor::handle_char_(uint8_t c) {
|
||||
// State machine for processing the header. Reset if something doesn't match.
|
||||
if (this->buffer_pos_ == 0 && c != HEADER_1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->buffer_pos_ == 1 && c != HEADER_2) {
|
||||
this->buffer_pos_ = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->buffer_pos_ == 2 && c != HEADER_3) {
|
||||
this->buffer_pos_ = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->buffer_pos_ == 3 && c != HEADER_4) {
|
||||
this->buffer_pos_ = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Add byte to buffer
|
||||
this->buffer_[this->buffer_pos_++] = c;
|
||||
|
||||
// Process complete packet
|
||||
if (this->buffer_pos_ >= PACKET_SIZE) {
|
||||
this->process_packet_();
|
||||
this->buffer_pos_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void WTS01Sensor::process_packet_() {
|
||||
// Based on Tasmota implementation
|
||||
// Format: 55 01 01 04 01 11 16 12 95
|
||||
// header T Td Ck - T = Temperature, Td = Temperature decimal, Ck = Checksum
|
||||
const uint8_t calculated_checksum = sum_bytes(packet, PACKET_SIZE - 1);
|
||||
const uint8_t calculated_checksum = sum_bytes(this->buffer_, PACKET_SIZE - 1);
|
||||
|
||||
uint8_t received_checksum = packet[PACKET_SIZE - 1];
|
||||
uint8_t received_checksum = this->buffer_[PACKET_SIZE - 1];
|
||||
if (calculated_checksum != received_checksum) {
|
||||
ESP_LOGW(TAG, "WTS01 Checksum doesn't match: 0x%02X != 0x%02X", received_checksum, calculated_checksum);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract temperature value
|
||||
const uint8_t raw = packet[6];
|
||||
const uint8_t raw = this->buffer_[6];
|
||||
|
||||
// WTS01 encodes sign in bit 7, magnitude in bits 0-6
|
||||
const bool negative = (raw & 0x80) != 0;
|
||||
const uint8_t magnitude = raw & 0x7F;
|
||||
|
||||
const float decimal = static_cast<float>(packet[7]) / 100.0f;
|
||||
const float decimal = static_cast<float>(this->buffer_[7]) / 100.0f;
|
||||
|
||||
float temperature = static_cast<float>(magnitude) + decimal;
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/frame_reader.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
@@ -15,9 +14,11 @@ class WTS01Sensor final : public sensor::Sensor, public uart::UARTDevice, public
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void process_packet_(const uint8_t *packet);
|
||||
uint8_t buffer_[PACKET_SIZE];
|
||||
uint8_t buffer_pos_{0};
|
||||
|
||||
FrameReader<PACKET_SIZE, 0x55, 0x01, 0x01, 0x04> reader_;
|
||||
void handle_char_(uint8_t c);
|
||||
void process_packet_();
|
||||
};
|
||||
|
||||
} // namespace esphome::wts01
|
||||
@@ -1,67 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <type_traits>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
/// Collects fixed length binary frames of N bytes that start with the header bytes HEADER...; the binary twin of
|
||||
/// LineReader. Bytes before a header are skipped; checksums are left to the caller.
|
||||
template<size_t N, uint8_t... HEADER> class FrameReader {
|
||||
using pos_t = std::conditional_t<(N <= 0xFF), uint8_t, uint16_t>;
|
||||
static constexpr size_t HEADER_LEN = sizeof...(HEADER);
|
||||
static_assert(HEADER_LEN >= 1 && HEADER_LEN <= 8, "FrameReader takes 1 to 8 header bytes"); // bounds replay_() depth
|
||||
static_assert(HEADER_LEN < N && N <= 0xFFFF, "FrameReader size out of range");
|
||||
|
||||
public:
|
||||
/// Feed one byte; returns the completed frame, header included, valid until the next call, or nullptr.
|
||||
const uint8_t *feed(uint8_t c) {
|
||||
pos_t pos = this->pos_;
|
||||
if (pos < HEADER_LEN && c != header_byte(pos)) {
|
||||
// A later byte of the broken header, or c, may start the real one
|
||||
this->buf_[pos] = c;
|
||||
this->replay_(pos);
|
||||
return nullptr;
|
||||
}
|
||||
this->buf_[pos++] = c;
|
||||
if (pos < N) {
|
||||
this->pos_ = pos;
|
||||
return nullptr;
|
||||
}
|
||||
this->pos_ = 0;
|
||||
return this->buf_;
|
||||
}
|
||||
|
||||
/// Number of bytes of the partial frame; 0 after a frame is returned and while looking for a header.
|
||||
size_t length() const { return this->pos_; }
|
||||
|
||||
/// Forget the partial frame and look for a header from the next byte.
|
||||
void reset() { this->pos_ = 0; }
|
||||
|
||||
/// For a frame the caller rejected (bad checksum): drop its first byte and realign on a header in the rest.
|
||||
/// Call only right after feed() returned that frame; use reset() otherwise.
|
||||
void resync() { this->replay_(N - 1); }
|
||||
|
||||
protected:
|
||||
// A fold, not an array, so no header table lands in RAM on ESP8266
|
||||
static constexpr uint8_t header_byte(size_t i) {
|
||||
size_t j = 0;
|
||||
uint8_t r = 0;
|
||||
((r = (j++ == i) ? HEADER : r), ...);
|
||||
return r;
|
||||
}
|
||||
|
||||
// Start over and feed buf_[1..last] again; never completes a frame as last < N
|
||||
void replay_(size_t last) {
|
||||
this->pos_ = 0;
|
||||
for (size_t i = 1; i <= last; i++) {
|
||||
this->feed(this->buf_[i]);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t buf_[N];
|
||||
pos_t pos_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome
|
||||
@@ -93,6 +93,16 @@ def run_schema_validation(config: ConfigType) -> None:
|
||||
r"value must be a power of two for dictionary value @ data\['draw_rounding'\]",
|
||||
id="invalid_draw_rounding",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"model": "custom",
|
||||
"dc_pin": 18,
|
||||
"dimensions": {"width": 320, "height": 240},
|
||||
"init_sequence": [[0xE0] + [0x00] * 129],
|
||||
},
|
||||
r"at most 128 data bytes",
|
||||
id="init_command_too_long",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_basic_configuration_errors(
|
||||
@@ -606,3 +616,19 @@ def test_identical_init_sequences_share_one_table(
|
||||
assert first is not None and second is not None
|
||||
assert first.group(1) == second.group(1)
|
||||
assert main_cpp.count("mipi_spi_init_sequence[] PROGMEM") == 1
|
||||
|
||||
|
||||
def test_init_command_at_buffer_limit(set_core_config: SetCoreConfigCallable) -> None:
|
||||
"""A command with exactly 128 data bytes fits the C++ copy buffer."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
run_schema_validation(
|
||||
{
|
||||
"model": "custom",
|
||||
"dc_pin": 18,
|
||||
"dimensions": {"width": 320, "height": 240},
|
||||
"init_sequence": [[0xE0] + [0x00] * 128],
|
||||
}
|
||||
)
|
||||
@@ -1,143 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <vector>
|
||||
#include "esphome/core/frame_reader.h"
|
||||
|
||||
namespace esphome::core::testing {
|
||||
|
||||
using Bytes = std::vector<uint8_t>;
|
||||
using Frames = std::vector<Bytes>;
|
||||
|
||||
// Feeds bytes and collects every completed frame.
|
||||
template<size_t N, uint8_t... H> static Frames feed_all(FrameReader<N, H...> &reader, const Bytes &input) {
|
||||
Frames frames;
|
||||
for (uint8_t c : input) {
|
||||
const uint8_t *frame = reader.feed(c);
|
||||
if (frame != nullptr) {
|
||||
EXPECT_EQ(reader.length(), 0u);
|
||||
frames.emplace_back(frame, frame + N);
|
||||
}
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
static_assert(sizeof(FrameReader<4, 0xFF>) == 5);
|
||||
static_assert(sizeof(FrameReader<9, 0x55, 0x01, 0x01, 0x04>) == 10);
|
||||
static_assert(sizeof(FrameReader<0xFF, 0xAA>) == 0x100);
|
||||
static_assert(sizeof(FrameReader<0x100, 0xAA>) == 0x102);
|
||||
|
||||
TEST(FrameReaderTest, SingleByteHeader) {
|
||||
FrameReader<4, 0xFF> reader;
|
||||
EXPECT_EQ(feed_all(reader, {0xFF, 0x01, 0x02, 0x02, 0xFF, 0x03, 0x04, 0x06}),
|
||||
(Frames{{0xFF, 0x01, 0x02, 0x02}, {0xFF, 0x03, 0x04, 0x06}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, SkipsBytesBeforeHeader) {
|
||||
FrameReader<3, 0xFF> reader;
|
||||
EXPECT_EQ(feed_all(reader, {0x01, 0x02, 0xFF, 0x10, 0x20}), (Frames{{0xFF, 0x10, 0x20}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, HeaderByteInsidePayloadIsData) {
|
||||
FrameReader<4, 0xFF> reader;
|
||||
EXPECT_EQ(feed_all(reader, {0xFF, 0xFF, 0xFF, 0x00}), (Frames{{0xFF, 0xFF, 0xFF, 0x00}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, LengthTracksPartialFrame) {
|
||||
FrameReader<4, 0xAA, 0xBB> reader;
|
||||
EXPECT_EQ(reader.length(), 0u);
|
||||
reader.feed(0x00);
|
||||
EXPECT_EQ(reader.length(), 0u);
|
||||
reader.feed(0xAA);
|
||||
EXPECT_EQ(reader.length(), 1u);
|
||||
reader.feed(0xBB);
|
||||
reader.feed(0x01);
|
||||
EXPECT_EQ(reader.length(), 3u);
|
||||
reader.reset();
|
||||
EXPECT_EQ(reader.length(), 0u);
|
||||
EXPECT_EQ(feed_all(reader, {0x02, 0xAA, 0xBB, 0x03, 0x04}), (Frames{{0xAA, 0xBB, 0x03, 0x04}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, MultiByteHeaderMismatchRestarts) {
|
||||
FrameReader<6, 0x55, 0x01, 0x01, 0x04> reader;
|
||||
EXPECT_EQ(feed_all(reader, {0x55, 0x01, 0x02, 0x55, 0x01, 0x01, 0x04, 0x11, 0x22}),
|
||||
(Frames{{0x55, 0x01, 0x01, 0x04, 0x11, 0x22}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, MismatchedByteCanStartHeader) {
|
||||
FrameReader<6, 0x55, 0x01, 0x01, 0x04> reader;
|
||||
// The second 0x55 breaks the first header and starts the real one
|
||||
EXPECT_EQ(feed_all(reader, {0x55, 0x01, 0x55, 0x01, 0x01, 0x04, 0x11, 0x22}),
|
||||
(Frames{{0x55, 0x01, 0x01, 0x04, 0x11, 0x22}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, RepeatedHeaderPrefixIsKept) {
|
||||
// AA AA AA BB: the mismatch at the third byte still leaves AA AA matched
|
||||
FrameReader<4, 0xAA, 0xAA, 0xBB> reader;
|
||||
EXPECT_EQ(feed_all(reader, {0xAA, 0xAA, 0xAA, 0xBB, 0x01}), (Frames{{0xAA, 0xAA, 0xBB, 0x01}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, OverlappingHeaderPrefix) {
|
||||
// A1 B2 A1 B2: the mismatch on the last B2 keeps the trailing A1 B2 as the start of the header
|
||||
FrameReader<5, 0xA1, 0xB2, 0xA1, 0xC3> reader;
|
||||
EXPECT_EQ(feed_all(reader, {0xA1, 0xB2, 0xA1, 0xB2, 0xA1, 0xC3, 0x07}), (Frames{{0xA1, 0xB2, 0xA1, 0xC3, 0x07}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, ResyncFindsHeaderInRejectedFrame) {
|
||||
FrameReader<4, 0xFF> reader;
|
||||
// Started mid-frame: the 0xFF taken as a header was payload; the real frame starts at the second 0xFF
|
||||
const uint8_t *frame = nullptr;
|
||||
for (uint8_t c : {0xFF, 0x12, 0xFF, 0x01}) {
|
||||
frame = reader.feed(c);
|
||||
}
|
||||
ASSERT_NE(frame, nullptr);
|
||||
reader.resync();
|
||||
EXPECT_EQ(reader.length(), 2u);
|
||||
EXPECT_EQ(feed_all(reader, {0x02, 0x02}), (Frames{{0xFF, 0x01, 0x02, 0x02}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, ResyncWithoutHeaderStartsOver) {
|
||||
FrameReader<4, 0xFF> reader;
|
||||
const uint8_t *frame = nullptr;
|
||||
for (uint8_t c : {0xFF, 0x01, 0x02, 0x03}) {
|
||||
frame = reader.feed(c);
|
||||
}
|
||||
ASSERT_NE(frame, nullptr);
|
||||
reader.resync();
|
||||
EXPECT_EQ(reader.length(), 0u);
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, ResyncKeepsPartialHeaderAtEnd) {
|
||||
FrameReader<5, 0x3C, 0x32> reader;
|
||||
const uint8_t *frame = nullptr;
|
||||
for (uint8_t c : {0x3C, 0x32, 0x00, 0x00, 0x3C}) {
|
||||
frame = reader.feed(c);
|
||||
}
|
||||
ASSERT_NE(frame, nullptr);
|
||||
reader.resync();
|
||||
EXPECT_EQ(reader.length(), 1u);
|
||||
EXPECT_EQ(feed_all(reader, {0x32, 0x01, 0x02, 0x03}), (Frames{{0x3C, 0x32, 0x01, 0x02, 0x03}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, ResyncMultiByteHeader) {
|
||||
FrameReader<6, 0x3C, 0x32> reader;
|
||||
const uint8_t *frame = nullptr;
|
||||
for (uint8_t c : {0x3C, 0x32, 0x3C, 0x3C, 0x32, 0x09}) {
|
||||
frame = reader.feed(c);
|
||||
}
|
||||
ASSERT_NE(frame, nullptr);
|
||||
reader.resync();
|
||||
EXPECT_EQ(reader.length(), 3u);
|
||||
EXPECT_EQ(feed_all(reader, {0x0A, 0x0B, 0x0C}), (Frames{{0x3C, 0x32, 0x09, 0x0A, 0x0B, 0x0C}}));
|
||||
}
|
||||
|
||||
TEST(FrameReaderTest, SixteenBitIndex) {
|
||||
FrameReader<300, 0xAA, 0x55> reader;
|
||||
Bytes input{0x00, 0xAA, 0x55};
|
||||
input.resize(input.size() + 298, 0x42);
|
||||
Frames frames = feed_all(reader, input);
|
||||
ASSERT_EQ(frames.size(), 1u);
|
||||
EXPECT_EQ(frames[0][0], 0xAA);
|
||||
EXPECT_EQ(frames[0][1], 0x55);
|
||||
EXPECT_EQ(frames[0][299], 0x42);
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
@@ -0,0 +1,82 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/components/display/init_sequence.h"
|
||||
|
||||
namespace esphome::display::testing {
|
||||
|
||||
TEST(InitSequenceReader, WalksCommandsAndDelays) {
|
||||
static const uint8_t SEQ[] = {0x11, 0x00, 120, 0xFF, 0x3A, 0x01, 0x55, 0x2A, 0x02, 0x00, 0xEF};
|
||||
InitSequenceReader reader(SEQ, sizeof(SEQ));
|
||||
InitSequenceEntry entry{};
|
||||
|
||||
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_COMMAND);
|
||||
EXPECT_EQ(entry.value, 0x11);
|
||||
EXPECT_EQ(entry.count, 0);
|
||||
|
||||
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_DELAY);
|
||||
EXPECT_EQ(entry.value, 120);
|
||||
|
||||
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_COMMAND);
|
||||
EXPECT_EQ(entry.value, 0x3A);
|
||||
ASSERT_EQ(entry.count, 1);
|
||||
EXPECT_EQ(entry.args, &SEQ[6]);
|
||||
|
||||
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_COMMAND);
|
||||
EXPECT_EQ(entry.value, 0x2A);
|
||||
ASSERT_EQ(entry.count, 2);
|
||||
EXPECT_EQ(entry.args[0], 0x00);
|
||||
EXPECT_EQ(entry.args[1], 0xEF);
|
||||
|
||||
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_END);
|
||||
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_END);
|
||||
}
|
||||
|
||||
TEST(InitSequenceReader, EmptySequenceEnds) {
|
||||
InitSequenceReader reader(nullptr, 0);
|
||||
InitSequenceEntry entry{};
|
||||
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_END);
|
||||
}
|
||||
|
||||
// A count above 127 is a real length, not a flag bit.
|
||||
TEST(InitSequenceReader, HonoursFullCount) {
|
||||
uint8_t seq[2 + 253 + 2] = {0x32, 253};
|
||||
seq[2 + 253] = 0x29;
|
||||
InitSequenceReader reader(seq, sizeof(seq));
|
||||
InitSequenceEntry entry{};
|
||||
|
||||
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_COMMAND);
|
||||
EXPECT_EQ(entry.value, 0x32);
|
||||
EXPECT_EQ(entry.count, 253);
|
||||
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_COMMAND);
|
||||
EXPECT_EQ(entry.value, 0x29);
|
||||
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_END);
|
||||
}
|
||||
|
||||
TEST(InitSequenceReader, TruncatedHeaderIsMalformed) {
|
||||
static const uint8_t SEQ[] = {0x11, 0x00, 0x29};
|
||||
InitSequenceReader reader(SEQ, sizeof(SEQ));
|
||||
InitSequenceEntry entry{};
|
||||
ASSERT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_COMMAND);
|
||||
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_MALFORMED);
|
||||
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_END);
|
||||
}
|
||||
|
||||
TEST(InitSequenceReader, ShortArgsAreMalformed) {
|
||||
static const uint8_t SEQ[] = {0x2A, 0x04, 0x00, 0x00, 0x00};
|
||||
InitSequenceReader reader(SEQ, sizeof(SEQ));
|
||||
InitSequenceEntry entry{};
|
||||
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_MALFORMED);
|
||||
EXPECT_EQ(reader.next(entry), INIT_SEQUENCE_STEP_END);
|
||||
}
|
||||
|
||||
TEST(InitSequenceReader, CountOverMaxIsMalformed) {
|
||||
static const uint8_t SEQ[] = {10, 0xFF, 0x2A, 0x02, 0x00, 0xEF};
|
||||
InitSequenceReader reader(SEQ, sizeof(SEQ));
|
||||
InitSequenceEntry entry{};
|
||||
ASSERT_EQ(reader.next(entry, 1), INIT_SEQUENCE_STEP_DELAY);
|
||||
EXPECT_EQ(reader.next(entry, 1), INIT_SEQUENCE_STEP_MALFORMED);
|
||||
}
|
||||
|
||||
} // namespace esphome::display::testing
|
||||
@@ -20,6 +20,8 @@
|
||||
|
||||
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 PASSIVE_START_BYTE_2_WITH_ID = 0x96;
|
||||
static constexpr uint8_t FRAME_STOP_BYTE = 0x16;
|
||||
@@ -129,7 +131,7 @@ class TestableUFM01 : public UFM01Component {
|
||||
|
||||
StartupPhase startup_phase() const { return this->startup_phase_; }
|
||||
|
||||
int32_t read_index() const { return this->active_reader_.length(); }
|
||||
int32_t read_index() const { return this->read_index_; }
|
||||
|
||||
uint8_t passive_index() const { return this->passive_index_; }
|
||||
|
||||
@@ -163,7 +165,7 @@ class TestableUFM01 : public UFM01Component {
|
||||
this->operating_mode_ = OperatingMode::ACTIVE_STREAM;
|
||||
this->last_valid_frame_ms_ = now();
|
||||
this->passive_read_pending_ = false;
|
||||
this->active_reader_.reset();
|
||||
this->read_index_ = 0;
|
||||
}
|
||||
|
||||
void begin_active_wait_frame() {
|
||||
@@ -180,7 +182,7 @@ class TestableUFM01 : public UFM01Component {
|
||||
}
|
||||
|
||||
void reset_state() {
|
||||
this->active_reader_.reset();
|
||||
this->read_index_ = 0;
|
||||
this->last_valid_frame_ms_ = 0;
|
||||
this->passive_index_ = 0;
|
||||
this->passive_read_pending_ = false;
|
||||
|
||||
@@ -30,15 +30,6 @@ TEST_F(UFM01Test, InvalidActiveFrameChecksumRejected) {
|
||||
EXPECT_EQ(this->ufm01_.last_valid_frame_ms(), 0u);
|
||||
}
|
||||
|
||||
TEST_F(UFM01Test, RejectedActiveFrameResyncsOnStartBytesInside) {
|
||||
// A cut off frame start makes the first 32 bytes invalid; the real frame starts inside them
|
||||
this->mock_uart_.enqueue({FRAME_START_BYTE_1, FRAME_START_BYTE_2, 0x00, 0x00});
|
||||
this->mock_uart_.enqueue(make_active_frame());
|
||||
|
||||
EXPECT_TRUE(this->ufm01_.process_active_stream());
|
||||
EXPECT_EQ(this->ufm01_.read_index(), 0);
|
||||
}
|
||||
|
||||
TEST_F(UFM01Test, ValidPassiveFrameReadSuccess) {
|
||||
auto frame = make_passive_frame();
|
||||
this->mock_uart_.enqueue(frame);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for display.add_writer."""
|
||||
"""Tests for display.add_writer and display.config_has_writer."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
@@ -6,7 +6,8 @@ import pytest
|
||||
|
||||
from esphome import codegen as cg
|
||||
from esphome.components import display
|
||||
from esphome.const import CONF_LAMBDA
|
||||
from esphome.components.display import CONF_SHOW_TEST_CARD
|
||||
from esphome.const import CONF_AUTO_CLEAR_ENABLED, CONF_LAMBDA, CONF_PAGES
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -39,3 +40,20 @@ async def test_add_writer_without_lambda_does_nothing() -> None:
|
||||
|
||||
process.assert_not_awaited()
|
||||
add.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "expected"),
|
||||
[
|
||||
({}, False),
|
||||
({CONF_AUTO_CLEAR_ENABLED: True}, True),
|
||||
({CONF_AUTO_CLEAR_ENABLED: False}, False),
|
||||
({CONF_AUTO_CLEAR_ENABLED: "unspecified"}, False),
|
||||
({CONF_PAGES: []}, True),
|
||||
({CONF_LAMBDA: "x"}, True),
|
||||
({CONF_SHOW_TEST_CARD: True}, True),
|
||||
({CONF_SHOW_TEST_CARD: False}, False),
|
||||
],
|
||||
)
|
||||
def test_config_has_writer(config: dict, expected: bool) -> None:
|
||||
assert display.config_has_writer(config) is expected
|
||||
Reference in new issue
Block a user