mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
Merge branch 'neutral-ble-client' into radon-eye-single-node
This commit is contained in:
@@ -439,6 +439,7 @@ esphome/components/ota/* @esphome/core
|
||||
esphome/components/output/* @esphome/core
|
||||
esphome/components/packet_transport/* @clydebarrow
|
||||
esphome/components/pca6416a/* @Mat931
|
||||
esphome/components/pca9505/* @ajfriesen @ssieb
|
||||
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
|
||||
esphome/components/pcf85063/* @brogon
|
||||
esphome/components/pcf8563/* @KoenBreeman
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <set>
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bedjet {
|
||||
|
||||
static const char *const TAG = "bedjet";
|
||||
ESPHOME_LOG_TAG(TAG, "bedjet");
|
||||
|
||||
/// Converts a BedJet fan step to a speed percentage, in the range of 5% to 100%.
|
||||
inline static uint8_t bedjet_fan_step_to_speed(const uint8_t fan) {
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
#include "esphome/components/bme280_base/bme280_base.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bme280_i2c {
|
||||
|
||||
static const char *const TAG = "bme280_i2c.sensor";
|
||||
ESPHOME_LOG_TAG(TAG, "bme280_i2c.sensor");
|
||||
|
||||
class BME280I2CComponent final : public esphome::bme280_base::BME280Component, public i2c::I2CDevice {
|
||||
bool read_byte(uint8_t a_register, uint8_t *data) override;
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
#include "esphome/components/bmp280_base/bmp280_base.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bmp280_i2c {
|
||||
|
||||
static const char *const TAG = "bmp280_i2c.sensor";
|
||||
ESPHOME_LOG_TAG(TAG, "bmp280_i2c.sensor");
|
||||
|
||||
/// This class implements support for the BMP280 Temperature+Pressure i2c sensor.
|
||||
class BMP280I2CComponent final : public esphome::bmp280_base::BMP280Component, public i2c::I2CDevice {
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
#include "esphome/components/bmp581_base/bmp581_base.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bmp581_i2c {
|
||||
|
||||
static const char *const TAG = "bmp581_i2c.sensor";
|
||||
ESPHOME_LOG_TAG(TAG, "bmp581_i2c.sensor");
|
||||
|
||||
/// This class implements support for the BMP581 Temperature+Pressure i2c sensor.
|
||||
class BMP581I2CComponent final : public esphome::bmp581_base::BMP581Component, public i2c::I2CDevice {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
namespace esphome::chsc6x {
|
||||
|
||||
static const char *const TAG = "chsc6x.touchscreen";
|
||||
ESPHOME_LOG_TAG(TAG, "chsc6x.touchscreen");
|
||||
|
||||
static const uint8_t CHSC6X_REG_STATUS = 0x00;
|
||||
static const uint8_t CHSC6X_REG_STATUS_TOUCH = 0x00;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
namespace esphome::cst816 {
|
||||
|
||||
static const char *const TAG = "cst816.touchscreen";
|
||||
ESPHOME_LOG_TAG(TAG, "cst816.touchscreen");
|
||||
|
||||
static const uint8_t REG_STATUS = 0x00;
|
||||
static const uint8_t REG_TOUCH_NUM = 0x02;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
namespace esphome::cst9220 {
|
||||
|
||||
static const char *const TAG = "cst9220.touchscreen";
|
||||
ESPHOME_LOG_TAG(TAG, "cst9220.touchscreen");
|
||||
|
||||
// The CST92xx family uses 16-bit (big-endian) register addresses.
|
||||
static const uint16_t REG_TOUCH_DATA = 0xD000; // touch report
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include "esphome/core/color.h"
|
||||
|
||||
namespace esphome::display {
|
||||
@@ -154,4 +156,54 @@ class ColorUtil {
|
||||
return color;
|
||||
}
|
||||
};
|
||||
|
||||
/** Rec.601 luma weights (0.299/0.587/0.114), scaled by 256 */
|
||||
static constexpr uint16_t REC601_LUMA_R = 77;
|
||||
static constexpr uint16_t REC601_LUMA_G = 150;
|
||||
static constexpr uint16_t REC601_LUMA_B = 29;
|
||||
|
||||
/** Rec.601 luma of an RGB color, for optimum perceptual brightness */
|
||||
constexpr uint8_t rec601_luma(Color color) {
|
||||
return (uint8_t) ((REC601_LUMA_R * color.r + REC601_LUMA_G * color.g + REC601_LUMA_B * color.b + 128u) >> 8);
|
||||
}
|
||||
|
||||
/** Map an RGB color to a single monochrome bit: 1 = white, 0 = black */
|
||||
constexpr uint8_t color_to_mono(Color color) { return rec601_luma(color) >= 128 ? 1 : 0; }
|
||||
|
||||
/** color_to_mono() of an RGB565 value as ColorUtil::to_color() reads it, from table lookups instead of a division
|
||||
* per channel.
|
||||
*/
|
||||
class Rgb565ToMono {
|
||||
public:
|
||||
explicit Rgb565ToMono(ColorOrder order) {
|
||||
// Rec.601 weight of the 5-bit, 6-bit and 5-bit fields, by the channel each one holds in this order
|
||||
uint16_t high = REC601_LUMA_R, middle = REC601_LUMA_G, low = REC601_LUMA_B;
|
||||
if (order == COLOR_ORDER_BGR) {
|
||||
high = REC601_LUMA_B;
|
||||
low = REC601_LUMA_R;
|
||||
} else if (order == COLOR_ORDER_GRB) {
|
||||
high = REC601_LUMA_G;
|
||||
middle = REC601_LUMA_R;
|
||||
}
|
||||
// Each field value scaled to 0..255, then weighted
|
||||
for (unsigned i = 0; i != 32; i++) {
|
||||
this->high_[i] = high * esp_scale(i, 31);
|
||||
this->low_[i] = low * esp_scale(i, 31);
|
||||
}
|
||||
for (unsigned i = 0; i != 64; i++)
|
||||
this->middle_[i] = middle * esp_scale(i, 63);
|
||||
}
|
||||
|
||||
uint8_t operator()(uint16_t value) const {
|
||||
const uint32_t weighted = this->high_[value >> 11] + this->middle_[(value >> 5) & 0x3F] + this->low_[value & 0x1F];
|
||||
return ((weighted + 128u) >> 8) >= 128 ? 1 : 0;
|
||||
}
|
||||
|
||||
protected:
|
||||
// Weighted contribution of each value of each field
|
||||
std::array<uint16_t, 32> high_{};
|
||||
std::array<uint16_t, 64> middle_{};
|
||||
std::array<uint16_t, 32> low_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::display
|
||||
|
||||
@@ -23,11 +23,18 @@ DEPENDENCIES = ["uart"]
|
||||
|
||||
emontx_ns = cg.esphome_ns.namespace("emontx")
|
||||
EmonTx = emontx_ns.class_("EmonTx", cg.Component, uart.UARTDevice)
|
||||
LockOnReboot = emontx_ns.enum("LockOnReboot", is_class=True)
|
||||
|
||||
CONF_EMONTX_ID = "emontx_id"
|
||||
CONF_TAG_NAME = "tag_name"
|
||||
CONF_ON_JSON = "on_json"
|
||||
CONF_LOCK_ON_REBOOT = "lock_on_reboot"
|
||||
LOCK_ON_REBOOT_AUTO = "auto"
|
||||
LOCK_ON_REBOOT_MODES = {
|
||||
LOCK_ON_REBOOT_AUTO: LockOnReboot.LOCK_ON_REBOOT_AUTO,
|
||||
True: LockOnReboot.LOCK_ON_REBOOT_ALWAYS,
|
||||
False: LockOnReboot.LOCK_ON_REBOOT_NEVER,
|
||||
}
|
||||
|
||||
DOMAIN = "emontx"
|
||||
|
||||
@@ -59,7 +66,9 @@ CONFIG_SCHEMA = (
|
||||
cv.GenerateID(): cv.declare_id(EmonTx),
|
||||
cv.Optional(CONF_ON_JSON): _synchronous_automation_schema(),
|
||||
cv.Optional(CONF_ON_DATA): _synchronous_automation_schema(),
|
||||
cv.Optional(CONF_LOCK_ON_REBOOT, default=False): cv.boolean,
|
||||
cv.Optional(CONF_LOCK_ON_REBOOT, default=LOCK_ON_REBOOT_AUTO): cv.Any(
|
||||
cv.boolean, cv.one_of(LOCK_ON_REBOOT_AUTO, lower=True)
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
@@ -135,8 +144,11 @@ async def to_code(config: ConfigType) -> None:
|
||||
await uart.register_uart_device(var, config)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
if config[CONF_LOCK_ON_REBOOT]:
|
||||
cg.add(var.set_lock_on_reboot(True))
|
||||
lock = config[CONF_LOCK_ON_REBOOT]
|
||||
cg.add(var.set_lock_on_reboot(LOCK_ON_REBOOT_MODES[lock]))
|
||||
if lock == LOCK_ON_REBOOT_AUTO:
|
||||
# Locking depends on the firmware version the emonTx reports
|
||||
cg.add_define("USE_EMONTX_VERSION")
|
||||
|
||||
|
||||
# Action: emontx.send_command
|
||||
|
||||
@@ -7,30 +7,85 @@ namespace esphome::emontx {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "emontx");
|
||||
|
||||
static constexpr uint32_t UNLOCK_ID = 0;
|
||||
// The emonTx joins bytes received while a command is pending into that command, so the next one waits
|
||||
static constexpr uint32_t COMMAND_GAP_MS = 250;
|
||||
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
static constexpr uint32_t VERSION_REQUEST_ID = 0;
|
||||
// The emonTx joins bytes received while a command is pending into that command, so 'v' waits
|
||||
static constexpr uint32_t VERSION_REQUEST_DELAY_MS = 250;
|
||||
static constexpr uint32_t VERSION_REQUEST_ID = 1;
|
||||
// Without a reply, 'v' is sent again every VERSION_RETRY_FRAMES data lines, VERSION_MAX_ATTEMPTS times in all
|
||||
static constexpr uint8_t VERSION_RETRY_FRAMES = 2;
|
||||
static constexpr uint8_t VERSION_MAX_ATTEMPTS = 3;
|
||||
// Line of the 'v' reply (and of the boot banner) carrying the release, e.g. " - Version: v1.1.1"
|
||||
static const char VERSION_PREFIX[] PROGMEM = " - Version:";
|
||||
// Every data line starts with the message counter, in JSON or text format
|
||||
static const char MSG_JSON_PREFIX[] PROGMEM = "{\"MSG\":";
|
||||
static const char MSG_TEXT_PREFIX[] PROGMEM = "MSG:";
|
||||
// First firmware with 'emonlock' and 'emonunlock'; older firmware takes them for the bootloader command
|
||||
static constexpr uint32_t LOCK_MIN_MAJOR = 1;
|
||||
static constexpr uint32_t LOCK_MIN_MINOR = 2;
|
||||
|
||||
// True if line starts with the PROGMEM prefix and has more after it
|
||||
static bool starts_with(const char *line, size_t len, const char *prefix, size_t prefix_len) {
|
||||
return len > prefix_len && ESPHOME_strncmp_P(line, prefix, prefix_len) == 0;
|
||||
}
|
||||
|
||||
// Reads the decimal number (up to 9 digits) at pos and moves pos past it; false if there is none
|
||||
static bool read_number(const char *&pos, const char *end, uint32_t &value) {
|
||||
const char *start = pos;
|
||||
value = 0;
|
||||
while (pos < end && pos - start < 9 && *pos >= '0' && *pos <= '9')
|
||||
value = value * 10 + (*pos++ - '0');
|
||||
return pos != start;
|
||||
}
|
||||
|
||||
// True for a release such as "v1.2.0" or "v1.3.0-rc1" from LOCK_MIN_MAJOR.LOCK_MIN_MINOR on
|
||||
static bool supports_lock(const char *version, const char *end) {
|
||||
if (version < end && (*version == 'v' || *version == 'V'))
|
||||
version++;
|
||||
uint32_t major;
|
||||
if (!read_number(version, end, major) || version == end || *version != '.')
|
||||
return false;
|
||||
version++;
|
||||
uint32_t minor;
|
||||
if (!read_number(version, end, minor))
|
||||
return false;
|
||||
return major > LOCK_MIN_MAJOR || (major == LOCK_MIN_MAJOR && minor >= LOCK_MIN_MINOR);
|
||||
}
|
||||
#endif
|
||||
|
||||
void EmonTx::setup() {
|
||||
this->buffer_pos_ = 0;
|
||||
if (this->lock_on_reboot_) {
|
||||
// The line feed ends any boot noise first
|
||||
ESP_LOGI(TAG, "Unlocking emonTx after boot");
|
||||
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS) {
|
||||
// The line feed ends any boot noise
|
||||
this->write_byte('\n');
|
||||
this->send_command("emonunlock");
|
||||
this->set_timeout(UNLOCK_ID, COMMAND_GAP_MS, [this]() {
|
||||
this->unlock_();
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
if (this->wants_version_())
|
||||
this->request_version_();
|
||||
#endif
|
||||
});
|
||||
return;
|
||||
}
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
// With AUTO, the reply decides whether to unlock
|
||||
if (this->wants_version_())
|
||||
this->request_version_();
|
||||
#endif
|
||||
}
|
||||
|
||||
void EmonTx::unlock_() {
|
||||
ESP_LOGI(TAG, "Unlocking emonTx");
|
||||
this->send_command("emonunlock");
|
||||
}
|
||||
|
||||
void EmonTx::on_shutdown() {
|
||||
if (!this->lock_on_reboot_)
|
||||
bool lock = this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS;
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
lock |= this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO && this->lock_supported_;
|
||||
#endif
|
||||
if (!lock)
|
||||
return;
|
||||
if (this->is_paused()) {
|
||||
ESP_LOGW(TAG, "Not locking emonTx before shutdown, UART is paused");
|
||||
@@ -63,7 +118,7 @@ void EmonTx::loop() {
|
||||
this->discarding_line_ = dropped != '\n';
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
// The emonTx may have been flashed, or a request dropped, during the pause
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
if (this->wants_version_())
|
||||
this->request_version_();
|
||||
#endif
|
||||
}
|
||||
@@ -96,8 +151,10 @@ void EmonTx::loop() {
|
||||
this->parse_json_(this->buffer_.data(), len);
|
||||
}
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
if (this->wants_version_()) {
|
||||
this->parse_msg_number_(this->buffer_.data(), len);
|
||||
this->parse_version_line_(this->buffer_.data(), len);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
} else if (received == '\r' || this->discarding_line_) {
|
||||
@@ -124,16 +181,6 @@ void EmonTx::parse_json_(const char *data, size_t len) {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
auto msg = root["MSG"];
|
||||
if (this->version_text_sensor_ != nullptr && msg.is<uint32_t>()) {
|
||||
// MSG restarts at 1 when the emonTx reboots, as it does after a firmware update
|
||||
uint32_t msg_number = msg;
|
||||
if (msg_number <= this->last_msg_number_)
|
||||
this->request_version_();
|
||||
this->last_msg_number_ = msg_number;
|
||||
}
|
||||
#endif
|
||||
|
||||
this->json_callbacks_.call(root, StringRef(data, len));
|
||||
return true;
|
||||
@@ -145,17 +192,50 @@ void EmonTx::parse_json_(const char *data, size_t len) {
|
||||
}
|
||||
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
// The line feed ends any pending input; the reply is picked up in loop() by parse_version_line_()
|
||||
void EmonTx::request_version_() {
|
||||
// The firmware may have changed; until the reply, an unknown version gets no lock commands
|
||||
this->lock_supported_ = false;
|
||||
this->version_frames_left_ = VERSION_RETRY_FRAMES * VERSION_MAX_ATTEMPTS;
|
||||
this->send_version_request_();
|
||||
}
|
||||
|
||||
// The line feed ends any pending input; the reply is picked up in loop() by parse_version_line_()
|
||||
void EmonTx::send_version_request_() {
|
||||
if (this->is_paused())
|
||||
return;
|
||||
this->write_byte('\n');
|
||||
this->set_timeout(VERSION_REQUEST_ID, VERSION_REQUEST_DELAY_MS, [this]() { this->send_command("v"); });
|
||||
this->set_timeout(VERSION_REQUEST_ID, COMMAND_GAP_MS, [this]() { this->send_command("v"); });
|
||||
}
|
||||
|
||||
void EmonTx::parse_msg_number_(const char *line, size_t len) {
|
||||
const char *pos;
|
||||
if (starts_with(line, len, MSG_JSON_PREFIX, sizeof(MSG_JSON_PREFIX) - 1)) {
|
||||
pos = line + sizeof(MSG_JSON_PREFIX) - 1;
|
||||
} else if (starts_with(line, len, MSG_TEXT_PREFIX, sizeof(MSG_TEXT_PREFIX) - 1)) {
|
||||
pos = line + sizeof(MSG_TEXT_PREFIX) - 1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
uint32_t msg_number;
|
||||
if (!read_number(pos, line + len, msg_number))
|
||||
return;
|
||||
|
||||
// MSG restarts at 1 when the emonTx reboots, as it does after a firmware update
|
||||
if (msg_number <= this->last_msg_number_) {
|
||||
this->request_version_();
|
||||
} else if (this->version_frames_left_ > 0) {
|
||||
if (--this->version_frames_left_ == 0) {
|
||||
ESP_LOGW(TAG, "No firmware version from the emonTx, it may still be locked");
|
||||
} else if (this->version_frames_left_ % VERSION_RETRY_FRAMES == 0) {
|
||||
this->send_version_request_();
|
||||
}
|
||||
}
|
||||
this->last_msg_number_ = msg_number;
|
||||
}
|
||||
|
||||
void EmonTx::parse_version_line_(const char *line, size_t len) {
|
||||
constexpr size_t prefix_len = sizeof(VERSION_PREFIX) - 1;
|
||||
if (len <= prefix_len || ESPHOME_strncmp_P(line, VERSION_PREFIX, prefix_len) != 0)
|
||||
if (!starts_with(line, len, VERSION_PREFIX, prefix_len))
|
||||
return;
|
||||
const char *version = line + prefix_len;
|
||||
const char *end = line + len;
|
||||
@@ -164,7 +244,23 @@ void EmonTx::parse_version_line_(const char *line, size_t len) {
|
||||
if (version == end)
|
||||
return;
|
||||
ESP_LOGI(TAG, "emonTx firmware version: %.*s", static_cast<int>(end - version), version);
|
||||
this->version_text_sensor_->publish_state(version, end - version);
|
||||
this->version_frames_left_ = 0;
|
||||
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
this->version_text_sensor_->publish_state(version, end - version);
|
||||
#endif
|
||||
|
||||
bool supported = supports_lock(version, end);
|
||||
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO && supported && !this->unlock_sent_) {
|
||||
// Once the rest of the reply has gone out; a pause drops it, and the version request on resume retries
|
||||
this->set_timeout(UNLOCK_ID, COMMAND_GAP_MS, [this]() {
|
||||
if (this->is_paused())
|
||||
return;
|
||||
this->unlock_();
|
||||
this->unlock_sent_ = true;
|
||||
});
|
||||
}
|
||||
this->lock_supported_ = supported;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -173,7 +269,13 @@ void EmonTx::parse_version_line_(const char *line, size_t len) {
|
||||
*/
|
||||
void EmonTx::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "EmonTx:");
|
||||
ESP_LOGCONFIG(TAG, " Lock on reboot: %s", YESNO(this->lock_on_reboot_));
|
||||
const char *lock = LOG_STR_LITERAL("no");
|
||||
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS) {
|
||||
lock = LOG_STR_LITERAL("yes");
|
||||
} else if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO) {
|
||||
lock = LOG_STR_LITERAL("auto");
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Lock on reboot: %s", lock);
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
ESP_LOGCONFIG(TAG, " Registered sensors: %zu", this->sensors_.size());
|
||||
@@ -183,7 +285,7 @@ void EmonTx::dump_config() {
|
||||
#else
|
||||
ESP_LOGCONFIG(TAG, " Sensor support: DISABLED");
|
||||
#endif
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
LOG_TEXT_SENSOR(" ", "Firmware version", this->version_text_sensor_);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#endif
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#endif
|
||||
|
||||
@@ -22,6 +22,12 @@ namespace esphome::emontx {
|
||||
/// Maximum line length in bytes (plus one byte reserved for null terminator)
|
||||
static constexpr size_t MAX_LINE_LENGTH = 1024;
|
||||
|
||||
enum class LockOnReboot : uint8_t {
|
||||
LOCK_ON_REBOOT_NEVER,
|
||||
LOCK_ON_REBOOT_ALWAYS,
|
||||
LOCK_ON_REBOOT_AUTO, // Only when the emonTx reports firmware v1.2.0 or later
|
||||
};
|
||||
|
||||
/**
|
||||
* @class EmonTx
|
||||
* @brief Main class for the EmonTx component.
|
||||
@@ -30,7 +36,7 @@ static constexpr size_t MAX_LINE_LENGTH = 1024;
|
||||
* extracts tags and values, and publishes them to registered sensors.
|
||||
*/
|
||||
class EmonTx final : public Component, public uart::UARTDevice {
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
SUB_TEXT_SENSOR(version)
|
||||
#endif
|
||||
public:
|
||||
@@ -56,8 +62,8 @@ class EmonTx final : public Component, public uart::UARTDevice {
|
||||
bool is_paused() const { return this->paused_.load(std::memory_order_relaxed); }
|
||||
|
||||
/// Lock the emonTx ('emonlock') before a reboot, so serial garbage during the reboot is not taken
|
||||
/// as a command, and unlock it in setup(). Needs emonTx firmware v1.2.0 or later.
|
||||
void set_lock_on_reboot(bool lock_on_reboot) { this->lock_on_reboot_ = lock_on_reboot; }
|
||||
/// as a command, and unlock it after boot. Needs emonTx firmware v1.2.0 or later, which AUTO checks.
|
||||
void set_lock_on_reboot(LockOnReboot lock_on_reboot) { this->lock_on_reboot_ = lock_on_reboot; }
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
void init_sensors(size_t count) { this->sensors_.init(count); }
|
||||
@@ -66,8 +72,18 @@ class EmonTx final : public Component, public uart::UARTDevice {
|
||||
|
||||
protected:
|
||||
void parse_json_(const char *data, size_t len);
|
||||
void unlock_();
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
bool wants_version_() const {
|
||||
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
if (this->version_text_sensor_ != nullptr)
|
||||
return true;
|
||||
#endif
|
||||
return this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO;
|
||||
}
|
||||
void request_version_();
|
||||
void send_version_request_();
|
||||
void parse_msg_number_(const char *line, size_t len);
|
||||
void parse_version_line_(const char *line, size_t len);
|
||||
#endif
|
||||
|
||||
@@ -79,9 +95,12 @@ class EmonTx final : public Component, public uart::UARTDevice {
|
||||
uint16_t buffer_pos_{0};
|
||||
std::atomic<bool> paused_{false};
|
||||
bool was_paused_{false};
|
||||
bool lock_on_reboot_{false};
|
||||
LockOnReboot lock_on_reboot_{LockOnReboot::LOCK_ON_REBOOT_NEVER};
|
||||
bool discarding_line_{false}; // Set after an overflow or a pause until the end of that line
|
||||
#ifdef USE_EMONTX_VERSION
|
||||
bool lock_supported_{false}; // The last version reported is v1.2.0 or later
|
||||
bool unlock_sent_{false}; // AUTO has sent 'emonunlock' since boot
|
||||
uint8_t version_frames_left_{0}; // Data lines still to wait for a version reply; 0 when none is owed
|
||||
uint32_t last_msg_number_{0};
|
||||
#endif
|
||||
std::array<char, MAX_LINE_LENGTH + 1> buffer_{};
|
||||
|
||||
@@ -21,3 +21,4 @@ async def to_code(config: ConfigType) -> None:
|
||||
text_sensors = text_sensor.sub_text_sensors(config)
|
||||
if await text_sensors(CONF_VERSION, hub.set_version_text_sensor) is not None:
|
||||
cg.add_define("USE_EMONTX_VERSION")
|
||||
cg.add_define("USE_EMONTX_VERSION_TEXT_SENSOR")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <algorithm>
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
#include "esphome/core/color.h"
|
||||
|
||||
/* Utility for converting internal \a Color RGB representation to supported IC hardware color keys
|
||||
@@ -13,21 +14,15 @@
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
// The Rec.601 luma helpers and the RGB565 mono lookup live in the shared display utilities, so other
|
||||
// drivers (e.g. it8951 in its 1 bit per pixel mode) can use them too.
|
||||
using display::color_to_mono;
|
||||
using display::rec601_luma;
|
||||
using display::Rgb565ToMono;
|
||||
|
||||
/** Delta for when to regard as gray */
|
||||
static constexpr uint8_t COLORCONV_GRAY_THRESHOLD = 50;
|
||||
|
||||
/** Rec.601 luma (0.299/0.587/0.114 weights, scaled by 256) for optimum perceptual brightness */
|
||||
constexpr uint8_t rec601_luma(Color color) {
|
||||
return (uint8_t) ((77u * color.r + 150u * color.g + 29u * color.b + 128u) >> 8);
|
||||
}
|
||||
|
||||
/** Map RGB color to a single monochrome bit
|
||||
*
|
||||
* @param color RGB color to convert from
|
||||
* @return 1 = white, 0 = black
|
||||
*/
|
||||
constexpr uint8_t color_to_mono(Color color) { return rec601_luma(color) >= 128 ? 1 : 0; }
|
||||
|
||||
/** Map RGB color to one of 4 discrete gray levels (2 bits per pixel)
|
||||
*
|
||||
* @param color RGB color to convert from
|
||||
|
||||
@@ -131,9 +131,7 @@ void EPaperBase::update() {
|
||||
}
|
||||
|
||||
void EPaperBase::wait_for_idle_(bool should_wait) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
this->waiting_for_idle_start_ = millis();
|
||||
#endif
|
||||
this->waiting_for_idle_ = should_wait;
|
||||
}
|
||||
|
||||
@@ -156,6 +154,10 @@ void EPaperBase::loop() {
|
||||
ESP_LOGV(TAG, "Screen was busy for %u ms", (unsigned) (millis() - this->waiting_for_idle_start_));
|
||||
#endif
|
||||
} else {
|
||||
if (now - this->waiting_for_idle_start_ > this->busy_timeout_seconds_ * 1000u) {
|
||||
this->abandon_update_();
|
||||
return;
|
||||
}
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
if (now - this->waiting_for_idle_last_print_ >= 1000) {
|
||||
ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_());
|
||||
@@ -168,6 +170,20 @@ void EPaperBase::loop() {
|
||||
this->process_state_();
|
||||
}
|
||||
|
||||
// The busy pin never cleared: give up on this update so later ones are not refused, and make the next one full
|
||||
void EPaperBase::abandon_update_() {
|
||||
ESP_LOGE(TAG, "Display stayed busy for %u s in state %s; check the busy pin", (unsigned) this->busy_timeout_seconds_,
|
||||
this->epaper_state_to_string_());
|
||||
this->status_set_warning(LOG_STR("Busy pin timeout"));
|
||||
if (this->state_ == EPaperState::TRANSFER_DATA)
|
||||
this->abort_transfer();
|
||||
// Leave the panel powered down rather than with its high-voltage drivers enabled
|
||||
this->power_off();
|
||||
this->full_update_requested_ = true;
|
||||
this->current_data_index_ = 0;
|
||||
this->set_state_(EPaperState::IDLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the state machine.
|
||||
* Typical state sequence:
|
||||
@@ -242,6 +258,7 @@ void EPaperBase::process_state_() {
|
||||
break;
|
||||
case EPaperState::DEEP_SLEEP:
|
||||
this->deep_sleep();
|
||||
this->status_clear_warning();
|
||||
this->set_state_(EPaperState::IDLE);
|
||||
ESP_LOGD(TAG, "Display update took %" PRIu32 " ms", millis() - this->update_start_time_);
|
||||
break;
|
||||
@@ -345,6 +362,120 @@ void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// One source row of an RGB565 block, written into a contiguous 1 bit per pixel buffer. Without SwapAxes the row lies
|
||||
// along a buffer row and the bit moves by step; with it the row lies along a bit column and the byte moves by step
|
||||
// rows.
|
||||
template<bool SwapAxes, bool BigEndianSource>
|
||||
void HOT write_rgb565_row(uint8_t *byte, uint8_t mask, int step, size_t row_width, const uint8_t *source, int count,
|
||||
const Rgb565ToMono &to_mono) {
|
||||
const ptrdiff_t byte_step = SwapAxes ? step * static_cast<ptrdiff_t>(row_width) : 0;
|
||||
for (; count != 0; count--, source += 2) {
|
||||
const uint16_t value = BigEndianSource ? (source[0] << 8) | source[1] : source[0] | (source[1] << 8);
|
||||
if (to_mono(value)) {
|
||||
*byte |= mask;
|
||||
} else {
|
||||
*byte &= ~mask;
|
||||
}
|
||||
if constexpr (SwapAxes) {
|
||||
byte += byte_step;
|
||||
} else if (step > 0) {
|
||||
mask >>= 1;
|
||||
if (mask == 0) {
|
||||
mask = 0x80;
|
||||
byte++;
|
||||
}
|
||||
} else {
|
||||
mask <<= 1;
|
||||
if (mask == 0) {
|
||||
mask = 0x01;
|
||||
byte--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Each source column maps to one buffer coordinate and each source row to the other, each moving by 1 per pixel, so
|
||||
// the visible part of a block and its dirty area are worked out once for the whole block.
|
||||
void HOT EPaperBase::draw_rgb565_block_(uint8_t *buffer, int x_start, int y_start, int w, int h, const uint8_t *ptr,
|
||||
ColorOrder order, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
const bool swap = this->effective_transform_ & SWAP_XY;
|
||||
const bool mirror_x = this->effective_transform_ & MIRROR_X;
|
||||
const bool mirror_y = this->effective_transform_ & MIRROR_Y;
|
||||
// The buffer axis a source column moves along, and the one a source row moves along
|
||||
const int column_limit = swap ? this->height_ : this->width_;
|
||||
const int row_limit = swap ? this->width_ : this->height_;
|
||||
const bool column_mirrored = swap ? mirror_y : mirror_x;
|
||||
const bool row_mirrored = swap ? mirror_x : mirror_y;
|
||||
const int first_column = std::max(0, -x_start);
|
||||
const int end_column = std::min(w, column_limit - x_start);
|
||||
const int first_row = std::max(0, -y_start);
|
||||
const int end_row = std::min(h, row_limit - y_start);
|
||||
if (first_column >= end_column || first_row >= end_row)
|
||||
return;
|
||||
auto column_to_buffer = [&](int column) {
|
||||
const int u = x_start + column;
|
||||
return column_mirrored ? column_limit - 1 - u : u;
|
||||
};
|
||||
auto row_to_buffer = [&](int row) {
|
||||
const int v = y_start + row;
|
||||
return row_mirrored ? row_limit - 1 - v : v;
|
||||
};
|
||||
|
||||
const int column_low = std::min(column_to_buffer(first_column), column_to_buffer(end_column - 1));
|
||||
const int row_low = std::min(row_to_buffer(first_row), row_to_buffer(end_row - 1));
|
||||
const int columns = end_column - first_column;
|
||||
const int rows = end_row - first_row;
|
||||
const int x_low = swap ? row_low : column_low;
|
||||
const int y_low = swap ? column_low : row_low;
|
||||
const int x_high = x_low + (swap ? rows : columns);
|
||||
const int y_high = y_low + (swap ? columns : rows);
|
||||
|
||||
const Rgb565ToMono to_mono(order);
|
||||
const size_t line_stride = x_offset + w + x_pad;
|
||||
const size_t row_width = this->row_width_;
|
||||
const int step = column_mirrored ? -1 : 1;
|
||||
for (int row = first_row; row != end_row; row++) {
|
||||
const uint8_t *source = ptr + ((y_offset + row) * line_stride + x_offset + first_column) * 2;
|
||||
const int along = column_to_buffer(first_column);
|
||||
const int across = row_to_buffer(row);
|
||||
const int x = swap ? across : along;
|
||||
const int y = swap ? along : across;
|
||||
uint8_t *byte = buffer + y * row_width + x / 8;
|
||||
const uint8_t mask = 0x80u >> (x % 8);
|
||||
if (swap) {
|
||||
if (big_endian) {
|
||||
write_rgb565_row<true, true>(byte, mask, step, row_width, source, columns, to_mono);
|
||||
} else {
|
||||
write_rgb565_row<true, false>(byte, mask, step, row_width, source, columns, to_mono);
|
||||
}
|
||||
} else if (big_endian) {
|
||||
write_rgb565_row<false, true>(byte, mask, step, row_width, source, columns, to_mono);
|
||||
} else {
|
||||
write_rgb565_row<false, false>(byte, mask, step, row_width, source, columns, to_mono);
|
||||
}
|
||||
}
|
||||
this->x_low_ = clamp_at_most(this->x_low_, x_low);
|
||||
this->x_high_ = clamp_at_least(this->x_high_, x_high);
|
||||
this->y_low_ = clamp_at_most(this->y_low_, y_low);
|
||||
this->y_high_ = clamp_at_least(this->y_high_, y_high);
|
||||
}
|
||||
|
||||
// Writes an RGB565 block of pixels, as LVGL sends them, straight into a 1 bit per pixel buffer that is one block (the
|
||||
// usual case): the same result as draw_pixel_at() for each pixel. Anything else goes through draw_pixel_at().
|
||||
void HOT EPaperBase::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
size_t contiguous_length;
|
||||
uint8_t *contiguous = this->buffer_.get_span(0, contiguous_length);
|
||||
if (!this->buffer_is_mono() || this->get_clipping().is_set() || bitness != COLOR_BITNESS_565 ||
|
||||
contiguous_length != this->buffer_length_) {
|
||||
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
|
||||
return;
|
||||
}
|
||||
this->draw_rgb565_block_(contiguous, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset, x_pad);
|
||||
}
|
||||
|
||||
void EPaperBase::dump_config() {
|
||||
LOG_DISPLAY("", "E-Paper SPI", this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
|
||||
@@ -107,6 +107,8 @@ class EPaperBase : public Display,
|
||||
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 draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||
// Make the next update a full one. Applied when that update starts, so one in progress is not affected.
|
||||
void request_full_update() { this->full_update_requested_ = true; }
|
||||
// True from the start of an update until the display has refreshed and gone back to sleep.
|
||||
@@ -125,9 +127,14 @@ class EPaperBase : public Display,
|
||||
virtual bool initialise(bool partial);
|
||||
void send_init_sequence_(const uint8_t *sequence, size_t length);
|
||||
void wait_for_idle_(bool should_wait);
|
||||
void abandon_update_();
|
||||
bool init_buffer_(size_t buffer_length);
|
||||
void update_effective_transform_();
|
||||
bool rotate_coordinates_(int &x, int &y);
|
||||
void draw_rgb565_block_(uint8_t *buffer, int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
|
||||
bool big_endian, int x_offset, int y_offset, int x_pad);
|
||||
// Whether the buffer holds 1 bit per pixel as draw_pixel_at() writes it, so draw_pixels_at() can write it directly
|
||||
virtual bool buffer_is_mono() const { return false; }
|
||||
|
||||
/**
|
||||
* Methods that must be implemented by concrete classes to control the display
|
||||
@@ -137,6 +144,8 @@ class EPaperBase : public Display,
|
||||
* @return true if done, false if it should be called next loop
|
||||
*/
|
||||
virtual bool transfer_data() = 0;
|
||||
// Called when an update is abandoned in the middle of transfer_data(), to release anything it left open
|
||||
virtual void abort_transfer() {}
|
||||
/**
|
||||
* Refresh the screen after data transfer
|
||||
*/
|
||||
@@ -187,18 +196,20 @@ class EPaperBase : public Display,
|
||||
// point to the pixel past the last one updated, i.e. may range up to width/height.
|
||||
uint16_t x_low_{}, y_low_{}, x_high_{}, y_high_{};
|
||||
|
||||
uint32_t waiting_for_idle_start_{};
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
uint32_t waiting_for_idle_last_print_{};
|
||||
uint32_t waiting_for_idle_start_{};
|
||||
#endif
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
|
||||
uint32_t update_start_time_{};
|
||||
#endif
|
||||
|
||||
// properties with specific initialisers go last
|
||||
EPaperState state_{EPaperState::IDLE};
|
||||
uint32_t reset_duration_{10};
|
||||
// Longer than any panel takes to refresh; past it the busy pin is assumed stuck
|
||||
uint16_t busy_timeout_seconds_{120};
|
||||
uint8_t full_update_every_{1};
|
||||
EPaperState state_{EPaperState::IDLE};
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
|
||||
@@ -15,6 +15,7 @@ class EPaperMono : public EPaperBase {
|
||||
}
|
||||
|
||||
protected:
|
||||
bool buffer_is_mono() const override { return true; }
|
||||
void refresh_screen(bool partial) override;
|
||||
void power_on() override {}
|
||||
void power_off() override{};
|
||||
|
||||
@@ -58,7 +58,9 @@ bool HOT EPaperSSD1677::transfer_data() {
|
||||
// A full update ignores 0x26, and the copy may not hold a real frame yet (first update after
|
||||
// boot): send the new frame to both planes.
|
||||
const bool send_copy = this->plane_ == 0 && this->update_count_ != 0;
|
||||
const bool direct = send_copy || this->buffer_is_plane();
|
||||
// A mono buffer already holds the frame as a RAM plane does, so it can be sent as it is; a deeper
|
||||
// buffer (the four-level model) returns false and packs the plane in plane_row().
|
||||
const bool direct = send_copy || this->buffer_is_mono();
|
||||
const auto &source = send_copy ? this->sent_ : this->buffer_;
|
||||
const size_t row_length = this->plane_row_length_();
|
||||
const size_t plane_length = row_length * this->height_;
|
||||
|
||||
@@ -34,9 +34,6 @@ class EPaperSSD1677 : public EPaperMono {
|
||||
void init_comparison_frame_();
|
||||
// Bytes in one row of a RAM plane: 8 pixels per byte, whatever the buffer holds.
|
||||
size_t plane_row_length_() const { return (this->width_ + 7) / 8; }
|
||||
// Whether the buffer already holds the frame as a RAM plane does, so it can be sent as it is.
|
||||
// A subclass with a deeper buffer returns false and overrides plane_row().
|
||||
virtual bool buffer_is_plane() const { return true; }
|
||||
// Row y of the frame as a RAM plane holds it, 1 bit per pixel with 1 = white.
|
||||
virtual void plane_row(size_t y, uint8_t *out);
|
||||
bool reset() override;
|
||||
|
||||
@@ -38,9 +38,9 @@ class EPaperSSD1677Gray4 : public EPaperSSD1677 {
|
||||
void draw_pixel_at(int x, int y, Color color) override;
|
||||
|
||||
protected:
|
||||
bool buffer_is_mono() const override { return false; }
|
||||
// A partial update if partial updates are enabled and this is not a full one.
|
||||
bool is_partial_push_() const { return this->update_count_ != 0 && this->sent_.is_valid(); }
|
||||
bool buffer_is_plane() const override { return false; }
|
||||
void plane_row(size_t y, uint8_t *out) override;
|
||||
void refresh_screen(bool partial) override;
|
||||
bool transfer_data() override;
|
||||
|
||||
@@ -364,6 +364,15 @@ bool HOT EPaperT133A01::transfer_data() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void EPaperT133A01::abort_transfer() {
|
||||
// A data stream is open, with one of the chip selects held low, whenever transfer_data() has started sending rows
|
||||
if (this->current_data_index_ == 0)
|
||||
return;
|
||||
this->disable();
|
||||
this->cs_pin_->digital_write(true);
|
||||
this->cs1_pin_->digital_write(true);
|
||||
}
|
||||
|
||||
void EPaperT133A01::dump_config() {
|
||||
EPaperBase::dump_config();
|
||||
LOG_PIN(" CS Pin: ", this->cs_pin_);
|
||||
|
||||
@@ -46,6 +46,7 @@ class EPaperT133A01 : public EPaperBase {
|
||||
void deep_sleep() override;
|
||||
|
||||
bool transfer_data() override;
|
||||
void abort_transfer() override;
|
||||
|
||||
/**
|
||||
* Send a command (and optional data) selecting one or both controllers.
|
||||
|
||||
@@ -37,6 +37,7 @@ class EPaperUC8179 final : public EPaperBase {
|
||||
}
|
||||
|
||||
protected:
|
||||
bool buffer_is_mono() const override { return true; }
|
||||
bool initialise(bool partial) override;
|
||||
bool transfer_data() override;
|
||||
void refresh_screen(bool partial) override;
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <deque>
|
||||
|
||||
namespace esphome::ezo {
|
||||
|
||||
static const char *const TAG = "ezo.sensor";
|
||||
ESPHOME_LOG_TAG(TAG, "ezo.sensor");
|
||||
|
||||
enum EzoCommandType : uint8_t {
|
||||
EZO_READ = 0,
|
||||
|
||||
@@ -330,7 +330,9 @@ async def write_image(
|
||||
if byte_order is not None:
|
||||
# Check for valid type has already been done in validate_settings
|
||||
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
|
||||
pixels = encoder.convert(image.resize((width, height)), path).getdata()
|
||||
pixels = encoder.convert(
|
||||
image.resize((width, height)), path
|
||||
).get_flattened_data()
|
||||
for row in range(height):
|
||||
for col in range(width):
|
||||
encoder.encode(pixels[row * width + col])
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "ili9xxx_defines.h"
|
||||
#include "ili9xxx_init.h"
|
||||
|
||||
namespace esphome::ili9xxx {
|
||||
|
||||
static const char *const TAG = "ili9xxx";
|
||||
ESPHOME_LOG_TAG(TAG, "ili9xxx");
|
||||
const size_t ILI9XXX_TRANSFER_BUFFER_SIZE = 126; // ensure this is divisible by 6
|
||||
|
||||
enum ILI9XXXColorMode {
|
||||
|
||||
@@ -891,12 +891,10 @@ static uint8_t color_to_nibble(const Color &color) {
|
||||
if (color.raw_32 == 0xFFFFFFFF)
|
||||
return 0x0F; // white
|
||||
|
||||
// Derive luma from RGB using Rec.601 weights (0.299/0.587/0.114, scaled by
|
||||
// 256). Rec.601 is the standard for converting SDR images to grayscale and
|
||||
// spreads saturated colours across the mid-range; Rec.709 instead crams them
|
||||
// against white/black where the 16 panel levels are hard to tell apart.
|
||||
auto luma = static_cast<uint8_t>((77u * color.r + 150u * color.g + 29u * color.b + 128u) >> 8);
|
||||
return quantize_8bit_to_nibble(luma);
|
||||
// Rec.601 luma is the standard for converting SDR images to grayscale and spreads saturated colours
|
||||
// across the mid-range; Rec.709 instead crams them against white/black where the 16 panel levels are
|
||||
// hard to tell apart.
|
||||
return quantize_8bit_to_nibble(display::rec601_luma(color));
|
||||
}
|
||||
|
||||
// 4x4 ordered (Bayer) dither threshold over the weighted-luma range (0..65535).
|
||||
|
||||
@@ -214,7 +214,7 @@ enum class Orientation : uint8_t {
|
||||
};
|
||||
|
||||
class LvglComponent final : public PollingComponent {
|
||||
constexpr static const char *const TAG = "lvgl";
|
||||
constexpr static const char *const TAG = "lvgl"; // NOLINT: lvgl does not build for ESP8266
|
||||
|
||||
public:
|
||||
LvglComponent(std::vector<display::Display *> displays, float buffer_frac, bool full_refresh, int draw_rounding,
|
||||
|
||||
@@ -19,7 +19,7 @@ using alloc_string_t = std::basic_string<char, std::char_traits<char>, RAMAlloca
|
||||
* @tparam V The type of the value in the mapping. Should be a basic type or pointer.
|
||||
*/
|
||||
|
||||
static const char *const TAG = "mapping";
|
||||
ESPHOME_LOG_TAG(TAG, "mapping");
|
||||
|
||||
template<typename K, typename V> class Mapping {
|
||||
public:
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
#ifdef USE_DASHBOARD_IMPORT
|
||||
#include "esphome/components/dashboard_import/dashboard_import.h"
|
||||
#endif
|
||||
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
|
||||
#include "esphome/components/ethernet/ethernet_component.h"
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::mdns {
|
||||
|
||||
@@ -39,6 +43,45 @@ MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp");
|
||||
// Wrap build-time defines into flash storage
|
||||
MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION);
|
||||
|
||||
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
|
||||
MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
|
||||
MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet");
|
||||
|
||||
// Same order as the default route in NetworkComponent::loop()
|
||||
const char *MDNSComponent::connected_network_() const {
|
||||
const bool ethernet = ethernet::global_eth_component->is_connected();
|
||||
const bool wifi = wifi::global_wifi_component->is_connected();
|
||||
#ifdef USE_NETWORK_PRIMARY_INTERFACE_WIFI
|
||||
if (wifi)
|
||||
return NETWORK_WIFI;
|
||||
#endif
|
||||
if (ethernet)
|
||||
return NETWORK_ETHERNET;
|
||||
if (wifi)
|
||||
return NETWORK_WIFI;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void MDNSComponent::update_network_txt_() {
|
||||
if (this->network_txt_ == nullptr)
|
||||
return;
|
||||
// With no interface connected, keep the last value instead of flapping to wifi
|
||||
const char *network = this->connected_network_();
|
||||
if (network == nullptr || network == this->network_txt_)
|
||||
return;
|
||||
if (!this->publish_network_txt_(network)) {
|
||||
// The _esphomelib service is missing, so a retry would fail too
|
||||
this->network_txt_ = nullptr;
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Network TXT: %s", network);
|
||||
this->network_txt_ = network;
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
this->network_record_->value = MDNS_STR(network);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
void MDNSComponent::setup_buffers_and_register_(PlatformRegisterFn platform_register) {
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
auto &services = this->services_;
|
||||
@@ -160,7 +203,15 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
|
||||
|
||||
txt_records.push_back({MDNS_STR(TXT_BOARD), MDNS_STR(VALUE_BOARD)});
|
||||
|
||||
#if defined(USE_WIFI)
|
||||
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
|
||||
const char *network = this->connected_network_();
|
||||
this->network_txt_ = network != nullptr ? network : NETWORK_WIFI;
|
||||
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(this->network_txt_)});
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
// services is services_ here, which never reallocates
|
||||
this->network_record_ = &txt_records.back();
|
||||
#endif
|
||||
#elif defined(USE_WIFI)
|
||||
MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
|
||||
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)});
|
||||
#elif defined(USE_ETHERNET)
|
||||
|
||||
@@ -32,6 +32,12 @@
|
||||
#define USE_MDNS_DEVICE_INFO_TXT
|
||||
#endif
|
||||
|
||||
// With both wifi and ethernet, the _esphomelib "network" TXT value follows the connected interface.
|
||||
// OpenThread hands its services to the SRP client once, so it keeps the fixed value.
|
||||
#if defined(USE_API) && defined(USE_WIFI) && defined(USE_ETHERNET) && !defined(USE_OPENTHREAD)
|
||||
#define USE_MDNS_DYNAMIC_NETWORK_TXT
|
||||
#endif
|
||||
|
||||
namespace esphome::mdns {
|
||||
|
||||
// Helper struct that identifies strings that may be stored in flash storage (similar to LogString)
|
||||
@@ -150,6 +156,22 @@ class MDNSComponent final : public Component
|
||||
|
||||
void setup_buffers_and_register_(PlatformRegisterFn platform_register);
|
||||
|
||||
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
|
||||
static constexpr uint32_t NETWORK_TXT_CHECK_INTERVAL_MS = 1000;
|
||||
/// The first connected interface in network: priority order ("ethernet" or "wifi"), else nullptr.
|
||||
const char *connected_network_() const;
|
||||
/// Check the connected interface and publish a changed "network" TXT value.
|
||||
void update_network_txt_();
|
||||
/// Platform-specific: replace the "network" TXT value of the registered _esphomelib service.
|
||||
bool publish_network_txt_(const char *network);
|
||||
/// Advertised value; nullptr until the _esphomelib service is registered, and after a failed publish.
|
||||
const char *network_txt_{nullptr};
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
/// The "network" record in services_, kept in step with network_txt_.
|
||||
MDNSTXTRecord *network_record_{nullptr};
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef USE_MDNS_DYNAMIC_TXT
|
||||
/// Storage for runtime-generated TXT values from user lambdas
|
||||
/// Pre-sized at compile time via MDNS_DYNAMIC_TXT_COUNT to avoid heap allocations.
|
||||
|
||||
@@ -89,7 +89,24 @@ bool MDNSComponent::set_service_enabled(const char *service_type, const char *pr
|
||||
}
|
||||
#endif // USE_MDNS_SUPPORTS_ENABLE_DISABLE && !USE_OPENTHREAD
|
||||
|
||||
void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp32); }
|
||||
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
|
||||
bool MDNSComponent::publish_network_txt_(const char *network) {
|
||||
// ESP-IDF copies the value and announces the changed record
|
||||
esp_err_t err = mdns_service_txt_item_set("_esphomelib", "_tcp", "network", network);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Failed to update network TXT: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void MDNSComponent::setup() {
|
||||
this->setup_buffers_and_register_(register_esp32);
|
||||
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
|
||||
this->set_interval(NETWORK_TXT_CHECK_INTERVAL_MS, [this]() { this->update_network_txt_(); });
|
||||
#endif
|
||||
}
|
||||
|
||||
void MDNSComponent::on_shutdown() {
|
||||
#ifndef USE_OPENTHREAD
|
||||
|
||||
@@ -46,7 +46,26 @@ void MDNSComponent::start_polling_window_() {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
|
||||
ESPHOME_LOG_TAG(TAG, "mdns");
|
||||
|
||||
bool MDNSComponent::publish_network_txt_(const char *network) {
|
||||
// LEAmDNS replaces the value of an existing key; announce() sends the change
|
||||
if (!MDNS.addServiceTxt("esphomelib", "tcp", "network", network)) {
|
||||
ESP_LOGW(TAG, "Failed to update network TXT");
|
||||
return false;
|
||||
}
|
||||
MDNS.announce();
|
||||
this->start_polling_window_();
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void MDNSComponent::setup() {
|
||||
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
|
||||
// No-op until the first IP-up event registers the services
|
||||
this->set_interval(NETWORK_TXT_CHECK_INTERVAL_MS, [this]() { this->update_network_txt_(); });
|
||||
#endif
|
||||
// arduino-pico stubs out LwipIntf::stateUpCB (the netif status callback LEAmDNS uses
|
||||
// on ESP8266 for auto-restart), so we must drive begin()/notifyAPChange() from our
|
||||
// own IP state listener. Both WiFi and Ethernet have the same listener signature —
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
namespace esphome::midea::ac {
|
||||
|
||||
const char *const Constants::TAG = "midea";
|
||||
const char Constants::TAG[] PROGMEM = "midea";
|
||||
const char *const Constants::FREEZE_PROTECTION = "Freeze Protection";
|
||||
const char *const Constants::SILENT = "Silent";
|
||||
const char *const Constants::TURBO = "Turbo";
|
||||
|
||||
@@ -17,7 +17,7 @@ using MideaPreset = dudanov::midea::ac::Preset;
|
||||
|
||||
class Constants {
|
||||
public:
|
||||
static const char *const TAG;
|
||||
static const char TAG[]; // PROGMEM, like ESPHOME_LOG_TAG
|
||||
static const char *const FREEZE_PROTECTION;
|
||||
static const char *const SILENT;
|
||||
static const char *const TURBO;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
namespace esphome::mipi_dsi {
|
||||
|
||||
constexpr static const char *const TAG = "display.mipi_dsi";
|
||||
ESPHOME_LOG_TAG(TAG, "display.mipi_dsi");
|
||||
const uint8_t SW_RESET_CMD = 0x01;
|
||||
const uint8_t SLEEP_OUT = 0x11;
|
||||
const uint8_t SDIR_CMD = 0xC7;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S31)
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
#include <esp_lcd_panel_rgb.h>
|
||||
#ifdef USE_SPI
|
||||
@@ -10,7 +11,7 @@
|
||||
|
||||
namespace esphome::mipi_rgb {
|
||||
|
||||
constexpr static const char *const TAG = "display.mipi_rgb";
|
||||
ESPHOME_LOG_TAG(TAG, "display.mipi_rgb");
|
||||
const uint8_t SW_RESET_CMD = 0x01;
|
||||
const uint8_t SLEEP_OUT = 0x11;
|
||||
const uint8_t SDIR_CMD = 0xC7;
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::mipi_spi {
|
||||
|
||||
constexpr static const char *const TAG = "display.mipi_spi";
|
||||
ESPHOME_LOG_TAG(TAG, "display.mipi_spi");
|
||||
|
||||
// Maximum bytes to log for commands (truncated if larger)
|
||||
static constexpr size_t MIPI_SPI_MAX_CMD_LOG_BYTES = 64;
|
||||
|
||||
@@ -1040,11 +1040,13 @@ def run_compile(args, config: ConfigType) -> bool:
|
||||
framework_ver = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||
bootloader = zephyr_data()[KEY_BOOTLOADER]
|
||||
|
||||
# (dev_type, sd_req) per bootloader — values from Nordic SoftDevice release notes
|
||||
# (dev_type, sd_req) per bootloader — values from Nordic SoftDevice release notes.
|
||||
# The bootloader resets on a package whose sd_req does not list its SoftDevice,
|
||||
# so v7 accepts S140 7.0.1, 7.2.0 and 7.3.0 (e.g. Seeed XIAO nRF52840).
|
||||
_GENPKG_PARAMS = {
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD132: ("0x0051", "0x009D"),
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: ("0x0052", "0x00B6"),
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: ("0x0052", "0x00CA"),
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: ("0x0052", "0x00CA,0x0100,0x0123"),
|
||||
}
|
||||
# UF2 family IDs — nRF52832 vs nRF52840 per SoftDevice variant
|
||||
_UF2_FAMILY_IDS = {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_INPUT,
|
||||
CONF_INTERRUPT_PIN,
|
||||
CONF_INVERTED,
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
CONF_OUTPUT,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["gpio_expander"]
|
||||
CODEOWNERS = ["@ssieb", "@ajfriesen"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "pca9505"
|
||||
MULTI_CONF = True
|
||||
|
||||
pca9505_ns = cg.esphome_ns.namespace("pca9505")
|
||||
|
||||
PCA9505Component = pca9505_ns.class_("PCA9505Component", cg.Component, i2c.I2CDevice)
|
||||
PCA9505GPIOPin = pca9505_ns.class_(
|
||||
"PCA9505GPIOPin", cg.GPIOPin, cg.Parented.template(PCA9505Component)
|
||||
)
|
||||
|
||||
CONF_PCA9505 = "pca9505"
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(PCA9505Component),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x20))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
if interrupt_pin := config.get(CONF_INTERRUPT_PIN):
|
||||
cg.add(var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin)))
|
||||
|
||||
|
||||
def validate_mode(value: ConfigType) -> ConfigType:
|
||||
if not (value[CONF_INPUT] or value[CONF_OUTPUT]):
|
||||
raise cv.Invalid("Mode must be either input or output")
|
||||
if value[CONF_INPUT] and value[CONF_OUTPUT]:
|
||||
raise cv.Invalid("Mode must be either input or output")
|
||||
return value
|
||||
|
||||
|
||||
PCA9505_PIN_SCHEMA = pins.gpio_base_schema(
|
||||
PCA9505GPIOPin,
|
||||
cv.int_range(min=0, max=39),
|
||||
modes=[CONF_INPUT, CONF_OUTPUT],
|
||||
mode_validator=validate_mode,
|
||||
).extend(
|
||||
{
|
||||
cv.Required(CONF_PCA9505): cv.use_id(PCA9505Component),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pins.PIN_SCHEMA_REGISTRY.register(CONF_PCA9505, PCA9505_PIN_SCHEMA)
|
||||
async def pca9505_pin_to_code(config: ConfigType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_parented(var, config[CONF_PCA9505])
|
||||
|
||||
cg.add(var.set_pin(config[CONF_NUMBER]))
|
||||
cg.add(var.set_inverted(config[CONF_INVERTED]))
|
||||
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
|
||||
return var
|
||||
@@ -0,0 +1,165 @@
|
||||
#include "pca9505.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pca9505 {
|
||||
|
||||
// Each register block holds one byte per port. Add the port number to reach a single
|
||||
// port, or set the auto-increment bit to transfer the whole block in one go.
|
||||
static constexpr uint8_t INPUT_REG = 0x00;
|
||||
static constexpr uint8_t OUTPUT_REG = 0x08;
|
||||
static constexpr uint8_t INVERT_REG = 0x10;
|
||||
static constexpr uint8_t CONFIG_REG = 0x18;
|
||||
static constexpr uint8_t MASK_REG = 0x20;
|
||||
static constexpr uint8_t AUTO_INCREMENT = 0x80;
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "pca9505");
|
||||
|
||||
void PCA9505Component::setup() {
|
||||
// Test to see if device exists
|
||||
if (!this->read_bank_(0)) {
|
||||
ESP_LOGE(TAG, "PCA9505 not detected at 0x%02X", this->address_);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// No polarity inversion
|
||||
this->write_all_banks_(INVERT_REG, 0x00);
|
||||
// All outputs low, before anything is switched to being an output
|
||||
this->write_all_banks_(OUTPUT_REG, 0x00);
|
||||
// All inputs at initialization. The part sees a 1 as an input, so this is the
|
||||
// inverse of config_, which is all zeroes here.
|
||||
this->write_all_banks_(CONFIG_REG, 0xFF);
|
||||
|
||||
// Mask every interrupt. This must be written, not assumed: the chip keeps its
|
||||
// registers through an MCU reboot, so a previous run may have left pins unmasked.
|
||||
for (uint8_t &bank : this->mask_) {
|
||||
bank = 0xFF;
|
||||
}
|
||||
this->write_all_banks_(MASK_REG, 0xFF);
|
||||
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->attach_interrupt(&PCA9505Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
// Don't invalidate cache on read - only invalidate when interrupt fires
|
||||
this->set_invalidate_on_read_(false);
|
||||
}
|
||||
|
||||
// Nothing to poll until pin_mode() registers an input pin.
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void IRAM_ATTR PCA9505Component::gpio_intr(PCA9505Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
void PCA9505Component::loop() {
|
||||
// Invalidate the cache so the next digital_read() triggers a fresh I2C read
|
||||
this->reset_pin_cache_();
|
||||
// Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the
|
||||
// I2C read leave INT asserted without re-firing a falling edge, which would strand us with
|
||||
// stale state forever; keep looping until the line is released so we self-heal.
|
||||
if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void PCA9505Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "PCA9505:");
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
LOG_I2C_DEVICE(this)
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
}
|
||||
|
||||
bool PCA9505Component::digital_read_hw(uint8_t pin) { return this->read_bank_(pin / 8); }
|
||||
|
||||
bool PCA9505Component::digital_read_cache(uint8_t pin) { return this->input_[pin / 8] & (1 << (pin % 8)); }
|
||||
|
||||
void PCA9505Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
const uint8_t bank = pin / 8;
|
||||
const uint8_t mask = 1 << (pin % 8);
|
||||
if (value) {
|
||||
this->output_[bank] |= mask;
|
||||
} else {
|
||||
this->output_[bank] &= ~mask;
|
||||
}
|
||||
this->write_bank_(OUTPUT_REG, bank, this->output_[bank]);
|
||||
}
|
||||
|
||||
void PCA9505Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
const uint8_t bank = pin / 8;
|
||||
const uint8_t mask = 1 << (pin % 8);
|
||||
if (flags & gpio::FLAG_INPUT) {
|
||||
this->config_[bank] &= ~mask;
|
||||
// An input may raise INT; the polling loop is only needed when there is no interrupt pin.
|
||||
this->mask_[bank] &= ~mask;
|
||||
if (this->interrupt_pin_ == nullptr) {
|
||||
this->enable_loop();
|
||||
}
|
||||
} else if (flags & gpio::FLAG_OUTPUT) {
|
||||
this->config_[bank] |= mask;
|
||||
// An output must never raise INT.
|
||||
this->mask_[bank] |= mask;
|
||||
}
|
||||
// Invert as the part sees a 1 as an input
|
||||
this->write_bank_(CONFIG_REG, bank, ~this->config_[bank]);
|
||||
// Without an interrupt pin INT is unconnected, so the masks written at setup can stay
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->write_bank_(MASK_REG, bank, this->mask_[bank]);
|
||||
}
|
||||
}
|
||||
|
||||
bool PCA9505Component::read_bank_(uint8_t bank) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
|
||||
if (this->read_register(INPUT_REG + bank, &this->input_[bank], 1) != i2c::ERROR_OK) {
|
||||
this->status_set_warning(LOG_STR("Failed to read input register"));
|
||||
return false;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PCA9505Component::write_bank_(uint8_t reg, uint8_t bank, uint8_t value) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
|
||||
if (this->write_register(reg + bank, &value, 1) != i2c::ERROR_OK) {
|
||||
this->status_set_warning(LOG_STR("Failed to write register"));
|
||||
return false;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PCA9505Component::write_all_banks_(uint8_t reg, uint8_t value) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
|
||||
uint8_t values[BANK_COUNT];
|
||||
for (uint8_t &bank : values) {
|
||||
bank = value;
|
||||
}
|
||||
if (this->write_register(reg | AUTO_INCREMENT, values, sizeof(values)) != i2c::ERROR_OK) {
|
||||
this->status_set_warning(LOG_STR("Failed to write register"));
|
||||
return false;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
return true;
|
||||
}
|
||||
|
||||
float PCA9505Component::get_setup_priority() const { return setup_priority::IO; }
|
||||
|
||||
void PCA9505GPIOPin::setup() { this->pin_mode(this->flags_); }
|
||||
|
||||
void PCA9505GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
|
||||
|
||||
bool PCA9505GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
|
||||
|
||||
void PCA9505GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }
|
||||
|
||||
size_t PCA9505GPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return buf_append_printf(buffer, len, 0, "%u via PCA9505", this->pin_);
|
||||
}
|
||||
|
||||
} // namespace esphome::pca9505
|
||||
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/gpio_expander/cached_gpio.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome::pca9505 {
|
||||
|
||||
/// Number of I/O pins on the PCA9505 and PCA9506.
|
||||
static constexpr uint8_t PIN_COUNT = 40;
|
||||
/// The pins are grouped into five ports of eight, each with its own set of registers.
|
||||
static constexpr uint8_t BANK_COUNT = PIN_COUNT / 8;
|
||||
|
||||
class PCA9505Component final : public Component,
|
||||
public i2c::I2CDevice,
|
||||
public gpio_expander::CachedGpioExpander<uint8_t, PIN_COUNT> {
|
||||
public:
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
static void IRAM_ATTR gpio_intr(PCA9505Component *arg);
|
||||
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
void digital_write_hw(uint8_t pin, bool value) override;
|
||||
|
||||
bool read_bank_(uint8_t bank);
|
||||
bool write_bank_(uint8_t reg, uint8_t bank, uint8_t value);
|
||||
bool write_all_banks_(uint8_t reg, uint8_t value);
|
||||
|
||||
/// The pin config per bank - 1 means OUTPUT, 0 means INPUT
|
||||
uint8_t config_[BANK_COUNT]{};
|
||||
/// The output state per bank - 1 means HIGH, 0 means LOW
|
||||
uint8_t output_[BANK_COUNT]{};
|
||||
/// The input pin states per bank - 1 means HIGH, 0 means LOW
|
||||
uint8_t input_[BANK_COUNT]{};
|
||||
/// Which pins may pull INT low - 1 means masked, matching the chip
|
||||
uint8_t mask_[BANK_COUNT]{};
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
};
|
||||
|
||||
class PCA9505GPIOPin final : public GPIOPin, public Parented<PCA9505Component> {
|
||||
public:
|
||||
void setup() override;
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
|
||||
void set_pin(uint8_t pin) { this->pin_ = pin; }
|
||||
void set_inverted(bool inverted) { this->inverted_ = inverted; }
|
||||
void set_flags(gpio::Flags flags) { this->flags_ = flags; }
|
||||
|
||||
gpio::Flags get_flags() const override { return this->flags_; }
|
||||
|
||||
protected:
|
||||
uint8_t pin_{0};
|
||||
bool inverted_{false};
|
||||
gpio::Flags flags_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::pca9505
|
||||
@@ -35,7 +35,7 @@ CONFIG_SCHEMA = cv.ensure_list(
|
||||
|
||||
|
||||
async def to_code(config: list[ConfigType]) -> None:
|
||||
cg.add_library("wjtje/qr-code-generator-library", "^1.7.0")
|
||||
cg.add_library("wjtje/qr-code-generator-library", "1.7.0")
|
||||
|
||||
for entry in config:
|
||||
var = cg.new_Pvariable(entry[CONF_ID])
|
||||
|
||||
@@ -8,12 +8,13 @@
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "esp_lcd_panel_rgb.h"
|
||||
|
||||
namespace esphome::qspi_dbi {
|
||||
|
||||
constexpr static const char *const TAG = "display.qspi_dbi";
|
||||
ESPHOME_LOG_TAG(TAG, "display.qspi_dbi");
|
||||
static const uint8_t SW_RESET_CMD = 0x01;
|
||||
static const uint8_t SLEEP_OUT = 0x11;
|
||||
static const uint8_t NORON = 0x13;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
namespace esphome::rpi_dpi_rgb {
|
||||
|
||||
constexpr static const char *const TAG = "rpi_dpi_rgb";
|
||||
ESPHOME_LOG_TAG(TAG, "rpi_dpi_rgb");
|
||||
|
||||
class RpiDpiRgb final : public display::Display {
|
||||
public:
|
||||
|
||||
@@ -14,6 +14,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
CODEOWNERS = ["@glmnet", "@ximex"]
|
||||
DOMAIN = "rtttl"
|
||||
CONF_RTTTL = "rtttl"
|
||||
# The player keeps the song length in 16 bits
|
||||
MAX_SONG_LENGTH = 65535
|
||||
CONF_ON_FINISHED_PLAYBACK = "on_finished_playback"
|
||||
|
||||
rtttl_ns = cg.esphome_ns.namespace("rtttl")
|
||||
@@ -97,16 +99,23 @@ async def to_code(config: ConfigType) -> None:
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
def _static_song(config: ConfigType, value: str) -> str:
|
||||
# A constant song plays straight from static storage, flash on ESP8266, with no copy per play
|
||||
return f"esphome::rtttl::StaticSong{{{cg.FlashStringLiteral(value)}}}"
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
"rtttl.play",
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.use_id(Rtttl),
|
||||
cv.Required(CONF_RTTTL): cv.templatable(cv.string),
|
||||
cv.Required(CONF_RTTTL): cv.templatable(
|
||||
cv.All(cv.string, cv.ByteLength(max=MAX_SONG_LENGTH))
|
||||
),
|
||||
},
|
||||
key=CONF_RTTTL,
|
||||
),
|
||||
automation.ApplyField(CONF_RTTTL, "play", cg.std_string),
|
||||
automation.ApplyField(CONF_RTTTL, "play", cg.std_string, const_fn=_static_song),
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "rtttl.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
@@ -147,14 +150,14 @@ void Rtttl::loop() {
|
||||
#endif // USE_SPEAKER
|
||||
|
||||
// Align to note: most rtttl's out there does not add any space after the ',' separator but just in case
|
||||
while (this->position_ < this->rtttl_.length()) {
|
||||
char c = this->rtttl_[this->position_];
|
||||
while (this->position_ < this->song_len_) {
|
||||
char c = this->song_at_(this->position_);
|
||||
if (c != ',' && c != ' ')
|
||||
break;
|
||||
this->position_++;
|
||||
}
|
||||
|
||||
if (this->position_ >= this->rtttl_.length()) {
|
||||
if (this->position_ >= this->song_len_) {
|
||||
this->finish_();
|
||||
return;
|
||||
}
|
||||
@@ -169,12 +172,12 @@ void Rtttl::loop() {
|
||||
this->note_duration_ = this->wholenote_duration_ / this->default_note_denominator_;
|
||||
}
|
||||
|
||||
uint8_t note_index_in_octave = note_index_from_char(this->rtttl_[this->position_]);
|
||||
uint8_t note_index_in_octave = note_index_from_char(this->song_at_(this->position_));
|
||||
|
||||
this->position_++;
|
||||
|
||||
// Now, get optional '#' sharp
|
||||
if (this->rtttl_[this->position_] == '#') {
|
||||
if (this->song_at_(this->position_) == '#') {
|
||||
note_index_in_octave++;
|
||||
this->position_++;
|
||||
}
|
||||
@@ -192,7 +195,7 @@ void Rtttl::loop() {
|
||||
}
|
||||
|
||||
// Now, get optional '.' dotted note
|
||||
if (this->rtttl_[this->position_] == '.') {
|
||||
if (this->song_at_(this->position_) == '.') {
|
||||
this->note_duration_ += this->note_duration_ / 2; // Duration +50%
|
||||
this->position_++;
|
||||
}
|
||||
@@ -264,15 +267,68 @@ void Rtttl::loop() {
|
||||
}
|
||||
|
||||
void Rtttl::play(std::string rtttl) {
|
||||
if (this->state_ != State::STOPPED && this->state_ != State::STOPPING) {
|
||||
size_t pos = this->rtttl_.find(':');
|
||||
size_t len = (pos != std::string::npos) ? pos : this->rtttl_.length();
|
||||
ESP_LOGW(TAG, "Already playing: %.*s", (int) len, this->rtttl_.c_str());
|
||||
if (this->is_busy_()) {
|
||||
return;
|
||||
}
|
||||
this->owned_song_ = std::move(rtttl);
|
||||
this->set_song_(this->owned_song_.data(), this->owned_song_.size());
|
||||
this->start_();
|
||||
}
|
||||
|
||||
this->rtttl_ = std::move(rtttl);
|
||||
void Rtttl::play(StaticSong song) {
|
||||
if (this->is_busy_()) {
|
||||
return;
|
||||
}
|
||||
// Release a previously owned song; the static one needs no storage.
|
||||
std::string().swap(this->owned_song_);
|
||||
const char *data = reinterpret_cast<const char *>(song.rtttl);
|
||||
this->set_song_(data, ESPHOME_strlen_P(data));
|
||||
this->start_();
|
||||
}
|
||||
|
||||
void Rtttl::set_song_(const char *song, size_t len) {
|
||||
this->song_ = song;
|
||||
// Anything past what the 16 bit length holds is dropped; the validator keeps configured songs shorter.
|
||||
this->song_len_ = static_cast<uint16_t>(std::min<size_t>(len, std::numeric_limits<uint16_t>::max()));
|
||||
}
|
||||
|
||||
bool Rtttl::is_busy_() const {
|
||||
if (this->state_ == State::STOPPED || this->state_ == State::STOPPING) {
|
||||
return false;
|
||||
}
|
||||
char name[SONG_NAME_LENGTH_LIMIT + 1];
|
||||
this->song_name_(name, sizeof(name), this->song_find_(':', 0, this->song_len_));
|
||||
ESP_LOGW(TAG, "Already playing: %s", name);
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t Rtttl::song_find_(char c, size_t from, size_t end) const {
|
||||
for (size_t pos = from; pos < end; pos++) {
|
||||
if (this->song_at_(pos) == c)
|
||||
return pos;
|
||||
}
|
||||
return end;
|
||||
}
|
||||
|
||||
size_t Rtttl::song_find_control_(char key, size_t from, size_t end) const {
|
||||
size_t pos = from;
|
||||
while ((pos = this->song_find_(key, pos, end)) < end) {
|
||||
if (this->song_at_(pos + 1) == '=')
|
||||
return pos;
|
||||
pos++;
|
||||
}
|
||||
return end;
|
||||
}
|
||||
|
||||
void Rtttl::song_name_(char *buf, size_t size, size_t name_len) const {
|
||||
size_t len = std::min(name_len, size - 1);
|
||||
for (size_t pos = 0; pos < len; pos++) {
|
||||
buf[pos] = this->song_at_(pos);
|
||||
}
|
||||
buf[len] = '\0';
|
||||
}
|
||||
|
||||
void Rtttl::start_() {
|
||||
this->default_note_denominator_ = DEFAULT_NOTE_DENOMINATOR;
|
||||
this->default_octave_ = DEFAULT_OCTAVE;
|
||||
this->note_duration_ = 0;
|
||||
@@ -281,9 +337,9 @@ void Rtttl::play(std::string rtttl) {
|
||||
uint16_t num; // Used for: default note-denominator, default octave, BPM
|
||||
|
||||
// Get name
|
||||
this->position_ = this->rtttl_.find(':');
|
||||
this->position_ = this->song_find_(':', 0, this->song_len_);
|
||||
|
||||
if (this->position_ == std::string::npos) {
|
||||
if (this->position_ == this->song_len_) {
|
||||
ESP_LOGE(TAG, "Unable to determine name; missing ':'");
|
||||
return;
|
||||
}
|
||||
@@ -292,18 +348,20 @@ void Rtttl::play(std::string rtttl) {
|
||||
static_cast<unsigned>(SONG_NAME_LENGTH_LIMIT));
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Playing song %.*s", (int) this->position_, this->rtttl_.c_str());
|
||||
char name[SONG_NAME_LENGTH_LIMIT + 1];
|
||||
this->song_name_(name, sizeof(name), this->position_);
|
||||
ESP_LOGD(TAG, "Playing song %s", name);
|
||||
|
||||
size_t name_end_position = this->position_;
|
||||
size_t control_end = this->rtttl_.find(':', name_end_position + 1);
|
||||
if (control_end == std::string::npos) {
|
||||
size_t control_end = this->song_find_(':', name_end_position + 1, this->song_len_);
|
||||
if (control_end == this->song_len_) {
|
||||
ESP_LOGE(TAG, "Missing second ':'");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get default duration
|
||||
size_t pos = this->rtttl_.find("d=", name_end_position);
|
||||
if (pos == std::string::npos || pos >= control_end) {
|
||||
size_t pos = this->song_find_control_('d', name_end_position, control_end);
|
||||
if (pos == control_end) {
|
||||
ESP_LOGW(TAG, "Missing 'd='; use default duration %d", this->default_note_denominator_);
|
||||
} else {
|
||||
this->position_ = pos + 2;
|
||||
@@ -317,8 +375,8 @@ void Rtttl::play(std::string rtttl) {
|
||||
}
|
||||
|
||||
// Get default octave
|
||||
pos = this->rtttl_.find("o=", name_end_position);
|
||||
if (pos == std::string::npos || pos >= control_end) {
|
||||
pos = this->song_find_control_('o', name_end_position, control_end);
|
||||
if (pos == control_end) {
|
||||
ESP_LOGW(TAG, "Missing 'o='; use default octave %d", this->default_octave_);
|
||||
} else {
|
||||
this->position_ = pos + 2;
|
||||
@@ -332,8 +390,8 @@ void Rtttl::play(std::string rtttl) {
|
||||
}
|
||||
|
||||
// Get BPM
|
||||
pos = this->rtttl_.find("b=", name_end_position);
|
||||
if (pos == std::string::npos || pos >= control_end) {
|
||||
pos = this->song_find_control_('b', name_end_position, control_end);
|
||||
if (pos == control_end) {
|
||||
ESP_LOGW(TAG, "Missing 'b='; use default BPM %d", bpm);
|
||||
} else {
|
||||
this->position_ = pos + 2;
|
||||
@@ -387,7 +445,7 @@ void Rtttl::stop() {
|
||||
}
|
||||
#endif // USE_SPEAKER
|
||||
|
||||
this->position_ = this->rtttl_.length();
|
||||
this->position_ = this->song_len_;
|
||||
this->note_duration_ = 0;
|
||||
}
|
||||
|
||||
@@ -411,7 +469,7 @@ void Rtttl::finish_() {
|
||||
#endif // USE_SPEAKER
|
||||
|
||||
// Ensure no more notes are played in case finish_() is called for an error.
|
||||
this->position_ = this->rtttl_.length();
|
||||
this->position_ = this->song_len_;
|
||||
this->note_duration_ = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
#ifdef USE_OUTPUT
|
||||
#include "esphome/components/output/float_output.h"
|
||||
@@ -14,6 +15,10 @@
|
||||
|
||||
namespace esphome::rtttl {
|
||||
|
||||
namespace testing {
|
||||
class RtttlParserAccess;
|
||||
} // namespace testing
|
||||
|
||||
inline constexpr uint8_t DEFAULT_NOTE_DENOMINATOR = 4; // Default note-denominator (quarter note)
|
||||
inline constexpr uint8_t DEFAULT_OCTAVE =
|
||||
6; // Default octave for a note (see: `MIN_OCTAVE`, `MAX_OCTAVE` in `rtttl.cpp`)
|
||||
@@ -26,6 +31,11 @@ enum class State : uint8_t {
|
||||
STOPPING,
|
||||
};
|
||||
|
||||
/// A song in storage that outlives playback, such as a literal from generated code; PROGMEM on ESP8266.
|
||||
struct StaticSong {
|
||||
ProgmemStr rtttl;
|
||||
};
|
||||
|
||||
class Rtttl final : public Component {
|
||||
public:
|
||||
#ifdef USE_OUTPUT
|
||||
@@ -38,7 +48,10 @@ class Rtttl final : public Component {
|
||||
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
/// Play a song; the string is copied so it may come from anywhere.
|
||||
void play(std::string rtttl);
|
||||
/// Play a song from static storage without copying it.
|
||||
void play(StaticSong song);
|
||||
void stop();
|
||||
|
||||
float get_gain() { return this->gain_; }
|
||||
@@ -55,11 +68,29 @@ class Rtttl final : public Component {
|
||||
protected:
|
||||
inline uint16_t get_integer_() {
|
||||
uint16_t ret = 0;
|
||||
while (isdigit(this->rtttl_[this->position_])) {
|
||||
ret = (ret * 10) + (this->rtttl_[this->position_++] - '0');
|
||||
while (isdigit(this->song_at_(this->position_))) {
|
||||
ret = (ret * 10) + (this->song_at_(this->position_++) - '0');
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
/// The byte of the song at pos, or NUL past its end; the read is flash safe for a PROGMEM song on ESP8266.
|
||||
char song_at_(size_t pos) const {
|
||||
if (pos >= this->song_len_)
|
||||
return '\0';
|
||||
return static_cast<char>(progmem_read_byte(reinterpret_cast<const uint8_t *>(this->song_) + pos));
|
||||
}
|
||||
/// Index of the first `c` in [from, end), or `end` when absent.
|
||||
size_t song_find_(char c, size_t from, size_t end) const;
|
||||
/// Index of `key` followed by '=' in [from, end), or `end` when absent.
|
||||
size_t song_find_control_(char key, size_t from, size_t end) const;
|
||||
/// Copies the first name_len bytes of the song, its name, into buf for logging.
|
||||
void song_name_(char *buf, size_t size, size_t name_len) const;
|
||||
/// Points the view at a song, clamped to what song_len_ can hold.
|
||||
void set_song_(const char *song, size_t len);
|
||||
/// Parses the header and starts playback of the song the view points at.
|
||||
void start_();
|
||||
/// Logs and returns true when a song is still playing, so a new one must wait.
|
||||
bool is_busy_() const;
|
||||
/**
|
||||
* @brief Finalizes the playback of the RTTTL string.
|
||||
*
|
||||
@@ -70,26 +101,30 @@ class Rtttl final : public Component {
|
||||
void finish_();
|
||||
void set_state_(State state);
|
||||
|
||||
/// The RTTTL string to play.
|
||||
std::string rtttl_;
|
||||
/// The current position in the RTTTL string.
|
||||
size_t position_{0};
|
||||
/// The default duration of a note (e.g. 4 for a quarter note).
|
||||
uint8_t default_note_denominator_{DEFAULT_NOTE_DENOMINATOR};
|
||||
/// The default octave for a note.
|
||||
uint8_t default_octave_{DEFAULT_OCTAVE};
|
||||
/// The duration of the current note in milliseconds.
|
||||
uint16_t note_duration_{0};
|
||||
/// The duration of a whole note in milliseconds.
|
||||
uint16_t wholenote_duration_;
|
||||
/// The current state of the RTTTL player.
|
||||
State state_{State::STOPPED};
|
||||
/// The song being played; points at owned_song_ or at static storage.
|
||||
const char *song_{nullptr};
|
||||
/// Backing store for play(std::string).
|
||||
std::string owned_song_;
|
||||
/// The time in milliseconds since microcontroller boot when the last note was started.
|
||||
uint32_t last_note_start_time_;
|
||||
/// The frequency of the current note in Hz.
|
||||
uint32_t output_freq_{0};
|
||||
/// The gain of the output.
|
||||
float gain_{0.6f};
|
||||
/// Length of the song in bytes; songs are a few hundred bytes, so 16 bits is ample.
|
||||
uint16_t song_len_{0};
|
||||
/// The current position in the song.
|
||||
uint16_t position_{0};
|
||||
/// The duration of the current note in milliseconds.
|
||||
uint16_t note_duration_{0};
|
||||
/// The duration of a whole note in milliseconds.
|
||||
uint16_t wholenote_duration_;
|
||||
/// The default duration of a note (e.g. 4 for a quarter note).
|
||||
uint8_t default_note_denominator_{DEFAULT_NOTE_DENOMINATOR};
|
||||
/// The default octave for a note.
|
||||
uint8_t default_octave_{DEFAULT_OCTAVE};
|
||||
/// The current state of the RTTTL player.
|
||||
State state_{State::STOPPED};
|
||||
|
||||
#ifdef USE_OUTPUT
|
||||
/// The output to write the sound to.
|
||||
@@ -113,6 +148,8 @@ class Rtttl final : public Component {
|
||||
/// The callback to call when playback is finished.
|
||||
CallbackManager<void()> on_finished_playback_callback_;
|
||||
#endif
|
||||
|
||||
friend class testing::RtttlParserAccess;
|
||||
};
|
||||
|
||||
} // namespace esphome::rtttl
|
||||
|
||||
@@ -115,7 +115,7 @@ class PNGFormat(Format):
|
||||
|
||||
def actions(self) -> None:
|
||||
cg.add_define("USE_RUNTIME_IMAGE_PNG")
|
||||
cg.add_library("pngle", "1.1.0")
|
||||
cg.add_library("kikuchan98/pngle", "1.1.0")
|
||||
|
||||
|
||||
class QOIFormat(Format):
|
||||
|
||||
@@ -15,7 +15,7 @@ class Component; // Forward declaration
|
||||
|
||||
namespace runtime_stats {
|
||||
|
||||
static const char *const TAG = "runtime_stats";
|
||||
ESPHOME_LOG_TAG(TAG, "runtime_stats");
|
||||
|
||||
class RuntimeStatsCollector final {
|
||||
public:
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
namespace esphome::sdl {
|
||||
|
||||
constexpr static const char *const TAG = "sdl";
|
||||
ESPHOME_LOG_TAG(TAG, "sdl");
|
||||
|
||||
class Sdl final : public display::Display, public snapshot::Snapshot {
|
||||
public:
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
namespace esphome::spi_led_strip {
|
||||
|
||||
static const char *const TAG = "spi_led_strip";
|
||||
ESPHOME_LOG_TAG(TAG, "spi_led_strip");
|
||||
class SpiLedStrip final : public light::AddressableLight,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH,
|
||||
spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_1MHZ> {
|
||||
|
||||
@@ -8,13 +8,14 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esp_lcd_panel_ops.h"
|
||||
|
||||
#include "esp_lcd_panel_rgb.h"
|
||||
|
||||
namespace esphome::st7701s {
|
||||
|
||||
constexpr static const char *const TAG = "display.st7701s";
|
||||
ESPHOME_LOG_TAG(TAG, "display.st7701s");
|
||||
const uint8_t SW_RESET_CMD = 0x01;
|
||||
const uint8_t SLEEP_OUT = 0x11;
|
||||
const uint8_t SDIR_CMD = 0xC7;
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
#include "esphome/components/logger/logger.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/time.h"
|
||||
|
||||
namespace esphome::syslog {
|
||||
|
||||
// Map log levels to syslog severity using an array, indexed by ESPHome log level (1-7)
|
||||
constexpr int LOG_LEVEL_TO_SYSLOG_SEVERITY[] = {
|
||||
static constexpr uint8_t LOG_LEVEL_TO_SYSLOG_SEVERITY[] PROGMEM = {
|
||||
3, // NONE
|
||||
3, // ERROR
|
||||
4, // WARN
|
||||
@@ -35,7 +36,7 @@ void Syslog::log_(const int level, const char *tag, const char *message, size_t
|
||||
// Syslog PRI calculation: facility * 8 + severity
|
||||
int severity = 7;
|
||||
if ((unsigned) level <= 7) {
|
||||
severity = LOG_LEVEL_TO_SYSLOG_SEVERITY[level];
|
||||
severity = progmem_read_byte(&LOG_LEVEL_TO_SYSLOG_SEVERITY[level]);
|
||||
}
|
||||
int pri = this->facility_ * 8 + severity;
|
||||
|
||||
|
||||
@@ -240,12 +240,6 @@ def validate_thermostat(config: ConfigType) -> ConfigType:
|
||||
CONF_SUPPLEMENTAL_HEATING_ACTION,
|
||||
CONF_SUPPLEMENTAL_HEATING_DELTA,
|
||||
],
|
||||
CONF_MIN_COOLING_OFF_TIME: [
|
||||
CONF_COOL_ACTION,
|
||||
],
|
||||
CONF_MIN_COOLING_RUN_TIME: [
|
||||
CONF_COOL_ACTION,
|
||||
],
|
||||
CONF_MIN_FANNING_OFF_TIME: [
|
||||
CONF_FAN_ONLY_ACTION,
|
||||
],
|
||||
@@ -284,6 +278,17 @@ def validate_thermostat(config: ConfigType) -> ConfigType:
|
||||
f"{req_trigger} must be defined to use {config_trigger}"
|
||||
)
|
||||
|
||||
# Dry mode runs on the cooling timers, so a dehumidifier needs no cool_action.
|
||||
for timer in (CONF_MIN_COOLING_OFF_TIME, CONF_MIN_COOLING_RUN_TIME):
|
||||
if (
|
||||
timer in config
|
||||
and CONF_COOL_ACTION not in config
|
||||
and CONF_DRY_ACTION not in config
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"{CONF_COOL_ACTION} or {CONF_DRY_ACTION} must be defined to use {timer}"
|
||||
)
|
||||
|
||||
if CONF_FAN_ONLY_ACTION in config:
|
||||
# determine validation requirements based on fan_only_action_uses_fan_mode_timer setting
|
||||
if config[CONF_FAN_ONLY_ACTION_USES_FAN_MODE_TIMER] is True:
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
from esphome.components.tmc22xx import CONF_ANALOG_CURRENT_SCALE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_AMPERE,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import TMC2209Stepper, tmc2209_ns
|
||||
|
||||
CONF_ACTUAL_CURRENT = "actual_current"
|
||||
CONF_MOTOR_LOAD = "motor_load"
|
||||
CONF_PWM_GRAD_AUTO = "pwm_grad_auto"
|
||||
CONF_PWM_OFS_AUTO = "pwm_ofs_auto"
|
||||
CONF_PWM_SCALE_AUTO = "pwm_scale_auto"
|
||||
CONF_PWM_SCALE_SUM = "pwm_scale_sum"
|
||||
CONF_STALLGUARD_RESULT = "stallguard_result"
|
||||
CONF_TMC2209_ID = "tmc2209_id"
|
||||
|
||||
TMC2209Sensor = tmc2209_ns.class_("TMC2209Sensor", cg.PollingComponent)
|
||||
|
||||
_diagnostic_schema = sensor.sensor_schema(
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(TMC2209Sensor),
|
||||
cv.GenerateID(CONF_TMC2209_ID): cv.use_id(TMC2209Stepper),
|
||||
cv.Optional(CONF_ACTUAL_CURRENT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_MOTOR_LOAD): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_STALLGUARD_RESULT): _diagnostic_schema,
|
||||
cv.Optional(CONF_PWM_SCALE_SUM): _diagnostic_schema,
|
||||
cv.Optional(CONF_PWM_SCALE_AUTO): _diagnostic_schema,
|
||||
cv.Optional(CONF_PWM_OFS_AUTO): _diagnostic_schema,
|
||||
cv.Optional(CONF_PWM_GRAD_AUTO): _diagnostic_schema,
|
||||
}
|
||||
).extend(cv.polling_component_schema("60s"))
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> ConfigType:
|
||||
if CONF_ACTUAL_CURRENT not in config:
|
||||
return config
|
||||
full_config = fv.full_config.get()
|
||||
path = full_config.get_path_for_id(config[CONF_TMC2209_ID])[:-1]
|
||||
if full_config.get_config_for_path(path).get(CONF_ANALOG_CURRENT_SCALE):
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_ACTUAL_CURRENT}' can't be used with '{CONF_ANALOG_CURRENT_SCALE}', "
|
||||
"the current then depends on VREF",
|
||||
path=[CONF_ACTUAL_CURRENT],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_TMC2209_ID])
|
||||
var = cg.new_Pvariable(config[CONF_ID], hub)
|
||||
await cg.register_component(var, config)
|
||||
sensors = sensor.sub_sensors(config)
|
||||
await sensors(CONF_ACTUAL_CURRENT, var.set_actual_current_sensor)
|
||||
await sensors(CONF_STALLGUARD_RESULT, var.set_stallguard_result_sensor)
|
||||
await sensors(CONF_MOTOR_LOAD, var.set_motor_load_sensor)
|
||||
await sensors(CONF_PWM_SCALE_SUM, var.set_pwm_scale_sum_sensor)
|
||||
await sensors(CONF_PWM_SCALE_AUTO, var.set_pwm_scale_auto_sensor)
|
||||
await sensors(CONF_PWM_OFS_AUTO, var.set_pwm_ofs_auto_sensor)
|
||||
await sensors(CONF_PWM_GRAD_AUTO, var.set_pwm_grad_auto_sensor)
|
||||
@@ -0,0 +1,95 @@
|
||||
#include "tmc2209_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::tmc2209 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "tmc2209.sensor");
|
||||
|
||||
using tmc22xx::TMC22XXStepper;
|
||||
|
||||
// Highest StallGuard result, reached with no load
|
||||
static constexpr float STALLGUARD_RESULT_MAX = 510.0f;
|
||||
|
||||
static void publish_field(sensor::Sensor *sensor, const optional<uint32_t> &data, const RegisterField &field) {
|
||||
if (sensor == nullptr)
|
||||
return;
|
||||
if (!data.has_value()) {
|
||||
sensor->publish_state(NAN);
|
||||
return;
|
||||
}
|
||||
uint32_t value = TMC22XXStepper::extract_field(*data, field);
|
||||
sensor->publish_state(field.is_signed ? static_cast<float>(static_cast<int32_t>(value)) : static_cast<float>(value));
|
||||
}
|
||||
|
||||
void TMC2209Sensor::update() {
|
||||
if (this->parent_->is_failed())
|
||||
return;
|
||||
|
||||
// Each failed read blocks the loop for the UART timeout. Leave the bus alone while the driver is known to be
|
||||
// unreachable or after the first failed read; the remaining sensors then publish NaN.
|
||||
bool reachable = !this->parent_->is_unreachable();
|
||||
auto read = [this, &reachable](uint8_t reg) -> optional<uint32_t> {
|
||||
if (!reachable)
|
||||
return {};
|
||||
auto data = this->parent_->read_register(reg);
|
||||
reachable = data.has_value();
|
||||
return data;
|
||||
};
|
||||
|
||||
if (this->actual_current_sensor_ != nullptr) {
|
||||
auto drv_status = read(tmc22xx::REG_DRV_STATUS);
|
||||
float current = NAN;
|
||||
if (drv_status.has_value()) {
|
||||
auto scale = static_cast<uint8_t>(TMC22XXStepper::extract_field(*drv_status, tmc22xx::CS_ACTUAL));
|
||||
current = this->parent_->scale_to_current(scale);
|
||||
}
|
||||
this->actual_current_sensor_->publish_state(current);
|
||||
}
|
||||
|
||||
// StallGuard only measures the load while the motor turns
|
||||
bool moving = !this->parent_->has_reached_target();
|
||||
if (this->stallguard_result_sensor_ != nullptr || (this->motor_load_sensor_ != nullptr && moving)) {
|
||||
auto sg_result = read(REG_SG_RESULT);
|
||||
publish_field(this->stallguard_result_sensor_, sg_result, SG_RESULT);
|
||||
if (this->motor_load_sensor_ != nullptr) {
|
||||
// 100% is the load at which a stall is reported, where the result drops below twice the threshold
|
||||
float stall_result = 2.0f * this->parent_->get_stallguard_threshold();
|
||||
float load = NAN;
|
||||
if (moving && sg_result.has_value() && stall_result < STALLGUARD_RESULT_MAX) {
|
||||
float result = TMC22XXStepper::extract_field(*sg_result, SG_RESULT);
|
||||
load = (STALLGUARD_RESULT_MAX - result) / (STALLGUARD_RESULT_MAX - stall_result) * 100.0f;
|
||||
}
|
||||
this->motor_load_sensor_->publish_state(load);
|
||||
}
|
||||
} else if (this->motor_load_sensor_ != nullptr) {
|
||||
this->motor_load_sensor_->publish_state(NAN);
|
||||
}
|
||||
|
||||
if (this->pwm_scale_sum_sensor_ != nullptr || this->pwm_scale_auto_sensor_ != nullptr) {
|
||||
auto pwm_scale = read(tmc22xx::REG_PWM_SCALE);
|
||||
publish_field(this->pwm_scale_sum_sensor_, pwm_scale, tmc22xx::PWM_SCALE_SUM);
|
||||
publish_field(this->pwm_scale_auto_sensor_, pwm_scale, tmc22xx::PWM_SCALE_AUTO);
|
||||
}
|
||||
|
||||
if (this->pwm_ofs_auto_sensor_ != nullptr || this->pwm_grad_auto_sensor_ != nullptr) {
|
||||
auto pwm_auto = read(tmc22xx::REG_PWM_AUTO);
|
||||
publish_field(this->pwm_ofs_auto_sensor_, pwm_auto, tmc22xx::PWM_OFS_AUTO);
|
||||
publish_field(this->pwm_grad_auto_sensor_, pwm_auto, tmc22xx::PWM_GRAD_AUTO);
|
||||
}
|
||||
}
|
||||
|
||||
void TMC2209Sensor::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "TMC2209 Sensor:");
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Actual Current", this->actual_current_sensor_);
|
||||
LOG_SENSOR(" ", "StallGuard Result", this->stallguard_result_sensor_);
|
||||
LOG_SENSOR(" ", "Motor Load", this->motor_load_sensor_);
|
||||
LOG_SENSOR(" ", "PWM Scale Sum", this->pwm_scale_sum_sensor_);
|
||||
LOG_SENSOR(" ", "PWM Scale Auto", this->pwm_scale_auto_sensor_);
|
||||
LOG_SENSOR(" ", "PWM Offset Auto", this->pwm_ofs_auto_sensor_);
|
||||
LOG_SENSOR(" ", "PWM Gradient Auto", this->pwm_grad_auto_sensor_);
|
||||
}
|
||||
|
||||
} // namespace esphome::tmc2209
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/tmc2209/tmc2209.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::tmc2209 {
|
||||
|
||||
class TMC2209Sensor : public PollingComponent {
|
||||
public:
|
||||
explicit TMC2209Sensor(TMC2209Stepper *parent) : parent_(parent) {}
|
||||
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_actual_current_sensor(sensor::Sensor *sensor) { this->actual_current_sensor_ = sensor; }
|
||||
void set_stallguard_result_sensor(sensor::Sensor *sensor) { this->stallguard_result_sensor_ = sensor; }
|
||||
void set_motor_load_sensor(sensor::Sensor *sensor) { this->motor_load_sensor_ = sensor; }
|
||||
void set_pwm_scale_sum_sensor(sensor::Sensor *sensor) { this->pwm_scale_sum_sensor_ = sensor; }
|
||||
void set_pwm_scale_auto_sensor(sensor::Sensor *sensor) { this->pwm_scale_auto_sensor_ = sensor; }
|
||||
void set_pwm_ofs_auto_sensor(sensor::Sensor *sensor) { this->pwm_ofs_auto_sensor_ = sensor; }
|
||||
void set_pwm_grad_auto_sensor(sensor::Sensor *sensor) { this->pwm_grad_auto_sensor_ = sensor; }
|
||||
|
||||
protected:
|
||||
TMC2209Stepper *parent_;
|
||||
sensor::Sensor *actual_current_sensor_{nullptr};
|
||||
sensor::Sensor *stallguard_result_sensor_{nullptr};
|
||||
sensor::Sensor *motor_load_sensor_{nullptr};
|
||||
sensor::Sensor *pwm_scale_sum_sensor_{nullptr};
|
||||
sensor::Sensor *pwm_scale_auto_sensor_{nullptr};
|
||||
sensor::Sensor *pwm_ofs_auto_sensor_{nullptr};
|
||||
sensor::Sensor *pwm_grad_auto_sensor_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace esphome::tmc2209
|
||||
@@ -9,10 +9,12 @@ using tmc22xx::RegisterField;
|
||||
// StallGuard and CoolStep registers, which the TMC2208 does not have
|
||||
static constexpr uint8_t REG_TCOOLTHRS = 0x14;
|
||||
static constexpr uint8_t REG_SGTHRS = 0x40;
|
||||
static constexpr uint8_t REG_SG_RESULT = 0x41;
|
||||
static constexpr uint8_t REG_COOLCONF = 0x42;
|
||||
|
||||
static constexpr RegisterField TCOOLTHRS{REG_TCOOLTHRS, 0, 20, false};
|
||||
static constexpr RegisterField SGTHRS{REG_SGTHRS, 0, 8, false};
|
||||
static constexpr RegisterField SG_RESULT{REG_SG_RESULT, 0, 10, false};
|
||||
static constexpr RegisterField SEMIN{REG_COOLCONF, 0, 4, false};
|
||||
static constexpr RegisterField SEUP{REG_COOLCONF, 5, 2, false};
|
||||
static constexpr RegisterField SEMAX{REG_COOLCONF, 8, 4, false};
|
||||
@@ -30,6 +32,7 @@ class TMC2209Stepper : public tmc22xx::TMC22XXStepper {
|
||||
|
||||
/// A stall is reported when the StallGuard result drops below twice this value.
|
||||
void set_stallguard_threshold(uint8_t threshold) { this->write_field(SGTHRS, threshold); }
|
||||
uint8_t get_stallguard_threshold() const { return extract_field(this->sgthrs_, SGTHRS); }
|
||||
/// Lower velocity limit, as TSTEP, for CoolStep and the StallGuard output on DIAG.
|
||||
void set_tcool_threshold(uint32_t threshold) { this->write_field(TCOOLTHRS, threshold); }
|
||||
void set_semin(uint8_t semin) { this->write_field(SEMIN, semin); }
|
||||
|
||||
@@ -33,6 +33,8 @@ static constexpr uint8_t REG_VACTUAL = 0x22;
|
||||
static constexpr uint8_t REG_CHOPCONF = 0x6C;
|
||||
static constexpr uint8_t REG_DRV_STATUS = 0x6F;
|
||||
static constexpr uint8_t REG_PWMCONF = 0x70;
|
||||
static constexpr uint8_t REG_PWM_SCALE = 0x71;
|
||||
static constexpr uint8_t REG_PWM_AUTO = 0x72;
|
||||
|
||||
// Readable registers that a driver reset sets back to their defaults; the last written values are restored
|
||||
static constexpr std::array<uint8_t, 4> RESTORED_REGISTERS{REG_GCONF, REG_FACTORY_CONF, REG_CHOPCONF, REG_PWMCONF};
|
||||
@@ -59,7 +61,12 @@ static constexpr RegisterField VSENSE{REG_CHOPCONF, 17, 1, false};
|
||||
static constexpr RegisterField MRES{REG_CHOPCONF, 24, 4, false};
|
||||
static constexpr RegisterField INTPOL{REG_CHOPCONF, 28, 1, false};
|
||||
static constexpr RegisterField DEDGE{REG_CHOPCONF, 29, 1, false};
|
||||
static constexpr RegisterField CS_ACTUAL{REG_DRV_STATUS, 16, 5, false};
|
||||
static constexpr RegisterField FREEWHEEL{REG_PWMCONF, 20, 2, false};
|
||||
static constexpr RegisterField PWM_SCALE_SUM{REG_PWM_SCALE, 0, 8, false};
|
||||
static constexpr RegisterField PWM_SCALE_AUTO{REG_PWM_SCALE, 16, 9, true};
|
||||
static constexpr RegisterField PWM_OFS_AUTO{REG_PWM_AUTO, 0, 8, false};
|
||||
static constexpr RegisterField PWM_GRAD_AUTO{REG_PWM_AUTO, 16, 8, false};
|
||||
|
||||
enum StandstillMode : uint8_t {
|
||||
STANDSTILL_MODE_NORMAL = 0,
|
||||
@@ -129,6 +136,8 @@ class TMC22XXStepper : public stepper::Stepper, public Component, public uart::U
|
||||
/// Enable or disable the motor outputs, using the ENN pin when configured and TOFF otherwise.
|
||||
void set_enabled(bool enabled);
|
||||
bool is_enabled() const { return this->enabled_; }
|
||||
/// True while the driver does not answer, until the status poll reads it again (every 10 s).
|
||||
bool is_unreachable() const { return this->status_poll_skip_ > 0; }
|
||||
|
||||
/// Called with the condition and whether it became active or cleared.
|
||||
template<typename F> void add_on_status_callback(F &&callback) {
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::toshiba {
|
||||
|
||||
struct RacPt1411hwruFanSpeed {
|
||||
@@ -69,8 +67,8 @@ const uint8_t RAC_PT1411HWRU_CS_FOOTER_COOL = 0x72;
|
||||
const uint8_t RAC_PT1411HWRU_CS_FOOTER_HEAT = 0x7E;
|
||||
// RAC-PT1411HWRU Swing
|
||||
const uint8_t RAC_PT1411HWRU_SWING_HEADER = 0xB9;
|
||||
const std::vector<uint8_t> RAC_PT1411HWRU_SWING_VERTICAL{0xB9, 0x46, 0xF5, 0x0A, 0x04, 0xFB};
|
||||
const std::vector<uint8_t> RAC_PT1411HWRU_SWING_OFF{0xB9, 0x46, 0xF5, 0x0A, 0x05, 0xFA};
|
||||
static constexpr uint8_t RAC_PT1411HWRU_SWING_VERTICAL[] PROGMEM = {0xB9, 0x46, 0xF5, 0x0A, 0x04, 0xFB};
|
||||
static constexpr uint8_t RAC_PT1411HWRU_SWING_OFF[] PROGMEM = {0xB9, 0x46, 0xF5, 0x0A, 0x05, 0xFA};
|
||||
// RAC-PT1411HWRU Fan speeds
|
||||
const uint8_t RAC_PT1411HWRU_FAN_OFF = 0x7B;
|
||||
constexpr RacPt1411hwruFanSpeed RAC_PT1411HWRU_FAN_AUTO{0xBF, 0x66};
|
||||
@@ -92,12 +90,12 @@ const uint8_t RAC_PT1411HWRU_TEMPERATURE_FAN_ONLY = 0x0E;
|
||||
// Hence these look-up tables. In addition, the upper nibble is used here for additional
|
||||
// "negative" and "fractional value" flags as required for some temperatures.
|
||||
// RAC-PT1411HWRU °C Temperatures (short codes)
|
||||
const std::vector<uint8_t> RAC_PT1411HWRU_TEMPERATURE_C{0x10, 0x00, 0x01, 0x03, 0x02, 0x06, 0x07, 0x05,
|
||||
0x04, 0x0C, 0x0D, 0x09, 0x08, 0x0A, 0x0B};
|
||||
static constexpr uint8_t RAC_PT1411HWRU_TEMPERATURE_C[] PROGMEM = {0x10, 0x00, 0x01, 0x03, 0x02, 0x06, 0x07, 0x05,
|
||||
0x04, 0x0C, 0x0D, 0x09, 0x08, 0x0A, 0x0B};
|
||||
// RAC-PT1411HWRU °F Temperatures (short codes)
|
||||
const std::vector<uint8_t> RAC_PT1411HWRU_TEMPERATURE_F{0x10, 0x30, 0x00, 0x20, 0x01, 0x21, 0x03, 0x23, 0x02,
|
||||
0x22, 0x06, 0x26, 0x07, 0x05, 0x25, 0x04, 0x24, 0x0C,
|
||||
0x2C, 0x0D, 0x2D, 0x09, 0x08, 0x28, 0x0A, 0x2A, 0x0B};
|
||||
static constexpr uint8_t RAC_PT1411HWRU_TEMPERATURE_F[] PROGMEM = {
|
||||
0x10, 0x30, 0x00, 0x20, 0x01, 0x21, 0x03, 0x23, 0x02, 0x22, 0x06, 0x26, 0x07, 0x05,
|
||||
0x25, 0x04, 0x24, 0x0C, 0x2C, 0x0D, 0x2D, 0x09, 0x08, 0x28, 0x0A, 0x2A, 0x0B};
|
||||
|
||||
// RAS-2819T protocol constants
|
||||
const uint16_t RAS_2819T_HEADER1 = 0xC23D;
|
||||
@@ -213,7 +211,7 @@ static constexpr uint8_t SEIYA_SWING_HORIZONTAL = 0x09;
|
||||
static constexpr uint8_t SEIYA_SWING_NEUTRAL = 0x17; // no swing change in this frame
|
||||
|
||||
// RAS-2819T temperature codes for 18-30°C
|
||||
static const uint8_t RAS_2819T_TEMP_CODES[] = {
|
||||
static constexpr uint8_t RAS_2819T_TEMP_CODES[] PROGMEM = {
|
||||
0x10, // 18°C
|
||||
0x30, // 19°C
|
||||
0x20, // 20°C
|
||||
@@ -328,7 +326,7 @@ static uint8_t get_ras_2819t_temp_code(float temperature) {
|
||||
return 0x40; // Default to 24°C
|
||||
}
|
||||
|
||||
return RAS_2819T_TEMP_CODES[temp_index];
|
||||
return progmem_read_byte(&RAS_2819T_TEMP_CODES[temp_index]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -339,7 +337,7 @@ static float decode_ras_2819t_temperature(uint8_t temp_code) {
|
||||
|
||||
// Find the code in the temperature array
|
||||
for (size_t temp_index = 0; temp_index < sizeof(RAS_2819T_TEMP_CODES); temp_index++) {
|
||||
if (RAS_2819T_TEMP_CODES[temp_index] == base_temp_code) {
|
||||
if (progmem_read_byte(&RAS_2819T_TEMP_CODES[temp_index]) == base_temp_code) {
|
||||
return static_cast<float>(temp_index + 18); // 18°C is the minimum
|
||||
}
|
||||
}
|
||||
@@ -680,10 +678,10 @@ void ToshibaClimate::transmit_rac_pt1411hwru_() {
|
||||
index = static_cast<uint8_t>(roundf(temp_adjd));
|
||||
|
||||
if (this->model_ == MODEL_RAC_PT1411HWRU_F) {
|
||||
code = RAC_PT1411HWRU_TEMPERATURE_F[index];
|
||||
code = progmem_read_byte(&RAC_PT1411HWRU_TEMPERATURE_F[index]);
|
||||
message[9] |= RAC_PT1411HWRU_FLAG_FAH;
|
||||
} else {
|
||||
code = RAC_PT1411HWRU_TEMPERATURE_C[index];
|
||||
code = progmem_read_byte(&RAC_PT1411HWRU_TEMPERATURE_C[index]);
|
||||
}
|
||||
if ((this->mode == climate::CLIMATE_MODE_FAN_ONLY) || (this->mode == climate::CLIMATE_MODE_OFF)) {
|
||||
code = RAC_PT1411HWRU_TEMPERATURE_FAN_ONLY;
|
||||
@@ -752,15 +750,11 @@ void ToshibaClimate::transmit_rac_pt1411hwru_() {
|
||||
// Swing Mode
|
||||
data->reset();
|
||||
data->space(TOSHIBA_PACKET_SPACE);
|
||||
switch (this->swing_mode) {
|
||||
case climate::CLIMATE_SWING_VERTICAL:
|
||||
this->encode_(data, &RAC_PT1411HWRU_SWING_VERTICAL[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
|
||||
break;
|
||||
|
||||
case climate::CLIMATE_SWING_OFF:
|
||||
default:
|
||||
this->encode_(data, &RAC_PT1411HWRU_SWING_OFF[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
|
||||
}
|
||||
const uint8_t *swing_frame =
|
||||
this->swing_mode == climate::CLIMATE_SWING_VERTICAL ? RAC_PT1411HWRU_SWING_VERTICAL : RAC_PT1411HWRU_SWING_OFF;
|
||||
uint8_t swing[RAC_PT1411HWRU_MESSAGE_LENGTH];
|
||||
progmem_memcpy(swing, swing_frame, sizeof(swing));
|
||||
this->encode_(data, swing, sizeof(swing), 1);
|
||||
|
||||
data->space(TOSHIBA_PACKET_SPACE);
|
||||
transmit.perform();
|
||||
@@ -1300,14 +1294,14 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
temperature_code =
|
||||
(message[4] >> 4) | (message[14] & RAC_PT1411HWRU_FLAG_FRAC) | (message[15] & RAC_PT1411HWRU_FLAG_NEG);
|
||||
if (message[15] & RAC_PT1411HWRU_FLAG_FAH) {
|
||||
for (size_t i = 0; i < RAC_PT1411HWRU_TEMPERATURE_F.size(); i++) {
|
||||
if (RAC_PT1411HWRU_TEMPERATURE_F[i] == temperature_code) {
|
||||
for (size_t i = 0; i < sizeof(RAC_PT1411HWRU_TEMPERATURE_F); i++) {
|
||||
if (progmem_read_byte(&RAC_PT1411HWRU_TEMPERATURE_F[i]) == temperature_code) {
|
||||
this->target_temperature = static_cast<float>((i + TOSHIBA_RAC_PT1411HWRU_TEMP_F_MIN - 32) * 5) / 9;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (size_t i = 0; i < RAC_PT1411HWRU_TEMPERATURE_C.size(); i++) {
|
||||
if (RAC_PT1411HWRU_TEMPERATURE_C[i] == temperature_code) {
|
||||
for (size_t i = 0; i < sizeof(RAC_PT1411HWRU_TEMPERATURE_C); i++) {
|
||||
if (progmem_read_byte(&RAC_PT1411HWRU_TEMPERATURE_C[i]) == temperature_code) {
|
||||
this->target_temperature = i + TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN;
|
||||
}
|
||||
}
|
||||
@@ -1333,7 +1327,7 @@ bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
break;
|
||||
// Swing mode
|
||||
case RAC_PT1411HWRU_SWING_HEADER:
|
||||
if (message[4] == RAC_PT1411HWRU_SWING_VERTICAL[4]) {
|
||||
if (message[4] == progmem_read_byte(&RAC_PT1411HWRU_SWING_VERTICAL[4])) {
|
||||
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
|
||||
} else {
|
||||
this->swing_mode = climate::CLIMATE_SWING_OFF;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <freertos/task.h>
|
||||
#include "esphome/core/lock_free_queue.h"
|
||||
#include "esphome/core/event_pool.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <atomic>
|
||||
#include <span>
|
||||
|
||||
@@ -39,7 +40,7 @@ namespace esphome::usb_host {
|
||||
// - Main loop controls backpressure by deciding when to restart after consuming data
|
||||
// The atomic bitmask ensures thread-safe allocation/deallocation without mutex blocking.
|
||||
|
||||
static const char *const TAG = "usb_host";
|
||||
ESPHOME_LOG_TAG(TAG, "usb_host");
|
||||
|
||||
// Forward declarations
|
||||
struct TransferRequest;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "esphome/components/usb_host/usb_host.h"
|
||||
#include "esphome/core/lock_free_queue.h"
|
||||
#include "esphome/core/event_pool.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
|
||||
@@ -19,7 +20,7 @@ class USBUartComponent;
|
||||
class USBUartChannelBase;
|
||||
class USBUartTypePL2303;
|
||||
|
||||
static const char *const TAG = "usb_uart";
|
||||
ESPHOME_LOG_TAG(TAG, "usb_uart");
|
||||
|
||||
static constexpr uint8_t USB_CDC_SUBCLASS_ACM = 0x02;
|
||||
static constexpr uint8_t USB_SUBCLASS_COMMON = 0x02;
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
static const char *const TAG = "Zio Ultrasonic";
|
||||
ESPHOME_LOG_TAG(TAG, "Zio Ultrasonic");
|
||||
|
||||
namespace esphome::zio_ultrasonic {
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
#define USE_DISPLAY
|
||||
#define DISPLAY_PAGE_CHANGE_CALLBACK_COUNT 1
|
||||
#define USE_EMONTX_VERSION
|
||||
#define USE_EMONTX_VERSION_TEXT_SENSOR
|
||||
#define USE_ENTITY_DEVICE_CLASS
|
||||
#define USE_ENTITY_ICON
|
||||
#define USE_ENTITY_UNIT_OF_MEASUREMENT
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <cmath>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
@@ -315,6 +316,57 @@ size_t truncate_name_to(char *buffer, size_t buffer_size, const char *name, size
|
||||
|
||||
// Parsing & formatting
|
||||
|
||||
// A plain decimal converts exactly with one division: the digits accumulate
|
||||
// into an integer that stays below 2^24 and the fraction's scale is a power of
|
||||
// ten up to 1e9 (2^9 * 5^9), so both convert to float exactly and IEEE division
|
||||
// rounds the quotient correctly, the same result strtof produces.
|
||||
static constexpr uint32_t PARSE_FLOAT_MANTISSA_LIMIT = 1000000; // one more digit stays below 2^24
|
||||
static constexpr uint32_t PARSE_FLOAT_MAX_FRACTION_DIGITS = 9;
|
||||
|
||||
optional<float> parse_float(const char *str) {
|
||||
const char *p = str;
|
||||
const bool negative = *p == '-';
|
||||
if (negative || *p == '+')
|
||||
p++;
|
||||
const char *digits = p;
|
||||
const char *dot = nullptr;
|
||||
uint32_t mantissa = 0;
|
||||
for (;; p++) {
|
||||
const uint32_t d = static_cast<unsigned char>(*p) - '0';
|
||||
if (d > 9) {
|
||||
if (*p == '.' && dot == nullptr) {
|
||||
dot = p;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (mantissa >= PARSE_FLOAT_MANTISSA_LIMIT)
|
||||
break;
|
||||
mantissa = mantissa * 10 + d;
|
||||
}
|
||||
const uint32_t fraction_digits = dot != nullptr ? p - dot - 1 : 0;
|
||||
const bool any_digit = p - digits > (dot != nullptr ? 1 : 0);
|
||||
if (*p == '\0' && any_digit && fraction_digits <= PARSE_FLOAT_MAX_FRACTION_DIGITS) {
|
||||
// The mantissa is exact in a float, so the sign goes on before the one rounding step
|
||||
float value = static_cast<float>(mantissa);
|
||||
if (negative)
|
||||
value = -value;
|
||||
if (fraction_digits != 0) {
|
||||
uint32_t scale = 10;
|
||||
for (uint32_t i = 1; i < fraction_digits; i++)
|
||||
scale *= 10;
|
||||
value /= static_cast<float>(scale);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
// Exponents, hex, inf, nan, leading whitespace and longer inputs take the libc path.
|
||||
char *end = nullptr;
|
||||
const float value = ::strtof(str, &end);
|
||||
if (end == str || *end != '\0' || value == HUGE_VALF)
|
||||
return {};
|
||||
return value;
|
||||
}
|
||||
|
||||
size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) {
|
||||
size_t chars = std::min(length, 2 * count);
|
||||
for (size_t i = 2 * count - chars; i < 2 * count; i++, str++) {
|
||||
|
||||
@@ -1299,13 +1299,11 @@ template<typename T, enable_if_t<(std::is_integral<T>::value && std::is_signed<T
|
||||
optional<T> parse_number(const std::string &str) {
|
||||
return parse_number<T>(str.c_str());
|
||||
}
|
||||
/// Parse a decimal floating-point number; plain decimals skip the libc parser.
|
||||
optional<float> parse_float(const char *str);
|
||||
/// Parse a decimal floating-point number from a null-terminated string.
|
||||
template<typename T, enable_if_t<(std::is_same<T, float>::value), int> = 0> optional<T> parse_number(const char *str) {
|
||||
char *end = nullptr;
|
||||
float value = ::strtof(str, &end);
|
||||
if (end == str || *end != '\0' || value == HUGE_VALF)
|
||||
return {};
|
||||
return value;
|
||||
return parse_float(str);
|
||||
}
|
||||
/// Parse a decimal floating-point number.
|
||||
template<typename T, enable_if_t<(std::is_same<T, float>::value), int> = 0>
|
||||
|
||||
+3
-2
@@ -190,8 +190,9 @@ struct LogString;
|
||||
|
||||
#endif
|
||||
|
||||
// Declares a static log tag at namespace scope in a .cpp file; on ESP8266 it lives in flash, so read it with
|
||||
// the _P functions, never strcmp/strlen.
|
||||
// Declares a static log tag at namespace scope, in a source file or a header; on ESP8266 it lives in flash, so
|
||||
// read it with the _P functions, never strcmp/strlen. Not usable for a class member: the ESP8266 form is a
|
||||
// PROGMEM array, which cannot be initialized in a class.
|
||||
#ifdef USE_ESP8266
|
||||
#define ESPHOME_LOG_TAG(name, tag) static const char name[] PROGMEM = tag
|
||||
#else
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace esphome {
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
std::atomic<uint8_t> g_wake_requested{0};
|
||||
|
||||
static const char *const TAG = "wake";
|
||||
ESPHOME_LOG_TAG(TAG, "wake");
|
||||
|
||||
namespace internal {
|
||||
// File-scope state — referenced inline by wake_drain_notifications() and
|
||||
|
||||
@@ -21,6 +21,8 @@ dependencies:
|
||||
version: "1.8.2"
|
||||
espressif/esp-tflite-micro:
|
||||
version: 1.3.3~1
|
||||
espressif/esp-nn:
|
||||
version: 1.1.2
|
||||
espressif/esp32-camera:
|
||||
version: 2.1.7
|
||||
espressif/mdns:
|
||||
|
||||
+5
-5
@@ -46,9 +46,9 @@ lib_deps =
|
||||
${common.lib_deps_base}
|
||||
https://github.com/dudanov/MideaUART.git#eeea6c3e9b4474f067054592b435be1c4e466815 ; midea
|
||||
improv/Improv@1.2.7 ; improv_serial / improv_ble
|
||||
kikuchan98/pngle@1.1.0 ; online_image
|
||||
kikuchan98/pngle@1.1.0 ; runtime_image
|
||||
; Using the repository directly, otherwise ESP-IDF can't use the library
|
||||
https://github.com/bitbank2/JPEGDEC.git#1.8.4 ; online_image
|
||||
https://github.com/bitbank2/JPEGDEC.git#1.8.4 ; runtime_image
|
||||
; This dependency is used only in unit tests.
|
||||
; Must coincide with PLATFORMIO_GOOGLE_TEST_LIB in scripts/cpp_unit_test.py
|
||||
; See scripts/cpp_unit_test.py and tests/components/README.md
|
||||
@@ -82,7 +82,7 @@ lib_deps =
|
||||
esphome/noise-c@0.1.31 ; noise (api, ota)
|
||||
SPI ; spi (Arduino built-in)
|
||||
Wire ; i2c (Arduino built-int)
|
||||
heman/AsyncMqttClient-esphome@1.0.0 ; mqtt
|
||||
heman/AsyncMqttClient-esphome@2.0.0 ; mqtt
|
||||
freekode/TM1651@1.0.1 ; tm1651
|
||||
tonia/HeatpumpIR@1.0.42 ; heatpumpir
|
||||
build_flags =
|
||||
@@ -252,8 +252,8 @@ lib_deps =
|
||||
DNSServer ; captive_portal
|
||||
heman/AsyncMqttClient-esphome@2.0.0 ; mqtt
|
||||
improv/Improv@1.2.7 ; improv_serial
|
||||
kikuchan98/pngle@1.1.0 ; online_image
|
||||
https://github.com/bitbank2/JPEGDEC.git#1.8.4 ; online_image
|
||||
kikuchan98/pngle@1.1.0 ; runtime_image
|
||||
https://github.com/bitbank2/JPEGDEC.git#1.8.4 ; runtime_image
|
||||
build_flags =
|
||||
${common:arduino.build_flags}
|
||||
-DUSE_LIBRETINY
|
||||
|
||||
+12
-6
@@ -1269,11 +1269,15 @@ def lint_no_std_nothrow(fname, match):
|
||||
)
|
||||
|
||||
|
||||
# Every C++ source and header; plain C files cannot use the macro
|
||||
LOG_TAG_INCLUDE = ["esphome/*.cpp", "esphome/*.h"]
|
||||
|
||||
|
||||
@lint_re_check(
|
||||
r"^\s*(?:(?:static|constexpr|inline)\s+)*(?:const\s+)?char\s*(?:\*\s*(?:const\s+)?TAG|\s(?:const\s+)?TAG\s*\[\w*\])"
|
||||
r"\s*(?:=\s*\{?|[{(])\s*\"",
|
||||
r"^\s*(?:(?:static|constexpr|inline)\s+)*(?:const\s+)?char\s*(?:\*\s*(?:const\s+)?(?:\w+::)*TAG|\s(?:const\s+)?(?:\w+::)*TAG\s*\[\w*\])"
|
||||
r"\s*(?:=\s*\{?|[{(])\s*\"" + CPP_RE_EOL,
|
||||
prefilter="TAG",
|
||||
include=["esphome/components/*.cpp", "esphome/components/**/*.cpp"],
|
||||
include=LOG_TAG_INCLUDE,
|
||||
)
|
||||
def lint_log_tag_macro(fname, match):
|
||||
return (
|
||||
@@ -1281,6 +1285,8 @@ def lint_log_tag_macro(fname, match):
|
||||
f"ESP8266.\n"
|
||||
f" Before: {highlight('static const char *const TAG = "name";')}\n"
|
||||
f" After: {highlight('ESPHOME_LOG_TAG(TAG, "name");')}\n"
|
||||
f"A class member tag cannot use the macro; declare {highlight('static const char TAG[];')} in the class "
|
||||
f"and define {highlight('const char Foo::TAG[] PROGMEM = "name";')} in the .cpp.\n"
|
||||
f"Never pass such a TAG to set_timeout/set_interval names or string functions.\n"
|
||||
f"(If strictly necessary, add `// NOLINT` to the end of the line)"
|
||||
)
|
||||
@@ -1288,11 +1294,11 @@ def lint_log_tag_macro(fname, match):
|
||||
|
||||
@lint_re_check(
|
||||
r"(?:\b(?:set_timeout|set_interval|set_retry|cancel_timeout|cancel_interval|cancel_retry|defer)"
|
||||
r"|\b(?:strcmp|strncmp|strcasecmp|strlen|strcpy|strncpy)|std::string)\s*\((?:(?:[^();]|\([^()]*\))*,)?\s*TAG\b"
|
||||
r"|\bstd::string\s+\w+\s*[={(]\s*TAG\b",
|
||||
r"|\b(?:strcmp|strncmp|strcasecmp|strlen|strcpy|strncpy)|std::string)\s*\((?:(?:[^();]|\([^()]*\))*,)?\s*(?:\w+::)*TAG\b"
|
||||
r"|\bstd::string\s+\w+\s*[={(]\s*(?:\w+::)*TAG\b",
|
||||
mask=True,
|
||||
prefilter="TAG",
|
||||
include=["esphome/components/*.cpp", "esphome/components/**/*.cpp"],
|
||||
include=LOG_TAG_INCLUDE,
|
||||
)
|
||||
def lint_log_tag_as_string(fname, match):
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#if defined(USE_API_PLAINTEXT) && defined(USE_API_HOMEASSISTANT_STATES) && defined(USE_SENSOR)
|
||||
|
||||
#include <benchmark/benchmark.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
#include "esphome/components/api/api_connection.h"
|
||||
#include "esphome/components/api/api_server.h"
|
||||
#include "esphome/components/homeassistant/sensor/homeassistant_sensor.h"
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
namespace esphome::homeassistant::benchmarks {
|
||||
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// Each benchmark gets its own API server so subscriptions from one do not
|
||||
// stay in the list scanned by the next. The constructor installs itself as
|
||||
// global_api_server, which is what HomeassistantSensor::setup() subscribes to.
|
||||
class ScopedApiServer {
|
||||
public:
|
||||
ScopedApiServer() : previous_(api::global_api_server) {}
|
||||
~ScopedApiServer() { api::global_api_server = this->previous_; }
|
||||
api::APIServer *get() { return &this->server_; }
|
||||
|
||||
protected:
|
||||
api::APIServer *previous_;
|
||||
api::APIServer server_;
|
||||
};
|
||||
|
||||
// The handler only reads the server's subscription list, so the connection is
|
||||
// never started and its socket never used.
|
||||
static std::unique_ptr<api::APIConnection> create_connection(api::APIServer *server) {
|
||||
const int fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
return std::make_unique<api::APIConnection>(std::make_unique<socket::Socket>(fd), server);
|
||||
}
|
||||
|
||||
// State strings as Home Assistant sends them. The handler writes a terminator
|
||||
// one byte past the state, so the buffers are mutable and the terminator it
|
||||
// writes lands on the one the literal already has.
|
||||
static constexpr int kStateCount = 4;
|
||||
static constexpr size_t kStateBufferSize = 16;
|
||||
using StateBuffers = char[kStateCount][kStateBufferSize];
|
||||
using Messages = std::array<api::HomeAssistantStateResponse, kStateCount>;
|
||||
|
||||
static StateBuffers NUMERIC_STATES = {"100.50", "101.25", "99.75", "100.00"};
|
||||
static StateBuffers INTEGER_STATES = {"100", "101", "99", "0"};
|
||||
static StateBuffers UNAVAILABLE_STATES = {"unavailable", "unknown", "unavailable", "unknown"};
|
||||
|
||||
static Messages make_messages(const char *entity_id, const char *attribute, StateBuffers &states) {
|
||||
Messages msgs;
|
||||
for (int i = 0; i < kStateCount; i++) {
|
||||
msgs[i].entity_id = StringRef(entity_id);
|
||||
if (attribute != nullptr) {
|
||||
msgs[i].attribute = StringRef(attribute);
|
||||
}
|
||||
msgs[i].state = StringRef(states[i]);
|
||||
}
|
||||
return msgs;
|
||||
}
|
||||
|
||||
// --- One imported sensor, numeric state that changes every message ---
|
||||
// Exercises: subscription match, in-place terminator, parse_number, publish_state.
|
||||
|
||||
static void HomeassistantSensor_StateResponse(benchmark::State &state) {
|
||||
ScopedApiServer server;
|
||||
auto conn = create_connection(server.get());
|
||||
const Messages msgs = make_messages("sensor.power", nullptr, NUMERIC_STATES);
|
||||
|
||||
HomeassistantSensor sensor;
|
||||
sensor.set_entity_id("sensor.power");
|
||||
sensor.setup();
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
conn->on_home_assistant_state_response(msgs[i % kStateCount]);
|
||||
}
|
||||
benchmark::DoNotOptimize(sensor.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(HomeassistantSensor_StateResponse);
|
||||
|
||||
// --- The same sensor with integer states such as a percentage or a count ---
|
||||
// Exercises: the same path with no fractional digits to parse. Only the state
|
||||
// differs from the decimal case, so the gap between the two is the parse.
|
||||
|
||||
static void HomeassistantSensor_StateResponse_Integer(benchmark::State &state) {
|
||||
ScopedApiServer server;
|
||||
auto conn = create_connection(server.get());
|
||||
const Messages msgs = make_messages("sensor.power", nullptr, INTEGER_STATES);
|
||||
|
||||
HomeassistantSensor sensor;
|
||||
sensor.set_entity_id("sensor.power");
|
||||
sensor.setup();
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
conn->on_home_assistant_state_response(msgs[i % kStateCount]);
|
||||
}
|
||||
benchmark::DoNotOptimize(sensor.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(HomeassistantSensor_StateResponse_Integer);
|
||||
|
||||
// --- Attribute import: the subscription and the message both carry an attribute ---
|
||||
// Exercises: the extra attribute compare before the callback.
|
||||
|
||||
static void HomeassistantSensor_StateResponse_Attribute(benchmark::State &state) {
|
||||
ScopedApiServer server;
|
||||
auto conn = create_connection(server.get());
|
||||
const Messages msgs = make_messages("climate.living_room", "current_temperature", NUMERIC_STATES);
|
||||
|
||||
HomeassistantSensor sensor;
|
||||
sensor.set_entity_id("climate.living_room");
|
||||
sensor.set_attribute("current_temperature");
|
||||
sensor.setup();
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
conn->on_home_assistant_state_response(msgs[i % kStateCount]);
|
||||
}
|
||||
benchmark::DoNotOptimize(sensor.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(HomeassistantSensor_StateResponse_Attribute);
|
||||
|
||||
// --- Sixteen imported sensors, message for the last one ---
|
||||
// Exercises: the linear scan over the subscription list with a string compare
|
||||
// per entry, which is what a device with many imported entities pays per update.
|
||||
// The IDs share a length and differ only in the last two bytes, so every
|
||||
// entry runs a near-full compare, the worst case for the scan.
|
||||
|
||||
static constexpr int kSensorCount = 16;
|
||||
|
||||
static void HomeassistantSensor_StateResponse_ManySubscriptions(benchmark::State &state) {
|
||||
ScopedApiServer server;
|
||||
auto conn = create_connection(server.get());
|
||||
|
||||
static constexpr const char *const ENTITY_IDS[kSensorCount] = {
|
||||
"sensor.import_00", "sensor.import_01", "sensor.import_02", "sensor.import_03",
|
||||
"sensor.import_04", "sensor.import_05", "sensor.import_06", "sensor.import_07",
|
||||
"sensor.import_08", "sensor.import_09", "sensor.import_10", "sensor.import_11",
|
||||
"sensor.import_12", "sensor.import_13", "sensor.import_14", "sensor.import_15",
|
||||
};
|
||||
HomeassistantSensor sensors[kSensorCount];
|
||||
for (int i = 0; i < kSensorCount; i++) {
|
||||
sensors[i].set_entity_id(ENTITY_IDS[i]);
|
||||
sensors[i].setup();
|
||||
}
|
||||
const Messages msgs = make_messages(ENTITY_IDS[kSensorCount - 1], nullptr, NUMERIC_STATES);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
conn->on_home_assistant_state_response(msgs[i % kStateCount]);
|
||||
}
|
||||
benchmark::DoNotOptimize(sensors[kSensorCount - 1].state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(HomeassistantSensor_StateResponse_ManySubscriptions);
|
||||
|
||||
// --- Non-numeric state such as "unavailable" ---
|
||||
// Exercises: the failed parse, the warning the platform logs for it, and the
|
||||
// NAN publish. The benchmark logger runs at the production default level with
|
||||
// console output off, so a log line at DEBUG or above costs its formatting
|
||||
// here, the same as on a device, and a log level change in the platform shows
|
||||
// up as a delta.
|
||||
|
||||
static void HomeassistantSensor_StateResponse_Unavailable(benchmark::State &state) {
|
||||
ScopedApiServer server;
|
||||
auto conn = create_connection(server.get());
|
||||
const Messages msgs = make_messages("sensor.power", nullptr, UNAVAILABLE_STATES);
|
||||
|
||||
HomeassistantSensor sensor;
|
||||
sensor.set_entity_id("sensor.power");
|
||||
sensor.setup();
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
conn->on_home_assistant_state_response(msgs[i % kStateCount]);
|
||||
}
|
||||
benchmark::DoNotOptimize(sensor.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(HomeassistantSensor_StateResponse_Unavailable);
|
||||
|
||||
} // namespace esphome::homeassistant::benchmarks
|
||||
|
||||
#endif // USE_API_PLAINTEXT && USE_API_HOMEASSISTANT_STATES && USE_SENSOR
|
||||
@@ -0,0 +1,3 @@
|
||||
# The homeassistant platforms subscribe through the API server.
|
||||
api:
|
||||
sensor:
|
||||
@@ -0,0 +1,13 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.types import ConfigType
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# In production the platform's to_code emits this define; the host build
|
||||
# suppresses that to_code, and the API server needs the define for the
|
||||
# state subscription path the benchmarks drive.
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_STATES")
|
||||
|
||||
manifest.to_code = to_code
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Tests for the emontx lock_on_reboot option: auto (the default), true or false."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.emontx import (
|
||||
CONF_LOCK_ON_REBOOT,
|
||||
CONFIG_SCHEMA,
|
||||
LOCK_ON_REBOOT_AUTO,
|
||||
)
|
||||
|
||||
|
||||
def _lock_key() -> cv.Optional:
|
||||
return next(key for key in CONFIG_SCHEMA.schema if key == CONF_LOCK_ON_REBOOT)
|
||||
|
||||
|
||||
def test_default_is_auto() -> None:
|
||||
assert _lock_key().default() == LOCK_ON_REBOOT_AUTO
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "expected"),
|
||||
[
|
||||
("auto", LOCK_ON_REBOOT_AUTO),
|
||||
("AUTO", LOCK_ON_REBOOT_AUTO),
|
||||
(True, True),
|
||||
(False, False),
|
||||
("yes", True),
|
||||
("off", False),
|
||||
],
|
||||
)
|
||||
def test_accepted_values(value: object, expected: object) -> None:
|
||||
assert CONFIG_SCHEMA.schema[_lock_key()](value) == expected
|
||||
|
||||
|
||||
def test_other_values_rejected() -> None:
|
||||
with pytest.raises(cv.Invalid):
|
||||
CONFIG_SCHEMA.schema[_lock_key()]("sometimes")
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 78 B |
@@ -30,7 +30,7 @@ display:
|
||||
|
||||
image:
|
||||
- platform: file
|
||||
file: mdi:battery
|
||||
file: image.png
|
||||
id: my_image
|
||||
resize: 8x8
|
||||
type: binary
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
logger:
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: temp_sensor
|
||||
lambda: return 21.0;
|
||||
|
||||
climate:
|
||||
- platform: thermostat
|
||||
id: heater
|
||||
name: Heater
|
||||
sensor: temp_sensor
|
||||
min_idle_time: 30s
|
||||
min_heating_off_time: 300s
|
||||
min_heating_run_time: 300s
|
||||
min_cooling_off_time: 300s
|
||||
heat_action:
|
||||
- logger.log: heat
|
||||
idle_action:
|
||||
- logger.log: idle
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
logger:
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: temp_sensor
|
||||
lambda: return 21.0;
|
||||
|
||||
climate:
|
||||
- platform: thermostat
|
||||
id: dehumidifier
|
||||
name: Dehumidifier
|
||||
sensor: temp_sensor
|
||||
min_idle_time: 0s
|
||||
min_cooling_off_time: 0s
|
||||
min_cooling_run_time: 0s
|
||||
min_fanning_off_time: 0s
|
||||
min_fanning_run_time: 0s
|
||||
idle_action:
|
||||
- logger.log: idle
|
||||
fan_only_action:
|
||||
- logger.log: fan only
|
||||
dry_action:
|
||||
- logger.log: dry
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Tests for the thermostat config validation."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.config import read_config
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
def test_dry_only_config_is_valid(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""A dehumidifier has dry mode but no cool_action; dry mode uses the cooling timers."""
|
||||
main_cpp = generate_main(component_config_path("dry_only.yaml"))
|
||||
|
||||
assert "dehumidifier->set_cooling_minimum_off_time_in_sec(0);" in main_cpp
|
||||
assert "dehumidifier->set_cooling_minimum_run_time_in_sec(0);" in main_cpp
|
||||
assert "dehumidifier->set_supports_dry(true);" in main_cpp
|
||||
assert "dehumidifier->set_supports_cool(" not in main_cpp
|
||||
|
||||
|
||||
def test_cooling_timer_needs_cool_or_dry_action(
|
||||
component_config_path: Callable[[str], Path],
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
"""A cooling timer with neither cool_action nor dry_action is still rejected."""
|
||||
CORE.config_path = component_config_path("cooling_timer_without_action.yaml")
|
||||
assert read_config({}) is None
|
||||
captured = capsys.readouterr()
|
||||
assert (
|
||||
"cool_action or dry_action must be defined to use min_cooling_off_time"
|
||||
in captured.out
|
||||
)
|
||||
@@ -0,0 +1,77 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::core::testing {
|
||||
|
||||
static uint32_t bits(float value) {
|
||||
uint32_t out;
|
||||
memcpy(&out, &value, sizeof(out));
|
||||
return out;
|
||||
}
|
||||
|
||||
// strtof is the reference: every accepted input must give the same bits.
|
||||
static void expect_matches_strtof(const char *str) {
|
||||
char *end = nullptr;
|
||||
const float expected = ::strtof(str, &end);
|
||||
const auto parsed = parse_number<float>(str);
|
||||
if (!parsed.has_value()) {
|
||||
ADD_FAILURE() << str << " was rejected";
|
||||
return;
|
||||
}
|
||||
const float actual = parsed.value();
|
||||
EXPECT_EQ(bits(actual), bits(expected)) << str << " parsed " << actual << " expected " << expected;
|
||||
}
|
||||
|
||||
TEST(ParseFloat, PlainDecimalsMatchStrtof) {
|
||||
for (const char *str :
|
||||
{"0", "-0", "+0", "1", "-1", "100.50", "99.75", "0.1", "0.3",
|
||||
"2.675", "9999999", "0.9999999", "1234.567", "-1234.567", "0.000001", "0.000000001", "12.", ".5",
|
||||
"-.5", "007", "00.50", "123.4567", "0.009999999", "0.001234567", "-0.009999999"}) {
|
||||
expect_matches_strtof(str);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ParseFloat, SweepOfTwoDecimalValuesMatchesStrtof) {
|
||||
char buf[16];
|
||||
for (int i = -20000; i <= 20000; i++) {
|
||||
snprintf(buf, sizeof(buf), "%s%d.%02d", i < 0 ? "-" : "", std::abs(i) / 100, std::abs(i) % 100);
|
||||
expect_matches_strtof(buf);
|
||||
}
|
||||
}
|
||||
|
||||
// A fraction longer than the fast path accepts must fall back, however long it is.
|
||||
TEST(ParseFloat, LongFractionsMatchStrtof) {
|
||||
std::string str = "0.";
|
||||
str.append(300, '0');
|
||||
str += "25";
|
||||
expect_matches_strtof(str.c_str());
|
||||
expect_matches_strtof(("-" + str).c_str());
|
||||
str = "1.";
|
||||
str.append(300, '0');
|
||||
expect_matches_strtof(str.c_str());
|
||||
}
|
||||
|
||||
TEST(ParseFloat, LibcPathInputsMatchStrtof) {
|
||||
for (const char *str : {"1e3", "-2.5E-2", "0x10", "nan", " 12", "123456789", "16777215", "1.0000000", "0.0000000001",
|
||||
"0.00000000001", "1.23456789", "3.4028235e38"}) {
|
||||
expect_matches_strtof(str);
|
||||
}
|
||||
}
|
||||
|
||||
// Positive overflow and a positive literal infinity both come back as HUGE_VALF from strtof, and the parser
|
||||
// rejects both; the negative forms pass through, as they did before the fast path.
|
||||
TEST(ParseFloat, RejectsInvalidInput) {
|
||||
for (const char *str :
|
||||
{"", "-", "+", ".", "abc", "12abc", "1.2.3", "1,5", "12 ", "--1", "1e", "1e400", "inf", "infinity"}) {
|
||||
EXPECT_FALSE(parse_number<float>(str).has_value()) << str;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
@@ -0,0 +1,9 @@
|
||||
emontx:
|
||||
id: test_emontx
|
||||
lock_on_reboot: true
|
||||
|
||||
text_sensor:
|
||||
- platform: emontx
|
||||
emontx_id: test_emontx
|
||||
version:
|
||||
name: emonTx firmware version
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <vector>
|
||||
#include "esphome/components/uart/uart_component.h"
|
||||
#include "esphome/components/emontx/emontx.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::emontx::testing {
|
||||
|
||||
@@ -42,4 +44,23 @@ class MockUARTComponent : public uart::UARTComponent {
|
||||
#endif // defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
};
|
||||
|
||||
// The test main does not construct App as generated code does
|
||||
inline void construct_app() {
|
||||
static const bool APP_CONSTRUCTED = (new (&App) Application(), true);
|
||||
(void) APP_CONSTRUCTED;
|
||||
}
|
||||
|
||||
inline void cancel_timers(EmonTx *emontx) {
|
||||
for (uint32_t id = 0; id < 3; id++)
|
||||
App.scheduler.cancel_timeout(emontx, id);
|
||||
App.scheduler.call(millis());
|
||||
}
|
||||
|
||||
// Lets timeouts due within duration_ms run; the scheduler reads the real clock
|
||||
inline void run_timers(uint32_t duration_ms) {
|
||||
App.scheduler.call(millis());
|
||||
delay(duration_ms); // NOLINT - test only
|
||||
App.scheduler.call(millis());
|
||||
}
|
||||
|
||||
} // namespace esphome::emontx::testing
|
||||
|
||||
@@ -8,7 +8,6 @@ button:
|
||||
|
||||
emontx:
|
||||
id: test_emontx
|
||||
lock_on_reboot: true
|
||||
on_json:
|
||||
- then:
|
||||
- logger.log: "Got JSON"
|
||||
@@ -24,9 +23,3 @@ sensor:
|
||||
tag_name: P1
|
||||
emontx_id: test_emontx
|
||||
unit_of_measurement: W
|
||||
|
||||
text_sensor:
|
||||
- platform: emontx
|
||||
emontx_id: test_emontx
|
||||
version:
|
||||
name: emonTx firmware version
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -13,11 +14,16 @@ namespace {
|
||||
class EmonTxTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
construct_app();
|
||||
this->sut_.set_uart_parent(&this->uart_);
|
||||
this->sut_.add_on_data_callback([this](StringRef line) { this->lines_.emplace_back(line.c_str(), line.size()); });
|
||||
this->sut_.add_on_json_callback([this](JsonObject, StringRef) { this->json_count_++; });
|
||||
}
|
||||
|
||||
void TearDown() override { cancel_timers(&this->sut_); }
|
||||
|
||||
std::string tx_() const { return std::string(this->uart_.tx.begin(), this->uart_.tx.end()); }
|
||||
|
||||
MockUARTComponent uart_;
|
||||
EmonTx sut_;
|
||||
std::vector<std::string> lines_;
|
||||
@@ -160,7 +166,7 @@ TEST_F(EmonTxTest, ResumeAfterCompleteBacklogKeepsNextLine) {
|
||||
|
||||
TEST_F(EmonTxTest, ShutdownLocksWhenEnabled) {
|
||||
EXPECT_CALL(this->uart_, flush()).Times(1);
|
||||
this->sut_.set_lock_on_reboot(true);
|
||||
this->sut_.set_lock_on_reboot(LockOnReboot::LOCK_ON_REBOOT_ALWAYS);
|
||||
this->sut_.on_shutdown();
|
||||
EXPECT_EQ(std::string(this->uart_.tx.begin(), this->uart_.tx.end()), "emonlock\n");
|
||||
}
|
||||
@@ -174,16 +180,20 @@ TEST_F(EmonTxTest, ShutdownSendsNothingByDefault) {
|
||||
|
||||
TEST_F(EmonTxTest, ShutdownSendsNothingWhilePaused) {
|
||||
EXPECT_CALL(this->uart_, flush()).Times(0);
|
||||
this->sut_.set_lock_on_reboot(true);
|
||||
this->sut_.set_lock_on_reboot(LockOnReboot::LOCK_ON_REBOOT_ALWAYS);
|
||||
this->sut_.set_paused(true);
|
||||
this->sut_.on_shutdown();
|
||||
EXPECT_TRUE(this->uart_.tx.empty());
|
||||
}
|
||||
|
||||
TEST_F(EmonTxTest, SetupUnlocksWhenEnabled) {
|
||||
this->sut_.set_lock_on_reboot(true);
|
||||
// The emonTx would join "emonunlock" to the pending empty line if both came in one burst
|
||||
TEST_F(EmonTxTest, SetupUnlocksAfterLineFeedWhenEnabled) {
|
||||
this->sut_.set_lock_on_reboot(LockOnReboot::LOCK_ON_REBOOT_ALWAYS);
|
||||
this->sut_.setup();
|
||||
EXPECT_EQ(std::string(this->uart_.tx.begin(), this->uart_.tx.end()), "\nemonunlock\n");
|
||||
EXPECT_EQ(this->tx_(), "\n");
|
||||
|
||||
run_timers(300);
|
||||
EXPECT_EQ(this->tx_(), "\nemonunlock\n");
|
||||
}
|
||||
|
||||
TEST_F(EmonTxTest, SetupSendsNothingByDefault) {
|
||||
@@ -191,4 +201,169 @@ TEST_F(EmonTxTest, SetupSendsNothingByDefault) {
|
||||
EXPECT_TRUE(this->uart_.tx.empty());
|
||||
}
|
||||
|
||||
class EmonTxLockAutoTest : public EmonTxTest {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
EmonTxTest::SetUp();
|
||||
this->sut_.set_lock_on_reboot(LockOnReboot::LOCK_ON_REBOOT_AUTO);
|
||||
}
|
||||
|
||||
void report_version_(const char *version) {
|
||||
this->uart_.push_rx(" - Version: ");
|
||||
this->uart_.push_rx(version);
|
||||
this->uart_.push_rx("\r\n");
|
||||
this->sut_.loop();
|
||||
}
|
||||
};
|
||||
|
||||
// Only 'v' goes out until the reply shows the firmware knows 'emonunlock'
|
||||
TEST_F(EmonTxLockAutoTest, SetupOnlyRequestsVersion) {
|
||||
this->sut_.setup();
|
||||
run_timers(300);
|
||||
EXPECT_EQ(this->tx_(), "\nv\n");
|
||||
}
|
||||
|
||||
TEST_F(EmonTxLockAutoTest, SupportedFirmwareUnlockedAndLockedOnShutdown) {
|
||||
this->report_version_("v1.2.0");
|
||||
EXPECT_TRUE(this->uart_.tx.empty());
|
||||
run_timers(300);
|
||||
EXPECT_EQ(this->tx_(), "emonunlock\n");
|
||||
|
||||
EXPECT_CALL(this->uart_, flush()).Times(1);
|
||||
this->sut_.on_shutdown();
|
||||
EXPECT_EQ(this->tx_(), "emonunlock\nemonlock\n");
|
||||
}
|
||||
|
||||
// Firmware before v1.2.0 reads "emon..." as the bootloader command
|
||||
TEST_F(EmonTxLockAutoTest, OlderFirmwareGetsNoLockCommands) {
|
||||
this->report_version_("v1.1.1");
|
||||
run_timers(300);
|
||||
EXPECT_CALL(this->uart_, flush()).Times(0);
|
||||
this->sut_.on_shutdown();
|
||||
EXPECT_TRUE(this->uart_.tx.empty());
|
||||
}
|
||||
|
||||
TEST_F(EmonTxLockAutoTest, NothingSentWithoutVersion) {
|
||||
EXPECT_CALL(this->uart_, flush()).Times(0);
|
||||
this->sut_.on_shutdown();
|
||||
EXPECT_TRUE(this->uart_.tx.empty());
|
||||
}
|
||||
|
||||
TEST_F(EmonTxLockAutoTest, VersionFormats) {
|
||||
for (const char *version : {"V1.10.0", "v2.0.0", "1.2.0", "v1.3.0-rc1"}) {
|
||||
this->report_version_("v0.99.0"); // So the next one is a change
|
||||
this->report_version_(version);
|
||||
EXPECT_CALL(this->uart_, flush()).Times(1);
|
||||
this->sut_.on_shutdown();
|
||||
EXPECT_NE(this->tx_().find("emonlock"), std::string::npos) << version;
|
||||
::testing::Mock::VerifyAndClearExpectations(&this->uart_);
|
||||
this->uart_.tx.clear();
|
||||
}
|
||||
for (const char *version : {"v1.1.1", "v0.99.9-test", "None", "768be1b", "v1", "v.2"}) {
|
||||
this->report_version_(version);
|
||||
EXPECT_CALL(this->uart_, flush()).Times(0);
|
||||
this->sut_.on_shutdown();
|
||||
EXPECT_TRUE(this->uart_.tx.empty()) << version;
|
||||
::testing::Mock::VerifyAndClearExpectations(&this->uart_);
|
||||
}
|
||||
}
|
||||
|
||||
// The emonTx clears its lock when it reboots, so a repeated report needs no second unlock
|
||||
TEST_F(EmonTxLockAutoTest, UnlockedOnlyOnce) {
|
||||
this->report_version_("v1.2.0");
|
||||
run_timers(300);
|
||||
this->report_version_("v1.2.0");
|
||||
run_timers(300);
|
||||
EXPECT_EQ(this->tx_(), "emonunlock\n");
|
||||
}
|
||||
|
||||
TEST_F(EmonTxLockAutoTest, RequestRetriedEverySecondDataLine) {
|
||||
this->sut_.setup();
|
||||
run_timers(300);
|
||||
this->uart_.push_rx("{\"MSG\":10}\n");
|
||||
this->sut_.loop();
|
||||
EXPECT_EQ(this->tx_(), "\nv\n");
|
||||
|
||||
// Text format counts too
|
||||
this->uart_.push_rx("MSG:11,V1:230.1\n");
|
||||
this->sut_.loop();
|
||||
run_timers(300);
|
||||
EXPECT_EQ(this->tx_(), "\nv\n\nv\n");
|
||||
|
||||
this->report_version_("v1.1.1");
|
||||
this->uart_.push_rx("{\"MSG\":12}\n{\"MSG\":13}\n{\"MSG\":14}\n");
|
||||
this->sut_.loop();
|
||||
run_timers(300);
|
||||
EXPECT_EQ(this->tx_(), "\nv\n\nv\n");
|
||||
}
|
||||
|
||||
// Each attempt sends its line feed at once; counting those needs no wait for the delayed 'v'
|
||||
TEST_F(EmonTxLockAutoTest, RetriesStopAfterThreeAttempts) {
|
||||
this->sut_.setup();
|
||||
for (int msg = 10; msg < 20; msg++) {
|
||||
char line[24];
|
||||
snprintf(line, sizeof(line), "{\"MSG\":%d}\n", msg);
|
||||
this->uart_.push_rx(line);
|
||||
this->sut_.loop();
|
||||
}
|
||||
EXPECT_EQ(this->tx_(), "\n\n\n");
|
||||
}
|
||||
|
||||
// E.g. a downgrade: after the reboot, the new firmware never answers 'v'
|
||||
TEST_F(EmonTxLockAutoTest, NoLockAfterUnansweredRequest) {
|
||||
this->report_version_("v1.2.0");
|
||||
run_timers(300);
|
||||
for (int msg : {50, 1, 2, 3, 4, 5, 6, 7}) {
|
||||
char line[24];
|
||||
snprintf(line, sizeof(line), "MSG:%d\n", msg);
|
||||
this->uart_.push_rx(line);
|
||||
this->sut_.loop();
|
||||
}
|
||||
this->uart_.tx.clear();
|
||||
|
||||
EXPECT_CALL(this->uart_, flush()).Times(0);
|
||||
this->sut_.on_shutdown();
|
||||
EXPECT_TRUE(this->uart_.tx.empty());
|
||||
}
|
||||
|
||||
TEST_F(EmonTxLockAutoTest, NoLockWhileVersionPending) {
|
||||
this->report_version_("v1.2.0");
|
||||
run_timers(300);
|
||||
this->uart_.push_rx("MSG:50\nMSG:1\n");
|
||||
this->sut_.loop();
|
||||
this->uart_.tx.clear();
|
||||
|
||||
EXPECT_CALL(this->uart_, flush()).Times(0);
|
||||
this->sut_.on_shutdown();
|
||||
EXPECT_TRUE(this->uart_.tx.empty());
|
||||
}
|
||||
|
||||
TEST_F(EmonTxLockAutoTest, UnlockDroppedByPauseSentAfterResume) {
|
||||
this->report_version_("v1.2.0");
|
||||
this->sut_.set_paused(true);
|
||||
this->sut_.loop();
|
||||
run_timers(300);
|
||||
EXPECT_TRUE(this->uart_.tx.empty());
|
||||
|
||||
// Resume asks for the version again, and its reply brings the unlock
|
||||
this->sut_.set_paused(false);
|
||||
this->sut_.loop();
|
||||
run_timers(300);
|
||||
this->uart_.push_rx("> Firmware:\r\n"); // Ends the line skipped after a resume
|
||||
this->report_version_("v1.2.0");
|
||||
run_timers(300);
|
||||
EXPECT_EQ(this->tx_(), "\nv\nemonunlock\n");
|
||||
}
|
||||
|
||||
// The lines of a 'v' reply before the version line are not data lines
|
||||
TEST_F(EmonTxLockAutoTest, ReplyBannerNotCountedAsDataLines) {
|
||||
this->sut_.setup();
|
||||
run_timers(300);
|
||||
this->uart_.push_rx(
|
||||
"> Board:\r\n - emonPi3/emonTx6 (arch. rev. 1)\r\n> Firmware:\r\n - Bootloader: UART (.bin)\r\n");
|
||||
this->sut_.loop();
|
||||
run_timers(300);
|
||||
EXPECT_EQ(this->tx_(), "\nv\n");
|
||||
}
|
||||
|
||||
} // namespace esphome::emontx::testing
|
||||
|
||||
@@ -2,3 +2,4 @@ packages:
|
||||
uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml
|
||||
emontx: !include common.yaml
|
||||
actions: !include common-actions.yaml
|
||||
version: !include common-version.yaml
|
||||
|
||||
@@ -7,5 +7,6 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# The platform's own to_code needs a configured hub; only its define is wanted here.
|
||||
async def to_code_testing(config: ConfigType) -> None:
|
||||
cg.add_define("USE_EMONTX_VERSION")
|
||||
cg.add_define("USE_EMONTX_VERSION_TEXT_SENSOR")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
#include "esphome/components/emontx/emontx.h"
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "../common.h"
|
||||
|
||||
namespace esphome::emontx::testing {
|
||||
@@ -14,24 +12,15 @@ namespace {
|
||||
class EmonTxVersionTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// The test main does not construct App as generated code does
|
||||
static const bool APP_CONSTRUCTED = (new (&App) Application(), true);
|
||||
(void) APP_CONSTRUCTED;
|
||||
construct_app();
|
||||
this->sut_.set_uart_parent(&this->uart_);
|
||||
this->sut_.set_version_text_sensor(&this->version_);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
App.scheduler.cancel_timeout(&this->sut_, uint32_t{0});
|
||||
App.scheduler.call(millis());
|
||||
}
|
||||
void TearDown() override { cancel_timers(&this->sut_); }
|
||||
|
||||
// Lets the delayed 'v' go out
|
||||
static void run_timers() {
|
||||
App.scheduler.call(millis());
|
||||
delay(300); // NOLINT - test only, the scheduler reads the real clock
|
||||
App.scheduler.call(millis());
|
||||
}
|
||||
static void run_timers() { testing::run_timers(300); }
|
||||
|
||||
std::string tx_() const { return std::string(this->uart_.tx.begin(), this->uart_.tx.end()); }
|
||||
|
||||
@@ -51,8 +40,11 @@ TEST_F(EmonTxVersionTest, SetupRequestsVersionAfterDelay) {
|
||||
}
|
||||
|
||||
TEST_F(EmonTxVersionTest, SetupUnlocksBeforeRequestingVersion) {
|
||||
this->sut_.set_lock_on_reboot(true);
|
||||
this->sut_.set_lock_on_reboot(LockOnReboot::LOCK_ON_REBOOT_ALWAYS);
|
||||
this->sut_.setup();
|
||||
EXPECT_EQ(this->tx_(), "\n");
|
||||
|
||||
run_timers();
|
||||
EXPECT_EQ(this->tx_(), "\nemonunlock\n\n");
|
||||
|
||||
run_timers();
|
||||
@@ -88,6 +80,15 @@ TEST_F(EmonTxVersionTest, MsgCounterResetRequestsVersion) {
|
||||
EXPECT_EQ(this->tx_(), "\nv\n");
|
||||
}
|
||||
|
||||
TEST_F(EmonTxVersionTest, TextMsgCounterResetRequestsVersion) {
|
||||
this->uart_.push_rx("MSG:41,V1:230.1\r\nMSG:1,V1:230.4\r\n");
|
||||
this->sut_.loop();
|
||||
EXPECT_EQ(this->tx_(), "\n");
|
||||
|
||||
run_timers();
|
||||
EXPECT_EQ(this->tx_(), "\nv\n");
|
||||
}
|
||||
|
||||
// The emonTx may have been flashed during the pause, with MSG already past its old value
|
||||
TEST_F(EmonTxVersionTest, ResumeRequestsVersion) {
|
||||
this->sut_.set_paused(true);
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "../common.h"
|
||||
#include "esphome/components/epaper_spi/epaper_spi_ssd1683.h"
|
||||
#include "esphome/components/epaper_spi/epaper_spi_t133a01.h"
|
||||
|
||||
namespace esphome::epaper_spi::testing {
|
||||
|
||||
/// A busy pin the test can hold at "busy" and release.
|
||||
class SwitchableBusyPin : public RecordingPin {
|
||||
public:
|
||||
bool digital_read() override { return this->busy; }
|
||||
|
||||
bool busy{false};
|
||||
};
|
||||
|
||||
template<typename T> void loop_until_idle(T &display) {
|
||||
const uint32_t deadline = millis() + 1000;
|
||||
while (display.state() != EPaperState::IDLE && millis() < deadline)
|
||||
display.loop();
|
||||
}
|
||||
|
||||
class TestableSSD1683 : public EPaperSSD1683 {
|
||||
public:
|
||||
TestableSSD1683() : EPaperSSD1683("test", 16, 2, nullptr, 0) {}
|
||||
|
||||
void install(spi::SPIDelegate *delegate) {
|
||||
this->delegate_ = delegate;
|
||||
this->set_dc_pin(&this->dc);
|
||||
this->set_busy_pin(&this->busy);
|
||||
this->set_full_update_every(5);
|
||||
this->set_auto_clear(false);
|
||||
ASSERT_TRUE(this->init_buffer_(this->buffer_length_));
|
||||
this->busy_timeout_seconds_ = 0; // abandon as soon as the stuck pin is seen, so the test does not wait
|
||||
}
|
||||
|
||||
void draw_whole_panel() {
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
this->x_high_ = this->width_;
|
||||
this->y_high_ = this->height_;
|
||||
}
|
||||
|
||||
EPaperState state() const { return this->state_; }
|
||||
uint8_t update_count() const { return this->update_count_; }
|
||||
void set_update_count(uint8_t count) { this->update_count_ = count; }
|
||||
|
||||
RecordingPin dc;
|
||||
SwitchableBusyPin busy;
|
||||
};
|
||||
|
||||
/// If the busy pin never clears, the update is abandoned instead of waiting for ever: the display goes back to
|
||||
/// idle with a warning. Once the pin works again the next update is accepted and is a full one, even though nothing
|
||||
/// was drawn since, and completing it clears the warning.
|
||||
TEST(EPaperBusyTimeout, StuckBusyPinAbandonsTheUpdateAndRecovers) {
|
||||
TestableSSD1683 display;
|
||||
RecordingDelegate bus(&display.dc);
|
||||
display.install(&bus);
|
||||
display.set_update_count(3);
|
||||
|
||||
// Stick the pin after the data has been sent, when the dirty area has already been emptied
|
||||
display.draw_whole_panel();
|
||||
display.update();
|
||||
const uint32_t deadline = millis() + 1000;
|
||||
while (display.state() <= EPaperState::TRANSFER_DATA && millis() < deadline)
|
||||
display.loop();
|
||||
ASSERT_EQ(display.state(), EPaperState::POWER_ON);
|
||||
display.busy.busy = true;
|
||||
loop_until_idle(display);
|
||||
|
||||
EXPECT_EQ(display.state(), EPaperState::IDLE) << "the update was not abandoned";
|
||||
EXPECT_TRUE(display.status_has_warning());
|
||||
|
||||
display.busy.busy = false;
|
||||
display.update();
|
||||
EXPECT_NE(display.state(), EPaperState::IDLE) << "a later update was refused";
|
||||
loop_until_idle(display);
|
||||
|
||||
EXPECT_EQ(display.state(), EPaperState::IDLE) << "the later update did not complete";
|
||||
EXPECT_EQ(display.update_count(), 1) << "the later update was not a full one";
|
||||
EXPECT_FALSE(display.status_has_warning());
|
||||
}
|
||||
|
||||
class TestableT133A01 : public EPaperT133A01 {
|
||||
public:
|
||||
TestableT133A01() : EPaperT133A01("test", 8, 2, nullptr, 0) {}
|
||||
|
||||
void install(spi::SPIDelegate *delegate) {
|
||||
this->delegate_ = delegate;
|
||||
this->set_dc_pin(&this->dc);
|
||||
this->set_cs_pins(&this->cs, &this->cs1);
|
||||
this->set_busy_pin(&this->busy);
|
||||
ASSERT_TRUE(this->init_buffer_(this->buffer_length_));
|
||||
this->busy_timeout_seconds_ = 0; // abandon as soon as the stuck pin is seen, so the test does not wait
|
||||
}
|
||||
|
||||
void start_transfer() { this->set_state_(EPaperState::TRANSFER_DATA); }
|
||||
EPaperState state() const { return this->state_; }
|
||||
|
||||
RecordingPin dc, cs, cs1;
|
||||
SwitchableBusyPin busy;
|
||||
};
|
||||
|
||||
/// The T133A01 waits for the busy pin with a data stream open. Abandoning the update there must close the SPI
|
||||
/// transaction and release both chip selects, or the next command would block.
|
||||
TEST(EPaperBusyTimeout, AbandonedT133A01TransferReleasesTheBus) {
|
||||
TestableT133A01 display;
|
||||
TimedSPIDelegate delegate(MAX_TRANSFER_TIME + 5);
|
||||
display.install(&delegate);
|
||||
|
||||
display.start_transfer();
|
||||
display.loop();
|
||||
ASSERT_EQ(display.state(), EPaperState::TRANSFER_DATA);
|
||||
ASSERT_FALSE(display.cs.level) << "the transfer should have yielded with its data stream open";
|
||||
|
||||
display.busy.busy = true;
|
||||
loop_until_idle(display);
|
||||
|
||||
ASSERT_EQ(display.state(), EPaperState::IDLE) << "the update was not abandoned";
|
||||
EXPECT_EQ(delegate.begin_count, delegate.end_count) << "SPI transaction left open";
|
||||
EXPECT_TRUE(display.cs.level) << "CS left asserted";
|
||||
EXPECT_TRUE(display.cs1.level) << "CS1 left asserted";
|
||||
}
|
||||
|
||||
/// Abandoning an update powers the panel off, so it is not left with its high-voltage drivers enabled.
|
||||
TEST(EPaperBusyTimeout, AbandonedUpdatePowersThePanelOff) {
|
||||
TestableT133A01 display;
|
||||
RecordingDelegate bus(&display.dc, MAX_TRANSFER_TIME + 5); // burn time so the transfer yields mid-stream
|
||||
display.install(&bus);
|
||||
|
||||
display.start_transfer();
|
||||
display.loop();
|
||||
ASSERT_EQ(display.state(), EPaperState::TRANSFER_DATA);
|
||||
|
||||
display.busy.busy = true;
|
||||
loop_until_idle(display);
|
||||
ASSERT_EQ(display.state(), EPaperState::IDLE) << "the update was not abandoned";
|
||||
|
||||
EXPECT_NE(std::find(bus.commands.begin(), bus.commands.end(), 0x02), bus.commands.end())
|
||||
<< "the panel was not powered off (POF) when the update was abandoned";
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi::testing
|
||||
@@ -0,0 +1,193 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "../common.h"
|
||||
#include "esphome/components/epaper_spi/colorconv.h"
|
||||
#include "esphome/components/epaper_spi/epaper_spi_ssd1677_gray4.h"
|
||||
#include "esphome/components/epaper_spi/epaper_spi_ssd1683.h"
|
||||
|
||||
namespace esphome::epaper_spi::testing {
|
||||
|
||||
/// Exposes the buffer and the dirty bounds so the fast path can be compared with drawing pixel by pixel.
|
||||
template<typename Driver> class TestableDisplay : public Driver {
|
||||
public:
|
||||
TestableDisplay(uint16_t width, uint16_t height) : Driver("test", width, height, nullptr, 0) {
|
||||
EXPECT_TRUE(this->init_buffer_(this->buffer_length_));
|
||||
this->reset_bounds();
|
||||
}
|
||||
|
||||
void reset_bounds() {
|
||||
this->x_low_ = this->width_;
|
||||
this->y_low_ = this->height_;
|
||||
this->x_high_ = 0;
|
||||
this->y_high_ = 0;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> buffer() const {
|
||||
std::vector<uint8_t> out;
|
||||
for (size_t i = 0; i != this->buffer_length_; i++)
|
||||
out.push_back(this->buffer_[i]);
|
||||
return out;
|
||||
}
|
||||
|
||||
/// Reallocates the buffer in blocks of at most max_block bytes, as SplitBuffer does when memory is fragmented.
|
||||
void split_buffer(size_t max_block) {
|
||||
ASSERT_TRUE(this->buffer_.init(this->buffer_length_, max_block));
|
||||
ASSERT_GT(this->buffer_.get_buffer_count(), 1u);
|
||||
this->clear();
|
||||
this->reset_bounds();
|
||||
}
|
||||
|
||||
std::vector<uint16_t> bounds() const { return {this->x_low_, this->y_low_, this->x_high_, this->y_high_}; }
|
||||
|
||||
void draw_one_by_one(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
|
||||
// The per-pixel path the fast one must match
|
||||
display::Display::draw_pixels_at( // NOLINT(bugprone-parent-virtual-call)
|
||||
x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
|
||||
}
|
||||
};
|
||||
|
||||
/// RGB565 data with a spread of light and dark pixels, including values either side of the threshold.
|
||||
static std::vector<uint8_t> rgb565(size_t pixels, uint32_t seed) {
|
||||
std::vector<uint8_t> out;
|
||||
uint32_t state = seed;
|
||||
for (size_t i = 0; i != pixels; i++) {
|
||||
state = state * 1103515245u + 12345u;
|
||||
const uint16_t pixel = static_cast<uint16_t>(state >> 8);
|
||||
out.push_back(pixel & 0xFF);
|
||||
out.push_back(pixel >> 8);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/// Random bytes for a block of pixels, enough for 3 bytes a pixel.
|
||||
static std::vector<uint8_t> random_bytes(size_t pixels, uint32_t seed) {
|
||||
std::vector<uint8_t> out;
|
||||
uint32_t state = seed;
|
||||
for (size_t i = 0; i != pixels * 3; i++) {
|
||||
state = state * 1103515245u + 12345u;
|
||||
out.push_back(static_cast<uint8_t>(state >> 16));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/// For every rotation, mirroring and byte order, with source offsets and padding, and with blocks inside the panel,
|
||||
/// running off each edge and larger than the panel, drawing an RGB565 block directly gives the same buffer and the same
|
||||
/// dirty area as drawing it pixel by pixel. The panel width is not a multiple of 8.
|
||||
TEST(EPaperDrawPixels, SameResultAsDrawingPixelByPixel) {
|
||||
struct Block {
|
||||
int x, y, w, h;
|
||||
};
|
||||
const Block blocks[] = {{6, 5, 12, 6}, {-3, -2, 10, 8}, {14, 8, 12, 9}, {-5, 4, 30, 3}, {-2, -3, 26, 20}};
|
||||
const display::DisplayRotation rotations[] = {
|
||||
display::DISPLAY_ROTATION_0_DEGREES, display::DISPLAY_ROTATION_90_DEGREES, display::DISPLAY_ROTATION_180_DEGREES,
|
||||
display::DISPLAY_ROTATION_270_DEGREES};
|
||||
// Swapping the axes comes from the 90 and 270 degree rotations
|
||||
const uint8_t mirrorings[] = {NONE, MIRROR_X, MIRROR_Y, MIRROR_X | MIRROR_Y};
|
||||
uint32_t seed = 1;
|
||||
for (const auto rotation : rotations) {
|
||||
for (const uint8_t mirroring : mirrorings) {
|
||||
for (const bool big_endian : {false, true}) {
|
||||
for (const auto &block : blocks) {
|
||||
TestableDisplay<EPaperSSD1683> fast(21, 13);
|
||||
TestableDisplay<EPaperSSD1683> slow(21, 13);
|
||||
for (auto *display : {&fast, &slow}) {
|
||||
display->set_transform(mirroring);
|
||||
display->set_rotation(rotation);
|
||||
}
|
||||
// a 3 pixel left offset, 2 rows skipped and 4 pixels of padding
|
||||
const auto data = random_bytes((3 + block.w + 4) * (2 + block.h), seed++);
|
||||
fast.draw_pixels_at(block.x, block.y, block.w, block.h, data.data(), display::COLOR_ORDER_RGB,
|
||||
display::COLOR_BITNESS_565, big_endian, 3, 2, 4);
|
||||
slow.draw_one_by_one(block.x, block.y, block.w, block.h, data.data(), display::COLOR_ORDER_RGB,
|
||||
display::COLOR_BITNESS_565, big_endian, 3, 2, 4);
|
||||
const auto where = ::testing::Message() << "rotation " << rotation << " mirroring " << int(mirroring)
|
||||
<< " big endian " << big_endian << " block " << block.x << ","
|
||||
<< block.y << " " << block.w << "x" << block.h;
|
||||
EXPECT_EQ(fast.buffer(), slow.buffer()) << where;
|
||||
EXPECT_EQ(fast.bounds(), slow.bounds()) << where;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The colour order reaches the RGB565 block writer.
|
||||
TEST(EPaperDrawPixels, ColourOrderIsHonoured) {
|
||||
for (const auto order : {display::COLOR_ORDER_RGB, display::COLOR_ORDER_BGR, display::COLOR_ORDER_GRB}) {
|
||||
TestableDisplay<EPaperSSD1683> fast(21, 13);
|
||||
TestableDisplay<EPaperSSD1683> slow(21, 13);
|
||||
const auto data = random_bytes(21 * 13, 99);
|
||||
fast.draw_pixels_at(0, 0, 21, 13, data.data(), order, display::COLOR_BITNESS_565, false, 0, 0, 0);
|
||||
slow.draw_one_by_one(0, 0, 21, 13, data.data(), order, display::COLOR_BITNESS_565, false, 0, 0, 0);
|
||||
EXPECT_EQ(fast.buffer(), slow.buffer()) << "order " << int(order);
|
||||
}
|
||||
}
|
||||
|
||||
/// With a clipping region set the block goes through draw_pixel_at(), which honours it.
|
||||
TEST(EPaperDrawPixels, ClippingFallsBackToDrawingPixelByPixel) {
|
||||
TestableDisplay<EPaperSSD1683> fast(24, 16);
|
||||
TestableDisplay<EPaperSSD1683> slow(24, 16);
|
||||
const auto data = rgb565(24 * 16, 7);
|
||||
for (auto *display : {&fast, &slow})
|
||||
display->start_clipping(4, 4, 12, 10);
|
||||
fast.draw_pixels_at(0, 0, 24, 16, data.data(), display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, false, 0, 0, 0);
|
||||
slow.draw_one_by_one(0, 0, 24, 16, data.data(), display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, false, 0, 0, 0);
|
||||
EXPECT_EQ(fast.buffer(), slow.buffer());
|
||||
EXPECT_EQ(fast.bounds(), (std::vector<uint16_t>{4, 4, 12, 10}));
|
||||
}
|
||||
|
||||
/// A driver whose buffer is not 1 bit per pixel keeps its own draw_pixel_at() for blocks too.
|
||||
TEST(EPaperDrawPixels, OtherBufferLayoutsDrawPixelByPixel) {
|
||||
TestableDisplay<EPaperSSD1677Gray4> fast(16, 4);
|
||||
TestableDisplay<EPaperSSD1677Gray4> slow(16, 4);
|
||||
const auto data = rgb565(16 * 4, 11);
|
||||
fast.draw_pixels_at(0, 0, 16, 4, data.data(), display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, false, 0, 0, 0);
|
||||
slow.draw_one_by_one(0, 0, 16, 4, data.data(), display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, false, 0, 0, 0);
|
||||
EXPECT_EQ(fast.buffer(), slow.buffer());
|
||||
}
|
||||
|
||||
/// The RGB565 lookup gives exactly what converting the colour and thresholding it does, for every value and order.
|
||||
TEST(EPaperDrawPixels, Rgb565LookupMatchesTheColourConversion) {
|
||||
for (const auto order : {display::COLOR_ORDER_RGB, display::COLOR_ORDER_BGR, display::COLOR_ORDER_GRB}) {
|
||||
const Rgb565ToMono to_mono(order);
|
||||
size_t mismatches = 0;
|
||||
for (uint32_t value = 0; value != 0x10000; value++) {
|
||||
const uint8_t expected = color_to_mono(ColorUtil::to_color(value, order, display::COLOR_BITNESS_565));
|
||||
if (to_mono(static_cast<uint16_t>(value)) != expected)
|
||||
mismatches++;
|
||||
}
|
||||
EXPECT_EQ(mismatches, 0u) << "order " << int(order);
|
||||
}
|
||||
}
|
||||
|
||||
/// Other bit depths, and a buffer split into several blocks, go through draw_pixel_at() and give the same result.
|
||||
TEST(EPaperDrawPixels, OtherCasesDrawPixelByPixel) {
|
||||
{
|
||||
TestableDisplay<EPaperSSD1683> fast(24, 16);
|
||||
TestableDisplay<EPaperSSD1683> slow(24, 16);
|
||||
fast.split_buffer(7);
|
||||
slow.split_buffer(7);
|
||||
const auto data = rgb565(24 * 16, 5);
|
||||
fast.draw_pixels_at(-2, 3, 24, 16, data.data(), display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, false, 0, 0,
|
||||
0);
|
||||
slow.draw_one_by_one(-2, 3, 24, 16, data.data(), display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, false, 0, 0,
|
||||
0);
|
||||
EXPECT_EQ(fast.buffer(), slow.buffer()) << "split buffer";
|
||||
EXPECT_EQ(fast.bounds(), slow.bounds()) << "split buffer";
|
||||
}
|
||||
for (const auto bitness : {display::COLOR_BITNESS_888, display::COLOR_BITNESS_332}) {
|
||||
TestableDisplay<EPaperSSD1683> fast(24, 16);
|
||||
TestableDisplay<EPaperSSD1683> slow(24, 16);
|
||||
const auto data = random_bytes(24 * 16, 9);
|
||||
fast.draw_pixels_at(0, 0, 24, 16, data.data(), display::COLOR_ORDER_RGB, bitness, false, 0, 0, 0);
|
||||
slow.draw_one_by_one(0, 0, 24, 16, data.data(), display::COLOR_ORDER_RGB, bitness, false, 0, 0, 0);
|
||||
EXPECT_EQ(fast.buffer(), slow.buffer()) << "bitness " << int(bitness);
|
||||
EXPECT_EQ(fast.bounds(), slow.bounds()) << "bitness " << int(bitness);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi::testing
|
||||
@@ -0,0 +1,9 @@
|
||||
# wifi and ethernet together: the _esphomelib "network" TXT value follows
|
||||
# the connected interface, so the update path is compiled.
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
api:
|
||||
|
||||
mdns:
|
||||
@@ -0,0 +1,17 @@
|
||||
packages:
|
||||
mdns: !include common-dual-interface.yaml
|
||||
|
||||
ethernet:
|
||||
type: W5500
|
||||
clk_pin: GPIO19
|
||||
mosi_pin: GPIO21
|
||||
miso_pin: GPIO23
|
||||
cs_pin: GPIO18
|
||||
interrupt_pin: GPIO36
|
||||
reset_pin: GPIO22
|
||||
clock_speed: 10Mhz
|
||||
|
||||
network:
|
||||
priority:
|
||||
- ethernet
|
||||
- wifi
|
||||
@@ -0,0 +1,17 @@
|
||||
packages:
|
||||
mdns: !include common-dual-interface.yaml
|
||||
|
||||
ethernet:
|
||||
type: W5500
|
||||
clk_pin: 18
|
||||
mosi_pin: 19
|
||||
miso_pin: 16
|
||||
cs_pin: 17
|
||||
interrupt_pin: 21
|
||||
reset_pin: 20
|
||||
|
||||
# wifi first, so the USE_NETWORK_PRIMARY_INTERFACE_WIFI order is compiled
|
||||
network:
|
||||
priority:
|
||||
- wifi
|
||||
- ethernet
|
||||
@@ -0,0 +1,46 @@
|
||||
pca9505:
|
||||
- id: pca9505_hub
|
||||
i2c_id: i2c_bus
|
||||
address: 0x26
|
||||
- id: pca9505_hub_second
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
i2c_id: i2c_bus
|
||||
address: 0x27
|
||||
|
||||
binary_sensor:
|
||||
# Pins 0 and 39 are the first and last pins, so between them they cover every
|
||||
# bank index the five-port register maths has to get right.
|
||||
- platform: gpio
|
||||
id: pca9505_input_first
|
||||
name: PCA9505 Binary Sensor First
|
||||
pin:
|
||||
pca9505: pca9505_hub
|
||||
number: 0
|
||||
mode: INPUT
|
||||
inverted: true
|
||||
- platform: gpio
|
||||
id: pca9505_input_last
|
||||
name: PCA9505 Binary Sensor Last
|
||||
pin:
|
||||
pca9505: pca9505_hub_second
|
||||
number: 39
|
||||
mode: INPUT
|
||||
inverted: false
|
||||
|
||||
switch:
|
||||
- platform: gpio
|
||||
id: pca9505_output_low
|
||||
name: PCA9505 Switch Low
|
||||
pin:
|
||||
pca9505: pca9505_hub
|
||||
number: 7
|
||||
mode: OUTPUT
|
||||
inverted: false
|
||||
- platform: gpio
|
||||
id: pca9505_output_high
|
||||
name: PCA9505 Switch High
|
||||
pin:
|
||||
pca9505: pca9505_hub
|
||||
number: 32
|
||||
mode: OUTPUT
|
||||
inverted: true
|
||||
@@ -0,0 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
pca9505: !include common.yaml
|
||||
@@ -0,0 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
pca9505: !include common.yaml
|
||||
@@ -0,0 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO2
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
pca9505: !include common.yaml
|
||||
@@ -0,0 +1,312 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/pca9505/pca9505.h"
|
||||
|
||||
namespace esphome::pca9505::testing {
|
||||
|
||||
// Models the PCA9505 register map: five ports, each with its own input, output,
|
||||
// polarity inversion and configuration byte. The command byte selects a register
|
||||
// and its top bit asks the part to auto-increment through the block.
|
||||
class FakeBus : public i2c::I2CBus {
|
||||
public:
|
||||
static constexpr uint8_t ADDRESS = 0x26;
|
||||
|
||||
i2c::ErrorCode write_readv(uint8_t address, const uint8_t *write_buffer, size_t write_count, uint8_t *read_buffer,
|
||||
size_t read_count) override {
|
||||
EXPECT_EQ(address, ADDRESS);
|
||||
if (this->fail)
|
||||
return i2c::ERROR_TIMEOUT;
|
||||
|
||||
// Every transfer starts with the command byte.
|
||||
EXPECT_GE(write_count, 1u);
|
||||
const uint8_t command = write_buffer[0];
|
||||
const bool auto_increment = (command & 0x80) != 0;
|
||||
uint8_t *block = this->block_for(command & 0x7F);
|
||||
const uint8_t index = (command & 0x7F) % 8;
|
||||
|
||||
if (read_count != 0) {
|
||||
this->reads++;
|
||||
// Without auto-increment the part keeps returning the same register.
|
||||
EXPECT_TRUE(auto_increment || read_count == 1);
|
||||
for (size_t i = 0; i < read_count; i++)
|
||||
read_buffer[i] = block[auto_increment ? index + i : index];
|
||||
return i2c::ERROR_OK;
|
||||
}
|
||||
|
||||
this->writes++;
|
||||
EXPECT_TRUE(auto_increment || write_count == 2);
|
||||
for (size_t i = 1; i < write_count; i++)
|
||||
block[auto_increment ? index + i - 1 : index] = write_buffer[i];
|
||||
return i2c::ERROR_OK;
|
||||
}
|
||||
|
||||
uint8_t *block_for(uint8_t reg) {
|
||||
if (reg < 0x08)
|
||||
return this->input;
|
||||
if (reg < 0x10)
|
||||
return this->output;
|
||||
if (reg < 0x18)
|
||||
return this->invert;
|
||||
if (reg < 0x20)
|
||||
return this->config;
|
||||
return this->mask;
|
||||
}
|
||||
|
||||
// Power-on reset defaults from the datasheet.
|
||||
uint8_t input[BANK_COUNT]{0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
uint8_t output[BANK_COUNT]{0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
uint8_t invert[BANK_COUNT]{0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
uint8_t config[BANK_COUNT]{0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
uint8_t mask[BANK_COUNT]{0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
|
||||
unsigned reads{0};
|
||||
unsigned writes{0};
|
||||
bool fail{false};
|
||||
};
|
||||
|
||||
class PCA9505Test : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
this->component_.set_i2c_bus(&this->bus_);
|
||||
this->component_.set_i2c_address(FakeBus::ADDRESS);
|
||||
}
|
||||
|
||||
PCA9505GPIOPin make_pin_(uint8_t number, gpio::Flags flags, bool inverted = false) {
|
||||
PCA9505GPIOPin pin;
|
||||
pin.set_parent(&this->component_);
|
||||
pin.set_pin(number);
|
||||
pin.set_flags(flags);
|
||||
pin.set_inverted(inverted);
|
||||
return pin;
|
||||
}
|
||||
|
||||
FakeBus bus_;
|
||||
PCA9505Component component_;
|
||||
InternalGPIOPin int_pin_; // digital_read() returns the inverted flag on the host, so it doubles as the level
|
||||
};
|
||||
|
||||
TEST_F(PCA9505Test, SetupLeavesEveryPinAnInputWithOutputsLow) {
|
||||
this->component_.setup();
|
||||
|
||||
ASSERT_FALSE(this->component_.is_failed());
|
||||
for (uint8_t bank = 0; bank < BANK_COUNT; bank++) {
|
||||
EXPECT_EQ(this->bus_.config[bank], 0xFF) << "bank " << int(bank) << " should still be an input";
|
||||
EXPECT_EQ(this->bus_.output[bank], 0x00) << "bank " << int(bank) << " should be driven low";
|
||||
EXPECT_EQ(this->bus_.invert[bank], 0x00) << "bank " << int(bank) << " should not be inverted";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(PCA9505Test, SetupFailsWhenTheDeviceIsAbsent) {
|
||||
this->bus_.fail = true;
|
||||
this->component_.setup();
|
||||
|
||||
EXPECT_TRUE(this->component_.is_failed());
|
||||
}
|
||||
|
||||
// The chip has 40 pins, so pin numbers reach past the width of an int shift.
|
||||
// Every pin must land in its own bank and bit.
|
||||
TEST_F(PCA9505Test, EachPinMapsToItsOwnBankAndBit) {
|
||||
this->component_.setup();
|
||||
|
||||
for (uint8_t number = 0; number < PIN_COUNT; number++) {
|
||||
PCA9505GPIOPin pin = this->make_pin_(number, gpio::FLAG_OUTPUT);
|
||||
pin.setup();
|
||||
pin.digital_write(true);
|
||||
|
||||
for (uint8_t bank = 0; bank < BANK_COUNT; bank++) {
|
||||
const uint8_t want = (bank == number / 8) ? uint8_t(1 << (number % 8)) : 0x00;
|
||||
ASSERT_EQ(this->bus_.output[bank], want) << "pin " << int(number) << " wrote outside its own bit";
|
||||
}
|
||||
|
||||
pin.digital_write(false);
|
||||
ASSERT_EQ(this->bus_.output[number / 8], 0x00) << "pin " << int(number) << " did not clear";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(PCA9505Test, ConfiguringAnOutputOnlyTouchesItsOwnBank) {
|
||||
this->component_.setup();
|
||||
|
||||
PCA9505GPIOPin pin = this->make_pin_(35, gpio::FLAG_OUTPUT);
|
||||
pin.setup();
|
||||
|
||||
// Pin 35 is bit 3 of bank 4. The part reads a 1 as an input, so that bit clears.
|
||||
EXPECT_EQ(this->bus_.config[4], 0xFF & ~(1 << 3));
|
||||
for (uint8_t bank = 0; bank < 4; bank++)
|
||||
EXPECT_EQ(this->bus_.config[bank], 0xFF) << "bank " << int(bank) << " should be untouched";
|
||||
}
|
||||
|
||||
TEST_F(PCA9505Test, ReadsReportTheLevelOnTheHighestPin) {
|
||||
this->component_.setup();
|
||||
PCA9505GPIOPin pin = this->make_pin_(39, gpio::FLAG_INPUT);
|
||||
pin.setup();
|
||||
|
||||
this->bus_.input[4] = 0x80; // pin 39 high
|
||||
EXPECT_TRUE(pin.digital_read());
|
||||
|
||||
this->component_.loop(); // clears the cache so the next read hits the bus
|
||||
this->bus_.input[4] = 0x00;
|
||||
EXPECT_FALSE(pin.digital_read());
|
||||
}
|
||||
|
||||
TEST_F(PCA9505Test, InvertedPinFlipsTheReportedLevel) {
|
||||
this->component_.setup();
|
||||
PCA9505GPIOPin pin = this->make_pin_(39, gpio::FLAG_INPUT, /*inverted=*/true);
|
||||
pin.setup();
|
||||
|
||||
this->bus_.input[4] = 0x80;
|
||||
EXPECT_FALSE(pin.digital_read());
|
||||
|
||||
this->component_.loop();
|
||||
this->bus_.input[4] = 0x00;
|
||||
EXPECT_TRUE(pin.digital_read());
|
||||
}
|
||||
|
||||
// Eight pins sharing a port must cost one transfer per loop, not eight.
|
||||
TEST_F(PCA9505Test, PinsInOneBankShareASingleRead) {
|
||||
this->component_.setup();
|
||||
|
||||
PCA9505GPIOPin pins[8];
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
pins[i] = this->make_pin_(8 + i, gpio::FLAG_INPUT);
|
||||
pins[i].setup();
|
||||
}
|
||||
|
||||
this->bus_.input[1] = 0xA5;
|
||||
const unsigned before = this->bus_.reads;
|
||||
for (uint8_t i = 0; i < 8; i++)
|
||||
EXPECT_EQ(pins[i].digital_read(), ((0xA5 >> i) & 1) != 0) << "pin " << int(8 + i);
|
||||
EXPECT_EQ(this->bus_.reads - before, 1u);
|
||||
}
|
||||
|
||||
// Pins in different ports are cached separately, so each port costs its own read.
|
||||
TEST_F(PCA9505Test, EachBankIsReadSeparately) {
|
||||
this->component_.setup();
|
||||
|
||||
PCA9505GPIOPin pins[BANK_COUNT];
|
||||
for (uint8_t bank = 0; bank < BANK_COUNT; bank++) {
|
||||
pins[bank] = this->make_pin_(bank * 8, gpio::FLAG_INPUT);
|
||||
pins[bank].setup();
|
||||
}
|
||||
|
||||
const unsigned before = this->bus_.reads;
|
||||
for (auto &pin : pins)
|
||||
pin.digital_read();
|
||||
EXPECT_EQ(this->bus_.reads - before, BANK_COUNT);
|
||||
}
|
||||
|
||||
TEST_F(PCA9505Test, WritesAreSkippedOnceSetupHasFailed) {
|
||||
this->bus_.fail = true;
|
||||
this->component_.setup();
|
||||
ASSERT_TRUE(this->component_.is_failed());
|
||||
|
||||
this->bus_.fail = false;
|
||||
const unsigned before = this->bus_.writes;
|
||||
PCA9505GPIOPin pin = this->make_pin_(0, gpio::FLAG_OUTPUT);
|
||||
pin.setup();
|
||||
pin.digital_write(true);
|
||||
EXPECT_EQ(this->bus_.writes, before);
|
||||
}
|
||||
|
||||
// A 1 in a mask register blocks that pin from pulling INT low.
|
||||
TEST_F(PCA9505Test, SetupMasksEveryInterrupt) {
|
||||
// Pretend a previous run left port 2 unmasked. The chip keeps its registers
|
||||
// through an MCU reboot, so setup must write them rather than assume the reset state.
|
||||
this->bus_.mask[2] = 0x00;
|
||||
this->component_.setup();
|
||||
|
||||
for (uint8_t bank = 0; bank < BANK_COUNT; bank++)
|
||||
EXPECT_EQ(this->bus_.mask[bank], 0xFF) << "bank " << int(bank) << " should be masked";
|
||||
}
|
||||
|
||||
TEST_F(PCA9505Test, ConfiguringAnInputUnmasksOnlyThatPin) {
|
||||
this->component_.set_interrupt_pin(&this->int_pin_);
|
||||
this->component_.setup();
|
||||
|
||||
PCA9505GPIOPin pin = this->make_pin_(34, gpio::FLAG_INPUT);
|
||||
pin.setup();
|
||||
|
||||
// Pin 34 is bit 2 of bank 4, and a 0 lets it through.
|
||||
EXPECT_EQ(this->bus_.mask[4], 0xFF & ~(1 << 2));
|
||||
for (uint8_t bank = 0; bank < 4; bank++)
|
||||
EXPECT_EQ(this->bus_.mask[bank], 0xFF) << "bank " << int(bank) << " should be untouched";
|
||||
}
|
||||
|
||||
TEST_F(PCA9505Test, ConfiguringAnOutputLeavesItMasked) {
|
||||
this->component_.set_interrupt_pin(&this->int_pin_);
|
||||
this->component_.setup();
|
||||
|
||||
PCA9505GPIOPin pin = this->make_pin_(34, gpio::FLAG_OUTPUT);
|
||||
pin.setup();
|
||||
|
||||
// An output must not be able to pull INT low.
|
||||
EXPECT_EQ(this->bus_.mask[4], 0xFF);
|
||||
}
|
||||
|
||||
TEST_F(PCA9505Test, WithoutAnInterruptPinTheMasksAreNotRewritten) {
|
||||
this->component_.setup();
|
||||
const unsigned before = this->bus_.writes;
|
||||
|
||||
PCA9505GPIOPin pin = this->make_pin_(34, gpio::FLAG_INPUT);
|
||||
pin.setup();
|
||||
|
||||
EXPECT_EQ(this->bus_.writes, before + 1); // the configuration register only
|
||||
EXPECT_EQ(this->bus_.mask[4], 0xFF);
|
||||
}
|
||||
|
||||
// Without an interrupt pin an input turns the polling loop on; with one it stays off.
|
||||
TEST_F(PCA9505Test, AnInputEnablesPollingOnlyWithoutAnInterruptPin) {
|
||||
this->component_.setup();
|
||||
ASSERT_TRUE(this->component_.is_idle());
|
||||
PCA9505GPIOPin polled = this->make_pin_(3, gpio::FLAG_INPUT);
|
||||
polled.setup();
|
||||
EXPECT_TRUE(this->component_.is_in_loop_state());
|
||||
|
||||
PCA9505Component with_interrupt;
|
||||
with_interrupt.set_i2c_bus(&this->bus_);
|
||||
with_interrupt.set_i2c_address(FakeBus::ADDRESS);
|
||||
with_interrupt.set_interrupt_pin(&this->int_pin_);
|
||||
with_interrupt.setup();
|
||||
ASSERT_TRUE(with_interrupt.is_idle());
|
||||
PCA9505GPIOPin pin;
|
||||
pin.set_parent(&with_interrupt);
|
||||
pin.set_pin(3);
|
||||
pin.set_flags(gpio::FLAG_INPUT);
|
||||
pin.setup();
|
||||
EXPECT_TRUE(with_interrupt.is_idle());
|
||||
}
|
||||
|
||||
// With an interrupt pin a read is served from the cache until loop() clears it.
|
||||
TEST_F(PCA9505Test, InterruptPinKeepsTheCacheUntilTheLoopRuns) {
|
||||
this->component_.set_interrupt_pin(&this->int_pin_);
|
||||
this->component_.setup();
|
||||
PCA9505GPIOPin pin = this->make_pin_(3, gpio::FLAG_INPUT);
|
||||
pin.setup();
|
||||
|
||||
const unsigned before = this->bus_.reads;
|
||||
pin.digital_read();
|
||||
pin.digital_read();
|
||||
EXPECT_EQ(this->bus_.reads, before + 1);
|
||||
|
||||
this->component_.loop();
|
||||
pin.digital_read();
|
||||
EXPECT_EQ(this->bus_.reads, before + 2);
|
||||
}
|
||||
|
||||
// The loop only turns itself off once INT has gone high again, so a change that
|
||||
// lands during the read cannot leave the cache stale with no edge to wake it.
|
||||
TEST_F(PCA9505Test, LoopStaysOnUntilIntIsReleased) {
|
||||
this->component_.set_interrupt_pin(&this->int_pin_);
|
||||
this->component_.setup();
|
||||
this->component_.enable_loop();
|
||||
ASSERT_TRUE(this->component_.is_in_loop_state());
|
||||
|
||||
this->int_pin_.set_inverted(false); // the host pin reads back its inverted flag: INT still low
|
||||
this->component_.loop();
|
||||
EXPECT_TRUE(this->component_.is_in_loop_state());
|
||||
|
||||
this->int_pin_.set_inverted(true); // INT released
|
||||
this->component_.loop();
|
||||
EXPECT_TRUE(this->component_.is_idle());
|
||||
}
|
||||
|
||||
} // namespace esphome::pca9505::testing
|
||||
@@ -0,0 +1,109 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "esphome/components/rtttl/rtttl.h"
|
||||
|
||||
namespace esphome::rtttl::testing {
|
||||
|
||||
// Reads the parser state the tests check; Rtttl is final, so this is a friend rather than a subclass.
|
||||
class RtttlParserAccess {
|
||||
public:
|
||||
static uint8_t denominator(const Rtttl &p) { return p.default_note_denominator_; }
|
||||
static uint8_t octave(const Rtttl &p) { return p.default_octave_; }
|
||||
static uint16_t wholenote(const Rtttl &p) { return p.wholenote_duration_; }
|
||||
static uint16_t note_duration(const Rtttl &p) { return p.note_duration_; }
|
||||
static uint32_t freq(const Rtttl &p) { return p.output_freq_; }
|
||||
static size_t position(const Rtttl &p) { return p.position_; }
|
||||
static char at(const Rtttl &p, size_t pos) { return p.song_at_(pos); }
|
||||
static void set_running(Rtttl &p) { p.state_ = State::RUNNING; }
|
||||
};
|
||||
|
||||
using Access = RtttlParserAccess;
|
||||
|
||||
static const char SONG[] = "test:d=8,o=5,b=100:c";
|
||||
|
||||
TEST(RtttlParser, StaticSongParsesTheHeader) {
|
||||
Rtttl player;
|
||||
player.play(StaticSong{SONG});
|
||||
EXPECT_EQ(Access::denominator(player), 8);
|
||||
EXPECT_EQ(Access::octave(player), 5);
|
||||
EXPECT_EQ(Access::wholenote(player), 60 * 1000 * 4 / 100);
|
||||
// Positioned on the first note after the second ':'
|
||||
EXPECT_EQ(Access::position(player), 19u);
|
||||
EXPECT_EQ(Access::at(player, Access::position(player)), 'c');
|
||||
}
|
||||
|
||||
TEST(RtttlParser, OwnedSongParsesTheSameAndOutlivesTheCaller) {
|
||||
Rtttl player;
|
||||
{
|
||||
std::string song(SONG);
|
||||
player.play(song);
|
||||
}
|
||||
EXPECT_EQ(Access::denominator(player), 8);
|
||||
EXPECT_EQ(Access::octave(player), 5);
|
||||
EXPECT_EQ(Access::at(player, 0), 't');
|
||||
EXPECT_EQ(Access::at(player, Access::position(player)), 'c');
|
||||
EXPECT_EQ(Access::at(player, 100), '\0');
|
||||
}
|
||||
|
||||
TEST(RtttlParser, ControlOrderDoesNotMatter) {
|
||||
for (const char *song : {"a:b=100,o=5,d=8:c", "a:o=5,b=100,d=8:c", "a:d=8,b=100,o=5:c"}) {
|
||||
Rtttl player;
|
||||
player.play(StaticSong{song});
|
||||
EXPECT_EQ(Access::denominator(player), 8) << song;
|
||||
EXPECT_EQ(Access::octave(player), 5) << song;
|
||||
EXPECT_EQ(Access::wholenote(player), 2400) << song;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RtttlParser, MissingControlsUseDefaults) {
|
||||
Rtttl player;
|
||||
player.play(StaticSong{"a::c"});
|
||||
EXPECT_EQ(Access::denominator(player), DEFAULT_NOTE_DENOMINATOR);
|
||||
EXPECT_EQ(Access::octave(player), DEFAULT_OCTAVE);
|
||||
EXPECT_EQ(Access::wholenote(player), 60 * 1000 * 4 / 63);
|
||||
}
|
||||
|
||||
TEST(RtttlParser, NotesReadFromStaticStorage) {
|
||||
Rtttl player;
|
||||
player.play(StaticSong{"t:d=4,o=5,b=120:8c,p"});
|
||||
Access::set_running(player);
|
||||
player.loop();
|
||||
// C5 at 523 Hz for an eighth of a 2000 ms whole note
|
||||
EXPECT_EQ(Access::freq(player), 523u);
|
||||
EXPECT_EQ(Access::note_duration(player), 250);
|
||||
player.loop();
|
||||
// A pause for a default quarter note
|
||||
EXPECT_EQ(Access::freq(player), 0u);
|
||||
EXPECT_EQ(Access::note_duration(player), 500);
|
||||
player.loop();
|
||||
// End of the song
|
||||
EXPECT_EQ(Access::at(player, Access::position(player)), '\0');
|
||||
}
|
||||
|
||||
TEST(RtttlParser, RefusesANewSongWhilePlaying) {
|
||||
Rtttl player;
|
||||
player.play(StaticSong{"first:d=8:c"});
|
||||
Access::set_running(player);
|
||||
player.play(std::string("second:d=16:c"));
|
||||
EXPECT_EQ(Access::denominator(player), 8);
|
||||
EXPECT_EQ(Access::at(player, 0), 'f');
|
||||
}
|
||||
|
||||
TEST(RtttlParser, ClampsASongLongerThanTheLengthField) {
|
||||
Rtttl player;
|
||||
player.play(std::string(70000, 'a'));
|
||||
EXPECT_EQ(Access::at(player, 65534), 'a');
|
||||
EXPECT_EQ(Access::at(player, 65535), '\0');
|
||||
EXPECT_EQ(Access::note_duration(player), 0);
|
||||
}
|
||||
|
||||
TEST(RtttlParser, RejectsASongWithoutAName) {
|
||||
Rtttl player;
|
||||
player.play(StaticSong{"no colon here"});
|
||||
EXPECT_EQ(Access::note_duration(player), 0);
|
||||
EXPECT_EQ(Access::position(player), 13u);
|
||||
}
|
||||
|
||||
} // namespace esphome::rtttl::testing
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user