mirror of
https://github.com/esphome/esphome.git
synced 2026-10-24 15:05:33 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c32da9ff99 | ||
|
|
44118387d1 | ||
|
|
26d85ed066 |
No files matched your search
@@ -1,127 +1,129 @@
|
||||
#include "am43_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::am43 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "am43");
|
||||
const uint8_t START_PACKET[5] = {0x00, 0xff, 0x00, 0x00, 0x9a};
|
||||
|
||||
static constexpr uint8_t START_PACKET[5] = {0x00, 0xff, 0x00, 0x00, 0x9a};
|
||||
|
||||
Am43Packet Am43Encoder::get_battery_level_request() {
|
||||
Am43Packet *Am43Encoder::get_battery_level_request() {
|
||||
uint8_t data = 0x1;
|
||||
return encode(CMD_GET_BATTERY_LEVEL, &data, 1);
|
||||
return this->encode_(0xA2, &data, 1);
|
||||
}
|
||||
|
||||
Am43Packet Am43Encoder::get_light_level_request() {
|
||||
Am43Packet *Am43Encoder::get_light_level_request() {
|
||||
uint8_t data = 0x1;
|
||||
return encode(CMD_GET_LIGHT_LEVEL, &data, 1);
|
||||
return this->encode_(0xAA, &data, 1);
|
||||
}
|
||||
|
||||
Am43Packet Am43Encoder::get_position_request() {
|
||||
Am43Packet *Am43Encoder::get_position_request() {
|
||||
uint8_t data = 0x1;
|
||||
return encode(CMD_GET_POSITION, &data, 1);
|
||||
return this->encode_(CMD_GET_POSITION, &data, 1);
|
||||
}
|
||||
|
||||
Am43Packet Am43Encoder::get_send_pin_request(uint16_t pin) {
|
||||
Am43Packet *Am43Encoder::get_send_pin_request(uint16_t pin) {
|
||||
uint8_t data[2];
|
||||
data[0] = (pin & 0xFF00) >> 8;
|
||||
data[1] = pin & 0xFF;
|
||||
return encode(CMD_SEND_PIN, data, 2);
|
||||
return this->encode_(CMD_SEND_PIN, data, 2);
|
||||
}
|
||||
|
||||
Am43Packet Am43Encoder::get_open_request() {
|
||||
Am43Packet *Am43Encoder::get_open_request() {
|
||||
uint8_t data = 0xDD;
|
||||
return encode(CMD_SET_STATE, &data, 1);
|
||||
return this->encode_(CMD_SET_STATE, &data, 1);
|
||||
}
|
||||
|
||||
Am43Packet Am43Encoder::get_close_request() {
|
||||
Am43Packet *Am43Encoder::get_close_request() {
|
||||
uint8_t data = 0xEE;
|
||||
return encode(CMD_SET_STATE, &data, 1);
|
||||
return this->encode_(CMD_SET_STATE, &data, 1);
|
||||
}
|
||||
|
||||
Am43Packet Am43Encoder::get_stop_request() {
|
||||
Am43Packet *Am43Encoder::get_stop_request() {
|
||||
uint8_t data = 0xCC;
|
||||
return encode(CMD_SET_STATE, &data, 1);
|
||||
return this->encode_(CMD_SET_STATE, &data, 1);
|
||||
}
|
||||
|
||||
Am43Packet Am43Encoder::get_set_position_request(uint8_t position) { return encode(CMD_SET_POSITION, &position, 1); }
|
||||
|
||||
Am43Packet Am43Encoder::encode(uint8_t command, const uint8_t *data, uint8_t length) {
|
||||
Am43Packet packet;
|
||||
memcpy(packet.data, START_PACKET, sizeof(START_PACKET));
|
||||
packet.data[5] = command;
|
||||
packet.data[6] = length;
|
||||
memcpy(&packet.data[7], data, length);
|
||||
packet.length = length + 7;
|
||||
// The device checks the inverted XOR of everything before it
|
||||
packet.data[packet.length] = xor_bytes(packet.data, packet.length, 0xff);
|
||||
packet.length++;
|
||||
char hex_buf[format_hex_size(sizeof(packet.data))];
|
||||
ESP_LOGV(TAG, "ENC(%d): 0x%s", packet.length, format_hex_to(hex_buf, packet.data, packet.length));
|
||||
return packet;
|
||||
Am43Packet *Am43Encoder::get_set_position_request(uint8_t position) {
|
||||
return this->encode_(CMD_SET_POSITION, &position, 1);
|
||||
}
|
||||
|
||||
void Am43Encoder::checksum_() {
|
||||
uint8_t checksum = 0;
|
||||
int i = 0;
|
||||
for (i = 0; i < this->packet_.length; i++)
|
||||
checksum = checksum ^ this->packet_.data[i];
|
||||
this->packet_.data[i] = checksum ^ 0xff;
|
||||
this->packet_.length++;
|
||||
}
|
||||
|
||||
Am43Packet *Am43Encoder::encode_(uint8_t command, uint8_t *data, uint8_t length) {
|
||||
memcpy(this->packet_.data, START_PACKET, 5);
|
||||
this->packet_.data[5] = command;
|
||||
this->packet_.data[6] = length;
|
||||
memcpy(&this->packet_.data[7], data, length);
|
||||
this->packet_.length = length + 7;
|
||||
this->checksum_();
|
||||
char hex_buf[format_hex_size(sizeof(this->packet_.data))];
|
||||
ESP_LOGV("am43", "ENC(%d): 0x%s", this->packet_.length,
|
||||
format_hex_to(hex_buf, this->packet_.data, this->packet_.length));
|
||||
return &this->packet_;
|
||||
}
|
||||
|
||||
#define VERIFY_MIN_LENGTH(x) \
|
||||
if (length < (x)) \
|
||||
return;
|
||||
|
||||
void Am43Decoder::decode(const uint8_t *data, uint16_t length) {
|
||||
// Reset so a decoder reused across frames reports only the latest one
|
||||
this->has_battery_level_ = false;
|
||||
this->has_light_level_ = false;
|
||||
this->has_set_position_response_ = false;
|
||||
this->has_set_state_response_ = false;
|
||||
this->has_position_ = false;
|
||||
this->has_pin_response_ = false;
|
||||
char hex_buf[format_hex_size(sizeof(Am43Packet::data))]; // Max expected packet size
|
||||
ESP_LOGV(TAG, "DEC(%d): 0x%s", length, format_hex_to(hex_buf, data, length));
|
||||
char hex_buf[format_hex_size(24)]; // Max expected packet size
|
||||
ESP_LOGV("am43", "DEC(%d): 0x%s", length, format_hex_to(hex_buf, data, length));
|
||||
|
||||
if (length < 2 || data[0] != 0x9a)
|
||||
return;
|
||||
switch (data[1]) {
|
||||
case CMD_GET_BATTERY_LEVEL: {
|
||||
if (length < 8)
|
||||
return;
|
||||
VERIFY_MIN_LENGTH(8);
|
||||
this->battery_level_ = data[7];
|
||||
this->has_battery_level_ = true;
|
||||
break;
|
||||
}
|
||||
case CMD_GET_LIGHT_LEVEL: {
|
||||
if (length < 5)
|
||||
return;
|
||||
VERIFY_MIN_LENGTH(5);
|
||||
this->light_level_ = 100 * ((float) data[4] / 9);
|
||||
this->has_light_level_ = true;
|
||||
break;
|
||||
}
|
||||
case CMD_GET_POSITION: {
|
||||
if (length < 6)
|
||||
return;
|
||||
VERIFY_MIN_LENGTH(6);
|
||||
this->position_ = data[5];
|
||||
this->has_position_ = true;
|
||||
break;
|
||||
}
|
||||
case CMD_NOTIFY_POSITION: {
|
||||
if (length < 5)
|
||||
return;
|
||||
VERIFY_MIN_LENGTH(5);
|
||||
this->position_ = data[4];
|
||||
this->has_position_ = true;
|
||||
break;
|
||||
}
|
||||
case CMD_SEND_PIN: {
|
||||
if (length < 4)
|
||||
return;
|
||||
VERIFY_MIN_LENGTH(4);
|
||||
this->pin_ok_ = data[3] == RESPONSE_ACK;
|
||||
this->has_pin_response_ = true;
|
||||
break;
|
||||
}
|
||||
case CMD_SET_POSITION: {
|
||||
if (length < 4)
|
||||
return;
|
||||
VERIFY_MIN_LENGTH(4);
|
||||
this->set_position_ok_ = data[3] == RESPONSE_ACK;
|
||||
this->has_set_position_response_ = true;
|
||||
break;
|
||||
}
|
||||
case CMD_SET_STATE: {
|
||||
if (length < 4)
|
||||
return;
|
||||
VERIFY_MIN_LENGTH(4);
|
||||
this->set_state_ok_ = data[3] == RESPONSE_ACK;
|
||||
this->has_set_state_response_ = true;
|
||||
break;
|
||||
@@ -129,6 +131,6 @@ void Am43Decoder::decode(const uint8_t *data, uint16_t length) {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::am43
|
||||
@@ -1,57 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::am43 {
|
||||
|
||||
static constexpr uint16_t AM43_SERVICE_UUID = 0xFE50;
|
||||
static constexpr uint16_t AM43_CHARACTERISTIC_UUID = 0xFE51;
|
||||
static const uint16_t AM43_SERVICE_UUID = 0xFE50;
|
||||
static const uint16_t AM43_CHARACTERISTIC_UUID = 0xFE51;
|
||||
//
|
||||
// Tuya identifiers, only to detect and warn users as they are incompatible.
|
||||
static constexpr uint16_t AM43_TUYA_SERVICE_UUID = 0x1910;
|
||||
static constexpr uint16_t AM43_TUYA_CHARACTERISTIC_UUID = 0x2b11;
|
||||
static const uint16_t AM43_TUYA_SERVICE_UUID = 0x1910;
|
||||
static const uint16_t AM43_TUYA_CHARACTERISTIC_UUID = 0x2b11;
|
||||
|
||||
struct Am43Packet {
|
||||
uint8_t length;
|
||||
uint8_t data[24];
|
||||
};
|
||||
|
||||
static constexpr uint8_t CMD_GET_BATTERY_LEVEL = 0xA2;
|
||||
static constexpr uint8_t CMD_GET_LIGHT_LEVEL = 0xAA;
|
||||
static constexpr uint8_t CMD_GET_POSITION = 0xA7;
|
||||
static constexpr uint8_t CMD_SEND_PIN = 0x17;
|
||||
static constexpr uint8_t CMD_SET_STATE = 0x0A;
|
||||
static constexpr uint8_t CMD_SET_POSITION = 0x0D;
|
||||
static constexpr uint8_t CMD_NOTIFY_POSITION = 0xA1;
|
||||
static const uint8_t CMD_GET_BATTERY_LEVEL = 0xA2;
|
||||
static const uint8_t CMD_GET_LIGHT_LEVEL = 0xAA;
|
||||
static const uint8_t CMD_GET_POSITION = 0xA7;
|
||||
static const uint8_t CMD_SEND_PIN = 0x17;
|
||||
static const uint8_t CMD_SET_STATE = 0x0A;
|
||||
static const uint8_t CMD_SET_POSITION = 0x0D;
|
||||
static const uint8_t CMD_NOTIFY_POSITION = 0xA1;
|
||||
|
||||
static constexpr uint8_t RESPONSE_ACK = 0x5A;
|
||||
static constexpr uint8_t RESPONSE_NACK = 0xA5;
|
||||
static const uint8_t RESPONSE_ACK = 0x5A;
|
||||
static const uint8_t RESPONSE_NACK = 0xA5;
|
||||
|
||||
/// Builds request packets; each returns a packet the caller sends from its own stack.
|
||||
class Am43Encoder {
|
||||
public:
|
||||
static Am43Packet get_battery_level_request();
|
||||
static Am43Packet get_light_level_request();
|
||||
static Am43Packet get_position_request();
|
||||
static Am43Packet get_send_pin_request(uint16_t pin);
|
||||
static Am43Packet get_open_request();
|
||||
static Am43Packet get_close_request();
|
||||
static Am43Packet get_stop_request();
|
||||
static Am43Packet get_set_position_request(uint8_t position);
|
||||
Am43Packet *get_battery_level_request();
|
||||
Am43Packet *get_light_level_request();
|
||||
Am43Packet *get_position_request();
|
||||
Am43Packet *get_send_pin_request(uint16_t pin);
|
||||
Am43Packet *get_open_request();
|
||||
Am43Packet *get_close_request();
|
||||
Am43Packet *get_stop_request();
|
||||
Am43Packet *get_set_position_request(uint8_t position);
|
||||
|
||||
protected:
|
||||
static Am43Packet encode(uint8_t command, const uint8_t *data, uint8_t length);
|
||||
void checksum_();
|
||||
Am43Packet *encode_(uint8_t command, uint8_t *data, uint8_t length);
|
||||
Am43Packet packet_;
|
||||
};
|
||||
|
||||
/// Decodes one notification; construct it on the stack for each frame.
|
||||
class Am43Decoder {
|
||||
public:
|
||||
void decode(const uint8_t *data, uint16_t length);
|
||||
bool has_battery_level() const { return this->has_battery_level_; }
|
||||
bool has_light_level() const { return this->has_light_level_; }
|
||||
bool has_set_position_response() const { return this->has_set_position_response_; }
|
||||
bool has_set_state_response() const { return this->has_set_state_response_; }
|
||||
bool has_position() const { return this->has_position_; }
|
||||
bool has_pin_response() const { return this->has_pin_response_; }
|
||||
bool has_battery_level() { return this->has_battery_level_; }
|
||||
bool has_light_level() { return this->has_light_level_; }
|
||||
bool has_set_position_response() { return this->has_set_position_response_; }
|
||||
bool has_set_state_response() { return this->has_set_state_response_; }
|
||||
bool has_position() { return this->has_position_; }
|
||||
bool has_pin_response() { return this->has_pin_response_; }
|
||||
|
||||
union {
|
||||
uint8_t position_;
|
||||
@@ -63,12 +65,12 @@ class Am43Decoder {
|
||||
};
|
||||
|
||||
protected:
|
||||
bool has_battery_level_{false};
|
||||
bool has_light_level_{false};
|
||||
bool has_set_position_response_{false};
|
||||
bool has_set_state_response_{false};
|
||||
bool has_position_{false};
|
||||
bool has_pin_response_{false};
|
||||
bool has_battery_level_;
|
||||
bool has_light_level_;
|
||||
bool has_set_position_response_;
|
||||
bool has_set_state_response_;
|
||||
bool has_position_;
|
||||
bool has_pin_response_;
|
||||
};
|
||||
|
||||
} // namespace esphome::am43
|
||||
@@ -17,12 +17,25 @@ void Am43Component::dump_config() {
|
||||
this->pin_, (int) this->invert_position_);
|
||||
}
|
||||
|
||||
void Am43Component::setup() { this->position = COVER_OPEN; }
|
||||
void Am43Component::setup() {
|
||||
this->position = COVER_OPEN;
|
||||
this->encoder_ = make_unique<Am43Encoder>();
|
||||
this->decoder_ = make_unique<Am43Decoder>();
|
||||
this->logged_in_ = false;
|
||||
}
|
||||
|
||||
void Am43Component::loop() {
|
||||
if (this->node_state == espbt::ClientState::ESTABLISHED && !this->logged_in_) {
|
||||
auto *packet = this->encoder_->get_send_pin_request(this->pin_);
|
||||
auto status =
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
|
||||
packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
ESP_LOGI(TAG, "[%s] Logging into AM43", LOG_STR_ARG(this->get_log_name()));
|
||||
this->logged_in_ = this->write_packet_(Am43Encoder::get_send_pin_request(this->pin_), LOG_STR("set_pin"));
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] Error writing set_pin to device, error = %d", LOG_STR_ARG(this->get_log_name()), status);
|
||||
} else {
|
||||
this->logged_in_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,15 +53,29 @@ void Am43Component::control(const CoverCall &call) {
|
||||
ESP_LOGW(TAG, "[%s] Cannot send cover control, not connected", LOG_STR_ARG(this->get_log_name()));
|
||||
return;
|
||||
}
|
||||
if (call.get_stop())
|
||||
this->write_packet_(Am43Encoder::get_stop_request(), LOG_STR("stop"));
|
||||
if (call.get_stop()) {
|
||||
auto *packet = this->encoder_->get_stop_request();
|
||||
auto status =
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
|
||||
packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] Error writing stop command to device, error = %d", LOG_STR_ARG(this->get_log_name()), status);
|
||||
}
|
||||
}
|
||||
auto opt_pos = call.get_position();
|
||||
if (opt_pos.has_value()) {
|
||||
auto pos = *opt_pos;
|
||||
|
||||
if (this->invert_position_)
|
||||
pos = 1 - pos;
|
||||
this->write_packet_(Am43Encoder::get_set_position_request(100 - (uint8_t) (pos * 100)), LOG_STR("set_position"));
|
||||
auto *packet = this->encoder_->get_set_position_request(100 - (uint8_t) (pos * 100));
|
||||
auto status =
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
|
||||
packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] Error writing set_position command to device, error = %d", LOG_STR_ARG(this->get_log_name()),
|
||||
status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,11 +113,10 @@ void Am43Component::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
if (param->notify.handle != this->char_handle_)
|
||||
break;
|
||||
Am43Decoder decoder;
|
||||
decoder.decode(param->notify.value, param->notify.value_len);
|
||||
this->decoder_->decode(param->notify.value, param->notify.value_len);
|
||||
|
||||
if (decoder.has_position()) {
|
||||
this->position = ((float) decoder.position_ / 100.0f);
|
||||
if (this->decoder_->has_position()) {
|
||||
this->position = ((float) this->decoder_->position_ / 100.0f);
|
||||
if (!this->invert_position_)
|
||||
this->position = 1 - this->position;
|
||||
if (this->position > 0.97f)
|
||||
@@ -100,20 +126,27 @@ void Am43Component::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
if (decoder.has_pin_response()) {
|
||||
if (decoder.pin_ok_) {
|
||||
if (this->decoder_->has_pin_response()) {
|
||||
if (this->decoder_->pin_ok_) {
|
||||
ESP_LOGI(TAG, "[%s] AM43 pin accepted.", LOG_STR_ARG(this->get_log_name()));
|
||||
this->write_packet_(Am43Encoder::get_position_request(), LOG_STR("get_position"));
|
||||
auto *packet = this->encoder_->get_position_request();
|
||||
auto status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(),
|
||||
this->char_handle_, packet->length, packet->data,
|
||||
ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] Error writing set_position to device, error = %d", LOG_STR_ARG(this->get_log_name()),
|
||||
status);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "[%s] AM43 pin rejected!", LOG_STR_ARG(this->get_log_name()));
|
||||
}
|
||||
}
|
||||
|
||||
if (decoder.has_set_position_response() && !decoder.set_position_ok_) {
|
||||
if (this->decoder_->has_set_position_response() && !this->decoder_->set_position_ok_) {
|
||||
ESP_LOGW(TAG, "[%s] Got nack after sending set_position. Bad pin?", LOG_STR_ARG(this->get_log_name()));
|
||||
}
|
||||
|
||||
if (decoder.has_set_state_response() && !decoder.set_state_ok_) {
|
||||
if (this->decoder_->has_set_state_response() && !this->decoder_->set_state_ok_) {
|
||||
ESP_LOGW(TAG, "[%s] Got nack after sending set_state. Bad pin?", LOG_STR_ARG(this->get_log_name()));
|
||||
}
|
||||
break;
|
||||
@@ -123,19 +156,6 @@ void Am43Component::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
}
|
||||
}
|
||||
|
||||
bool Am43Component::write_packet_(const Am43Packet &packet, const LogString *what) {
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) the IDF API takes a mutable buffer it only reads
|
||||
auto status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(),
|
||||
this->char_handle_, packet.length, const_cast<uint8_t *>(packet.data),
|
||||
ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] Error writing %s to device, error = %d", LOG_STR_ARG(this->get_log_name()), LOG_STR_ARG(what),
|
||||
status);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::am43
|
||||
|
||||
#endif
|
||||
@@ -27,12 +27,14 @@ class Am43Component final : public cover::Cover, public esphome::ble_client::BLE
|
||||
|
||||
protected:
|
||||
void control(const cover::CoverCall &call) override;
|
||||
/// Writes a request to the control characteristic; logs and returns false on failure.
|
||||
bool write_packet_(const Am43Packet &packet, const LogString *what);
|
||||
uint16_t char_handle_{0};
|
||||
uint16_t pin_{0};
|
||||
bool invert_position_{false};
|
||||
bool logged_in_{false};
|
||||
uint16_t char_handle_;
|
||||
uint16_t pin_;
|
||||
bool invert_position_;
|
||||
std::unique_ptr<Am43Encoder> encoder_;
|
||||
std::unique_ptr<Am43Decoder> decoder_;
|
||||
bool logged_in_;
|
||||
|
||||
float position_;
|
||||
};
|
||||
|
||||
} // namespace esphome::am43
|
||||
|
||||
@@ -14,9 +14,24 @@ void Am43::dump_config() {
|
||||
LOG_SENSOR(" ", "Illuminance", this->illuminance_);
|
||||
}
|
||||
|
||||
void Am43::setup() {
|
||||
this->encoder_ = make_unique<Am43Encoder>();
|
||||
this->decoder_ = make_unique<Am43Decoder>();
|
||||
this->logged_in_ = false;
|
||||
this->last_battery_update_ = 0;
|
||||
this->current_sensor_ = 0;
|
||||
}
|
||||
|
||||
void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {
|
||||
switch (event) {
|
||||
case ESP_GATTC_OPEN_EVT: {
|
||||
if (param->open.status == ESP_GATT_OK) {
|
||||
this->logged_in_ = false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
this->logged_in_ = false;
|
||||
this->node_state = espbt::ClientState::IDLE;
|
||||
if (this->battery_ != nullptr)
|
||||
this->battery_->publish_state(NAN);
|
||||
@@ -45,21 +60,28 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
if (param->notify.handle != this->char_handle_)
|
||||
break;
|
||||
Am43Decoder decoder;
|
||||
decoder.decode(param->notify.value, param->notify.value_len);
|
||||
this->decoder_->decode(param->notify.value, param->notify.value_len);
|
||||
|
||||
if (this->battery_ != nullptr && decoder.has_battery_level() && millis() - this->last_battery_update_ > 10000) {
|
||||
this->battery_->publish_state(decoder.battery_level_);
|
||||
if (this->battery_ != nullptr && this->decoder_->has_battery_level() &&
|
||||
millis() - this->last_battery_update_ > 10000) {
|
||||
this->battery_->publish_state(this->decoder_->battery_level_);
|
||||
this->last_battery_update_ = millis();
|
||||
}
|
||||
|
||||
if (this->illuminance_ != nullptr && decoder.has_light_level()) {
|
||||
this->illuminance_->publish_state(decoder.light_level_);
|
||||
if (this->illuminance_ != nullptr && this->decoder_->has_light_level()) {
|
||||
this->illuminance_->publish_state(this->decoder_->light_level_);
|
||||
}
|
||||
|
||||
if (this->current_sensor_ > 0) {
|
||||
if (this->illuminance_ != nullptr)
|
||||
this->write_packet_(Am43Encoder::get_light_level_request());
|
||||
if (this->illuminance_ != nullptr) {
|
||||
auto *packet = this->encoder_->get_light_level_request();
|
||||
auto status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(),
|
||||
this->char_handle_, packet->length, packet->data,
|
||||
ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||
}
|
||||
}
|
||||
this->current_sensor_ = 0;
|
||||
}
|
||||
break;
|
||||
@@ -75,22 +97,19 @@ void Am43::update() {
|
||||
return;
|
||||
}
|
||||
if (this->current_sensor_ == 0) {
|
||||
if (this->battery_ != nullptr)
|
||||
this->write_packet_(Am43Encoder::get_battery_level_request());
|
||||
if (this->battery_ != nullptr) {
|
||||
auto *packet = this->encoder_->get_battery_level_request();
|
||||
auto status =
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
|
||||
packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||
}
|
||||
}
|
||||
this->current_sensor_++;
|
||||
}
|
||||
}
|
||||
|
||||
void Am43::write_packet_(const Am43Packet &packet) {
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) the IDF API takes a mutable buffer it only reads
|
||||
auto status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(),
|
||||
this->char_handle_, packet.length, const_cast<uint8_t *>(packet.data),
|
||||
ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::am43
|
||||
|
||||
#endif
|
||||
@@ -16,6 +16,7 @@ namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
class Am43 final : public esphome::ble_client::BLEClientNode, public PollingComponent {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
@@ -24,15 +25,16 @@ class Am43 final : public esphome::ble_client::BLEClientNode, public PollingComp
|
||||
void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; }
|
||||
|
||||
protected:
|
||||
/// Writes a request to the control characteristic; logs on failure.
|
||||
void write_packet_(const Am43Packet &packet);
|
||||
uint16_t char_handle_;
|
||||
std::unique_ptr<Am43Encoder> encoder_;
|
||||
std::unique_ptr<Am43Decoder> decoder_;
|
||||
bool logged_in_;
|
||||
sensor::Sensor *battery_{nullptr};
|
||||
sensor::Sensor *illuminance_{nullptr};
|
||||
uint8_t current_sensor_;
|
||||
// The AM43 often gets into a state where it spams loads of battery update
|
||||
// notifications. Here we will limit to no more than every 10s.
|
||||
uint32_t last_battery_update_{0};
|
||||
uint16_t char_handle_{0};
|
||||
uint8_t current_sensor_{0};
|
||||
uint32_t last_battery_update_;
|
||||
};
|
||||
|
||||
} // namespace esphome::am43
|
||||
|
||||
@@ -110,12 +110,12 @@ bool AS7341Component::read_channels(uint16_t *data) {
|
||||
this->set_smux_low_channels(true);
|
||||
this->enable_spectral_measurement(true);
|
||||
this->wait_for_data();
|
||||
bool low_success = this->read_bytes_16_le(AS7341_CH0_DATA_L, data, 6);
|
||||
bool low_success = this->read_bytes_16(AS7341_CH0_DATA_L, data, 6);
|
||||
|
||||
this->set_smux_low_channels(false);
|
||||
this->enable_spectral_measurement(true);
|
||||
this->wait_for_data();
|
||||
bool high_sucess = this->read_bytes_16_le(AS7341_CH0_DATA_L, &data[6], 6);
|
||||
bool high_sucess = this->read_bytes_16(AS7341_CH0_DATA_L, &data[6], 6);
|
||||
|
||||
return low_success && high_sucess;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ CONF_BITRATE = "bitrate"
|
||||
CONF_BLUE_CHANNEL = "blue_channel"
|
||||
CONF_BORDER_ROUTER = "border_router"
|
||||
CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
|
||||
CONF_BUTTONS = "buttons"
|
||||
CONF_BYTE_ORDER = "byte_order"
|
||||
CONF_CHANNEL_COLORS = "channel_colors"
|
||||
CONF_CLEAR_CHANNEL = "clear_channel"
|
||||
|
||||
@@ -18,7 +18,6 @@ from esphome.components import (
|
||||
text_sensor,
|
||||
valve,
|
||||
)
|
||||
from esphome.components.const import CONF_BUTTONS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ACCURACY_DECIMALS,
|
||||
@@ -168,6 +167,7 @@ VALVE_TYPES = {
|
||||
|
||||
|
||||
CONF_ALARM_CONTROL_PANELS = "alarm_control_panels"
|
||||
CONF_BUTTONS = "buttons"
|
||||
CONF_CLIMATES = "climates"
|
||||
CONF_COVERS = "covers"
|
||||
CONF_DATETIMES = "datetimes"
|
||||
|
||||
@@ -17,16 +17,20 @@ void IPAddressEthernetInfo::on_ip_state(const network::IPAddresses &ips, const n
|
||||
char buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
ips[0].str_to(buf);
|
||||
this->publish_state(buf);
|
||||
#ifdef ETHERNET_INFO_IP_SENSOR_COUNT
|
||||
uint8_t sensor = 0;
|
||||
for (const auto &ip : ips) {
|
||||
if (ip.is_set()) {
|
||||
if (this->ip_sensors_[sensor] != nullptr) {
|
||||
ip.str_to(buf);
|
||||
this->ip_sensors_[sensor]->publish_state(buf);
|
||||
}
|
||||
sensor++;
|
||||
if (!ip.is_set())
|
||||
continue;
|
||||
if (sensor == ETHERNET_INFO_IP_SENSOR_COUNT)
|
||||
break;
|
||||
if (this->ip_sensors_[sensor] != nullptr) {
|
||||
ip.str_to(buf);
|
||||
this->ip_sensors_[sensor]->publish_state(buf);
|
||||
}
|
||||
sensor++;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void DNSAddressEthernetInfo::setup() { ethernet::global_eth_component->add_ip_state_listener(this); }
|
||||
|
||||
@@ -18,14 +18,19 @@ class IPAddressEthernetInfo final : public Component,
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
#ifdef ETHERNET_INFO_IP_SENSOR_COUNT
|
||||
void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; }
|
||||
#endif
|
||||
|
||||
// EthernetIPStateListener interface
|
||||
void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
|
||||
const network::IPAddress &dns2) override;
|
||||
|
||||
protected:
|
||||
std::array<text_sensor::TextSensor *, 5> ip_sensors_{};
|
||||
#ifdef ETHERNET_INFO_IP_SENSOR_COUNT
|
||||
// Sized to the highest configured address_N
|
||||
std::array<text_sensor::TextSensor *, ETHERNET_INFO_IP_SENSOR_COUNT> ip_sensors_{};
|
||||
#endif
|
||||
};
|
||||
|
||||
class DNSAddressEthernetInfo final : public Component,
|
||||
|
||||
@@ -12,6 +12,7 @@ from esphome.types import ConfigType
|
||||
DEPENDENCIES = ["ethernet"]
|
||||
|
||||
ethernet_info_ns = cg.esphome_ns.namespace("ethernet_info")
|
||||
_request_ip_sensor_slot = cg.slot_counter("ETHERNET_INFO_IP_SENSOR_COUNT")
|
||||
|
||||
IPAddressEthernetInfo = ethernet_info_ns.class_(
|
||||
"IPAddressEthernetInfo", text_sensor.TextSensor, cg.Component
|
||||
@@ -57,10 +58,13 @@ async def to_code(config: ConfigType) -> None:
|
||||
if conf := config.get(CONF_IP_ADDRESS):
|
||||
ip_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS])
|
||||
await cg.register_component(ip_info, config[CONF_IP_ADDRESS])
|
||||
for x in range(5):
|
||||
if sensor_conf := conf.get(f"address_{x}"):
|
||||
sens = await text_sensor.new_text_sensor(sensor_conf)
|
||||
cg.add(ip_info.add_ip_sensors(x, sens))
|
||||
indexes = [x for x in range(5) if f"address_{x}" in conf]
|
||||
# One shared array size: the largest highest index of any entry, so add_ip_sensors(x) stays in range
|
||||
for _ in range(max(indexes, default=-1) + 1):
|
||||
_request_ip_sensor_slot(str(ip_info))
|
||||
for x in indexes:
|
||||
sens = await text_sensor.new_text_sensor(conf[f"address_{x}"])
|
||||
cg.add(ip_info.add_ip_sensors(x, sens))
|
||||
if conf := config.get(CONF_DNS_ADDRESS):
|
||||
dns_info = await text_sensor.new_text_sensor(conf)
|
||||
await cg.register_component(dns_info, conf)
|
||||
|
||||
@@ -56,7 +56,6 @@ CONF_HTTP_REQUEST_ID = "http_request_id"
|
||||
CONF_USERAGENT = "useragent"
|
||||
CONF_FOLLOW_REDIRECTS = "follow_redirects"
|
||||
CONF_REDIRECT_LIMIT = "redirect_limit"
|
||||
CONF_MAX_CONCURRENT_REQUESTS = "max_concurrent_requests"
|
||||
CONF_BUFFER_SIZE_RX = "buffer_size_rx"
|
||||
CONF_BUFFER_SIZE_TX = "buffer_size_tx"
|
||||
CONF_TLS_BUFFER_SIZE_RX = "tls_buffer_size_rx"
|
||||
@@ -145,10 +144,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
): cv.string,
|
||||
cv.Optional(CONF_FOLLOW_REDIRECTS, True): cv.boolean,
|
||||
cv.Optional(CONF_REDIRECT_LIMIT, 3): cv.int_,
|
||||
# Each request holds a client, and a TLS client about 40 kB
|
||||
cv.Optional(CONF_MAX_CONCURRENT_REQUESTS, default=1): cv.int_range(
|
||||
min=1, max=4
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_TIMEOUT, default="4.5s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
@@ -198,8 +193,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_useragent(config[CONF_USERAGENT]))
|
||||
cg.add(var.set_follow_redirects(config[CONF_FOLLOW_REDIRECTS]))
|
||||
cg.add(var.set_redirect_limit(config[CONF_REDIRECT_LIMIT]))
|
||||
if (max_concurrent := config[CONF_MAX_CONCURRENT_REQUESTS]) != 1:
|
||||
cg.add(var.set_max_concurrent_requests(max_concurrent))
|
||||
|
||||
if CORE.is_esp8266 and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]:
|
||||
cg.add_define("USE_HTTP_REQUEST_ESP8266_HTTPS")
|
||||
@@ -319,19 +312,19 @@ HTTP_REQUEST_SEND_ACTION_SCHEMA = HTTP_REQUEST_ACTION_SCHEMA.extend(
|
||||
"http_request.get",
|
||||
HttpRequestSendAction,
|
||||
HTTP_REQUEST_GET_ACTION_SCHEMA,
|
||||
synchronous=False,
|
||||
synchronous=True,
|
||||
)
|
||||
@automation.register_action(
|
||||
"http_request.post",
|
||||
HttpRequestSendAction,
|
||||
HTTP_REQUEST_POST_ACTION_SCHEMA,
|
||||
synchronous=False,
|
||||
synchronous=True,
|
||||
)
|
||||
@automation.register_action(
|
||||
"http_request.send",
|
||||
HttpRequestSendAction,
|
||||
HTTP_REQUEST_SEND_ACTION_SCHEMA,
|
||||
synchronous=False,
|
||||
synchronous=True,
|
||||
)
|
||||
async def http_request_action_to_code(
|
||||
config: ConfigType,
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#include "http_request.h"
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cinttypes>
|
||||
@@ -16,10 +14,8 @@ void HttpRequestComponent::dump_config() {
|
||||
" Timeout: %" PRIu32 "ms\n"
|
||||
" User-Agent: %s\n"
|
||||
" Follow redirects: %s\n"
|
||||
" Redirect limit: %d\n"
|
||||
" Max concurrent requests: %u",
|
||||
this->timeout_, this->useragent_, YESNO(this->follow_redirects_), this->redirect_limit_,
|
||||
this->max_concurrent_);
|
||||
" Redirect limit: %d",
|
||||
this->timeout_, this->useragent_, YESNO(this->follow_redirects_), this->redirect_limit_);
|
||||
if (this->watchdog_timeout_ > 0) {
|
||||
ESP_LOGCONFIG(TAG, " Watchdog Timeout: %" PRIu32 "ms", this->watchdog_timeout_);
|
||||
}
|
||||
@@ -36,209 +32,4 @@ std::string HttpContainer::get_response_header(const std::string &header_name) {
|
||||
return "";
|
||||
}
|
||||
|
||||
void PendingRequest::free_buffer() {
|
||||
if (this->buf != nullptr)
|
||||
RAMAllocator<uint8_t>().deallocate(this->buf, this->buf_size);
|
||||
this->buf = nullptr;
|
||||
}
|
||||
|
||||
void HttpRequestComponent::setup() {
|
||||
// An on_boot automation may already have queued a request
|
||||
if (this->requests_ == nullptr)
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void HttpRequestComponent::loop() {
|
||||
if (!this->process_())
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void HttpRequestComponent::on_safe_shutdown() {
|
||||
// Called before any on_shutdown(). Drop the requests already made, so that they do not hold up the reboot;
|
||||
// only the ones that on_shutdown automations make are sent.
|
||||
this->shutting_down_ = true;
|
||||
this->busy_++;
|
||||
unsigned dropped = 0;
|
||||
for (PendingRequest *request = this->requests_; request != nullptr; request = request->next) {
|
||||
if (request->finished || request->cancelled)
|
||||
continue;
|
||||
dropped++;
|
||||
request->cancelled = true;
|
||||
// As script.stop would, so that a script that was waiting for it can run again from on_shutdown
|
||||
request->handler(request, RequestEvent::REQUEST_EVENT_ABANDON);
|
||||
}
|
||||
this->busy_--;
|
||||
if (dropped != 0) {
|
||||
ESP_LOGW(TAG, "Shutting down; %u requests dropped", dropped);
|
||||
}
|
||||
if (this->busy_ == 0)
|
||||
this->remove_dead_();
|
||||
}
|
||||
|
||||
void HttpRequestComponent::on_shutdown() {
|
||||
// Shutdown runs no loop(), so finish the requests here, blocking as they did before. A forced reboot calls
|
||||
// only this, so it also finishes the requests made before it.
|
||||
this->shutting_down_ = true;
|
||||
this->drain_();
|
||||
}
|
||||
|
||||
void HttpRequestComponent::submit(PendingRequest *request) {
|
||||
this->busy_++;
|
||||
uint8_t running = 0;
|
||||
uint8_t waiting = 0;
|
||||
PendingRequest **link = &this->requests_;
|
||||
for (; *link != nullptr; link = &(*link)->next) {
|
||||
const PendingRequest *other = *link;
|
||||
if (other->finished || other->cancelled)
|
||||
continue;
|
||||
if (other->container == nullptr) {
|
||||
waiting++;
|
||||
} else {
|
||||
running++;
|
||||
}
|
||||
}
|
||||
*link = request;
|
||||
this->enable_loop();
|
||||
if (waiting >= MAX_QUEUED) {
|
||||
ESP_LOGW(TAG, "Too many requests waiting; request dropped");
|
||||
request->failed = true;
|
||||
this->finish_(request);
|
||||
} else if (waiting == 0 && running < this->max_concurrent_) {
|
||||
// Start at once; one that cannot start is reported at once
|
||||
this->begin_request_(request);
|
||||
if (request->failed)
|
||||
this->finish_(request);
|
||||
}
|
||||
this->busy_--;
|
||||
if (this->busy_ != 0)
|
||||
return;
|
||||
if (this->shutting_down_) {
|
||||
this->drain_();
|
||||
} else {
|
||||
this->remove_dead_();
|
||||
}
|
||||
}
|
||||
|
||||
void HttpRequestComponent::cancel(void *owner) {
|
||||
for (PendingRequest *request = this->requests_; request != nullptr; request = request->next) {
|
||||
if (request->owner == owner)
|
||||
request->cancelled = true;
|
||||
}
|
||||
if (this->busy_ == 0)
|
||||
this->remove_dead_();
|
||||
}
|
||||
|
||||
bool HttpRequestComponent::process_() {
|
||||
this->busy_++;
|
||||
// The first max_concurrent_ live requests run; a request finished here frees its slot on the next pass
|
||||
uint8_t slots = this->max_concurrent_;
|
||||
for (PendingRequest *request = this->requests_; request != nullptr && slots != 0; request = request->next) {
|
||||
if (request->finished || request->cancelled)
|
||||
continue;
|
||||
slots--;
|
||||
if (request->container == nullptr)
|
||||
this->begin_request_(request);
|
||||
if (!request->cancelled && (request->failed || this->advance_(request)))
|
||||
this->finish_(request);
|
||||
}
|
||||
this->busy_--;
|
||||
if (this->busy_ == 0)
|
||||
this->remove_dead_();
|
||||
return this->requests_ != nullptr;
|
||||
}
|
||||
|
||||
void HttpRequestComponent::drain_() {
|
||||
// A call further up holds request pointers, and runs the new requests itself
|
||||
if (this->busy_ != 0)
|
||||
return;
|
||||
while (this->process_()) {
|
||||
App.feed_wdt();
|
||||
delay(1);
|
||||
}
|
||||
}
|
||||
|
||||
void HttpRequestComponent::begin_request_(PendingRequest *request) {
|
||||
request->start_time = millis();
|
||||
request->handler(request, RequestEvent::REQUEST_EVENT_START);
|
||||
// Stopped from the request's own lambdas: it is removed without being polled
|
||||
if (!request->cancelled && request->container == nullptr)
|
||||
request->failed = true;
|
||||
}
|
||||
|
||||
bool HttpRequestComponent::advance_(PendingRequest *request) {
|
||||
if (!request->reading) {
|
||||
auto progress = request->container->poll();
|
||||
if (progress == HttpRequestProgress::HTTP_REQUEST_PROGRESS_PENDING)
|
||||
return false;
|
||||
if (progress == HttpRequestProgress::HTTP_REQUEST_PROGRESS_FAILED) {
|
||||
request->failed = true;
|
||||
return true;
|
||||
}
|
||||
if (!request->capture)
|
||||
return true;
|
||||
request->buf = RAMAllocator<uint8_t>().allocate(request->buf_size);
|
||||
if (request->buf == nullptr) {
|
||||
// As before, on_response still fires, with an empty body
|
||||
ESP_LOGW(TAG, "Could not allocate %zu bytes for the response body", request->buf_size);
|
||||
return true;
|
||||
}
|
||||
request->reading = true;
|
||||
// millis(), not the loop time: poll() may have blocked earlier in this pass
|
||||
request->last_data_time = millis();
|
||||
}
|
||||
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
|
||||
// millis(), not the loop time: the budget is shorter than one pass of the loop
|
||||
const uint32_t start = millis();
|
||||
while (request->read_index < request->buf_size) {
|
||||
int read_or_error =
|
||||
request->container->read_available(request->buf + request->read_index, request->buf_size - request->read_index);
|
||||
const uint32_t now = millis();
|
||||
// As before, a body that is cut off still fires on_response with what was read
|
||||
if (read_or_error < 0) {
|
||||
ESP_LOGW(TAG, "Response body read failed after %zu bytes", request->read_index);
|
||||
return true;
|
||||
}
|
||||
if (read_or_error == 0) {
|
||||
if (request->container->is_read_complete())
|
||||
return true;
|
||||
if (now - request->last_data_time < request->timeout)
|
||||
return false;
|
||||
ESP_LOGW(TAG, "Response body timed out after %zu bytes", request->read_index);
|
||||
return true;
|
||||
}
|
||||
request->read_index += read_or_error;
|
||||
request->last_data_time = now;
|
||||
if (now - start >= READ_BUDGET_MS)
|
||||
return false;
|
||||
}
|
||||
// As before, on_response still fires, with the part that fitted
|
||||
if (!request->container->is_read_complete()) {
|
||||
ESP_LOGW(TAG, "Response body is larger than max_response_buffer_size; kept the first %zu bytes", request->buf_size);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void HttpRequestComponent::finish_(PendingRequest *request) {
|
||||
request->finished = true;
|
||||
// The body was read over several passes, so time it from the start, as a blocking read would
|
||||
if (request->reading && request->container != nullptr)
|
||||
request->container->duration_ms = millis() - request->start_time;
|
||||
request->handler(request, RequestEvent::REQUEST_EVENT_FINISH);
|
||||
}
|
||||
|
||||
void HttpRequestComponent::remove_dead_() {
|
||||
PendingRequest **link = &this->requests_;
|
||||
while (PendingRequest *request = *link) {
|
||||
if (!request->finished && !request->cancelled) {
|
||||
link = &request->next;
|
||||
continue;
|
||||
}
|
||||
*link = request->next;
|
||||
if (request->container != nullptr)
|
||||
request->container->end();
|
||||
request->free_buffer();
|
||||
request->handler(request, RequestEvent::REQUEST_EVENT_DESTROY);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::http_request
|
||||
@@ -12,7 +12,6 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/alloc_helpers.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -187,13 +186,6 @@ enum class HttpReadLoopResult : uint8_t {
|
||||
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
|
||||
};
|
||||
|
||||
/// Progress of a request started with HttpRequestComponent::begin()
|
||||
enum class HttpRequestProgress : uint8_t {
|
||||
HTTP_REQUEST_PROGRESS_PENDING, ///< Still connecting, sending, or waiting for the response headers
|
||||
HTTP_REQUEST_PROGRESS_DONE, ///< Response headers received; status_code is set and the body can be read
|
||||
HTTP_REQUEST_PROGRESS_FAILED, ///< No response; the container must still be ended
|
||||
};
|
||||
|
||||
/// Process a read result with timeout tracking and delay handling
|
||||
/// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error
|
||||
/// @param last_data_time Time of last successful read, updated when data received
|
||||
@@ -257,15 +249,8 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
* - http_read_fully(): for simple "read N bytes" operations
|
||||
*/
|
||||
virtual int read(uint8_t *buf, size_t max_len) = 0;
|
||||
/// Like read(), but returns 0 at once when no data has arrived yet, where read() may wait for it.
|
||||
virtual int read_available(uint8_t *buf, size_t max_len) { return this->read(buf, max_len); }
|
||||
virtual void end() = 0;
|
||||
|
||||
/// Advance a request started with HttpRequestComponent::begin(); call from the main loop until it is no
|
||||
/// longer pending. Containers from start() are already done. Most calls return quickly, but on ESP-IDF the
|
||||
/// first call for each host waits for the name lookup and the TCP connection, up to the whole timeout.
|
||||
virtual HttpRequestProgress poll() { return HttpRequestProgress::HTTP_REQUEST_PROGRESS_DONE; }
|
||||
|
||||
void set_secure(bool secure) { this->secure_ = secure; }
|
||||
void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
|
||||
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
|
||||
@@ -347,56 +332,10 @@ class HttpRequestResponseTrigger final : public Trigger<std::shared_ptr<HttpCont
|
||||
}
|
||||
};
|
||||
|
||||
/// What HttpRequestComponent asks of the action that owns a request
|
||||
enum class RequestEvent : uint8_t {
|
||||
REQUEST_EVENT_START, ///< Build the request and call begin()
|
||||
REQUEST_EVENT_FINISH, ///< Fire the triggers and continue the action chain
|
||||
REQUEST_EVENT_DESTROY, ///< Free the request
|
||||
REQUEST_EVENT_ABANDON, ///< Dropped at shutdown: stop the action, so that it can run again
|
||||
};
|
||||
|
||||
/// A request from an HttpRequestSendAction, run from HttpRequestComponent::loop().
|
||||
/// The action derives from it to add the trigger arguments.
|
||||
struct PendingRequest {
|
||||
using Handler = void (*)(PendingRequest *request, RequestEvent event);
|
||||
PendingRequest(Handler handler, void *owner) : handler(handler), owner(owner) {}
|
||||
PendingRequest(const PendingRequest &) = delete;
|
||||
PendingRequest &operator=(const PendingRequest &) = delete;
|
||||
void free_buffer();
|
||||
|
||||
PendingRequest *next{nullptr};
|
||||
Handler handler;
|
||||
// The action, so that a stop of it can find its requests
|
||||
void *owner;
|
||||
// nullptr until the request has started
|
||||
std::shared_ptr<HttpContainer> container;
|
||||
uint8_t *buf{nullptr};
|
||||
size_t buf_size{0};
|
||||
size_t read_index{0};
|
||||
uint32_t last_data_time{0};
|
||||
uint32_t timeout{0};
|
||||
uint32_t start_time{0};
|
||||
bool capture{false};
|
||||
bool reading{false};
|
||||
bool failed{false};
|
||||
bool finished{false};
|
||||
bool cancelled{false};
|
||||
};
|
||||
|
||||
class HttpRequestComponent : public Component {
|
||||
public:
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void on_safe_shutdown() override;
|
||||
void on_shutdown() override;
|
||||
|
||||
/// For HttpRequestSendAction: queue a request and take ownership of it. Requests start in order, up to
|
||||
/// max_concurrent_requests at a time; one that can start at once is started here.
|
||||
void submit(PendingRequest *request);
|
||||
/// For HttpRequestSendAction: drop the requests of a stopped action without firing their triggers.
|
||||
void cancel(void *owner);
|
||||
|
||||
void set_useragent(const char *useragent) { this->useragent_ = useragent; }
|
||||
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
|
||||
@@ -410,7 +349,6 @@ class HttpRequestComponent : public Component {
|
||||
}
|
||||
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
|
||||
void set_redirect_limit(uint16_t limit) { this->redirect_limit_ = limit; }
|
||||
void set_max_concurrent_requests(uint8_t max_concurrent) { this->max_concurrent_ = max_concurrent; }
|
||||
|
||||
std::shared_ptr<HttpContainer> get(const char *url) { return this->start(url, "GET", "", std::vector<Header>{}); }
|
||||
std::shared_ptr<HttpContainer> get(const char *url, const std::vector<Header> &request_headers) {
|
||||
@@ -553,44 +491,16 @@ class HttpRequestComponent : public Component {
|
||||
return this->start(url.c_str(), method.c_str(), body, request_headers, lower_case_collect_headers);
|
||||
}
|
||||
|
||||
/// Start a request without waiting for the response, then poll() the container until it is not pending.
|
||||
/// Returns nullptr when the request cannot be started. Platforms without a non-blocking client complete
|
||||
/// the request here.
|
||||
virtual std::shared_ptr<HttpContainer> begin(const char *url, const char *method, std::string &&body,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
uint32_t timeout_ms) {
|
||||
return this->perform(url, method, body, request_headers, lower_case_collect_headers, timeout_ms);
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
uint32_t timeout_ms) = 0;
|
||||
/// Longest time one loop() pass spends reading a response body
|
||||
static constexpr uint32_t READ_BUDGET_MS = 10;
|
||||
/// Requests that wait for a free slot; more are dropped with on_error
|
||||
static constexpr uint8_t MAX_QUEUED = 4;
|
||||
|
||||
bool process_();
|
||||
void drain_();
|
||||
void begin_request_(PendingRequest *request);
|
||||
bool advance_(PendingRequest *request);
|
||||
void finish_(PendingRequest *request);
|
||||
void remove_dead_();
|
||||
|
||||
const char *useragent_{nullptr};
|
||||
bool follow_redirects_{};
|
||||
uint16_t redirect_limit_{};
|
||||
uint32_t timeout_{4500};
|
||||
uint32_t watchdog_timeout_{0};
|
||||
// Oldest first; the first max_concurrent_ live requests run
|
||||
PendingRequest *requests_{nullptr};
|
||||
// Depth of calls that hold request pointers; finished and cancelled requests are freed when it returns to 0
|
||||
uint8_t busy_{0};
|
||||
uint8_t max_concurrent_{1};
|
||||
bool shutting_down_{false};
|
||||
};
|
||||
|
||||
template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...> {
|
||||
@@ -632,42 +542,7 @@ template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...
|
||||
this->max_response_buffer_size_ = max_response_buffer_size;
|
||||
}
|
||||
|
||||
void play_complex(const Ts &...x) override {
|
||||
this->num_running_++;
|
||||
this->parent_->submit(new Request(this, x...)); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
|
||||
void play(const Ts &...x) override { /* see play_complex */ }
|
||||
|
||||
protected:
|
||||
struct Request final : PendingRequest {
|
||||
Request(HttpRequestSendAction *action, const Ts &...x)
|
||||
: PendingRequest(&HttpRequestSendAction::handle_event, action), args(x...) {}
|
||||
std::tuple<stored_trigger_arg_t<Ts>...> args;
|
||||
};
|
||||
|
||||
static void handle_event(PendingRequest *request, RequestEvent event) {
|
||||
auto *self = static_cast<Request *>(request);
|
||||
auto *action = static_cast<HttpRequestSendAction *>(request->owner);
|
||||
switch (event) {
|
||||
case RequestEvent::REQUEST_EVENT_START:
|
||||
std::apply([action, self](auto &...x) { action->start_request_(*self, x...); }, self->args);
|
||||
break;
|
||||
case RequestEvent::REQUEST_EVENT_FINISH:
|
||||
action->finish_(*self);
|
||||
break;
|
||||
case RequestEvent::REQUEST_EVENT_DESTROY:
|
||||
delete self; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
break;
|
||||
case RequestEvent::REQUEST_EVENT_ABANDON:
|
||||
action->stop_complex();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void stop() override { this->parent_->cancel(this); }
|
||||
|
||||
void start_request_(Request &request, const Ts &...x) {
|
||||
void play(const Ts &...x) override {
|
||||
std::string body;
|
||||
if (this->body_.has_value()) {
|
||||
body = this->body_.value(x...);
|
||||
@@ -684,47 +559,62 @@ template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...
|
||||
request_headers.push_back({header.name, header.value(x...)});
|
||||
}
|
||||
|
||||
request.timeout = this->timeout_.has_value() ? this->timeout_.value(x...) : this->parent_->get_timeout();
|
||||
#ifdef USE_HTTP_REQUEST_RESPONSE
|
||||
request.capture = this->capture_response_.value(x...);
|
||||
#endif
|
||||
request.buf_size = this->max_response_buffer_size_;
|
||||
request.container = this->parent_->begin(this->url_.value(x...).c_str(), this->method_.value(x...), std::move(body),
|
||||
request_headers, this->lower_case_collect_headers_, request.timeout);
|
||||
}
|
||||
const uint32_t timeout = this->timeout_.has_value() ? this->timeout_.value(x...) : this->parent_->get_timeout();
|
||||
auto container = this->parent_->start(this->url_.value(x...).c_str(), this->method_.value(x...), body,
|
||||
request_headers, this->lower_case_collect_headers_, timeout);
|
||||
|
||||
void finish_(Request &request) {
|
||||
auto &container = request.container;
|
||||
if (request.failed) {
|
||||
std::apply([this](auto &...args) { this->error_trigger_.trigger(args...); }, request.args);
|
||||
auto captured_args = std::make_tuple(x...);
|
||||
|
||||
if (container == nullptr) {
|
||||
std::apply([this](Ts... captured_args_inner) { this->error_trigger_.trigger(captured_args_inner...); },
|
||||
captured_args);
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_HTTP_REQUEST_RESPONSE
|
||||
else if (request.capture) {
|
||||
if (this->capture_response_.value(x...)) {
|
||||
size_t max_length = this->max_response_buffer_size_;
|
||||
std::string response_body;
|
||||
if (request.buf != nullptr)
|
||||
response_body.assign(reinterpret_cast<char *>(request.buf), request.read_index);
|
||||
request.free_buffer();
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
uint8_t *buf = allocator.allocate(max_length);
|
||||
if (buf != nullptr) {
|
||||
// NOTE: HttpContainer::read() has non-BSD socket semantics - see top of this file
|
||||
// Use http_read_loop_result() helper instead of checking return values directly
|
||||
size_t read_index = 0;
|
||||
uint32_t last_data_time = millis();
|
||||
const uint32_t read_timeout = timeout;
|
||||
while (container->get_bytes_read() < max_length) {
|
||||
int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
auto result =
|
||||
http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
|
||||
if (result == HttpReadLoopResult::RETRY)
|
||||
continue;
|
||||
if (result != HttpReadLoopResult::DATA)
|
||||
break; // COMPLETE, ERROR, or TIMEOUT
|
||||
read_index += read_or_error;
|
||||
}
|
||||
response_body.reserve(read_index);
|
||||
response_body.assign((char *) buf, read_index);
|
||||
allocator.deallocate(buf, max_length);
|
||||
}
|
||||
std::apply(
|
||||
[this, &container, &response_body](auto &...args) {
|
||||
this->success_trigger_with_response_.trigger(container, response_body, args...);
|
||||
[this, &container, &response_body](Ts... captured_args_inner) {
|
||||
this->success_trigger_with_response_.trigger(container, response_body, captured_args_inner...);
|
||||
},
|
||||
request.args);
|
||||
}
|
||||
captured_args);
|
||||
} else
|
||||
#endif
|
||||
else {
|
||||
std::apply([this, &container](auto &...args) { this->success_trigger_.trigger(container, args...); },
|
||||
request.args);
|
||||
{
|
||||
std::apply([this, &container](
|
||||
Ts... captured_args_inner) { this->success_trigger_.trigger(container, captured_args_inner...); },
|
||||
captured_args);
|
||||
}
|
||||
// End it before the chain can start another request, so that one client is held at a time
|
||||
if (container != nullptr) {
|
||||
container->end();
|
||||
container.reset();
|
||||
}
|
||||
// A stop from the triggers ended this run; a run started after it continues the chain instead
|
||||
if (!request.cancelled)
|
||||
std::apply([this](auto &...args) { this->play_next_(args...); }, request.args);
|
||||
container->end();
|
||||
}
|
||||
|
||||
protected:
|
||||
void encode_json_(Ts... x, JsonObject root) {
|
||||
for (const auto &item : this->json_) {
|
||||
auto val = item.second;
|
||||
|
||||
@@ -2,13 +2,8 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/select.h>
|
||||
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/components/watchdog/watchdog.h"
|
||||
|
||||
@@ -25,10 +20,6 @@
|
||||
namespace esphome::http_request {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "http_request");
|
||||
|
||||
// Request body sent per esp_http_client_write() call, and the time one poll() spends sending it
|
||||
static constexpr size_t SEND_CHUNK_SIZE = 1024;
|
||||
static constexpr uint32_t SEND_BUDGET_MS = 10;
|
||||
static constexpr uint32_t ERROR_DURATION_MS = 1000;
|
||||
|
||||
void HttpRequestIDF::dump_config() {
|
||||
@@ -60,9 +51,10 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
std::shared_ptr<HttpContainerIDF> HttpRequestIDF::create_container_(
|
||||
const char *url, const char *method, const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers, uint32_t timeout_ms, [[maybe_unused]] bool async) {
|
||||
std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const char *method, const std::string &body,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
uint32_t timeout_ms) {
|
||||
if (!network::is_connected()) {
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
ESP_LOGE(TAG, "HTTP Request failed; Not connected to network");
|
||||
@@ -114,12 +106,11 @@ std::shared_ptr<HttpContainerIDF> HttpRequestIDF::create_container_(
|
||||
config.buffer_size = this->buffer_size_rx_;
|
||||
config.buffer_size_tx = this->buffer_size_tx_;
|
||||
|
||||
const uint32_t start = millis();
|
||||
const uint32_t watchdog_timeout = this->get_watchdog_timeout(timeout_ms);
|
||||
watchdog::WatchdogManager wdm(watchdog_timeout);
|
||||
|
||||
config.event_handler = http_event_handler;
|
||||
#ifdef USE_HTTP_REQUEST_IDF_SOCKET_POLL
|
||||
// Only TLS has a non-blocking connect worth having: the handshake. A plain connection waits for
|
||||
// the TCP connection either way.
|
||||
config.is_async = async && secure;
|
||||
#endif
|
||||
|
||||
esp_http_client_handle_t client = esp_http_client_init(&config);
|
||||
if (client == nullptr) {
|
||||
@@ -130,9 +121,7 @@ std::shared_ptr<HttpContainerIDF> HttpRequestIDF::create_container_(
|
||||
|
||||
std::shared_ptr<HttpContainerIDF> container = std::make_shared<HttpContainerIDF>(client);
|
||||
container->set_parent(this);
|
||||
container->set_watchdog_timeout(this->get_watchdog_timeout(timeout_ms));
|
||||
container->timeout_ms_ = timeout_ms;
|
||||
container->async_ = config.is_async;
|
||||
container->set_watchdog_timeout(watchdog_timeout);
|
||||
|
||||
container->set_secure(secure);
|
||||
|
||||
@@ -143,229 +132,101 @@ std::shared_ptr<HttpContainerIDF> HttpRequestIDF::create_container_(
|
||||
esp_http_client_set_header(client, header.name.c_str(), header.value.c_str());
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
const int body_len = body.length();
|
||||
|
||||
std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const char *method, const std::string &body,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
uint32_t timeout_ms) {
|
||||
std::shared_ptr<HttpContainerIDF> container = this->start_container_(url, method, std::string(body), request_headers,
|
||||
lower_case_collect_headers, timeout_ms, false);
|
||||
if (container == nullptr) {
|
||||
esp_err_t err = esp_http_client_open(client, body_len);
|
||||
if (err != ESP_OK) {
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
|
||||
esp_http_client_cleanup(client);
|
||||
return nullptr;
|
||||
}
|
||||
// The same stages as begin(), run here until the response headers are in
|
||||
watchdog::WatchdogManager wdm(container->get_watchdog_timeout());
|
||||
HttpRequestProgress progress;
|
||||
while ((progress = container->poll()) == HttpRequestProgress::HTTP_REQUEST_PROGRESS_PENDING) {
|
||||
container->feed_wdt();
|
||||
delay(1);
|
||||
}
|
||||
if (progress == HttpRequestProgress::HTTP_REQUEST_PROGRESS_FAILED) {
|
||||
container->end();
|
||||
return nullptr;
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
std::shared_ptr<HttpContainer> HttpRequestIDF::begin(const char *url, const char *method, std::string &&body,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
uint32_t timeout_ms) {
|
||||
return this->start_container_(url, method, std::move(body), request_headers, lower_case_collect_headers, timeout_ms,
|
||||
true);
|
||||
}
|
||||
|
||||
std::shared_ptr<HttpContainerIDF> HttpRequestIDF::start_container_(
|
||||
const char *url, const char *method, std::string &&body, const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers, uint32_t timeout_ms, bool async) {
|
||||
std::shared_ptr<HttpContainerIDF> container =
|
||||
this->create_container_(url, method, request_headers, lower_case_collect_headers, timeout_ms, async);
|
||||
if (container == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
container->body_ = std::move(body);
|
||||
container->start_time_ = millis();
|
||||
container->progress_time_ = container->start_time_;
|
||||
container->redirects_left_ = this->follow_redirects_ ? this->redirect_limit_ : 0;
|
||||
container->stage_ = HttpContainerIDF::Stage::STAGE_CONNECT;
|
||||
return container;
|
||||
}
|
||||
|
||||
HttpRequestIDF *HttpContainerIDF::idf_parent_() { return static_cast<HttpRequestIDF *>(this->parent_); }
|
||||
|
||||
HttpRequestProgress HttpContainerIDF::fail_(esp_err_t err) {
|
||||
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
|
||||
this->parent_->status_momentary_error(ERROR_DURATION_MS);
|
||||
this->stage_ = Stage::STAGE_FAILED;
|
||||
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_FAILED;
|
||||
}
|
||||
|
||||
HttpRequestProgress HttpContainerIDF::wait_() {
|
||||
// millis(), not the loop time: progress_time_ is set after calls that block within this pass
|
||||
if (millis() - this->progress_time_ >= this->timeout_ms_) {
|
||||
return this->fail_(ESP_ERR_TIMEOUT);
|
||||
}
|
||||
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_PENDING;
|
||||
}
|
||||
|
||||
bool HttpContainerIDF::socket_ready_(bool write) {
|
||||
#ifdef USE_HTTP_REQUEST_IDF_SOCKET_POLL
|
||||
const int fd = esp_http_client_get_socket(this->client_);
|
||||
if (fd < 0) {
|
||||
return true;
|
||||
}
|
||||
fd_set fds;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(fd, &fds);
|
||||
timeval no_wait{};
|
||||
// An error also counts as ready, so that the call that follows reports it
|
||||
// Qualified: esphome::select is a namespace
|
||||
return ::select(fd + 1, write ? nullptr : &fds, write ? &fds : nullptr, nullptr, &no_wait) != 0;
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Each stage either finishes, or returns PENDING after a call that did not wait. The waits left are
|
||||
// the name lookup and TCP connection in the first esp_http_client_open() of each hop: a non-blocking
|
||||
// esp-tls connect that times out once never completes, as select() clears the sets it keeps.
|
||||
HttpRequestProgress HttpContainerIDF::poll() {
|
||||
while (true) {
|
||||
switch (this->stage_) {
|
||||
case Stage::STAGE_DONE:
|
||||
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_DONE;
|
||||
|
||||
case Stage::STAGE_FAILED:
|
||||
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_FAILED;
|
||||
|
||||
case Stage::STAGE_CONNECT: {
|
||||
// Only the first call of a hop waits
|
||||
watchdog::WatchdogManager wdm(this->connecting_ ? 0 : this->get_watchdog_timeout());
|
||||
esp_http_client_set_timeout_ms(this->client_, this->connecting_ ? 1 : this->timeout_ms_);
|
||||
this->feed_wdt();
|
||||
esp_err_t err = esp_http_client_open(this->client_, this->body_.size());
|
||||
this->feed_wdt();
|
||||
if (err == ESP_ERR_HTTP_EAGAIN) {
|
||||
this->connecting_ = true;
|
||||
return this->wait_();
|
||||
}
|
||||
this->connecting_ = false;
|
||||
if (err != ESP_OK) {
|
||||
return this->fail_(err);
|
||||
}
|
||||
#ifdef USE_HTTP_REQUEST_IDF_SOCKET_POLL
|
||||
if (this->async_) {
|
||||
// Back to a blocking socket, so that mbedtls waits for the rest of a record instead of
|
||||
// failing; reads only start once the socket is readable
|
||||
const int fd = esp_http_client_get_socket(this->client_);
|
||||
const int flags = fd < 0 ? -1 : fcntl(fd, F_GETFL, 0);
|
||||
if (flags < 0 || fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) < 0) {
|
||||
ESP_LOGE(TAG, "Could not make the socket blocking: fd %d, errno %d", fd, errno);
|
||||
return this->fail_(ESP_FAIL);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
esp_http_client_set_timeout_ms(this->client_, this->timeout_ms_);
|
||||
this->progress_time_ = millis();
|
||||
this->stage_ = this->body_sent_ < this->body_.size() ? Stage::STAGE_SEND_BODY : Stage::STAGE_HEADERS;
|
||||
if (body_len > 0) {
|
||||
int write_left = body_len;
|
||||
int write_index = 0;
|
||||
const char *buf = body.c_str();
|
||||
while (write_left > 0) {
|
||||
int written = esp_http_client_write(client, buf + write_index, write_left);
|
||||
if (written <= 0) {
|
||||
err = ESP_FAIL;
|
||||
break;
|
||||
}
|
||||
|
||||
case Stage::STAGE_SEND_BODY: {
|
||||
// esp_http_client_write() returns only once all it was given is sent, so give it a piece at a time
|
||||
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
|
||||
const uint32_t start = millis();
|
||||
while (this->body_sent_ < this->body_.size()) {
|
||||
if (!this->socket_ready_(true)) {
|
||||
return this->wait_();
|
||||
}
|
||||
const size_t len = std::min(this->body_.size() - this->body_sent_, SEND_CHUNK_SIZE);
|
||||
int written = esp_http_client_write(this->client_, this->body_.data() + this->body_sent_, len);
|
||||
this->feed_wdt();
|
||||
if (written < 0) {
|
||||
return this->fail_(ESP_FAIL);
|
||||
}
|
||||
if (written == 0) {
|
||||
return this->wait_();
|
||||
}
|
||||
this->body_sent_ += written;
|
||||
this->progress_time_ = millis();
|
||||
if (this->progress_time_ - start >= SEND_BUDGET_MS && this->body_sent_ < this->body_.size()) {
|
||||
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_PENDING;
|
||||
}
|
||||
}
|
||||
this->body_ = std::string();
|
||||
this->stage_ = Stage::STAGE_HEADERS;
|
||||
break;
|
||||
}
|
||||
|
||||
case Stage::STAGE_HEADERS: {
|
||||
if (!this->socket_ready_(false)) {
|
||||
if (millis() - this->progress_time_ < this->timeout_ms_) {
|
||||
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_PENDING;
|
||||
}
|
||||
// As perform(), where esp_http_client_fetch_headers() times out: no response is status -1
|
||||
return this->response_failed_(-1);
|
||||
}
|
||||
// The response has begun to arrive, so this waits only for the rest of the headers, or over TLS
|
||||
// the rest of the record that holds them
|
||||
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
|
||||
this->feed_wdt();
|
||||
// esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header)
|
||||
const int64_t length = esp_http_client_fetch_headers(this->client_);
|
||||
this->feed_wdt();
|
||||
this->duration_ms = millis() - this->start_time_;
|
||||
if (length < 0) {
|
||||
return this->response_failed_(-1);
|
||||
}
|
||||
this->content_length = length;
|
||||
this->set_chunked(esp_http_client_is_chunked_response(this->client_));
|
||||
this->status_code = esp_http_client_get_status_code(this->client_);
|
||||
if (is_success(this->status_code)) {
|
||||
this->stage_ = Stage::STAGE_DONE;
|
||||
break;
|
||||
}
|
||||
if (this->redirects_left_ > 0 && is_redirect(this->status_code)) {
|
||||
esp_err_t err = esp_http_client_set_redirection(this->client_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_http_client_set_redirection failed: %s", esp_err_to_name(err));
|
||||
return this->fail_(err);
|
||||
}
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char redirect_url[256]{};
|
||||
if (esp_http_client_get_url(this->client_, redirect_url, sizeof(redirect_url) - 1) == ESP_OK) {
|
||||
ESP_LOGV(TAG, "redirecting to url: %s", redirect_url);
|
||||
}
|
||||
#endif
|
||||
this->redirects_left_--;
|
||||
// Collect only the final response's headers; redirects do not resend the body
|
||||
this->response_headers_.clear();
|
||||
this->body_ = std::string();
|
||||
this->body_sent_ = 0;
|
||||
this->progress_time_ = millis();
|
||||
this->stage_ = Stage::STAGE_CONNECT;
|
||||
break;
|
||||
}
|
||||
if (is_redirect(this->status_code) && this->idf_parent_()->follow_redirects_) {
|
||||
ESP_LOGW(TAG, "Reach redirect limit count=%d", this->idf_parent_()->redirect_limit_);
|
||||
}
|
||||
return this->response_failed_(this->status_code);
|
||||
}
|
||||
write_left -= written;
|
||||
write_index += written;
|
||||
container->feed_wdt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HttpRequestProgress HttpContainerIDF::response_failed_(int status_code) {
|
||||
this->status_code = status_code;
|
||||
this->duration_ms = millis() - this->start_time_;
|
||||
char url[256]{};
|
||||
esp_http_client_get_url(this->client_, url, sizeof(url) - 1);
|
||||
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, this->status_code);
|
||||
this->parent_->status_momentary_error(ERROR_DURATION_MS);
|
||||
this->stage_ = Stage::STAGE_DONE;
|
||||
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_DONE;
|
||||
if (err != ESP_OK) {
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
|
||||
esp_http_client_cleanup(client);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
container->feed_wdt();
|
||||
// esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header).
|
||||
// The read() method handles content_length == 0 specially to support chunked responses.
|
||||
container->content_length = esp_http_client_fetch_headers(client);
|
||||
container->set_chunked(esp_http_client_is_chunked_response(client));
|
||||
container->feed_wdt();
|
||||
container->status_code = esp_http_client_get_status_code(client);
|
||||
container->feed_wdt();
|
||||
container->duration_ms = millis() - start;
|
||||
if (is_success(container->status_code)) {
|
||||
return container;
|
||||
}
|
||||
|
||||
if (this->follow_redirects_) {
|
||||
auto num_redirects = this->redirect_limit_;
|
||||
while (is_redirect(container->status_code) && num_redirects > 0) {
|
||||
err = esp_http_client_set_redirection(client);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_http_client_set_redirection failed: %s", esp_err_to_name(err));
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
esp_http_client_cleanup(client);
|
||||
return nullptr;
|
||||
}
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char redirect_url[256]{};
|
||||
if (esp_http_client_get_url(client, redirect_url, sizeof(redirect_url) - 1) == ESP_OK) {
|
||||
ESP_LOGV(TAG, "redirecting to url: %s", redirect_url);
|
||||
}
|
||||
#endif
|
||||
err = esp_http_client_open(client, 0);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_http_client_open failed: %s", esp_err_to_name(err));
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
esp_http_client_cleanup(client);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
container->feed_wdt();
|
||||
// IDF is the only backend reusing the container across redirect hops;
|
||||
// drop the previous hop's headers (Arduino/host collect only the final response)
|
||||
container->response_headers_.clear();
|
||||
container->content_length = esp_http_client_fetch_headers(client);
|
||||
container->set_chunked(esp_http_client_is_chunked_response(client));
|
||||
container->feed_wdt();
|
||||
container->status_code = esp_http_client_get_status_code(client);
|
||||
container->feed_wdt();
|
||||
container->duration_ms = millis() - start;
|
||||
if (is_success(container->status_code)) {
|
||||
return container;
|
||||
}
|
||||
|
||||
num_redirects--;
|
||||
}
|
||||
|
||||
if (num_redirects == 0) {
|
||||
ESP_LOGW(TAG, "Reach redirect limit count=%d", this->redirect_limit_);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, container->status_code);
|
||||
this->status_momentary_error(ERROR_DURATION_MS);
|
||||
return container;
|
||||
}
|
||||
|
||||
bool HttpContainerIDF::is_read_complete() const {
|
||||
@@ -404,21 +265,8 @@ bool HttpContainerIDF::is_read_complete() const {
|
||||
// after its internal transport timeout (configured via timeout_ms) expires.
|
||||
// This is passed through as a negative return value, which callers treat as an error.
|
||||
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
||||
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
|
||||
esp_http_client_set_timeout_ms(this->client_, this->timeout_ms_);
|
||||
return this->read_(buf, max_len);
|
||||
}
|
||||
|
||||
int HttpContainerIDF::read_available(uint8_t *buf, size_t max_len) {
|
||||
// A zero timeout only polls; TLS still waits for the rest of a record that has begun to arrive
|
||||
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
|
||||
esp_http_client_set_timeout_ms(this->client_, 0);
|
||||
int read_len_or_error = this->read_(buf, max_len);
|
||||
return read_len_or_error == -ESP_ERR_HTTP_EAGAIN ? 0 : read_len_or_error;
|
||||
}
|
||||
|
||||
int HttpContainerIDF::read_(uint8_t *buf, size_t max_len) {
|
||||
const uint32_t start = millis();
|
||||
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
|
||||
|
||||
// Check if we've already read all expected content (non-chunked and no-body only).
|
||||
// Use the base class check here, NOT the override: esp_http_client_is_complete_data_received()
|
||||
|
||||
@@ -5,71 +5,30 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <esp_event.h>
|
||||
#include <esp_idf_version.h>
|
||||
#include <esp_http_client.h>
|
||||
#include <esp_netif.h>
|
||||
#include <esp_tls.h>
|
||||
|
||||
// The socket is needed to wait for the response without blocking; esp_http_client_get_socket() is new in 5.5.3
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 3)
|
||||
#define USE_HTTP_REQUEST_IDF_SOCKET_POLL
|
||||
#endif
|
||||
|
||||
namespace esphome::http_request {
|
||||
|
||||
class HttpRequestIDF;
|
||||
|
||||
class HttpContainerIDF : public HttpContainer {
|
||||
public:
|
||||
HttpContainerIDF(esp_http_client_handle_t client) : client_(client) {}
|
||||
int read(uint8_t *buf, size_t max_len) override;
|
||||
int read_available(uint8_t *buf, size_t max_len) override;
|
||||
void end() override;
|
||||
bool is_read_complete() const override;
|
||||
HttpRequestProgress poll() override;
|
||||
|
||||
/// @brief Feeds the watchdog timer if the executing task has one attached
|
||||
void feed_wdt();
|
||||
|
||||
protected:
|
||||
friend class HttpRequestIDF;
|
||||
enum class Stage : uint8_t {
|
||||
STAGE_CONNECT,
|
||||
STAGE_SEND_BODY,
|
||||
STAGE_HEADERS,
|
||||
STAGE_DONE,
|
||||
STAGE_FAILED,
|
||||
};
|
||||
|
||||
int read_(uint8_t *buf, size_t max_len);
|
||||
/// True when the socket can be read or written without waiting; always true when it cannot be checked
|
||||
bool socket_ready_(bool write);
|
||||
HttpRequestProgress wait_();
|
||||
HttpRequestProgress fail_(esp_err_t err);
|
||||
/// Finish with an unsuccessful status, which is reported through on_response like a success
|
||||
HttpRequestProgress response_failed_(int status_code);
|
||||
HttpRequestIDF *idf_parent_();
|
||||
|
||||
esp_http_client_handle_t client_;
|
||||
// Owned copy (not a reference): must outlive perform() for the response-header event handler
|
||||
std::vector<std::string> collect_headers_;
|
||||
// Request body for begin(), released once sent
|
||||
std::string body_;
|
||||
size_t body_sent_{0};
|
||||
uint32_t timeout_ms_{0};
|
||||
// Time of the last step forward, for the request timeout of begin()
|
||||
uint32_t progress_time_{0};
|
||||
uint32_t start_time_{0};
|
||||
uint16_t redirects_left_{0};
|
||||
Stage stage_{Stage::STAGE_DONE};
|
||||
// An esp_http_client_open() of a non-blocking client returned ESP_ERR_HTTP_EAGAIN
|
||||
bool connecting_{false};
|
||||
bool async_{false};
|
||||
};
|
||||
|
||||
class HttpRequestIDF final : public HttpRequestComponent {
|
||||
friend class HttpContainerIDF;
|
||||
|
||||
public:
|
||||
// User provided, not "= default": `new(p) HttpRequestIDF()` would zero-fill .bss that is already zero.
|
||||
HttpRequestIDF() {}
|
||||
@@ -81,21 +40,7 @@ class HttpRequestIDF final : public HttpRequestComponent {
|
||||
void set_verify_ssl(bool verify_ssl) { this->verify_ssl_ = verify_ssl; }
|
||||
void set_ca_certificate(const char *ca_certificate) { this->ca_certificate_ = ca_certificate; }
|
||||
|
||||
std::shared_ptr<HttpContainer> begin(const char *url, const char *method, std::string &&body,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
uint32_t timeout_ms) override;
|
||||
|
||||
protected:
|
||||
/// A container that poll() takes from the connect through to the response headers
|
||||
std::shared_ptr<HttpContainerIDF> start_container_(const char *url, const char *method, std::string &&body,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
uint32_t timeout_ms, bool async);
|
||||
std::shared_ptr<HttpContainerIDF> create_container_(const char *url, const char *method,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
uint32_t timeout_ms, bool async);
|
||||
std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
|
||||
@@ -729,6 +729,7 @@ CONF_DISP_BG_COLOR = "disp_bg_color"
|
||||
CONF_DISP_BG_IMAGE = "disp_bg_image"
|
||||
CONF_DISP_BG_OPA = "disp_bg_opa"
|
||||
CONF_BODY = "body"
|
||||
CONF_BUTTONS = "buttons"
|
||||
CONF_CHANGE_RATE = "change_rate"
|
||||
CONF_CLICKABLE = "clickable"
|
||||
CONF_CLOSE_BUTTON = "close_button"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_BUTTONS, CONF_ROWS
|
||||
from esphome.components.const import CONF_ROWS
|
||||
from esphome.components.key_provider import KeyProvider
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH
|
||||
@@ -10,6 +10,7 @@ from esphome.types import Expression
|
||||
from ..automation import action_to_code
|
||||
from ..defines import (
|
||||
BUTTONMATRIX_CTRLS,
|
||||
CONF_BUTTONS,
|
||||
CONF_CONTROL,
|
||||
CONF_KEY_CODE,
|
||||
CONF_MAIN,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from esphome import codegen as cg, config_validation as cv
|
||||
from esphome.components.const import CONF_BUTTONS
|
||||
from esphome.const import CONF_BUTTON, CONF_HIDDEN, CONF_ID, CONF_TEXT
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
@@ -8,6 +7,7 @@ from esphome.cpp_types import nullptr
|
||||
from ..defines import (
|
||||
CONF_BODY,
|
||||
CONF_BUTTON_STYLE,
|
||||
CONF_BUTTONS,
|
||||
CONF_CLOSE_BUTTON,
|
||||
CONF_HEADER_BUTTONS,
|
||||
CONF_MAIN,
|
||||
|
||||
@@ -87,11 +87,6 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
import esphome.final_validate as fv
|
||||
from esphome.helpers import sanitize, snake_case
|
||||
from esphome.mqtt_topics import (
|
||||
DEFAULT_TOPIC_SUFFIXES,
|
||||
per_board_suffix,
|
||||
prefix_gets_mac_suffix,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["network"]
|
||||
@@ -271,31 +266,6 @@ def _validate_transport(value: str) -> str:
|
||||
return value
|
||||
|
||||
|
||||
# (config key, extra fields when defaulted from the topic prefix, (setter, disabler))
|
||||
_MESSAGES = (
|
||||
(
|
||||
CONF_BIRTH_MESSAGE,
|
||||
{CONF_PAYLOAD: "online"},
|
||||
lambda var: (var.set_birth_message, var.disable_birth_message),
|
||||
),
|
||||
(
|
||||
CONF_WILL_MESSAGE,
|
||||
{CONF_PAYLOAD: "offline"},
|
||||
lambda var: (var.set_last_will, var.disable_last_will),
|
||||
),
|
||||
(
|
||||
CONF_SHUTDOWN_MESSAGE,
|
||||
{CONF_PAYLOAD: "offline"},
|
||||
lambda var: (var.set_shutdown_message, var.disable_shutdown_message),
|
||||
),
|
||||
(
|
||||
CONF_LOG_TOPIC,
|
||||
{},
|
||||
lambda var: (var.set_log_message_template, var.disable_log_message),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def validate_config(value):
|
||||
if (
|
||||
value.get(CONF_TRANSPORT) not in (MQTT_TRANSPORT_WS, MQTT_TRANSPORT_WSS)
|
||||
@@ -310,19 +280,45 @@ def validate_config(value):
|
||||
topic_prefix = value[CONF_TOPIC_PREFIX]
|
||||
# If the topic prefix is not null and these messages are not configured, then set them to the default
|
||||
# If the topic prefix is null and these messages are not configured, then set them to null
|
||||
for key, extra, _ in _MESSAGES:
|
||||
if key in value:
|
||||
continue
|
||||
out[key] = (
|
||||
{}
|
||||
if topic_prefix == ""
|
||||
else {
|
||||
CONF_TOPIC: f"{topic_prefix}{DEFAULT_TOPIC_SUFFIXES[key]}",
|
||||
**extra,
|
||||
if CONF_BIRTH_MESSAGE not in value:
|
||||
if topic_prefix != "":
|
||||
out[CONF_BIRTH_MESSAGE] = {
|
||||
CONF_TOPIC: f"{topic_prefix}/status",
|
||||
CONF_PAYLOAD: "online",
|
||||
CONF_QOS: 0,
|
||||
CONF_RETAIN: True,
|
||||
}
|
||||
)
|
||||
else:
|
||||
out[CONF_BIRTH_MESSAGE] = {}
|
||||
if CONF_WILL_MESSAGE not in value:
|
||||
if topic_prefix != "":
|
||||
out[CONF_WILL_MESSAGE] = {
|
||||
CONF_TOPIC: f"{topic_prefix}/status",
|
||||
CONF_PAYLOAD: "offline",
|
||||
CONF_QOS: 0,
|
||||
CONF_RETAIN: True,
|
||||
}
|
||||
else:
|
||||
out[CONF_WILL_MESSAGE] = {}
|
||||
if CONF_SHUTDOWN_MESSAGE not in value:
|
||||
if topic_prefix != "":
|
||||
out[CONF_SHUTDOWN_MESSAGE] = {
|
||||
CONF_TOPIC: f"{topic_prefix}/status",
|
||||
CONF_PAYLOAD: "offline",
|
||||
CONF_QOS: 0,
|
||||
CONF_RETAIN: True,
|
||||
}
|
||||
else:
|
||||
out[CONF_SHUTDOWN_MESSAGE] = {}
|
||||
if CONF_LOG_TOPIC not in value:
|
||||
if topic_prefix != "":
|
||||
out[CONF_LOG_TOPIC] = {
|
||||
CONF_TOPIC: f"{topic_prefix}/debug",
|
||||
CONF_QOS: 0,
|
||||
CONF_RETAIN: True,
|
||||
}
|
||||
else:
|
||||
out[CONF_LOG_TOPIC] = {}
|
||||
return out
|
||||
|
||||
|
||||
@@ -562,7 +558,7 @@ def _subscriptions(config: ConfigType) -> Iterator[tuple[tuple[str, ...], ...]]:
|
||||
for device in esphome_config.get(CONF_DEVICES, [])
|
||||
}
|
||||
topic_prefix = config[CONF_TOPIC_PREFIX]
|
||||
if prefix_gets_mac_suffix(topic_prefix, CORE.name, mac_suffix):
|
||||
if mac_suffix and topic_prefix == CORE.name:
|
||||
topic_prefix += MAC_SUFFIX
|
||||
# Entities can sit at any depth (a platform can declare its own switches), so find them by mqtt_id
|
||||
for id_, path in iter_ids(full_config):
|
||||
@@ -611,14 +607,12 @@ def _validate_topic_lengths(config: ConfigType) -> ConfigType:
|
||||
FINAL_VALIDATE_SCHEMA = _validate_topic_lengths
|
||||
|
||||
|
||||
def exp_mqtt_message(
|
||||
config: ConfigType | None, topic: cg.MockObj | None = None
|
||||
) -> cg.Expression:
|
||||
def exp_mqtt_message(config):
|
||||
if config is None:
|
||||
return cg.optional(cg.TemplateArguments(MQTTMessage))
|
||||
return cg.StructInitializer(
|
||||
MQTTMessage,
|
||||
("topic", config[CONF_TOPIC] if topic is None else topic),
|
||||
("topic", config[CONF_TOPIC]),
|
||||
("payload", config.get(CONF_PAYLOAD, "")),
|
||||
("qos", config[CONF_QOS]),
|
||||
("retain", config[CONF_RETAIN]),
|
||||
@@ -702,33 +696,37 @@ async def to_code(config):
|
||||
)
|
||||
)
|
||||
|
||||
topic_prefix = config[CONF_TOPIC_PREFIX]
|
||||
# The MAC suffix is only known at runtime, so the prefix and the topics defaulted from it
|
||||
# are built from App.get_name(); topics written out by hand stay literal
|
||||
mac_suffix = CORE.config[CONF_ESPHOME].get(CONF_NAME_ADD_MAC_SUFFIX, False)
|
||||
mac_prefix = prefix_gets_mac_suffix(topic_prefix, CORE.name, mac_suffix)
|
||||
cg.add(var.set_topic_prefix(cg.App.get_name() if mac_prefix else topic_prefix))
|
||||
cg.add(var.set_topic_prefix(config[CONF_TOPIC_PREFIX], CORE.name))
|
||||
|
||||
if config[CONF_USE_ABBREVIATIONS]:
|
||||
cg.add_define("USE_MQTT_ABBREVIATIONS")
|
||||
|
||||
for key, _, methods in _MESSAGES:
|
||||
set_message, disable_message = methods(var)
|
||||
if not (message := config[key]):
|
||||
cg.add(disable_message())
|
||||
continue
|
||||
topic = (
|
||||
cg.App.get_name() + suffix
|
||||
if (suffix := per_board_suffix(config, key, CORE.name, mac_suffix))
|
||||
else None
|
||||
)
|
||||
cg.add(set_message(exp_mqtt_message(message, topic)))
|
||||
birth_message = config[CONF_BIRTH_MESSAGE]
|
||||
if not birth_message:
|
||||
cg.add(var.disable_birth_message())
|
||||
else:
|
||||
cg.add(var.set_birth_message(exp_mqtt_message(birth_message)))
|
||||
will_message = config[CONF_WILL_MESSAGE]
|
||||
if not will_message:
|
||||
cg.add(var.disable_last_will())
|
||||
else:
|
||||
cg.add(var.set_last_will(exp_mqtt_message(will_message)))
|
||||
shutdown_message = config[CONF_SHUTDOWN_MESSAGE]
|
||||
if not shutdown_message:
|
||||
cg.add(var.disable_shutdown_message())
|
||||
else:
|
||||
cg.add(var.set_shutdown_message(exp_mqtt_message(shutdown_message)))
|
||||
|
||||
if log_topic := config[CONF_LOG_TOPIC]:
|
||||
log_topic = config[CONF_LOG_TOPIC]
|
||||
if not log_topic:
|
||||
cg.add(var.disable_log_message())
|
||||
else:
|
||||
cg.add(var.set_log_message_template(exp_mqtt_message(log_topic)))
|
||||
# Request a log listener slot only when log topic is enabled
|
||||
logger.request_log_listener()
|
||||
if (level := log_topic.get(CONF_LEVEL)) is not None:
|
||||
cg.add(var.set_log_level(logger.LOG_LEVELS[level]))
|
||||
|
||||
if CONF_LEVEL in log_topic:
|
||||
cg.add(var.set_log_level(logger.LOG_LEVELS[log_topic[CONF_LEVEL]]))
|
||||
|
||||
cg.add(var.set_keep_alive(config[CONF_KEEPALIVE]))
|
||||
|
||||
|
||||
@@ -737,7 +737,14 @@ void MQTTClientComponent::register_mqtt_component(MQTTComponent *component) { th
|
||||
void MQTTClientComponent::set_keep_alive(uint16_t keep_alive_s) { this->mqtt_backend_.set_keep_alive(keep_alive_s); }
|
||||
void MQTTClientComponent::set_log_message_template(MQTTMessage &&message) { this->log_message_ = std::move(message); }
|
||||
const MQTTDiscoveryInfo &MQTTClientComponent::get_discovery_info() const { return this->discovery_info_; }
|
||||
void MQTTClientComponent::set_topic_prefix(std::string topic_prefix) { this->topic_prefix_ = std::move(topic_prefix); }
|
||||
void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix, const std::string &check_topic_prefix) {
|
||||
if (App.is_name_add_mac_suffix_enabled() && (topic_prefix == check_topic_prefix)) {
|
||||
char buf[ESPHOME_DEVICE_NAME_MAX_LEN + 1];
|
||||
this->topic_prefix_ = str_sanitize_to(buf, App.get_name().c_str());
|
||||
} else {
|
||||
this->topic_prefix_ = topic_prefix;
|
||||
}
|
||||
}
|
||||
const std::string &MQTTClientComponent::get_topic_prefix() const { return this->topic_prefix_; }
|
||||
void MQTTClientComponent::disable_birth_message() {
|
||||
this->birth_message_.topic = "";
|
||||
|
||||
@@ -201,7 +201,7 @@ class MQTTClientComponent final : public Component {
|
||||
*
|
||||
* @param topic_prefix The topic prefix. The last "/" is appended automatically.
|
||||
*/
|
||||
void set_topic_prefix(std::string topic_prefix);
|
||||
void set_topic_prefix(const std::string &topic_prefix, const std::string &check_topic_prefix);
|
||||
/// Get the topic prefix of this device, using default if necessary
|
||||
const std::string &get_topic_prefix() const;
|
||||
|
||||
|
||||
@@ -231,47 +231,30 @@ void OnlineImage::loop() {
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
} else {
|
||||
// Nothing new arrived. The decoder works for a limited time per call, so it may still have data to use.
|
||||
if (this->download_buffer_.unread() != 0) {
|
||||
auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread());
|
||||
if (consumed < 0) {
|
||||
ESP_LOGE(TAG, "Error decoding image: %s", esphome::runtime_image::decode_error_to_string(consumed));
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
if (consumed > 0) {
|
||||
this->download_buffer_.read(consumed);
|
||||
this->last_data_time_ = App.get_loop_component_start_time();
|
||||
} else if (this->downloader_->is_read_complete()) {
|
||||
// The response has ended, so data left in the buffer can never be completed
|
||||
size_t unread = this->download_buffer_.unread();
|
||||
if (unread == 0 && this->downloader_->get_bytes_read() > 0) {
|
||||
// A decoder that counts pixels knows the image was cut off; PNG cannot tell, so it passes
|
||||
if (this->is_decode_finished() || !this->decoder_knows_end()) {
|
||||
this->finish_download_();
|
||||
return;
|
||||
}
|
||||
ESP_LOGE(TAG, "Download ended before the image was complete");
|
||||
} else if (unread == 0) {
|
||||
ESP_LOGE(TAG, "Server sent an empty response");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Download ended with %zu bytes the decoder could not use", unread);
|
||||
}
|
||||
if (this->downloader_->is_read_complete()) {
|
||||
// The response has ended, so data left in the buffer can never be completed
|
||||
size_t unread = this->download_buffer_.unread();
|
||||
if (unread == 0 && this->downloader_->get_bytes_read() > 0) {
|
||||
// A decoder that counts pixels knows the image was cut off; PNG cannot tell, so it passes
|
||||
if (this->is_decode_finished() || !this->decoder_knows_end()) {
|
||||
this->finish_download_();
|
||||
return;
|
||||
}
|
||||
ESP_LOGE(TAG, "Download ended before the image was complete");
|
||||
} else if (unread == 0) {
|
||||
ESP_LOGE(TAG, "Server sent an empty response");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Download ended with %zu bytes the decoder could not use", unread);
|
||||
}
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
} else if (App.get_loop_component_start_time() - this->last_data_time_ >= this->parent_->get_timeout()) {
|
||||
// ESP-IDF keeps returning 0 after the server closes the connection early
|
||||
ESP_LOGE(TAG, "No data received for %" PRIu32 " ms", this->parent_->get_timeout());
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
} else if (App.get_loop_component_start_time() - this->last_data_time_ >= this->parent_->get_timeout()) {
|
||||
// ESP-IDF keeps returning 0 after the server closes the connection early
|
||||
ESP_LOGE(TAG, "No data received for %" PRIu32 " ms", this->parent_->get_timeout());
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Buffer is full, need to decode some data first
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#ifdef USE_RUNTIME_IMAGE_BMP
|
||||
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -143,25 +142,17 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
this->current_index_ = this->data_offset_;
|
||||
index = this->data_offset_;
|
||||
}
|
||||
// A call that has run for DECODE_TIME_LIMIT_MS stops, and the caller passes the rest again later
|
||||
const uint32_t start = millis();
|
||||
uint32_t steps = 0;
|
||||
auto out_of_time = [start, &steps]() {
|
||||
return ++steps % DECODE_TIME_CHECK_STEPS == 0 && millis() - start >= DECODE_TIME_LIMIT_MS;
|
||||
};
|
||||
// Bytes after the last pixel are used up without drawing anything
|
||||
const size_t total_pixels = static_cast<size_t>(this->width_) * this->height_;
|
||||
switch (this->bits_per_pixel_) {
|
||||
case 1: {
|
||||
size_t width = static_cast<size_t>(this->width_);
|
||||
while (index < size && this->paint_index_ < total_pixels) {
|
||||
while (index < size) {
|
||||
size_t x = this->paint_index_ % width;
|
||||
size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / width);
|
||||
size_t remaining_in_row = width - x;
|
||||
uint8_t pixels_in_byte = std::min<size_t>(remaining_in_row, 8);
|
||||
bool end_of_row = remaining_in_row <= 8;
|
||||
size_t needed = 1 + (end_of_row ? this->padding_bytes_ : 0);
|
||||
if (index + needed > size || out_of_time()) {
|
||||
if (index + needed > size) {
|
||||
this->decoded_bytes_ += index;
|
||||
return index;
|
||||
}
|
||||
@@ -184,11 +175,11 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
case 8: {
|
||||
size_t width = static_cast<size_t>(this->width_);
|
||||
size_t last_col = width - 1;
|
||||
while (index < size && this->paint_index_ < total_pixels) {
|
||||
while (index < size) {
|
||||
size_t x = this->paint_index_ % width;
|
||||
size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / width);
|
||||
size_t needed = 1 + ((x == last_col) ? this->padding_bytes_ : 0);
|
||||
if (index + needed > size || out_of_time()) {
|
||||
if (index + needed > size) {
|
||||
this->decoded_bytes_ += index;
|
||||
return index;
|
||||
}
|
||||
@@ -217,11 +208,11 @@ int HOT BmpDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
case 24: {
|
||||
size_t width = static_cast<size_t>(this->width_);
|
||||
size_t last_col = width - 1;
|
||||
while (index < size && this->paint_index_ < total_pixels) {
|
||||
while (index < size) {
|
||||
size_t x = this->paint_index_ % width;
|
||||
size_t y = static_cast<size_t>(this->height_ - 1) - (this->paint_index_ / width);
|
||||
size_t needed = 3 + ((x == last_col) ? this->padding_bytes_ : 0);
|
||||
if (index + needed > size || out_of_time()) {
|
||||
if (index + needed > size) {
|
||||
this->decoded_bytes_ += index;
|
||||
return index;
|
||||
}
|
||||
|
||||
@@ -1,106 +1,29 @@
|
||||
#include "image_decoder.h"
|
||||
#include "runtime_image.h"
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "image_decoder");
|
||||
|
||||
bool ImageDecoder::set_size(int width, int height) {
|
||||
if (!this->resize_image_(width, height))
|
||||
return false;
|
||||
this->prepare_draw_(width, height);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ImageDecoder::resize_image_(int width, int height) {
|
||||
this->size_valid_ = this->image_->resize(width, height) > 0;
|
||||
return this->size_valid_;
|
||||
}
|
||||
|
||||
void ImageDecoder::prepare_draw_(int width, int height) {
|
||||
this->draw_width_ = width;
|
||||
this->draw_height_ = height;
|
||||
this->direct_rgb565_ = this->image_->get_type() == image::IMAGE_TYPE_RGB565 && !this->image_->has_transparency();
|
||||
if (!this->direct_rgb565_)
|
||||
return;
|
||||
if ((this->x_boundaries_.capacity() < static_cast<size_t>(width + 1) && !this->x_boundaries_.try_init(width + 1)) ||
|
||||
(this->y_boundaries_.capacity() < static_cast<size_t>(height + 1) && !this->y_boundaries_.try_init(height + 1))) {
|
||||
ESP_LOGW(TAG, "No memory to draw straight into the buffer; drawing pixel by pixel");
|
||||
this->direct_rgb565_ = false;
|
||||
// Free the table that did fit, as nothing uses it
|
||||
this->x_boundaries_.release();
|
||||
this->y_boundaries_.release();
|
||||
return;
|
||||
}
|
||||
this->x_boundaries_.clear();
|
||||
this->y_boundaries_.clear();
|
||||
const int buffer_width = this->image_->get_buffer_width();
|
||||
const int buffer_height = this->image_->get_buffer_height();
|
||||
// The same boundaries as draw() uses, so both paths paint the same pixels
|
||||
for (int x = 0; x <= width; x++) {
|
||||
this->x_boundaries_.push_back(static_cast<uint16_t>(scale_boundary(x, width, buffer_width)));
|
||||
}
|
||||
for (int y = 0; y <= height; y++) {
|
||||
this->y_boundaries_.push_back(static_cast<uint16_t>(scale_boundary(y, height, buffer_height)));
|
||||
}
|
||||
}
|
||||
|
||||
void ImageDecoder::draw_rgb565_(int x, int y, int w, int h, uint16_t rgb565) {
|
||||
const int x_end = std::min(x + w, this->draw_width_);
|
||||
const int y_end = std::min(y + h, this->draw_height_);
|
||||
x = std::max(x, 0);
|
||||
y = std::max(y, 0);
|
||||
if (x >= x_end || y >= y_end)
|
||||
return;
|
||||
// A block of source pixels lands on one block of the buffer
|
||||
const uint16_t x1 = this->x_boundaries_[x];
|
||||
const uint16_t x2 = this->x_boundaries_[x_end];
|
||||
const uint16_t y1 = this->y_boundaries_[y];
|
||||
const uint16_t y2 = this->y_boundaries_[y_end];
|
||||
const bool big_endian = this->image_is_big_endian_();
|
||||
const uint8_t first = big_endian ? rgb565 >> 8 : rgb565 & 0xFF;
|
||||
const uint8_t second = big_endian ? rgb565 & 0xFF : rgb565 >> 8;
|
||||
uint8_t *const buffer = this->image_buffer_();
|
||||
const uint32_t buffer_width = this->image_->get_buffer_width();
|
||||
for (uint32_t row = y1; row < y2; row++) {
|
||||
uint8_t *destination = buffer + (row * buffer_width + x1) * 2;
|
||||
for (uint32_t col = x1; col < x2; col++) {
|
||||
destination[0] = first;
|
||||
destination[1] = second;
|
||||
destination += 2;
|
||||
}
|
||||
}
|
||||
bool success = this->image_->resize(width, height) > 0;
|
||||
this->size_valid_ = success;
|
||||
this->x_scale_ = static_cast<double>(this->image_->get_buffer_width()) / width;
|
||||
this->y_scale_ = static_cast<double>(this->image_->get_buffer_height()) / height;
|
||||
return success;
|
||||
}
|
||||
|
||||
void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
|
||||
if (!this->size_valid_) {
|
||||
return;
|
||||
}
|
||||
if (this->direct_rgb565_) {
|
||||
this->draw_rgb565_(x, y, w, h, display::ColorUtil::color_to_565(color));
|
||||
return;
|
||||
}
|
||||
// When shrinking, each target pixel is painted once, by the first source pixel that reaches it, rather than by every
|
||||
// pixel that lands on it. Integer maths: the S3 has no double FPU.
|
||||
const int buffer_width = this->image_->get_buffer_width();
|
||||
const int buffer_height = this->image_->get_buffer_height();
|
||||
int x1 = x;
|
||||
int y1 = y;
|
||||
int x2 = x + w;
|
||||
int y2 = y + h;
|
||||
if (buffer_width != this->draw_width_ || buffer_height != this->draw_height_) {
|
||||
x1 = scale_boundary(x, this->draw_width_, buffer_width);
|
||||
y1 = scale_boundary(y, this->draw_height_, buffer_height);
|
||||
x2 = scale_boundary(x + w, this->draw_width_, buffer_width);
|
||||
y2 = scale_boundary(y + h, this->draw_height_, buffer_height);
|
||||
}
|
||||
x2 = std::min(buffer_width, x2);
|
||||
y2 = std::min(buffer_height, y2);
|
||||
for (int i = x1; i < x2; i++) {
|
||||
for (int j = y1; j < y2; j++) {
|
||||
auto width = std::min(this->image_->get_buffer_width(), static_cast<int>(std::ceil((x + w) * this->x_scale_)));
|
||||
auto height = std::min(this->image_->get_buffer_height(), static_cast<int>(std::ceil((y + h) * this->y_scale_)));
|
||||
for (int i = x * this->x_scale_; i < width; i++) {
|
||||
for (int j = y * this->y_scale_; j < height; j++) {
|
||||
this->image_->draw_pixel(i, j, color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,10 @@
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/core/color.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "image_format.h"
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
/// The first of `target` pixels that pixel n of `source` paints, rounded up: ceil(n * target / source). The spans of
|
||||
/// neighbouring pixels meet without overlapping, so a shrunk image paints each target pixel once.
|
||||
inline int scale_boundary(int n, int source, int target) {
|
||||
if (source <= 0)
|
||||
return 0;
|
||||
return static_cast<int>((static_cast<uint32_t>(n) * static_cast<uint32_t>(target) + source - 1) /
|
||||
static_cast<uint32_t>(source));
|
||||
}
|
||||
|
||||
enum DecodeError : int {
|
||||
DECODE_ERROR_INVALID_TYPE = -1,
|
||||
DECODE_ERROR_UNSUPPORTED_FORMAT = -2,
|
||||
@@ -48,11 +38,6 @@ constexpr const char *decode_error_to_string(int error) {
|
||||
}
|
||||
}
|
||||
|
||||
// The longest one decode() call works before it returns, leaving the rest of the data for the next call
|
||||
static constexpr uint32_t DECODE_TIME_LIMIT_MS = 15;
|
||||
// Pixel loops read the clock once per this many steps
|
||||
static constexpr uint32_t DECODE_TIME_CHECK_STEPS = 64;
|
||||
|
||||
class RuntimeImage;
|
||||
|
||||
/// Input that a decoder reads itself as it goes, so the whole image never has to be in memory at once
|
||||
@@ -98,9 +83,8 @@ class ImageDecoder {
|
||||
this->expected_size_ = 0;
|
||||
this->decoded_bytes_ = 0;
|
||||
this->size_valid_ = true;
|
||||
this->draw_width_ = 0;
|
||||
this->draw_height_ = 0;
|
||||
this->direct_rgb565_ = false;
|
||||
this->x_scale_ = 1.0;
|
||||
this->y_scale_ = 1.0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,37 +179,19 @@ class ImageDecoder {
|
||||
/// Whether is_finished() can tell a complete image from a cut-off one
|
||||
virtual bool knows_end() const { return this->expected_size_ > 0; }
|
||||
|
||||
/// The image is opaque RGB565, so draw() writes it straight into the buffer
|
||||
bool uses_direct_rgb565() const { return this->direct_rgb565_; }
|
||||
|
||||
protected:
|
||||
/// Resize the image for a width x height source, without preparing to draw. prepare_draw_() must follow.
|
||||
bool resize_image_(int width, int height);
|
||||
/**
|
||||
* Draw a source of width x height over the whole buffer from now on. For an opaque RGB565 image this builds the
|
||||
* tables of where each source column and row lands; without the memory for them it draws pixel by pixel instead.
|
||||
*/
|
||||
void prepare_draw_(int width, int height);
|
||||
/// Paint a block of source pixels in one color straight into the buffer. Only while uses_direct_rgb565().
|
||||
void draw_rgb565_(int x, int y, int w, int h, uint16_t rgb565);
|
||||
|
||||
/// The image's pixel buffer, for decoders that write their output straight into it
|
||||
uint8_t *image_buffer_() const;
|
||||
bool image_is_big_endian_() const;
|
||||
|
||||
// Size of the image as the decoder draws it, which draw() spreads over the buffer
|
||||
int draw_width_ = 0;
|
||||
int draw_height_ = 0;
|
||||
// Boundary n is the first buffer column, or row, that source column, or row, n paints
|
||||
FixedVector<uint16_t> x_boundaries_{};
|
||||
FixedVector<uint16_t> y_boundaries_{};
|
||||
double x_scale_ = 1.0;
|
||||
double y_scale_ = 1.0;
|
||||
RuntimeImage *image_;
|
||||
size_t expected_size_ = 0; // Expected data size (0 if unknown)
|
||||
size_t decoded_bytes_ = 0; // Bytes processed so far
|
||||
const ImageFormat format_;
|
||||
bool active_ = false; // A decoding session is in progress
|
||||
bool size_valid_ = true; // Last set_size() result; draw() no-ops while false
|
||||
bool direct_rgb565_ = false;
|
||||
};
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
@@ -16,6 +16,32 @@ ESPHOME_LOG_TAG(TAG, "image_decoder.jpeg");
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
bool JpegDecoder::prepare_rgb565_scaling_(int width, int height) {
|
||||
if (this->x_boundaries_.capacity() < static_cast<size_t>(width + 1) && !this->x_boundaries_.try_init(width + 1)) {
|
||||
return false;
|
||||
}
|
||||
if (this->y_boundaries_.capacity() < static_cast<size_t>(height + 1) && !this->y_boundaries_.try_init(height + 1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->source_width_ = width;
|
||||
this->source_height_ = height;
|
||||
this->x_boundaries_.clear();
|
||||
this->y_boundaries_.clear();
|
||||
const uint32_t target_width = this->image_->get_buffer_width();
|
||||
const uint32_t target_height = this->image_->get_buffer_height();
|
||||
// Boundary n is the first target pixel source pixel n covers, rounded up. When shrinking, the source
|
||||
// pixel whose span lands on a target pixel paints it, so an 8 to 4 scale keeps columns 0, 2, 4 and 6;
|
||||
// the generic draw() path rounds down and keeps the odd ones instead
|
||||
for (uint32_t x = 0; x <= static_cast<uint32_t>(width); x++) {
|
||||
this->x_boundaries_.push_back(static_cast<uint16_t>((x * target_width + width - 1) / width));
|
||||
}
|
||||
for (uint32_t y = 0; y <= static_cast<uint32_t>(height); y++) {
|
||||
this->y_boundaries_.push_back(static_cast<uint16_t>((y * target_height + height - 1) / height));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void JpegDecoder::draw_rgb565(int x, int y, int w, int h, const uint8_t *pixels) {
|
||||
if (pixels == nullptr) {
|
||||
return;
|
||||
@@ -26,14 +52,14 @@ void JpegDecoder::draw_rgb565(int x, int y, int w, int h, const uint8_t *pixels)
|
||||
const uint8_t first = this->image_is_big_endian_() ? 1 : 0;
|
||||
for (int source_y = 0; source_y < h; source_y++) {
|
||||
const int absolute_y = y + source_y;
|
||||
if (absolute_y < 0 || absolute_y >= this->draw_height_) {
|
||||
if (absolute_y < 0 || absolute_y >= this->source_height_) {
|
||||
continue;
|
||||
}
|
||||
const uint16_t y1 = this->y_boundaries_[absolute_y];
|
||||
const uint16_t y2 = this->y_boundaries_[absolute_y + 1];
|
||||
for (int source_x = 0; source_x < w; source_x++) {
|
||||
const int absolute_x = x + source_x;
|
||||
if (absolute_x < 0 || absolute_x >= this->draw_width_) {
|
||||
if (absolute_x < 0 || absolute_x >= this->source_width_) {
|
||||
continue;
|
||||
}
|
||||
const uint16_t x1 = this->x_boundaries_[absolute_x];
|
||||
@@ -131,10 +157,17 @@ int JpegDecoder::start_() {
|
||||
ESP_LOGD(TAG, "Image size: %d x %d, bpp: %d", this->jpeg_.getWidth(), this->jpeg_.getHeight(), this->jpeg_.getBpp());
|
||||
|
||||
this->jpeg_.setUserPointer(this);
|
||||
this->direct_rgb565_ = this->image_->get_type() == image::IMAGE_TYPE_RGB565 && !this->image_->has_transparency();
|
||||
if (this->direct_rgb565_) {
|
||||
// JPEGDEC's RGB565_BIG_ENDIAN output is not byte swapped on every build (the x86 host build
|
||||
// returns little-endian words for it), so the byte order is applied in draw_rgb565 instead
|
||||
this->jpeg_.setPixelType(RGB565_LITTLE_ENDIAN);
|
||||
} else {
|
||||
this->jpeg_.setPixelType(RGB8888);
|
||||
}
|
||||
const int width = this->jpeg_.getWidth();
|
||||
const int height = this->jpeg_.getHeight();
|
||||
// The tables for drawing are only built once the decoded size is known, below
|
||||
if (!this->resize_image_(width, height)) {
|
||||
if (!this->set_size(width, height)) {
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
// JPEGDEC can decode at 1/2, 1/4 or 1/8 size for much less work. Use the smallest size that still has at least as
|
||||
@@ -148,16 +181,13 @@ int JpegDecoder::start_() {
|
||||
}
|
||||
const int decoded_width = (width + this->scale_ - 1) / this->scale_;
|
||||
const int decoded_height = (height + this->scale_ - 1) / this->scale_;
|
||||
this->x_scale_ = static_cast<double>(buffer_width) / decoded_width;
|
||||
this->y_scale_ = static_cast<double>(buffer_height) / decoded_height;
|
||||
if (this->scale_ != 1) {
|
||||
ESP_LOGD(TAG, "Decoding at 1/%d size, %d x %d", this->scale_, decoded_width, decoded_height);
|
||||
}
|
||||
this->prepare_draw_(decoded_width, decoded_height);
|
||||
if (this->direct_rgb565_) {
|
||||
// JPEGDEC's RGB565_BIG_ENDIAN output is not byte swapped on every build (the x86 host build
|
||||
// returns little-endian words for it), so the byte order is applied in draw_rgb565 instead
|
||||
this->jpeg_.setPixelType(RGB565_LITTLE_ENDIAN);
|
||||
} else {
|
||||
this->jpeg_.setPixelType(RGB8888);
|
||||
if (this->direct_rgb565_ && !this->prepare_rgb565_scaling_(decoded_width, decoded_height)) {
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -30,11 +30,13 @@ class JpegDecoder : public ImageDecoder {
|
||||
/// Decodes one row of MCUs (8 or 16 pixel rows), or waits part way through one for more of the source
|
||||
int HOT decode_step() override;
|
||||
|
||||
bool uses_direct_rgb565() const { return this->direct_rgb565_; }
|
||||
/// JPEGDEC decodes the image at 1/get_scale() of its size: 1, 2, 4 or 8
|
||||
int get_scale() const { return this->scale_; }
|
||||
void draw_rgb565(int x, int y, int w, int h, const uint8_t *pixels);
|
||||
|
||||
protected:
|
||||
bool prepare_rgb565_scaling_(int width, int height);
|
||||
/// Set up an opened image for decoding. Returns 0, or a DecodeError.
|
||||
int start_();
|
||||
/// The JPEGDEC option for scale_: JPEG_SCALE_HALF, _QUARTER and _EIGHTH are the divisors themselves
|
||||
@@ -47,8 +49,13 @@ class JpegDecoder : public ImageDecoder {
|
||||
static void source_close(void *handle) {}
|
||||
|
||||
JPEGDEC jpeg_{};
|
||||
FixedVector<uint16_t> x_boundaries_{};
|
||||
FixedVector<uint16_t> y_boundaries_{};
|
||||
int source_width_{0};
|
||||
int source_height_{0};
|
||||
int scale_{1}; // JPEGDEC decodes at 1/scale_ of the image size
|
||||
DecodeSource *source_{nullptr}; // Set while a begin_source() decode is in progress
|
||||
bool direct_rgb565_{false};
|
||||
bool source_failed_{false};
|
||||
};
|
||||
|
||||
|
||||
@@ -6,15 +6,10 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "image_decoder.png");
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
// Compressed bytes handed to pngle at a time
|
||||
static constexpr size_t FEED_PIECE_SIZE = 64;
|
||||
|
||||
/**
|
||||
* @brief Callback method that will be called by the PNGLE engine when the basic
|
||||
* data of the image is received (i.e. width and height);
|
||||
@@ -98,26 +93,16 @@ int HOT PngDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
ESP_LOGD(TAG, "Waiting for more data");
|
||||
return 0;
|
||||
}
|
||||
// A few compressed bytes can hold many rows of pixels, so feed small pieces and stop once the call has run for
|
||||
// DECODE_TIME_LIMIT_MS; the caller passes the rest again later
|
||||
const uint32_t start = millis();
|
||||
size_t total = 0;
|
||||
while (total < size) {
|
||||
const size_t piece = std::min(FEED_PIECE_SIZE, size - total);
|
||||
const int fed = pngle_feed(this->pngle_, buffer + total, piece);
|
||||
if (fed < 0) {
|
||||
ESP_LOGE(TAG, "Error decoding image: %s", pngle_error(this->pngle_));
|
||||
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
|
||||
}
|
||||
if (!this->size_valid_)
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
auto fed = pngle_feed(this->pngle_, buffer, size);
|
||||
if (fed < 0) {
|
||||
ESP_LOGE(TAG, "Error decoding image: %s", pngle_error(this->pngle_));
|
||||
return DECODE_ERROR_INTERNAL_DECODER_ERROR;
|
||||
} else if (!this->size_valid_) {
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
} else {
|
||||
this->decoded_bytes_ += fed;
|
||||
total += fed;
|
||||
// pngle takes less than the piece when a chunk header or CRC runs past its end; that is no reason to stop
|
||||
if (fed == 0 || millis() - start >= DECODE_TIME_LIMIT_MS)
|
||||
break;
|
||||
}
|
||||
return total;
|
||||
return fed;
|
||||
}
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
#ifdef USE_RUNTIME_IMAGE_QOI
|
||||
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
@@ -104,17 +102,12 @@ int HOT QoiDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
|
||||
Color color;
|
||||
const size_t total_pixels = this->width_ * this->height_;
|
||||
// A call that has run for DECODE_TIME_LIMIT_MS stops, and the caller passes the rest again later
|
||||
const uint32_t start = millis();
|
||||
uint32_t steps = 0;
|
||||
while (index < size && this->paint_index_ < total_pixels) {
|
||||
if (++steps % DECODE_TIME_CHECK_STEPS == 0 && millis() - start >= DECODE_TIME_LIMIT_MS)
|
||||
break;
|
||||
color = this->last_pixel_;
|
||||
uint8_t byte = buffer[index];
|
||||
if (byte == QOI_OP_RGB) {
|
||||
if (size < index + QOI_RGB_CHUNK_SIZE) {
|
||||
break; // Need more data for RGB chunk
|
||||
return index; // Need more data for RGB chunk
|
||||
}
|
||||
index++;
|
||||
color.r = buffer[index++];
|
||||
@@ -124,7 +117,7 @@ int HOT QoiDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
this->paint_index_++;
|
||||
} else if (byte == QOI_OP_RGBA) {
|
||||
if (size < index + QOI_RGBA_CHUNK_SIZE) {
|
||||
break; // Need more data for RGBA chunk
|
||||
return index; // Need more data for RGBA chunk
|
||||
}
|
||||
index++;
|
||||
color.r = buffer[index++];
|
||||
@@ -136,18 +129,15 @@ int HOT QoiDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
} else if ((byte & QOI_MASK_OP) == QOI_OP_RUN) {
|
||||
// QOI run chunk
|
||||
size_t run_length = (byte & QOI_MASK_VALUE) + 1; // run length is encoded in the lower 6 bits, plus one
|
||||
// Draw the part of the run in each row it covers at once
|
||||
while (run_length > 0) {
|
||||
const size_t x = this->paint_index_ % this->width_;
|
||||
const size_t count = std::min(run_length, this->width_ - x);
|
||||
this->draw(x, this->paint_index_ / this->width_, count, 1, color);
|
||||
this->paint_index_ += count;
|
||||
run_length -= count;
|
||||
for (size_t i = 0; i < run_length; i++) {
|
||||
// TODO: optimize by drawing runs of pixels at once instead of one by one
|
||||
this->draw(this->paint_index_ % this->width_, this->paint_index_ / this->width_, 1, 1, color);
|
||||
this->paint_index_++;
|
||||
}
|
||||
index++;
|
||||
} else if ((byte & QOI_MASK_OP) == QOI_OP_LUMA) {
|
||||
if (size < index + QOI_LUMA_CHUNK_SIZE) {
|
||||
break; // Need more data for LUMA chunk
|
||||
return index; // Need more data for LUMA chunk
|
||||
}
|
||||
index++;
|
||||
uint8_t byte2 = buffer[index++];
|
||||
@@ -175,10 +165,8 @@ int HOT QoiDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
this->last_pixel_ = color;
|
||||
this->color_table_[qoi_color_table_index(color)] = color;
|
||||
}
|
||||
if (this->paint_index_ >= total_pixels)
|
||||
index = size; // All that follows the last pixel is the end marker
|
||||
this->decoded_bytes_ += index;
|
||||
return index;
|
||||
this->decoded_bytes_ += size;
|
||||
return size;
|
||||
}
|
||||
|
||||
} // namespace esphome::runtime_image
|
||||
|
||||
@@ -113,9 +113,7 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
|
||||
}
|
||||
case image::IMAGE_TYPE_GRAYSCALE: {
|
||||
uint32_t pos = this->get_position_(x, y);
|
||||
// 0.2125, 0.7154 and 0.0721 in 1/256ths, summing to 256 so white stays 255. Integer maths: the S3 has no
|
||||
// double FPU
|
||||
auto gray = static_cast<uint8_t>((54 * color.r + 183 * color.g + 19 * color.b) >> 8);
|
||||
auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
|
||||
if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) {
|
||||
if (gray == 1) {
|
||||
gray = 0;
|
||||
|
||||
@@ -16,6 +16,7 @@ from esphome.types import ConfigType
|
||||
DEPENDENCIES = ["wifi"]
|
||||
|
||||
wifi_info_ns = cg.esphome_ns.namespace("wifi_info")
|
||||
_request_ip_sensor_slot = cg.slot_counter("WIFI_INFO_IP_SENSOR_COUNT")
|
||||
IPAddressWiFiInfo = wifi_info_ns.class_(
|
||||
"IPAddressWiFiInfo", text_sensor.TextSensor, cg.Component
|
||||
)
|
||||
@@ -110,7 +111,10 @@ async def to_code(config: ConfigType) -> None:
|
||||
if conf := config.get(CONF_IP_ADDRESS):
|
||||
wifi_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS])
|
||||
await cg.register_component(wifi_info, config[CONF_IP_ADDRESS])
|
||||
for x in range(5):
|
||||
if sensor_conf := conf.get(f"address_{x}"):
|
||||
sens = await text_sensor.new_text_sensor(sensor_conf)
|
||||
cg.add(wifi_info.add_ip_sensors(x, sens))
|
||||
indexes = [x for x in range(5) if f"address_{x}" in conf]
|
||||
# One shared array size: the largest highest index of any entry, so add_ip_sensors(x) stays in range
|
||||
for _ in range(max(indexes, default=-1) + 1):
|
||||
_request_ip_sensor_slot(str(wifi_info))
|
||||
for x in indexes:
|
||||
sens = await text_sensor.new_text_sensor(conf[f"address_{x}"])
|
||||
cg.add(wifi_info.add_ip_sensors(x, sens))
|
||||
@@ -26,16 +26,20 @@ void IPAddressWiFiInfo::on_ip_state(const network::IPAddresses &ips, const netwo
|
||||
char buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
ips[0].str_to(buf);
|
||||
this->publish_state(buf);
|
||||
#ifdef WIFI_INFO_IP_SENSOR_COUNT
|
||||
uint8_t sensor = 0;
|
||||
for (const auto &ip : ips) {
|
||||
if (ip.is_set()) {
|
||||
if (this->ip_sensors_[sensor] != nullptr) {
|
||||
ip.str_to(buf);
|
||||
this->ip_sensors_[sensor]->publish_state(buf);
|
||||
}
|
||||
sensor++;
|
||||
if (!ip.is_set())
|
||||
continue;
|
||||
if (sensor == WIFI_INFO_IP_SENSOR_COUNT)
|
||||
break;
|
||||
if (this->ip_sensors_[sensor] != nullptr) {
|
||||
ip.str_to(buf);
|
||||
this->ip_sensors_[sensor]->publish_state(buf);
|
||||
}
|
||||
sensor++;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/*********************
|
||||
|
||||
@@ -16,14 +16,19 @@ class IPAddressWiFiInfo final : public Component, public text_sensor::TextSensor
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
#ifdef WIFI_INFO_IP_SENSOR_COUNT
|
||||
void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; }
|
||||
#endif
|
||||
|
||||
// WiFiIPStateListener interface
|
||||
void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
|
||||
const network::IPAddress &dns2) override;
|
||||
|
||||
protected:
|
||||
std::array<text_sensor::TextSensor *, 5> ip_sensors_{};
|
||||
#ifdef WIFI_INFO_IP_SENSOR_COUNT
|
||||
// Sized to the highest configured address_N
|
||||
std::array<text_sensor::TextSensor *, WIFI_INFO_IP_SENSOR_COUNT> ip_sensors_{};
|
||||
#endif
|
||||
};
|
||||
|
||||
class DNSAddressWifiInfo final : public Component, public text_sensor::TextSensor, public wifi::WiFiIPStateListener {
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
#define LV_USE_LOG 1
|
||||
#define USE_MDNS
|
||||
#define USE_MDNS_STORE_SERVICES
|
||||
#define ETHERNET_INFO_IP_SENSOR_COUNT 5
|
||||
#define GPS_LISTENER_COUNT 1
|
||||
#define HDMI_CEC_MESSAGE_CALLBACK_COUNT 2
|
||||
#define I2C_PORT_FREQUENCY_COUNT 1
|
||||
@@ -193,6 +194,7 @@
|
||||
#define PYLONTECH_SENSOR_CELL_COUNT 2
|
||||
#define REMOTE_BASE_DUMPER_COUNT 1
|
||||
#define REMOTE_BASE_LISTENER_COUNT 1
|
||||
#define WIFI_INFO_IP_SENSOR_COUNT 5
|
||||
#define USE_REMOTE_PROTOCOL_ABBWELCOME
|
||||
#define USE_REMOTE_PROTOCOL_AEHA
|
||||
#define USE_REMOTE_PROTOCOL_AOK
|
||||
|
||||
+30
-88
@@ -5,7 +5,6 @@ from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
import ssl
|
||||
import tempfile
|
||||
import time
|
||||
@@ -24,7 +23,6 @@ from esphome.const import (
|
||||
CONF_LOG_TOPIC,
|
||||
CONF_MQTT,
|
||||
CONF_NAME,
|
||||
CONF_NAME_ADD_MAC_SUFFIX,
|
||||
CONF_PASSWORD,
|
||||
CONF_PAYLOAD,
|
||||
CONF_PORT,
|
||||
@@ -36,7 +34,6 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.helpers import get_int_env, get_str_env
|
||||
from esphome.mqtt_topics import per_board_suffix
|
||||
from esphome.types import ConfigType
|
||||
from esphome.util import safe_print
|
||||
|
||||
@@ -397,101 +394,48 @@ def show_logs(
|
||||
password: str | None = None,
|
||||
client_id: str | None = None,
|
||||
) -> int:
|
||||
# Where the device publishes its logs, also needed to match a topic given on the command line
|
||||
config_topic: str | None = None
|
||||
error: str | None = None
|
||||
mqtt_conf = config.get(CONF_MQTT, {})
|
||||
if CONF_MQTT not in config:
|
||||
error = "MQTT isn't setup, can't start MQTT logs"
|
||||
elif CONF_LOG_TOPIC in mqtt_conf:
|
||||
if mqtt_conf[CONF_LOG_TOPIC] is None:
|
||||
error = "MQTT log topic set to null, can't start MQTT logs"
|
||||
elif CONF_TOPIC not in mqtt_conf[CONF_LOG_TOPIC]:
|
||||
error = "MQTT log topic not available, can't start MQTT logs"
|
||||
if topic is not None:
|
||||
pass # already have topic
|
||||
elif CONF_MQTT in config:
|
||||
conf = config[CONF_MQTT]
|
||||
if CONF_LOG_TOPIC in conf:
|
||||
if config[CONF_MQTT][CONF_LOG_TOPIC] is None:
|
||||
_LOGGER.error("MQTT log topic set to null, can't start MQTT logs")
|
||||
return 1
|
||||
if CONF_TOPIC not in config[CONF_MQTT][CONF_LOG_TOPIC]:
|
||||
_LOGGER.error("MQTT log topic not available, can't start MQTT logs")
|
||||
return 1
|
||||
topic = config[CONF_MQTT][CONF_LOG_TOPIC][CONF_TOPIC]
|
||||
elif CONF_TOPIC_PREFIX in config[CONF_MQTT]:
|
||||
topic = f"{config[CONF_MQTT][CONF_TOPIC_PREFIX]}/debug"
|
||||
else:
|
||||
config_topic = mqtt_conf[CONF_LOG_TOPIC][CONF_TOPIC]
|
||||
elif CONF_TOPIC_PREFIX in mqtt_conf:
|
||||
config_topic = f"{mqtt_conf[CONF_TOPIC_PREFIX]}/debug"
|
||||
topic = f"{config[CONF_ESPHOME][CONF_NAME]}/debug"
|
||||
else:
|
||||
config_topic = f"{config[CONF_ESPHOME][CONF_NAME]}/debug"
|
||||
# A topic given on the command line is followed as written
|
||||
topic_from_cli = topic is not None
|
||||
if not topic_from_cli:
|
||||
if error is not None:
|
||||
_LOGGER.error(error)
|
||||
return 1
|
||||
topic = config_topic
|
||||
esphome_conf = config[CONF_ESPHOME]
|
||||
name = esphome_conf[CONF_NAME]
|
||||
# With the MAC suffix every board listens on esphome/logs/<name>-<mac>, which only its birth message names
|
||||
mac_suffix = esphome_conf.get(CONF_NAME_ADD_MAC_SUFFIX, False)
|
||||
_LOGGER.error("MQTT isn't setup, can't start MQTT logs")
|
||||
return 1
|
||||
_LOGGER.info("Starting log output from %s", topic)
|
||||
|
||||
logs_topic = f"esphome/logs/{config[CONF_ESPHOME][CONF_NAME]}"
|
||||
# Ask when the retained birth message reports online: a request sent while the device is down is
|
||||
# lost, and only a retained message tells a CLI that connects later that the device is already up.
|
||||
# An empty payload deletes the retained message instead, so it counts as no birth message.
|
||||
birth = mqtt_conf.get(CONF_BIRTH_MESSAGE) or {}
|
||||
birth = config.get(CONF_MQTT, {}).get(CONF_BIRTH_MESSAGE) or {}
|
||||
birth_payload = birth.get(CONF_PAYLOAD)
|
||||
birth_topic = birth.get(CONF_TOPIC) if birth_payload else None
|
||||
ask_on_connect = not mac_suffix and (birth_topic is None or not birth[CONF_RETAIN])
|
||||
ask_on_connect = birth_topic is None or not birth[CONF_RETAIN]
|
||||
|
||||
def per_board(present: str | None, key: str) -> re.Pattern[str] | None:
|
||||
# Topics the firmware builds from <name>-<mac>, which the CLI cannot know
|
||||
if present is None or not (
|
||||
suffix := per_board_suffix(mqtt_conf, key, name, mac_suffix)
|
||||
):
|
||||
return None
|
||||
return re.compile(rf"({re.escape(name)}-[0-9a-f]{{6}}){re.escape(suffix)}")
|
||||
|
||||
board_log_pattern = per_board(config_topic, CONF_LOG_TOPIC)
|
||||
board_logs = board_log_pattern is not None
|
||||
log_pattern = None if topic_from_cli else board_log_pattern
|
||||
birth_pattern = per_board(birth_topic, CONF_BIRTH_MESSAGE)
|
||||
_LOGGER.info(
|
||||
"Starting log output from %s",
|
||||
f"{name}-<mac>/debug of every device built from this config"
|
||||
if log_pattern
|
||||
else topic,
|
||||
)
|
||||
|
||||
def request_crash_log(client: mqtt.Client, board: str = name) -> None:
|
||||
logs_topic = f"esphome/logs/{board}"
|
||||
def request_crash_log(client: mqtt.Client) -> None:
|
||||
_LOGGER.debug("Requesting the crash log via topic: %s", logs_topic)
|
||||
client.publish(logs_topic, None, retain=False)
|
||||
|
||||
def shows_board_logs(board: str) -> bool:
|
||||
board_topic = f"{board}/debug" if board_logs else config_topic
|
||||
return board_topic is not None and mqtt.topic_matches_sub(topic, board_topic)
|
||||
|
||||
def board_to_ask(birth_msg_topic: str) -> str | None:
|
||||
if birth_pattern is None:
|
||||
return name if birth_msg_topic == birth_topic and not mac_suffix else None
|
||||
if (match := birth_pattern.fullmatch(birth_msg_topic)) and (
|
||||
# Replaying clears the stored crash, so with a --topic only ask the boards it shows
|
||||
not topic_from_cli or shows_board_logs(match[1])
|
||||
):
|
||||
return match[1]
|
||||
return None
|
||||
|
||||
def on_message(client: mqtt.Client, userdata: Any, msg: mqtt.MQTTMessage) -> None:
|
||||
t = msg.topic
|
||||
# The birth topic may sit inside the log subscription, so print by what was asked for
|
||||
shown = (
|
||||
log_pattern.fullmatch(t)
|
||||
if log_pattern
|
||||
else mqtt.topic_matches_sub(topic, t)
|
||||
)
|
||||
board = board_to_ask(t)
|
||||
if not (shown or board):
|
||||
return
|
||||
payload = msg.payload.decode(errors="backslashreplace")
|
||||
if board and payload == birth_payload:
|
||||
request_crash_log(client, board)
|
||||
if not shown:
|
||||
return
|
||||
if log_pattern:
|
||||
# Several boards can run this config, so name the one that logged
|
||||
payload = f"[{shown[1]}]{payload}"
|
||||
time_ = datetime.now().astimezone().time().strftime("[%H:%M:%S]")
|
||||
safe_print(time_ + payload)
|
||||
if msg.topic == birth_topic and payload == birth_payload:
|
||||
request_crash_log(client)
|
||||
# The birth topic may sit inside the log subscription, so print by what was asked for
|
||||
if mqtt.topic_matches_sub(topic, msg.topic):
|
||||
time_ = datetime.now().astimezone().time().strftime("[%H:%M:%S]")
|
||||
safe_print(time_ + payload)
|
||||
|
||||
def on_connect(
|
||||
client: mqtt.Client,
|
||||
@@ -503,9 +447,7 @@ def show_logs(
|
||||
if ask_on_connect:
|
||||
request_crash_log(client)
|
||||
|
||||
subscriptions = ["+/debug" if log_pattern else topic]
|
||||
if birth_topic is not None:
|
||||
subscriptions.append("+/status" if birth_pattern else birth_topic)
|
||||
subscriptions = [topic] if birth_topic is None else [topic, birth_topic]
|
||||
return initialize(
|
||||
config, subscriptions, on_message, on_connect, username, password, client_id
|
||||
)
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
"""MQTT topic rules shared by code generation and the MQTT log viewer."""
|
||||
|
||||
from esphome.const import (
|
||||
CONF_BIRTH_MESSAGE,
|
||||
CONF_LOG_TOPIC,
|
||||
CONF_SHUTDOWN_MESSAGE,
|
||||
CONF_TOPIC,
|
||||
CONF_TOPIC_PREFIX,
|
||||
CONF_WILL_MESSAGE,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
# Topic suffix of each message whose topic defaults to one under the topic prefix
|
||||
DEFAULT_TOPIC_SUFFIXES = {
|
||||
CONF_BIRTH_MESSAGE: "/status",
|
||||
CONF_WILL_MESSAGE: "/status",
|
||||
CONF_SHUTDOWN_MESSAGE: "/status",
|
||||
CONF_LOG_TOPIC: "/debug",
|
||||
}
|
||||
|
||||
|
||||
def prefix_gets_mac_suffix(topic_prefix: str, name: str, mac_suffix: bool) -> bool:
|
||||
"""Whether the firmware appends the MAC suffix to this topic prefix at runtime."""
|
||||
return mac_suffix and topic_prefix == name
|
||||
|
||||
|
||||
def per_board_suffix(
|
||||
mqtt_config: ConfigType, key: str, name: str, mac_suffix: bool
|
||||
) -> str | None:
|
||||
"""Topic suffix of a message the firmware sends on App.get_name() + suffix, else None.
|
||||
|
||||
A message missing from the config counts as the one defaulted from the topic prefix.
|
||||
"""
|
||||
topic_prefix = mqtt_config.get(CONF_TOPIC_PREFIX, name)
|
||||
if not prefix_gets_mac_suffix(topic_prefix, name, mac_suffix):
|
||||
return None
|
||||
suffix = DEFAULT_TOPIC_SUFFIXES[key]
|
||||
if (
|
||||
key in mqtt_config
|
||||
and (mqtt_config[key] or {}).get(CONF_TOPIC) != f"{topic_prefix}{suffix}"
|
||||
):
|
||||
return None
|
||||
return suffix
|
||||
@@ -1,12 +0,0 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: nodemcu-32s
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: testtest
|
||||
|
||||
http_request:
|
||||
max_concurrent_requests: 3
|
||||
@@ -1,20 +0,0 @@
|
||||
"""max_concurrent_requests is set only when it differs from the default of 1."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_max_concurrent_requests_default_is_not_set(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("test_esp32_stock.yaml"))
|
||||
assert "set_max_concurrent_requests" not in main_cpp
|
||||
|
||||
|
||||
def test_max_concurrent_requests_is_set(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("max_concurrent.yaml"))
|
||||
assert "->set_max_concurrent_requests(3);" in main_cpp
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
esphome:
|
||||
name: test-device
|
||||
name_add_mac_suffix: true
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
mqtt:
|
||||
broker: 192.168.1.2
|
||||
topic_prefix: custom
|
||||
@@ -1,14 +0,0 @@
|
||||
---
|
||||
esphome:
|
||||
name: test-device
|
||||
name_add_mac_suffix: true
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
mqtt:
|
||||
broker: 192.168.1.2
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
esphome:
|
||||
name: test-device
|
||||
name_add_mac_suffix: true
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
mqtt:
|
||||
broker: 192.168.1.2
|
||||
log_topic: test-device/debug
|
||||
birth_message:
|
||||
topic: test-device/status
|
||||
payload: online
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
esphome:
|
||||
name: test-device
|
||||
name_add_mac_suffix: true
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
mqtt:
|
||||
broker: 192.168.1.2
|
||||
log_topic: test-device/log
|
||||
birth_message:
|
||||
topic: test-device
|
||||
payload: online
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
esphome:
|
||||
name: test-device
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
mqtt:
|
||||
broker: 192.168.1.2
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
esphome:
|
||||
name: test-device
|
||||
name_add_mac_suffix: true
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_lite
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
mqtt:
|
||||
broker: 192.168.1.2
|
||||
topic_prefix:
|
||||
@@ -1,82 +0,0 @@
|
||||
"""Tests for MQTT topic generation with name_add_mac_suffix."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "present", "absent"),
|
||||
[
|
||||
pytest.param(
|
||||
"mac_suffix_default_topics.yaml",
|
||||
(
|
||||
"set_topic_prefix(App.get_name())",
|
||||
'(App.get_name() + "/status")',
|
||||
'(App.get_name() + "/debug")',
|
||||
),
|
||||
('"test-device/status"', '"test-device/debug"'),
|
||||
id="default topics follow the MAC suffix",
|
||||
),
|
||||
pytest.param(
|
||||
"mac_suffix_explicit_topics.yaml",
|
||||
(
|
||||
"set_topic_prefix(App.get_name())",
|
||||
# will and shutdown are still defaulted from the prefix
|
||||
'(App.get_name() + "/status")',
|
||||
'.topic = "test-device/log",',
|
||||
'.topic = "test-device",',
|
||||
),
|
||||
('App.get_name() + "/log"', 'App.get_name() + "/debug"'),
|
||||
id="explicit topics stay literal",
|
||||
),
|
||||
pytest.param(
|
||||
"mac_suffix_default_written_out.yaml",
|
||||
('(App.get_name() + "/status")', '(App.get_name() + "/debug")'),
|
||||
('"test-device/status"', '"test-device/debug"'),
|
||||
id="topics written out as the default follow the MAC suffix",
|
||||
),
|
||||
pytest.param(
|
||||
"mac_suffix_custom_prefix.yaml",
|
||||
('set_topic_prefix("custom")', '"custom/status"', '"custom/debug"'),
|
||||
("App.get_name()",),
|
||||
id="custom prefix stays literal",
|
||||
),
|
||||
pytest.param(
|
||||
"no_mac_suffix.yaml",
|
||||
(
|
||||
'set_topic_prefix("test-device")',
|
||||
'"test-device/status"',
|
||||
'"test-device/debug"',
|
||||
),
|
||||
("App.get_name()",),
|
||||
id="no MAC suffix",
|
||||
),
|
||||
pytest.param(
|
||||
"no_topic_prefix.yaml",
|
||||
(
|
||||
'set_topic_prefix("")',
|
||||
"disable_birth_message()",
|
||||
"disable_last_will()",
|
||||
"disable_shutdown_message()",
|
||||
"disable_log_message()",
|
||||
),
|
||||
(),
|
||||
id="null prefix disables default messages",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_topics(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
present: tuple[str, ...],
|
||||
absent: tuple[str, ...],
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path(config_file))
|
||||
|
||||
for text in present:
|
||||
assert text in main_cpp
|
||||
for text in absent:
|
||||
assert text not in main_cpp
|
||||
@@ -1,127 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/am43/am43_base.h"
|
||||
|
||||
namespace esphome::am43::testing {
|
||||
|
||||
static std::vector<uint8_t> bytes(const Am43Packet &packet) {
|
||||
return std::vector<uint8_t>(packet.data, packet.data + packet.length);
|
||||
}
|
||||
|
||||
TEST(Am43Encoder, RequestsMatchTheDeviceFraming) {
|
||||
// 00 ff 00 00 9a, command, length, data, then the inverted XOR of everything before it
|
||||
EXPECT_EQ(bytes(Am43Encoder::get_battery_level_request()),
|
||||
(std::vector<uint8_t>{0x00, 0xff, 0x00, 0x00, 0x9a, 0xa2, 0x01, 0x01, 0x38}));
|
||||
EXPECT_EQ(bytes(Am43Encoder::get_light_level_request()),
|
||||
(std::vector<uint8_t>{0x00, 0xff, 0x00, 0x00, 0x9a, 0xaa, 0x01, 0x01, 0x30}));
|
||||
EXPECT_EQ(bytes(Am43Encoder::get_position_request()),
|
||||
(std::vector<uint8_t>{0x00, 0xff, 0x00, 0x00, 0x9a, 0xa7, 0x01, 0x01, 0x3d}));
|
||||
EXPECT_EQ(bytes(Am43Encoder::get_send_pin_request(0x1234)),
|
||||
(std::vector<uint8_t>{0x00, 0xff, 0x00, 0x00, 0x9a, 0x17, 0x02, 0x12, 0x34, 0xa9}));
|
||||
EXPECT_EQ(bytes(Am43Encoder::get_open_request()),
|
||||
(std::vector<uint8_t>{0x00, 0xff, 0x00, 0x00, 0x9a, 0x0a, 0x01, 0xdd, 0x4c}));
|
||||
EXPECT_EQ(bytes(Am43Encoder::get_close_request()),
|
||||
(std::vector<uint8_t>{0x00, 0xff, 0x00, 0x00, 0x9a, 0x0a, 0x01, 0xee, 0x7f}));
|
||||
EXPECT_EQ(bytes(Am43Encoder::get_stop_request()),
|
||||
(std::vector<uint8_t>{0x00, 0xff, 0x00, 0x00, 0x9a, 0x0a, 0x01, 0xcc, 0x5d}));
|
||||
EXPECT_EQ(bytes(Am43Encoder::get_set_position_request(42)),
|
||||
(std::vector<uint8_t>{0x00, 0xff, 0x00, 0x00, 0x9a, 0x0d, 0x01, 0x2a, 0xbc}));
|
||||
}
|
||||
|
||||
TEST(Am43Decoder, ReadsEachResponse) {
|
||||
Am43Decoder decoder;
|
||||
const uint8_t battery[] = {0x9a, CMD_GET_BATTERY_LEVEL, 0, 0, 0, 0, 0, 77};
|
||||
decoder.decode(battery, sizeof(battery));
|
||||
ASSERT_TRUE(decoder.has_battery_level());
|
||||
EXPECT_EQ(decoder.battery_level_, 77);
|
||||
EXPECT_FALSE(decoder.has_position());
|
||||
|
||||
const uint8_t light[] = {0x9a, CMD_GET_LIGHT_LEVEL, 0, 0, 9};
|
||||
decoder.decode(light, sizeof(light));
|
||||
ASSERT_TRUE(decoder.has_light_level());
|
||||
EXPECT_FLOAT_EQ(decoder.light_level_, 100.0f);
|
||||
EXPECT_FALSE(decoder.has_battery_level()); // flags reset on every frame
|
||||
|
||||
const uint8_t position[] = {0x9a, CMD_GET_POSITION, 0, 0, 0, 30};
|
||||
decoder.decode(position, sizeof(position));
|
||||
ASSERT_TRUE(decoder.has_position());
|
||||
EXPECT_EQ(decoder.position_, 30);
|
||||
|
||||
const uint8_t notify[] = {0x9a, CMD_NOTIFY_POSITION, 0, 0, 55};
|
||||
decoder.decode(notify, sizeof(notify));
|
||||
ASSERT_TRUE(decoder.has_position());
|
||||
EXPECT_EQ(decoder.position_, 55);
|
||||
|
||||
const uint8_t pin_ack[] = {0x9a, CMD_SEND_PIN, 0, RESPONSE_ACK};
|
||||
decoder.decode(pin_ack, sizeof(pin_ack));
|
||||
ASSERT_TRUE(decoder.has_pin_response());
|
||||
EXPECT_TRUE(decoder.pin_ok_);
|
||||
|
||||
const uint8_t position_nack[] = {0x9a, CMD_SET_POSITION, 0, RESPONSE_NACK};
|
||||
decoder.decode(position_nack, sizeof(position_nack));
|
||||
ASSERT_TRUE(decoder.has_set_position_response());
|
||||
EXPECT_FALSE(decoder.set_position_ok_);
|
||||
|
||||
const uint8_t state_ack[] = {0x9a, CMD_SET_STATE, 0, RESPONSE_ACK};
|
||||
decoder.decode(state_ack, sizeof(state_ack));
|
||||
ASSERT_TRUE(decoder.has_set_state_response());
|
||||
EXPECT_TRUE(decoder.set_state_ok_);
|
||||
}
|
||||
|
||||
TEST(Am43Decoder, IgnoresShortAndForeignFrames) {
|
||||
Am43Decoder decoder;
|
||||
const uint8_t short_battery[] = {0x9a, CMD_GET_BATTERY_LEVEL, 0, 0};
|
||||
decoder.decode(short_battery, sizeof(short_battery));
|
||||
EXPECT_FALSE(decoder.has_battery_level());
|
||||
|
||||
const uint8_t foreign[] = {0x55, CMD_GET_POSITION, 0, 0, 0, 30};
|
||||
decoder.decode(foreign, sizeof(foreign));
|
||||
EXPECT_FALSE(decoder.has_position());
|
||||
}
|
||||
|
||||
TEST(Am43Decoder, EachResponseNeedsItsMinimumLength) {
|
||||
struct Case {
|
||||
uint8_t command;
|
||||
uint8_t min_length;
|
||||
bool (*has)(Am43Decoder &);
|
||||
};
|
||||
const Case cases[] = {
|
||||
{CMD_GET_BATTERY_LEVEL, 8, [](Am43Decoder &d) { return d.has_battery_level(); }},
|
||||
{CMD_GET_LIGHT_LEVEL, 5, [](Am43Decoder &d) { return d.has_light_level(); }},
|
||||
{CMD_GET_POSITION, 6, [](Am43Decoder &d) { return d.has_position(); }},
|
||||
{CMD_NOTIFY_POSITION, 5, [](Am43Decoder &d) { return d.has_position(); }},
|
||||
{CMD_SEND_PIN, 4, [](Am43Decoder &d) { return d.has_pin_response(); }},
|
||||
{CMD_SET_POSITION, 4, [](Am43Decoder &d) { return d.has_set_position_response(); }},
|
||||
{CMD_SET_STATE, 4, [](Am43Decoder &d) { return d.has_set_state_response(); }},
|
||||
};
|
||||
for (const auto &c : cases) {
|
||||
uint8_t frame[8] = {0x9a, c.command, 0, RESPONSE_ACK, 0, 0, 0, 0};
|
||||
Am43Decoder decoder;
|
||||
decoder.decode(frame, c.min_length - 1);
|
||||
EXPECT_FALSE(c.has(decoder)) << "command 0x" << std::hex << int(c.command);
|
||||
decoder.decode(frame, c.min_length);
|
||||
EXPECT_TRUE(c.has(decoder)) << "command 0x" << std::hex << int(c.command);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Am43Decoder, IgnoresTinyAndUnknownFrames) {
|
||||
auto expect_nothing = [](Am43Decoder &decoder) {
|
||||
EXPECT_FALSE(decoder.has_battery_level());
|
||||
EXPECT_FALSE(decoder.has_light_level());
|
||||
EXPECT_FALSE(decoder.has_position());
|
||||
EXPECT_FALSE(decoder.has_pin_response());
|
||||
EXPECT_FALSE(decoder.has_set_position_response());
|
||||
EXPECT_FALSE(decoder.has_set_state_response());
|
||||
};
|
||||
Am43Decoder decoder;
|
||||
const uint8_t one_byte[] = {0x9a};
|
||||
decoder.decode(one_byte, sizeof(one_byte));
|
||||
expect_nothing(decoder);
|
||||
const uint8_t unknown[] = {0x9a, 0x42, 0, RESPONSE_ACK, 0, 0, 0, 0};
|
||||
decoder.decode(unknown, sizeof(unknown));
|
||||
expect_nothing(decoder);
|
||||
}
|
||||
|
||||
} // namespace esphome::am43::testing
|
||||
@@ -1,6 +0,0 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# The driver lives in the sensor platform, so pull in what it builds against.
|
||||
manifest.dependencies = manifest.dependencies + ["i2c", "sensor"]
|
||||
@@ -1,57 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "esphome/components/as7341/as7341.h"
|
||||
|
||||
namespace esphome::as7341::testing {
|
||||
|
||||
static constexpr uint8_t ADDRESS = 0x39;
|
||||
static constexpr uint8_t SMUXEN = 1 << 4;
|
||||
static constexpr uint8_t AVALID = 1 << 6;
|
||||
static constexpr uint8_t NUM_ADC_CHANNELS = 6;
|
||||
|
||||
// The chip's registers behind an address pointer that moves on after every byte.
|
||||
// SMUX commands finish at once and spectral data is always valid.
|
||||
class RegisterBus final : public i2c::I2CBus {
|
||||
public:
|
||||
i2c::ErrorCode write_readv(uint8_t address, const uint8_t *data, size_t size, uint8_t *out,
|
||||
size_t out_size) override {
|
||||
EXPECT_EQ(address, ADDRESS);
|
||||
if (size != 0) {
|
||||
this->pointer_ = data[0];
|
||||
for (size_t i = 1; i < size; i++)
|
||||
this->registers[this->pointer_++] = data[i];
|
||||
}
|
||||
this->registers[AS7341_ENABLE] &= ~SMUXEN;
|
||||
for (size_t i = 0; i < out_size; i++)
|
||||
out[i] = this->registers[this->pointer_++];
|
||||
return i2c::ERROR_OK;
|
||||
}
|
||||
|
||||
std::array<uint8_t, 256> registers{};
|
||||
|
||||
protected:
|
||||
uint8_t pointer_{0};
|
||||
};
|
||||
|
||||
TEST(AS7341Test, ReadChannelsDecodesLowByteFirst) {
|
||||
RegisterBus bus;
|
||||
AS7341Component sensor;
|
||||
sensor.set_i2c_bus(&bus);
|
||||
sensor.set_i2c_address(ADDRESS);
|
||||
bus.registers[AS7341_STATUS2] = AVALID;
|
||||
|
||||
// CHn_DATA_L holds the low byte and CHn_DATA_H, one address up, the high byte.
|
||||
for (uint8_t channel = 0; channel < NUM_ADC_CHANNELS; channel++) {
|
||||
bus.registers[AS7341_CH0_DATA_L + 2 * channel] = 0x34 + channel;
|
||||
bus.registers[AS7341_CH0_DATA_H + 2 * channel] = 0x12;
|
||||
}
|
||||
|
||||
std::array<uint16_t, 2 * NUM_ADC_CHANNELS> data{};
|
||||
ASSERT_TRUE(sensor.read_channels(data.data()));
|
||||
for (size_t i = 0; i < data.size(); i++)
|
||||
EXPECT_EQ(data[i], 0x1234 + i % NUM_ADC_CHANNELS) << "value " << unsigned(i);
|
||||
}
|
||||
|
||||
} // namespace esphome::as7341::testing
|
||||
@@ -17,6 +17,10 @@ text_sensor:
|
||||
- platform: ethernet_info
|
||||
ip_address:
|
||||
name: IP Address
|
||||
address_0:
|
||||
name: IP Address 0
|
||||
address_1:
|
||||
name: IP Address 1
|
||||
dns_address:
|
||||
name: DNS Address
|
||||
mac_address:
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# The send action tests read the response body, which the real to_code enables for
|
||||
# configs that capture it.
|
||||
async def to_code_testing(config):
|
||||
cg.add_define("USE_HTTP_REQUEST_RESPONSE")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -87,7 +87,6 @@ esphome:
|
||||
http_request:
|
||||
useragent: esphome/tagreader
|
||||
timeout: 10s
|
||||
max_concurrent_requests: 2
|
||||
verify_ssl: ${verify_ssl}
|
||||
|
||||
output:
|
||||
|
||||
@@ -1,612 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/http_request/http_request.h"
|
||||
#include "esphome/core/base_automation.h"
|
||||
|
||||
namespace esphome::http_request::testing {
|
||||
|
||||
// A response that stays pending for a number of polls, then serves its body in fixed chunks
|
||||
class FakeContainer : public HttpContainer {
|
||||
public:
|
||||
HttpRequestProgress poll() override {
|
||||
this->polls++;
|
||||
if (this->poll_delay_ms > 0)
|
||||
delay(this->poll_delay_ms);
|
||||
if (this->pending_polls > 0) {
|
||||
this->pending_polls--;
|
||||
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_PENDING;
|
||||
}
|
||||
return this->result;
|
||||
}
|
||||
int read(uint8_t *buf, size_t max_len) override { return this->read_available(buf, max_len); }
|
||||
int read_available(uint8_t *buf, size_t max_len) override {
|
||||
if (this->stall)
|
||||
return 0;
|
||||
if (this->bytes_read_ >= this->fail_at)
|
||||
return -1;
|
||||
size_t len = std::min({max_len, this->chunk, this->body.size() - this->bytes_read_});
|
||||
std::memcpy(buf, this->body.data() + this->bytes_read_, len);
|
||||
this->bytes_read_ += len;
|
||||
return static_cast<int>(len);
|
||||
}
|
||||
void end() override { this->ends++; }
|
||||
|
||||
int pending_polls{0};
|
||||
// Time each poll() blocks, like the first connect on ESP-IDF
|
||||
uint32_t poll_delay_ms{0};
|
||||
HttpRequestProgress result{HttpRequestProgress::HTTP_REQUEST_PROGRESS_DONE};
|
||||
std::string body;
|
||||
size_t chunk{512};
|
||||
bool stall{false};
|
||||
// Reads fail once this many bytes have been read
|
||||
size_t fail_at{SIZE_MAX};
|
||||
int polls{0};
|
||||
int ends{0};
|
||||
};
|
||||
|
||||
class FakeHttpRequest : public HttpRequestComponent {
|
||||
public:
|
||||
std::shared_ptr<HttpContainer> begin(const char *url, const char *method, std::string &&body,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
uint32_t timeout_ms) override {
|
||||
this->begins++;
|
||||
if (this->refuse)
|
||||
return nullptr;
|
||||
auto container = std::make_shared<FakeContainer>();
|
||||
container->status_code = 200;
|
||||
container->pending_polls = this->pending_polls;
|
||||
container->poll_delay_ms = this->poll_delay_ms;
|
||||
container->body = this->response_body;
|
||||
container->content_length = this->response_body.size();
|
||||
this->containers.push_back(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
int begins{0};
|
||||
bool refuse{false};
|
||||
int pending_polls{2};
|
||||
uint32_t poll_delay_ms{0};
|
||||
std::string response_body;
|
||||
std::vector<std::shared_ptr<FakeContainer>> containers;
|
||||
|
||||
protected:
|
||||
std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
|
||||
const std::vector<Header> &request_headers,
|
||||
const std::vector<std::string> &lower_case_collect_headers,
|
||||
uint32_t timeout_ms) override {
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
// The URL lambda must be stateless, so the stop it makes is set up through this
|
||||
struct UrlStop {
|
||||
Automation<> *target{nullptr};
|
||||
int calls{0};
|
||||
int stop_at_call{0};
|
||||
};
|
||||
|
||||
UrlStop &url_stop() {
|
||||
static UrlStop state;
|
||||
return state;
|
||||
}
|
||||
|
||||
const char *url_that_can_stop() {
|
||||
UrlStop &state = url_stop();
|
||||
if (++state.calls == state.stop_at_call)
|
||||
state.target->stop();
|
||||
return "http://example.invalid/";
|
||||
}
|
||||
|
||||
class CountAction : public Action<> {
|
||||
public:
|
||||
explicit CountAction(int *count) : count_(count) {}
|
||||
|
||||
protected:
|
||||
void play() override { (*this->count_)++; }
|
||||
int *count_;
|
||||
};
|
||||
|
||||
class HttpRequestSendActionTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
this->action_.set_url([]() -> std::string { return "http://example.invalid/"; });
|
||||
this->action_.set_method([]() -> const char * { return "GET"; });
|
||||
this->action_.set_max_response_buffer_size(1024);
|
||||
this->automation_.add_actions({&this->action_, &this->next_});
|
||||
url_stop() = UrlStop{&this->automation_};
|
||||
}
|
||||
|
||||
// One pass of the main loop
|
||||
void loop_() {
|
||||
LoopBlockingGuard guard{nullptr, nullptr, millis()};
|
||||
this->http_.loop();
|
||||
}
|
||||
|
||||
void run_until_idle_() {
|
||||
for (int i = 0; i != 100 && this->automation_.is_running(); i++)
|
||||
this->loop_();
|
||||
}
|
||||
|
||||
void capture_response_() {
|
||||
this->action_.set_capture_response([]() { return true; });
|
||||
this->with_body_.add_action(&this->store_body_);
|
||||
}
|
||||
|
||||
FakeHttpRequest http_;
|
||||
HttpRequestSendAction<> action_{&this->http_};
|
||||
int next_count_{0};
|
||||
CountAction next_{&this->next_count_};
|
||||
Trigger<> trigger_;
|
||||
Automation<> automation_{&this->trigger_};
|
||||
|
||||
int responses_{0};
|
||||
int errors_{0};
|
||||
std::string body_;
|
||||
Automation<std::shared_ptr<HttpContainer>> on_response_{this->action_.get_success_trigger()};
|
||||
LambdaAction<std::shared_ptr<HttpContainer>> count_response_{
|
||||
[this](const std::shared_ptr<HttpContainer> &) { this->responses_++; }};
|
||||
Automation<> on_error_{this->action_.get_error_trigger()};
|
||||
CountAction count_error_{&this->errors_};
|
||||
Automation<std::shared_ptr<HttpContainer>, std::string &> with_body_{
|
||||
this->action_.get_success_trigger_with_response()};
|
||||
LambdaAction<std::shared_ptr<HttpContainer>, std::string &> store_body_{
|
||||
[this](const std::shared_ptr<HttpContainer> &, std::string &body) {
|
||||
this->responses_++;
|
||||
this->body_ = body;
|
||||
}};
|
||||
|
||||
public:
|
||||
HttpRequestSendActionTest() {
|
||||
this->on_response_.add_action(&this->count_response_);
|
||||
this->on_error_.add_action(&this->count_error_);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, ChainWaitsForTheResponse) {
|
||||
this->trigger_.trigger();
|
||||
EXPECT_EQ(this->http_.begins, 1);
|
||||
EXPECT_TRUE(this->automation_.is_running());
|
||||
EXPECT_EQ(this->next_count_, 0);
|
||||
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->next_count_, 0);
|
||||
EXPECT_EQ(this->responses_, 0);
|
||||
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
EXPECT_EQ(this->http_.containers[0]->ends, 1);
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, RequestThatCannotStartFiresOnErrorAtOnce) {
|
||||
this->http_.refuse = true;
|
||||
this->trigger_.trigger();
|
||||
EXPECT_EQ(this->errors_, 1);
|
||||
EXPECT_EQ(this->responses_, 0);
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, FailedRequestFiresOnError) {
|
||||
this->trigger_.trigger();
|
||||
this->http_.containers[0]->result = HttpRequestProgress::HTTP_REQUEST_PROGRESS_FAILED;
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->errors_, 1);
|
||||
EXPECT_EQ(this->responses_, 0);
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
EXPECT_EQ(this->http_.containers[0]->ends, 1);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, SecondTriggerWaitsForTheFirstRequest) {
|
||||
this->trigger_.trigger();
|
||||
this->trigger_.trigger();
|
||||
EXPECT_EQ(this->http_.begins, 1);
|
||||
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->http_.begins, 1);
|
||||
|
||||
// The first finishes, and the second starts on the next pass
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->http_.begins, 1);
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->http_.begins, 2);
|
||||
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 2);
|
||||
EXPECT_EQ(this->next_count_, 2);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, TriggersBeyondTheQueueAreDropped) {
|
||||
for (int i = 0; i != 7; i++)
|
||||
this->trigger_.trigger();
|
||||
// One running, four waiting, two dropped with on_error
|
||||
EXPECT_EQ(this->http_.begins, 1);
|
||||
EXPECT_EQ(this->errors_, 2);
|
||||
EXPECT_EQ(this->next_count_, 2);
|
||||
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->http_.begins, 5);
|
||||
EXPECT_EQ(this->responses_, 5);
|
||||
EXPECT_EQ(this->next_count_, 7);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, WaitingRequestThatCannotStartFiresOnError) {
|
||||
this->trigger_.trigger();
|
||||
this->trigger_.trigger();
|
||||
this->http_.refuse = true;
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->errors_, 1);
|
||||
EXPECT_EQ(this->next_count_, 2);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, StopEndsRequestsWithoutTriggers) {
|
||||
this->trigger_.trigger();
|
||||
this->trigger_.trigger();
|
||||
this->automation_.stop();
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
EXPECT_EQ(this->http_.containers[0]->ends, 1);
|
||||
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->http_.begins, 1);
|
||||
EXPECT_EQ(this->responses_, 0);
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
EXPECT_EQ(this->next_count_, 0);
|
||||
|
||||
// The action is usable again after a stop
|
||||
this->trigger_.trigger();
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, StopFromOnResponseDropsTheRest) {
|
||||
LambdaAction<std::shared_ptr<HttpContainer>> stop{
|
||||
[this](const std::shared_ptr<HttpContainer> &) { this->automation_.stop(); }};
|
||||
this->on_response_.add_action(&stop);
|
||||
this->trigger_.trigger();
|
||||
this->trigger_.trigger();
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->http_.begins, 1);
|
||||
EXPECT_EQ(this->next_count_, 0);
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
}
|
||||
|
||||
// A script in mode restart that executes itself from on_response
|
||||
TEST_F(HttpRequestSendActionTest, RestartFromOnResponseContinuesOnlyTheNewRun) {
|
||||
int restarts = 0;
|
||||
LambdaAction<std::shared_ptr<HttpContainer>> restart{[this, &restarts](const std::shared_ptr<HttpContainer> &) {
|
||||
if (restarts++ == 0) {
|
||||
this->automation_.stop();
|
||||
this->trigger_.trigger();
|
||||
}
|
||||
}};
|
||||
this->on_response_.add_action(&restart);
|
||||
this->trigger_.trigger();
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->http_.begins, 2);
|
||||
EXPECT_EQ(this->http_.containers[0]->ends, 1);
|
||||
EXPECT_EQ(this->next_count_, 0);
|
||||
EXPECT_TRUE(this->automation_.is_running());
|
||||
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 2);
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, RestartFromOnErrorContinuesOnlyTheNewRun) {
|
||||
LambdaAction<> restart{[this]() {
|
||||
if (this->http_.refuse) {
|
||||
this->http_.refuse = false;
|
||||
this->automation_.stop();
|
||||
this->trigger_.trigger();
|
||||
}
|
||||
}};
|
||||
this->on_error_.add_action(&restart);
|
||||
this->http_.refuse = true;
|
||||
this->trigger_.trigger();
|
||||
EXPECT_EQ(this->errors_, 1);
|
||||
EXPECT_EQ(this->http_.begins, 2);
|
||||
EXPECT_EQ(this->next_count_, 0);
|
||||
EXPECT_TRUE(this->automation_.is_running());
|
||||
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, TriggerFromOnResponseStartsAtOnce) {
|
||||
int retriggers = 0;
|
||||
LambdaAction<std::shared_ptr<HttpContainer>> again{[this, &retriggers](const std::shared_ptr<HttpContainer> &) {
|
||||
if (retriggers++ == 0)
|
||||
this->trigger_.trigger();
|
||||
}};
|
||||
this->on_response_.add_action(&again);
|
||||
this->trigger_.trigger();
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->http_.begins, 2);
|
||||
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 2);
|
||||
EXPECT_EQ(this->next_count_, 2);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, CapturedBodyIsReadAcrossPasses) {
|
||||
this->capture_response_();
|
||||
this->http_.pending_polls = 0;
|
||||
this->http_.response_body = std::string(900, 'a');
|
||||
this->trigger_.trigger();
|
||||
this->http_.containers[0]->chunk = 100;
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->body_, std::string(900, 'a'));
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, CapturedBodyStopsAtTheBufferSize) {
|
||||
this->capture_response_();
|
||||
this->action_.set_max_response_buffer_size(300);
|
||||
this->http_.response_body = std::string(1000, 'b');
|
||||
this->trigger_.trigger();
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->body_, std::string(300, 'b'));
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, StalledBodyEndsAtTheTimeout) {
|
||||
this->capture_response_();
|
||||
this->action_.set_timeout([]() -> uint32_t { return 20; });
|
||||
this->http_.response_body = std::string(1000, 'c');
|
||||
this->trigger_.trigger();
|
||||
this->http_.containers[0]->stall = true;
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
EXPECT_TRUE(this->automation_.is_running());
|
||||
|
||||
delay(30);
|
||||
this->loop_();
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_TRUE(this->body_.empty());
|
||||
}
|
||||
|
||||
// A url lambda that stops the action while the request is being started
|
||||
TEST_F(HttpRequestSendActionTest, StopFromLambdaWhileStartingEndsTheRequest) {
|
||||
this->action_.set_url([]() -> std::string { return url_that_can_stop(); });
|
||||
url_stop().stop_at_call = 1;
|
||||
this->trigger_.trigger();
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
ASSERT_EQ(this->http_.begins, 1);
|
||||
EXPECT_EQ(this->http_.containers[0]->ends, 1);
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 0);
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
EXPECT_EQ(this->next_count_, 0);
|
||||
|
||||
this->trigger_.trigger();
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, StopFromLambdaOfAWaitingRequestEndsIt) {
|
||||
this->action_.set_url([]() -> std::string { return url_that_can_stop(); });
|
||||
url_stop().stop_at_call = 2;
|
||||
this->trigger_.trigger();
|
||||
this->trigger_.trigger();
|
||||
this->run_until_idle_();
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
ASSERT_EQ(this->http_.begins, 2);
|
||||
EXPECT_EQ(this->http_.containers[1]->polls, 0);
|
||||
EXPECT_EQ(this->http_.containers[1]->ends, 1);
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
|
||||
// The slot counts survived the stop, so a new trigger starts at once
|
||||
this->trigger_.trigger();
|
||||
EXPECT_EQ(this->http_.begins, 3);
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 2);
|
||||
EXPECT_EQ(this->next_count_, 2);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, ReadErrorKeepsTheBodyReadSoFar) {
|
||||
this->capture_response_();
|
||||
this->http_.response_body = std::string(800, 'd');
|
||||
this->trigger_.trigger();
|
||||
this->http_.containers[0]->chunk = 100;
|
||||
this->http_.containers[0]->fail_at = 300;
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
EXPECT_EQ(this->body_, std::string(300, 'd'));
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, BufferThatCannotBeAllocatedGivesAnEmptyBody) {
|
||||
this->capture_response_();
|
||||
this->action_.set_max_response_buffer_size(SIZE_MAX);
|
||||
this->http_.response_body = "unread";
|
||||
this->body_ = "not set";
|
||||
this->trigger_.trigger();
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_TRUE(this->body_.empty());
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
}
|
||||
|
||||
// A forced reboot calls only on_shutdown(), which finishes the requests in progress
|
||||
TEST_F(HttpRequestSendActionTest, ShutdownFinishesTheRequestInProgress) {
|
||||
this->trigger_.trigger();
|
||||
this->trigger_.trigger();
|
||||
EXPECT_EQ(this->responses_, 0);
|
||||
this->http_.on_shutdown();
|
||||
EXPECT_EQ(this->responses_, 2);
|
||||
EXPECT_EQ(this->next_count_, 2);
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
}
|
||||
|
||||
// An on_shutdown automation that runs after the component's on_shutdown()
|
||||
TEST_F(HttpRequestSendActionTest, RequestAfterShutdownFinishesAtOnce) {
|
||||
this->http_.on_shutdown();
|
||||
this->trigger_.trigger();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, BodyTimeoutStartsAfterABlockingPoll) {
|
||||
this->capture_response_();
|
||||
this->action_.set_timeout([]() -> uint32_t { return 20; });
|
||||
this->http_.pending_polls = 0;
|
||||
this->http_.poll_delay_ms = 30;
|
||||
this->http_.response_body = std::string(100, 'e');
|
||||
this->trigger_.trigger();
|
||||
this->http_.containers[0]->stall = true;
|
||||
this->loop_();
|
||||
EXPECT_TRUE(this->automation_.is_running());
|
||||
|
||||
this->http_.containers[0]->stall = false;
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->body_, std::string(100, 'e'));
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, DurationCoversTheWholeBody) {
|
||||
this->capture_response_();
|
||||
this->http_.pending_polls = 0;
|
||||
this->http_.response_body = std::string(100, 'f');
|
||||
this->trigger_.trigger();
|
||||
auto container = this->http_.containers[0];
|
||||
container->duration_ms = 1;
|
||||
container->stall = true;
|
||||
this->loop_();
|
||||
delay(30);
|
||||
container->stall = false;
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_GE(container->duration_ms, 30u);
|
||||
}
|
||||
|
||||
// The limit is per http_request component, so that one client is held at a time
|
||||
TEST_F(HttpRequestSendActionTest, TwoActionsTakeTurns) {
|
||||
HttpRequestSendAction<> other{&this->http_};
|
||||
other.set_url([]() -> std::string { return "http://example.invalid/other"; });
|
||||
other.set_method([]() -> const char * { return "GET"; });
|
||||
int other_next = 0;
|
||||
CountAction count_other{&other_next};
|
||||
Trigger<> other_trigger;
|
||||
Automation<> other_automation{&other_trigger};
|
||||
other_automation.add_actions({&other, &count_other});
|
||||
|
||||
this->trigger_.trigger();
|
||||
other_trigger.trigger();
|
||||
EXPECT_EQ(this->http_.begins, 1);
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
EXPECT_EQ(other_next, 0);
|
||||
EXPECT_EQ(this->http_.begins, 1);
|
||||
|
||||
for (int i = 0; i != 10 && other_automation.is_running(); i++)
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->http_.begins, 2);
|
||||
EXPECT_EQ(other_next, 1);
|
||||
}
|
||||
|
||||
// Stopping one action leaves the requests of another alone
|
||||
TEST_F(HttpRequestSendActionTest, StopEndsOnlyThatActionsRequests) {
|
||||
HttpRequestSendAction<> other{&this->http_};
|
||||
other.set_url([]() -> std::string { return "http://example.invalid/other"; });
|
||||
other.set_method([]() -> const char * { return "GET"; });
|
||||
int other_next = 0;
|
||||
CountAction count_other{&other_next};
|
||||
Trigger<> other_trigger;
|
||||
Automation<> other_automation{&other_trigger};
|
||||
other_automation.add_actions({&other, &count_other});
|
||||
|
||||
other_trigger.trigger();
|
||||
this->trigger_.trigger();
|
||||
this->automation_.stop();
|
||||
for (int i = 0; i != 10 && other_automation.is_running(); i++)
|
||||
this->loop_();
|
||||
EXPECT_EQ(other_next, 1);
|
||||
EXPECT_EQ(this->next_count_, 0);
|
||||
EXPECT_EQ(this->http_.begins, 1);
|
||||
}
|
||||
|
||||
// A safe shutdown drops the requests already made, so they do not hold up the reboot,
|
||||
// and still sends the ones that on_shutdown automations make
|
||||
TEST_F(HttpRequestSendActionTest, SafeShutdownSendsOnlyTheShutdownRequests) {
|
||||
this->trigger_.trigger();
|
||||
this->trigger_.trigger();
|
||||
this->http_.on_safe_shutdown();
|
||||
EXPECT_EQ(this->http_.containers[0]->ends, 1);
|
||||
EXPECT_EQ(this->responses_, 0);
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
// Stopped like script.stop, so a mode single script could run it again from on_shutdown
|
||||
EXPECT_FALSE(this->automation_.is_running());
|
||||
|
||||
this->trigger_.trigger();
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
this->http_.on_shutdown();
|
||||
EXPECT_EQ(this->http_.begins, 2);
|
||||
EXPECT_EQ(this->responses_, 1);
|
||||
EXPECT_EQ(this->next_count_, 1);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, MaxConcurrentRequestsRunTogether) {
|
||||
this->http_.set_max_concurrent_requests(2);
|
||||
this->trigger_.trigger();
|
||||
this->trigger_.trigger();
|
||||
this->trigger_.trigger();
|
||||
EXPECT_EQ(this->http_.begins, 2);
|
||||
|
||||
// Both finish on the third pass; the third request takes a free slot on the next
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->responses_, 2);
|
||||
EXPECT_EQ(this->http_.begins, 2);
|
||||
this->loop_();
|
||||
EXPECT_EQ(this->http_.begins, 3);
|
||||
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 3);
|
||||
EXPECT_EQ(this->next_count_, 3);
|
||||
}
|
||||
|
||||
TEST_F(HttpRequestSendActionTest, MaxConcurrentRequestsKeepsTheQueueLimit) {
|
||||
this->http_.set_max_concurrent_requests(2);
|
||||
for (int i = 0; i != 8; i++)
|
||||
this->trigger_.trigger();
|
||||
// Two running, four waiting, two dropped
|
||||
EXPECT_EQ(this->http_.begins, 2);
|
||||
EXPECT_EQ(this->errors_, 2);
|
||||
this->run_until_idle_();
|
||||
EXPECT_EQ(this->responses_, 6);
|
||||
EXPECT_EQ(this->next_count_, 8);
|
||||
}
|
||||
|
||||
} // namespace esphome::http_request::testing
|
||||
@@ -12,10 +12,8 @@
|
||||
|
||||
#include "esphome/components/http_request/http_request.h"
|
||||
#include "esphome/components/online_image/online_image.h"
|
||||
#include "esphome/components/runtime_image/png_decoder.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "pngle.h"
|
||||
|
||||
namespace esphome::online_image::testing {
|
||||
|
||||
@@ -137,153 +135,6 @@ static constexpr std::array<uint8_t, 69> PNG_1X1{
|
||||
0x0C, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9C, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00, 0xC9,
|
||||
0xFE, 0x92, 0xEF, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82};
|
||||
|
||||
// 800x600 RGB PNG of a single color, 2312 bytes: the decoder needs several calls to draw it.
|
||||
// The same bytes are in tests/components/runtime_image/test_png_time_limit.cpp.
|
||||
static const uint8_t PNG_SOLID[] = {
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x03,
|
||||
0x20, 0x00, 0x00, 0x02, 0x58, 0x08, 0x02, 0x00, 0x00, 0x00, 0x15, 0x14, 0x15, 0x27, 0x00, 0x00, 0x08, 0xCF, 0x49,
|
||||
0x44, 0x41, 0x54, 0x78, 0xDA, 0xED, 0xD6, 0x31, 0x11, 0x00, 0x30, 0x08, 0x00, 0xB1, 0x52, 0x21, 0xE8, 0x44, 0x22,
|
||||
0xB2, 0x30, 0xC1, 0x31, 0x25, 0x12, 0x7E, 0xFA, 0xC8, 0xEA, 0x07, 0x00, 0xC0, 0x9E, 0x2F, 0x01, 0x00, 0x80, 0xC1,
|
||||
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
|
||||
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
|
||||
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00,
|
||||
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
|
||||
0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00,
|
||||
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30,
|
||||
0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58,
|
||||
0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
|
||||
0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00,
|
||||
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
|
||||
0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
|
||||
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
|
||||
0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
|
||||
0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
|
||||
0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01,
|
||||
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
|
||||
0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
|
||||
0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83,
|
||||
0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
|
||||
0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
|
||||
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
|
||||
0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00,
|
||||
0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
|
||||
0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
|
||||
0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C,
|
||||
0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
|
||||
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
|
||||
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
|
||||
0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00,
|
||||
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
|
||||
0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00,
|
||||
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30,
|
||||
0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
|
||||
0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01,
|
||||
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
|
||||
0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00,
|
||||
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
|
||||
0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
|
||||
0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83,
|
||||
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
|
||||
0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05,
|
||||
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
|
||||
0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x83, 0x25, 0x01, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
|
||||
0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C,
|
||||
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
|
||||
0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16,
|
||||
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
|
||||
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00,
|
||||
0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
|
||||
0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
|
||||
0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30,
|
||||
0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
|
||||
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58,
|
||||
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
|
||||
0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00,
|
||||
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
|
||||
0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
|
||||
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0,
|
||||
0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
|
||||
0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01,
|
||||
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
|
||||
0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00,
|
||||
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00,
|
||||
0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
|
||||
0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83,
|
||||
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
|
||||
0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
|
||||
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
|
||||
0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
|
||||
0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C,
|
||||
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
|
||||
0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
|
||||
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
|
||||
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00,
|
||||
0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
|
||||
0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
|
||||
0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30,
|
||||
0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
|
||||
0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58,
|
||||
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
|
||||
0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00,
|
||||
0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
|
||||
0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
|
||||
0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
|
||||
0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x25, 0x01, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
|
||||
0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01,
|
||||
0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00,
|
||||
0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00,
|
||||
0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
|
||||
0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83,
|
||||
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0,
|
||||
0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
|
||||
0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00,
|
||||
0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00,
|
||||
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
|
||||
0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C,
|
||||
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0xDC, 0x19, 0x79, 0xA5, 0x06, 0x0E, 0x33, 0xD4, 0xAF,
|
||||
0xF2, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
||||
};
|
||||
|
||||
// Draws that take a millisecond every fourth row, so decoding PNG_SOLID always outlasts the time limit of one call
|
||||
class SlowDraws : public runtime_image::PngDecoder, public runtime_image::RuntimeImage {
|
||||
public:
|
||||
static void install(runtime_image::RuntimeImage &image) {
|
||||
auto *decoder = static_cast<runtime_image::PngDecoder *>((image.*&SlowDraws::decoder_).get());
|
||||
pngle_set_draw_callback(decoder->*&SlowDraws::pngle_, draw);
|
||||
}
|
||||
|
||||
private:
|
||||
static void draw(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, const uint8_t rgba[4]) {
|
||||
if (x == 0 && y % 4 == 0) {
|
||||
const uint32_t until = millis() + 2;
|
||||
while (millis() < until) {
|
||||
}
|
||||
}
|
||||
static_cast<runtime_image::PngDecoder *>(pngle_get_user_data(pngle))
|
||||
->draw(x, y, w, h, Color(rgba[0], rgba[1], rgba[2], rgba[3]));
|
||||
}
|
||||
};
|
||||
|
||||
// 8x8 progressive JPEG, 521 bytes
|
||||
static const uint8_t JPEG_PROGRESSIVE[] = {
|
||||
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
|
||||
@@ -607,30 +458,6 @@ TEST_F(OnlineImageDownloadEnd, JpegBufferAllocationFailureReportsError) {
|
||||
EXPECT_EQ(this->finished_, 0);
|
||||
}
|
||||
|
||||
// The whole PNG arrives in one read, but the decoder stops after a short time per call: the rest is decoded on
|
||||
// later passes, not reported as data the decoder could not use
|
||||
TEST_F(OnlineImageDownloadEnd, PngDecodedOverSeveralPassesAfterTheDownloadEnds) {
|
||||
for (bool chunked : {false, true}) {
|
||||
this->finished_ = 0;
|
||||
this->errors_ = 0;
|
||||
auto image = this->make_image_(runtime_image::PNG, 4096);
|
||||
auto container = std::make_shared<FakeContainer>(std::vector<uint8_t>(PNG_SOLID, PNG_SOLID + sizeof(PNG_SOLID)),
|
||||
sizeof(PNG_SOLID), chunked);
|
||||
this->start_(*image, container);
|
||||
SlowDraws::install(*image);
|
||||
|
||||
int passes = 0;
|
||||
for (; passes < 1000 && this->finished_ + this->errors_ == 0; passes++)
|
||||
image->loop();
|
||||
|
||||
EXPECT_EQ(this->finished_, 1) << (chunked ? "chunked" : "with Content-Length");
|
||||
EXPECT_EQ(this->errors_, 0);
|
||||
EXPECT_GT(passes, 2);
|
||||
EXPECT_EQ(image->get_width(), 800);
|
||||
EXPECT_EQ(image->get_pixel(400, 300).b, 200);
|
||||
}
|
||||
}
|
||||
|
||||
// The JPEG decoded with all of it in memory at once, to compare the downloaded images with
|
||||
static std::vector<uint8_t> reference_jpeg_pixels() {
|
||||
runtime_image::RuntimeImage ref(runtime_image::JPEG, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/runtime_image/qoi_decoder.h"
|
||||
#include "esphome/components/runtime_image/runtime_image.h"
|
||||
|
||||
namespace esphome::runtime_image::testing {
|
||||
|
||||
// Big enough that decoding it takes well over DECODE_TIME_LIMIT_MS, little enough to build quickly
|
||||
static constexpr uint32_t WIDTH = 4096;
|
||||
static constexpr uint32_t HEIGHT = 2048;
|
||||
|
||||
static void put_le32(std::vector<uint8_t> &data, size_t offset, uint32_t value) {
|
||||
for (size_t i = 0; i < 4; i++)
|
||||
data[offset + i] = (value >> (8 * i)) & 0xFF;
|
||||
}
|
||||
|
||||
static void put_be32(std::vector<uint8_t> &data, size_t offset, uint32_t value) {
|
||||
for (size_t i = 0; i < 4; i++)
|
||||
data[offset + i] = (value >> (24 - 8 * i)) & 0xFF;
|
||||
}
|
||||
|
||||
// A 1-bit BMP with the left half of every row set: eight pixels for each byte read
|
||||
static std::vector<uint8_t> make_mono_bmp() {
|
||||
constexpr size_t data_offset = 14 + 40 + 2 * 4;
|
||||
constexpr size_t row_bytes = WIDTH / 8;
|
||||
std::vector<uint8_t> bmp(data_offset + row_bytes * HEIGHT, 0);
|
||||
bmp[0] = 'B';
|
||||
bmp[1] = 'M';
|
||||
put_le32(bmp, 2, bmp.size());
|
||||
put_le32(bmp, 10, data_offset);
|
||||
put_le32(bmp, 14, 40); // DIB header size
|
||||
put_le32(bmp, 18, WIDTH);
|
||||
put_le32(bmp, 22, HEIGHT);
|
||||
bmp[26] = 1; // color planes
|
||||
bmp[28] = 1; // bits per pixel
|
||||
for (size_t row = 0; row < HEIGHT; row++) {
|
||||
for (size_t i = 0; i < row_bytes / 2; i++)
|
||||
bmp[data_offset + row * row_bytes + i] = 0xFF;
|
||||
}
|
||||
return bmp;
|
||||
}
|
||||
|
||||
// A QOI image of one color: an RGB chunk, then runs of 62 pixels, each run a single byte
|
||||
static std::vector<uint8_t> make_solid_qoi() {
|
||||
std::vector<uint8_t> qoi(14, 0);
|
||||
qoi[0] = 'q';
|
||||
qoi[1] = 'o';
|
||||
qoi[2] = 'i';
|
||||
qoi[3] = 'f';
|
||||
put_be32(qoi, 4, WIDTH);
|
||||
put_be32(qoi, 8, HEIGHT);
|
||||
qoi[12] = 3; // channels
|
||||
qoi.insert(qoi.end(), {0xFE, 30, 120, 200});
|
||||
for (size_t left = WIDTH * HEIGHT - 1; left > 0;) {
|
||||
const size_t run = std::min<size_t>(left, 62);
|
||||
qoi.push_back(0xC0 | (run - 1));
|
||||
left -= run;
|
||||
}
|
||||
qoi.insert(qoi.end(), {0, 0, 0, 0, 0, 0, 0, 1});
|
||||
return qoi;
|
||||
}
|
||||
|
||||
// Feed the whole image as often as it takes, and return how many calls that was
|
||||
static int decode_in_calls(RuntimeImage &img, std::vector<uint8_t> &data) {
|
||||
EXPECT_TRUE(img.begin_decode(data.size()));
|
||||
size_t offset = 0;
|
||||
int calls = 0;
|
||||
while (offset < data.size() && calls < 10000) {
|
||||
const int consumed = img.feed_data(data.data() + offset, data.size() - offset);
|
||||
EXPECT_GT(consumed, 0);
|
||||
if (consumed <= 0)
|
||||
break;
|
||||
offset += consumed;
|
||||
calls++;
|
||||
}
|
||||
EXPECT_EQ(offset, data.size());
|
||||
EXPECT_TRUE(img.end_decode());
|
||||
return calls;
|
||||
}
|
||||
|
||||
TEST(DecodeTimeLimit, BmpIsDecodedOverSeveralCalls) {
|
||||
RuntimeImage img(BMP, image::IMAGE_TYPE_GRAYSCALE, image::TRANSPARENCY_OPAQUE);
|
||||
auto data = make_mono_bmp();
|
||||
EXPECT_GT(decode_in_calls(img, data), 1) << "the whole image was decoded in one call";
|
||||
ASSERT_EQ(img.get_width(), static_cast<int>(WIDTH));
|
||||
ASSERT_EQ(img.get_height(), static_cast<int>(HEIGHT));
|
||||
for (auto [col, row] : {std::pair{0, 0}, std::pair{1000, 1000}, std::pair{4095, 2047}}) {
|
||||
const uint8_t expected = col < static_cast<int>(WIDTH / 2) ? 255 : 0;
|
||||
EXPECT_EQ(img.get_pixel(col, row).r, expected) << col << "," << row;
|
||||
}
|
||||
}
|
||||
|
||||
// Bytes after the pixel data are used up, not drawn; here they would be colour indexes past the end of the table
|
||||
TEST(DecodeTimeLimit, BmpIgnoresBytesAfterThePixels) {
|
||||
RuntimeImage img(BMP, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
constexpr size_t data_offset = 14 + 40 + 2 * 4;
|
||||
std::vector<uint8_t> data(data_offset, 0);
|
||||
data[0] = 'B';
|
||||
data[1] = 'M';
|
||||
put_le32(data, 10, data_offset);
|
||||
put_le32(data, 14, 40); // DIB header size
|
||||
put_le32(data, 18, 2); // width
|
||||
put_le32(data, 22, 1); // height
|
||||
data[26] = 1; // color planes
|
||||
data[28] = 8; // bits per pixel
|
||||
data[46] = 2; // colors in the table
|
||||
put_le32(data, 54, 0xFF0000);
|
||||
put_le32(data, 58, 0x0000FF);
|
||||
data.insert(data.end(), {0, 1, 0, 0, 0xFF, 0xFF, 0xFF, 0xFF});
|
||||
put_le32(data, 2, data.size());
|
||||
|
||||
ASSERT_TRUE(img.begin_decode(data.size()));
|
||||
EXPECT_EQ(img.feed_data(data.data(), data.size()), static_cast<int>(data.size()));
|
||||
ASSERT_TRUE(img.end_decode());
|
||||
EXPECT_EQ(img.get_pixel(0, 0).r, 255);
|
||||
EXPECT_EQ(img.get_pixel(1, 0).b, 255);
|
||||
}
|
||||
|
||||
TEST(DecodeTimeLimit, QoiIsDecodedOverSeveralCalls) {
|
||||
RuntimeImage img(QOI, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
auto data = make_solid_qoi();
|
||||
EXPECT_GT(decode_in_calls(img, data), 1) << "the whole image was decoded in one call";
|
||||
for (auto [col, row] : {std::pair{0, 0}, std::pair{2000, 1000}, std::pair{4095, 2047}}) {
|
||||
const Color pixel = img.get_pixel(col, row);
|
||||
EXPECT_EQ(pixel.r, 30);
|
||||
EXPECT_EQ(pixel.g, 120);
|
||||
EXPECT_EQ(pixel.b, 200);
|
||||
}
|
||||
}
|
||||
|
||||
// A chunk split across two calls: the bytes used before the split count towards the decoded total
|
||||
TEST(DecodeTimeLimit, QoiCountsTheBytesBeforeASplitChunk) {
|
||||
RuntimeImage img(QOI, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
std::vector<uint8_t> data = {'q', 'o', 'i', 'f', 0, 0, 0, 2, 0, 0, 0, 1, 3, 0};
|
||||
data.insert(data.end(), {0xFE, 10, 20, 30, 0xFE, 40, 50, 60, 0, 0, 0, 0, 0, 0, 0, 1});
|
||||
QoiDecoder decoder(&img);
|
||||
ASSERT_EQ(decoder.prepare(data.size()), 0);
|
||||
|
||||
// The header and the first chunk, and half of the second
|
||||
EXPECT_EQ(decoder.decode(data.data(), 20), 18);
|
||||
EXPECT_EQ(decoder.get_decoded_bytes(), 18u);
|
||||
EXPECT_EQ(decoder.decode(data.data() + 18, data.size() - 18), static_cast<int>(data.size() - 18));
|
||||
EXPECT_EQ(decoder.get_decoded_bytes(), data.size());
|
||||
EXPECT_TRUE(decoder.is_finished());
|
||||
}
|
||||
|
||||
} // namespace esphome::runtime_image::testing
|
||||
@@ -5,11 +5,9 @@
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
#include "esphome/components/runtime_image/image_decoder.h"
|
||||
#include "esphome/components/runtime_image/jpeg_decoder.h"
|
||||
#include "esphome/components/runtime_image/runtime_image.h"
|
||||
@@ -367,23 +365,6 @@ static std::vector<uint8_t> pixel_bytes(TestableRuntimeImage &img) {
|
||||
return std::vector<uint8_t>(start, start + img.get_width_stride() * img.get_height());
|
||||
}
|
||||
|
||||
// Shrinking a PNG keeps the first source pixel of each span, as the JPEG path does: the even rows and columns
|
||||
TEST(RuntimeImageDecoder, ShrunkPngKeepsTheFirstPixelOfEachSpan) {
|
||||
TestableRuntimeImage img(PNG, image::IMAGE_TYPE_RGB, false, 2, 2);
|
||||
ASSERT_TRUE(decode_all(img, PNG_RGB, sizeof(PNG_RGB), PNG));
|
||||
ASSERT_EQ(img.get_width(), 2);
|
||||
ASSERT_EQ(img.get_height(), 2);
|
||||
for (int row = 0; row < 2; row++) {
|
||||
for (int col = 0; col < 2; col++) {
|
||||
const Color pixel = img.get_pixel(col, row);
|
||||
const uint8_t *expected = PNG_RGB_EXPECTED[row * 2][col * 2];
|
||||
EXPECT_EQ(pixel.r, expected[0]) << col << "," << row;
|
||||
EXPECT_EQ(pixel.g, expected[1]) << col << "," << row;
|
||||
EXPECT_EQ(pixel.b, expected[2]) << col << "," << row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RuntimeImageDecoder, JpegDecoderStaysWarmAcrossDecodes) {
|
||||
TestableRuntimeImage img(JPEG);
|
||||
|
||||
@@ -472,37 +453,9 @@ TEST(RuntimeImageDecoder, JpegDecodesToScaledRgb) {
|
||||
}
|
||||
|
||||
// Target pixel index each source pixel paints when `source` pixels map onto `target`, the way
|
||||
// prepare_draw_ rounds: source n covers targets ceil(n * target / source) up to the next start
|
||||
// prepare_rgb565_scaling_ rounds: source n covers targets ceil(n * target / source) up to the next start
|
||||
static uint16_t scaled_boundary(int n, int source, int target) { return (n * target + source - 1) / source; }
|
||||
|
||||
// Every target pixel of a PNG drawn at a size that is not a whole multiple: enlarged with spans 1 or 2 pixels wide, and
|
||||
// shrunk with spans of 0 or 1 pixels
|
||||
TEST(RuntimeImageDecoder, ScaledPngPaintsEachSourcePixelOverItsSpan) {
|
||||
// A fixed size keeps the aspect ratio, so both are square
|
||||
for (auto [width, height] : {std::pair{6, 6}, std::pair{3, 3}}) {
|
||||
TestableRuntimeImage img(PNG, image::IMAGE_TYPE_RGB, false, width, height);
|
||||
ASSERT_TRUE(decode_all(img, PNG_RGB, sizeof(PNG_RGB), PNG));
|
||||
ASSERT_EQ(img.get_width(), width);
|
||||
ASSERT_EQ(img.get_height(), height);
|
||||
int painted = 0;
|
||||
for (int m = 0; m < 4; m++) {
|
||||
for (int n = 0; n < 4; n++) {
|
||||
const uint8_t *expected = PNG_RGB_EXPECTED[m][n];
|
||||
for (int y = scaled_boundary(m, 4, height); y < scaled_boundary(m + 1, 4, height); y++) {
|
||||
for (int x = scaled_boundary(n, 4, width); x < scaled_boundary(n + 1, 4, width); x++) {
|
||||
const Color pixel = img.get_pixel(x, y);
|
||||
EXPECT_EQ(pixel.r, expected[0]) << width << "x" << height << " at " << x << "," << y;
|
||||
EXPECT_EQ(pixel.g, expected[1]) << width << "x" << height << " at " << x << "," << y;
|
||||
EXPECT_EQ(pixel.b, expected[2]) << width << "x" << height << " at " << x << "," << y;
|
||||
painted++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(painted, width * height);
|
||||
}
|
||||
}
|
||||
|
||||
// JPEGDEC decodes at the smallest of full, 1/2, 1/4 and 1/8 size that still covers the buffer
|
||||
TEST(RuntimeImageDecoder, JpegDecodesAtTheSmallestSizeThatCoversTheBuffer) {
|
||||
for (auto [size, scale] :
|
||||
@@ -596,110 +549,4 @@ TEST(RuntimeImageDecoder, SessionFlagsTrackLifecycle) {
|
||||
EXPECT_FALSE(img.is_decode_finished()) << "no session means nothing is 'finished'";
|
||||
}
|
||||
|
||||
// 5x3 QOI whose runs carry on from one row into the next
|
||||
static const uint8_t QOI_RUNS[] = {
|
||||
'q', 'o', 'i', 'f', 0, 0, 0, 5, 0, 0, 0, 3, 3, 0, // header
|
||||
0xFE, 10, 20, 30, // 1 pixel
|
||||
0xC5, // a run of 6
|
||||
0xFE, 200, 100, 50, // 1 pixel
|
||||
0xC6, // a run of 7
|
||||
0, 0, 0, 0, 0, 0, 0, 1, // end marker
|
||||
};
|
||||
|
||||
/// Decode a whole image into img
|
||||
static void decode_into(RuntimeImage &img, ImageFormat format, const uint8_t *data, size_t len) {
|
||||
std::vector<uint8_t> buffer(data, data + len);
|
||||
ASSERT_TRUE(img.begin_decode(len, format));
|
||||
size_t offset = 0;
|
||||
while (offset < len) {
|
||||
const int consumed = img.feed_data(buffer.data() + offset, len - offset);
|
||||
ASSERT_GT(consumed, 0);
|
||||
offset += consumed;
|
||||
}
|
||||
ASSERT_TRUE(img.end_decode());
|
||||
}
|
||||
|
||||
// An opaque RGB565 image is written straight into the buffer. It must hold the same pixels, in the same places, as an
|
||||
// RGB image filled through draw_pixel(), at the image's own size and scaled.
|
||||
TEST(RuntimeImageDecoder, DirectRgb565MatchesTheGenericPath) {
|
||||
const std::tuple<ImageFormat, const uint8_t *, size_t> images[] = {
|
||||
{BMP, BMP_24BPP, sizeof(BMP_24BPP)},
|
||||
{BMP, BMP_8BPP, sizeof(BMP_8BPP)},
|
||||
{PNG, PNG_RGB, sizeof(PNG_RGB)},
|
||||
{QOI, QOI_RUNS, sizeof(QOI_RUNS)},
|
||||
};
|
||||
for (auto [format, data, len] : images) {
|
||||
for (auto [width, height] : {std::pair{0, 0}, std::pair{7, 5}, std::pair{2, 2}, std::pair{9, 2}}) {
|
||||
RuntimeImage reference(format, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE, nullptr, false, width, height);
|
||||
decode_into(reference, format, data, len);
|
||||
for (bool big_endian : {false, true}) {
|
||||
RuntimeImage img(format, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE, nullptr, big_endian, width,
|
||||
height);
|
||||
decode_into(img, format, data, len);
|
||||
ASSERT_EQ(img.get_width(), reference.get_width());
|
||||
ASSERT_EQ(img.get_height(), reference.get_height());
|
||||
const uint8_t *pixels = img.get_data_start();
|
||||
for (int i = 0; i < img.get_width() * img.get_height(); i++) {
|
||||
const Color color = reference.get_pixel(i % img.get_width(), i / img.get_width());
|
||||
const uint16_t expected = display::ColorUtil::color_to_565(color);
|
||||
const uint16_t actual =
|
||||
big_endian ? (pixels[i * 2] << 8 | pixels[i * 2 + 1]) : (pixels[i * 2] | pixels[i * 2 + 1] << 8);
|
||||
EXPECT_EQ(actual, expected) << "image of " << len << " bytes into " << width << "x" << height
|
||||
<< (big_endian ? " big-endian" : " little-endian") << ", pixel " << i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The runs fill the rows they carry on into
|
||||
TEST(RuntimeImageDecoder, QoiRunsCrossRows) {
|
||||
TestableRuntimeImage img(QOI);
|
||||
ASSERT_TRUE(decode_all(img, QOI_RUNS, sizeof(QOI_RUNS)));
|
||||
for (int i = 0; i < 15; i++) {
|
||||
const Color pixel = img.get_pixel(i % 5, i / 5);
|
||||
const bool second = i >= 7;
|
||||
EXPECT_EQ(pixel.r, second ? 200 : 10) << "pixel " << i;
|
||||
EXPECT_EQ(pixel.g, second ? 100 : 20) << "pixel " << i;
|
||||
EXPECT_EQ(pixel.b, second ? 50 : 30) << "pixel " << i;
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads a decoder's protected boundary tables
|
||||
struct BoundaryTables : ImageDecoder {
|
||||
static size_t x_capacity(const ImageDecoder &decoder) {
|
||||
return (decoder.*(&BoundaryTables::x_boundaries_)).capacity();
|
||||
}
|
||||
static size_t y_capacity(const ImageDecoder &decoder) {
|
||||
return (decoder.*(&BoundaryTables::y_boundaries_)).capacity();
|
||||
}
|
||||
static bool size_valid(const ImageDecoder &decoder) { return decoder.*(&BoundaryTables::size_valid_); }
|
||||
static void prepare_draw(ImageDecoder &decoder, int width, int height) {
|
||||
(decoder.*(&BoundaryTables::prepare_draw_))(width, height);
|
||||
}
|
||||
};
|
||||
|
||||
// A JPEG decoded at reduced size builds its tables once, for the decoded size, not first for the full size
|
||||
TEST(RuntimeImageDecoder, JpegTablesAreBuiltForTheDecodedSize) {
|
||||
TestableRuntimeImage img(JPEG, image::IMAGE_TYPE_RGB565, false, 1, 1);
|
||||
ASSERT_TRUE(decode_all(img, JPEG_GRADIENT, sizeof(JPEG_GRADIENT)));
|
||||
ASSERT_EQ(static_cast<JpegDecoder *>(img.decoder())->get_scale(), 8);
|
||||
EXPECT_EQ(BoundaryTables::x_capacity(*img.decoder()), 2u);
|
||||
EXPECT_EQ(BoundaryTables::y_capacity(*img.decoder()), 2u);
|
||||
}
|
||||
|
||||
// Without memory for the tables an RGB565 image is drawn pixel by pixel, rather than failing the decode
|
||||
TEST(RuntimeImageDecoder, DirectDrawFallsBackWhenTablesCannotBeAllocated) {
|
||||
TestableRuntimeImage img(BMP, image::IMAGE_TYPE_RGB565, false, 0, 0);
|
||||
ASSERT_TRUE(img.begin_decode(sizeof(BMP_24BPP)));
|
||||
BoundaryTables::prepare_draw(*img.decoder(), 3, 2);
|
||||
ASSERT_TRUE(img.decoder()->uses_direct_rgb565());
|
||||
// The x table already fits; a y table of SIZE_MAX entries can never be allocated
|
||||
BoundaryTables::prepare_draw(*img.decoder(), 3, -2);
|
||||
EXPECT_FALSE(img.decoder()->uses_direct_rgb565());
|
||||
EXPECT_TRUE(BoundaryTables::size_valid(*img.decoder())) << "draw() would paint nothing";
|
||||
EXPECT_EQ(BoundaryTables::x_capacity(*img.decoder()), 0u) << "the unused x table must be freed";
|
||||
EXPECT_EQ(BoundaryTables::y_capacity(*img.decoder()), 0u);
|
||||
}
|
||||
|
||||
} // namespace esphome::runtime_image::testing
|
||||
@@ -1,35 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "esphome/components/runtime_image/runtime_image.h"
|
||||
|
||||
namespace esphome::runtime_image::testing {
|
||||
|
||||
class GrayscaleImage : public RuntimeImage {
|
||||
public:
|
||||
GrayscaleImage() : RuntimeImage(BMP, image::IMAGE_TYPE_GRAYSCALE, image::TRANSPARENCY_OPAQUE) {}
|
||||
uint8_t pixel(int x) const { return this->buffer_[x]; }
|
||||
};
|
||||
|
||||
// The integer weights stay within one step of the floating point formula they replace, and keep white at 255
|
||||
TEST(GrayscalePixel, MatchesTheFloatingPointWeights) {
|
||||
GrayscaleImage img;
|
||||
ASSERT_GT(img.resize(256, 1), 0);
|
||||
for (int r = 0; r < 256; r += 5) {
|
||||
for (int g = 0; g < 256; g += 5) {
|
||||
for (int b = 0; b < 256; b += 5) {
|
||||
img.draw_pixel(0, 0, Color(r, g, b));
|
||||
const int expected = static_cast<int>(0.2125 * r + 0.7154 * g + 0.0721 * b);
|
||||
ASSERT_LE(std::abs(img.pixel(0) - expected), 1) << "r=" << r << " g=" << g << " b=" << b;
|
||||
}
|
||||
}
|
||||
}
|
||||
img.draw_pixel(1, 0, Color(255, 255, 255));
|
||||
EXPECT_EQ(img.pixel(1), 255);
|
||||
img.draw_pixel(2, 0, Color(0, 0, 0));
|
||||
EXPECT_EQ(img.pixel(2), 0);
|
||||
}
|
||||
|
||||
} // namespace esphome::runtime_image::testing
|
||||
@@ -1,204 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/runtime_image/png_decoder.h"
|
||||
#include "esphome/components/runtime_image/runtime_image.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "pngle.h"
|
||||
|
||||
namespace esphome::runtime_image::testing {
|
||||
|
||||
// 800x600 RGB PNG of a single color (30, 120, 200), 2312 bytes: little data, but many pixels to draw.
|
||||
// The same bytes are in tests/components/online_image/test_download_end.cpp.
|
||||
static const uint8_t PNG_SOLID[] = {
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x03,
|
||||
0x20, 0x00, 0x00, 0x02, 0x58, 0x08, 0x02, 0x00, 0x00, 0x00, 0x15, 0x14, 0x15, 0x27, 0x00, 0x00, 0x08, 0xCF, 0x49,
|
||||
0x44, 0x41, 0x54, 0x78, 0xDA, 0xED, 0xD6, 0x31, 0x11, 0x00, 0x30, 0x08, 0x00, 0xB1, 0x52, 0x21, 0xE8, 0x44, 0x22,
|
||||
0xB2, 0x30, 0xC1, 0x31, 0x25, 0x12, 0x7E, 0xFA, 0xC8, 0xEA, 0x07, 0x00, 0xC0, 0x9E, 0x2F, 0x01, 0x00, 0x80, 0xC1,
|
||||
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
|
||||
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
|
||||
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00,
|
||||
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
|
||||
0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00,
|
||||
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30,
|
||||
0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58,
|
||||
0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
|
||||
0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00,
|
||||
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
|
||||
0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
|
||||
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
|
||||
0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
|
||||
0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
|
||||
0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01,
|
||||
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
|
||||
0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
|
||||
0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83,
|
||||
0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
|
||||
0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
|
||||
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
|
||||
0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00,
|
||||
0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
|
||||
0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
|
||||
0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C,
|
||||
0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
|
||||
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
|
||||
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
|
||||
0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00,
|
||||
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
|
||||
0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00,
|
||||
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30,
|
||||
0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
|
||||
0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01,
|
||||
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
|
||||
0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00,
|
||||
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
|
||||
0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
|
||||
0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83,
|
||||
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
|
||||
0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05,
|
||||
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
|
||||
0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x83, 0x25, 0x01, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
|
||||
0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C,
|
||||
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
|
||||
0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16,
|
||||
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
|
||||
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00,
|
||||
0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
|
||||
0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
|
||||
0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30,
|
||||
0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
|
||||
0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58,
|
||||
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
|
||||
0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00,
|
||||
0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
|
||||
0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
|
||||
0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0,
|
||||
0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
|
||||
0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01,
|
||||
0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00,
|
||||
0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00,
|
||||
0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00,
|
||||
0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
|
||||
0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0x00, 0x83,
|
||||
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C,
|
||||
0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
|
||||
0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00,
|
||||
0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
|
||||
0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C,
|
||||
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0,
|
||||
0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16,
|
||||
0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00,
|
||||
0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00,
|
||||
0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00,
|
||||
0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80,
|
||||
0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30,
|
||||
0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1,
|
||||
0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58,
|
||||
0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02,
|
||||
0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00,
|
||||
0x00, 0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00,
|
||||
0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00,
|
||||
0x06, 0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0,
|
||||
0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x25, 0x01, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06,
|
||||
0x0B, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60,
|
||||
0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B,
|
||||
0x00, 0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01,
|
||||
0x00, 0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00,
|
||||
0x00, 0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00,
|
||||
0x18, 0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00,
|
||||
0x83, 0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18,
|
||||
0x2C, 0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83,
|
||||
0x05, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0,
|
||||
0x00, 0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05,
|
||||
0x00, 0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00,
|
||||
0x00, 0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0x30, 0x58, 0x00, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00,
|
||||
0x60, 0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00,
|
||||
0x0C, 0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x18, 0x2C, 0x00, 0x00, 0x83, 0x05, 0x00, 0x60,
|
||||
0xB0, 0x00, 0x00, 0x30, 0x58, 0x00, 0x00, 0x06, 0x0B, 0x00, 0xC0, 0x60, 0x01, 0x00, 0x60, 0xB0, 0x00, 0x00, 0x0C,
|
||||
0x16, 0x00, 0x80, 0xC1, 0x02, 0x00, 0xC0, 0x60, 0x01, 0x00, 0xDC, 0x19, 0x79, 0xA5, 0x06, 0x0E, 0x33, 0xD4, 0xAF,
|
||||
0xF2, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
|
||||
};
|
||||
|
||||
// A 1x1 RGB PNG whose IEND chunk header crosses the end of the first 64 byte piece
|
||||
static const uint8_t PNG_1X1[] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48,
|
||||
0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x02, 0x00, 0x00,
|
||||
0x00, 0x90, 0x77, 0x53, 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x78,
|
||||
0x9C, 0x63, 0xF8, 0xCF, 0xC0, 0x00, 0x00, 0x03, 0x01, 0x01, 0x00, 0xC9, 0xFE, 0x92,
|
||||
0xEF, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82};
|
||||
|
||||
// Draws that take a millisecond every fourth row, so decoding PNG_SOLID always outlasts the time limit of one call
|
||||
class SlowDraws : public PngDecoder, public RuntimeImage {
|
||||
public:
|
||||
static void install(RuntimeImage &image) {
|
||||
auto *decoder = static_cast<PngDecoder *>((image.*&SlowDraws::decoder_).get());
|
||||
pngle_set_draw_callback(decoder->*&SlowDraws::pngle_, draw);
|
||||
}
|
||||
|
||||
private:
|
||||
static void draw(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, const uint8_t rgba[4]) {
|
||||
if (x == 0 && y % 4 == 0) {
|
||||
const uint32_t until = millis() + 2;
|
||||
while (millis() < until) {
|
||||
}
|
||||
}
|
||||
static_cast<PngDecoder *>(pngle_get_user_data(pngle))->draw(x, y, w, h, Color(rgba[0], rgba[1], rgba[2], rgba[3]));
|
||||
}
|
||||
};
|
||||
|
||||
// One feed_data() call stops after a short time, so a large image is decoded over several calls
|
||||
TEST(PngTimeLimit, LargeImageIsDecodedOverSeveralCalls) {
|
||||
RuntimeImage img(PNG, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
std::vector<uint8_t> data(PNG_SOLID, PNG_SOLID + sizeof(PNG_SOLID));
|
||||
ASSERT_TRUE(img.begin_decode(data.size()));
|
||||
SlowDraws::install(img);
|
||||
|
||||
size_t offset = 0;
|
||||
int calls = 0;
|
||||
while (offset < data.size() && calls < 1000) {
|
||||
const int consumed = img.feed_data(data.data() + offset, data.size() - offset);
|
||||
ASSERT_GT(consumed, 0);
|
||||
offset += consumed;
|
||||
calls++;
|
||||
}
|
||||
EXPECT_GT(calls, 1) << "the whole image was decoded in one call";
|
||||
ASSERT_EQ(offset, data.size());
|
||||
ASSERT_TRUE(img.end_decode());
|
||||
|
||||
ASSERT_EQ(img.get_width(), 800);
|
||||
ASSERT_EQ(img.get_height(), 600);
|
||||
for (auto [col, row] : {std::pair{0, 0}, std::pair{400, 300}, std::pair{799, 599}}) {
|
||||
const Color pixel = img.get_pixel(col, row);
|
||||
EXPECT_EQ(pixel.r, 30);
|
||||
EXPECT_EQ(pixel.g, 120);
|
||||
EXPECT_EQ(pixel.b, 200);
|
||||
}
|
||||
}
|
||||
|
||||
// A chunk header that crosses the end of a piece does not end the call
|
||||
TEST(PngTimeLimit, ChunkAcrossAPieceDoesNotEndTheCall) {
|
||||
RuntimeImage img(PNG, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE);
|
||||
std::vector<uint8_t> data(PNG_1X1, PNG_1X1 + sizeof(PNG_1X1));
|
||||
ASSERT_TRUE(img.begin_decode(data.size()));
|
||||
EXPECT_EQ(img.feed_data(data.data(), data.size()), static_cast<int>(data.size()));
|
||||
}
|
||||
|
||||
} // namespace esphome::runtime_image::testing
|
||||
@@ -8,6 +8,8 @@ text_sensor:
|
||||
name: Scan Results
|
||||
ip_address:
|
||||
name: IP Address
|
||||
address_1:
|
||||
name: IP Address 1
|
||||
ssid:
|
||||
name: SSID
|
||||
bssid:
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
@@ -25,16 +24,13 @@ from esphome.const import (
|
||||
CONF_CLIENT_CERTIFICATE,
|
||||
CONF_CLIENT_CERTIFICATE_KEY,
|
||||
CONF_ESPHOME,
|
||||
CONF_LOG_TOPIC,
|
||||
CONF_MQTT,
|
||||
CONF_NAME,
|
||||
CONF_NAME_ADD_MAC_SUFFIX,
|
||||
CONF_PAYLOAD,
|
||||
CONF_PORT,
|
||||
CONF_RETAIN,
|
||||
CONF_SKIP_CERT_CN_CHECK,
|
||||
CONF_TOPIC,
|
||||
CONF_TOPIC_PREFIX,
|
||||
)
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.mqtt import (
|
||||
@@ -103,10 +99,6 @@ def test_show_logs_requests_crash_log_when_device_reports_online(
|
||||
|
||||
on_message(client, None, MagicMock(topic="test-device/status", payload=b"offline"))
|
||||
client.publish.assert_not_called()
|
||||
# The birth payload on another topic is only a log line
|
||||
on_message(client, None, MagicMock(topic="test-device/debug", payload=b"online"))
|
||||
client.publish.assert_not_called()
|
||||
mock_print.reset_mock()
|
||||
on_message(client, None, MagicMock(topic="test-device/status", payload=b"online"))
|
||||
client.publish.assert_called_once_with(
|
||||
"esphome/logs/test-device", None, retain=False
|
||||
@@ -144,184 +136,6 @@ def test_show_logs_prints_a_birth_message_inside_the_log_subscription(
|
||||
assert mock_print.call_args.args[0].endswith("online")
|
||||
|
||||
|
||||
@patch("esphome.mqtt.safe_print")
|
||||
@patch("esphome.mqtt.initialize")
|
||||
def test_show_logs_follows_the_mac_suffix_of_default_topics(
|
||||
mock_initialize: MagicMock, mock_print: MagicMock
|
||||
) -> None:
|
||||
"""With name_add_mac_suffix the default topics carry a MAC the CLI cannot know, so any suffix matches."""
|
||||
show_logs(_mac_config(log_topic="test-device/debug"))
|
||||
|
||||
assert mock_initialize.call_args.args[1] == ["+/debug", "+/status"]
|
||||
on_message = mock_initialize.call_args.args[2]
|
||||
client = MagicMock()
|
||||
for topic in (
|
||||
"other-device/status",
|
||||
"test-device/status",
|
||||
"test-device-abc/status",
|
||||
):
|
||||
_online(on_message, client, topic)
|
||||
client.publish.assert_not_called()
|
||||
_online(on_message, client, "test-device-a1b2c3/status")
|
||||
client.publish.assert_called_once_with(
|
||||
"esphome/logs/test-device-a1b2c3", None, retain=False
|
||||
)
|
||||
|
||||
on_message(client, None, MagicMock(topic="other-device/debug", payload=b"no"))
|
||||
mock_print.assert_not_called()
|
||||
on_message(
|
||||
client, None, MagicMock(topic="test-device-a1b2c3/debug", payload=b"[I]: hi")
|
||||
)
|
||||
assert mock_print.call_args.args[0].endswith("[test-device-a1b2c3][I]: hi")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mac_suffix", "options", "cli_topic", "subscriptions"),
|
||||
[
|
||||
pytest.param(
|
||||
True,
|
||||
{"log_topic": "custom/debug", "birth_topic": None},
|
||||
None,
|
||||
["custom/debug"],
|
||||
id="custom log topic with the mac suffix",
|
||||
),
|
||||
pytest.param(
|
||||
True,
|
||||
{"birth_topic": None},
|
||||
"test-device/debug",
|
||||
["test-device/debug"],
|
||||
id="command line topic as written",
|
||||
),
|
||||
pytest.param(
|
||||
False,
|
||||
{"prefix": "custom", "birth_topic": None},
|
||||
None,
|
||||
["custom/debug"],
|
||||
id="logs under the topic prefix",
|
||||
),
|
||||
pytest.param(
|
||||
True,
|
||||
{"prefix": "custom", "log_topic": "test-device/debug"},
|
||||
None,
|
||||
["test-device/debug", "test-device/status"],
|
||||
id="written out topics under a custom prefix",
|
||||
),
|
||||
],
|
||||
)
|
||||
@patch("esphome.mqtt.initialize")
|
||||
def test_show_logs_subscribes_to_literal_topics(
|
||||
mock_initialize: MagicMock,
|
||||
mac_suffix: bool,
|
||||
options: dict,
|
||||
cli_topic: str | None,
|
||||
subscriptions: list[str],
|
||||
) -> None:
|
||||
"""Topics the firmware does not build from the MAC-suffixed name are subscribed to as written."""
|
||||
config = _mac_config(**options)
|
||||
config[CONF_ESPHOME][CONF_NAME_ADD_MAC_SUFFIX] = mac_suffix
|
||||
|
||||
show_logs(config, topic=cli_topic)
|
||||
|
||||
assert mock_initialize.call_args.args[1] == subscriptions
|
||||
|
||||
|
||||
@patch("esphome.mqtt.initialize")
|
||||
def test_show_logs_asks_only_the_board_a_command_line_topic_shows(
|
||||
mock_initialize: MagicMock,
|
||||
) -> None:
|
||||
"""Replaying a crash clears it, so boards whose logs are not shown are left alone."""
|
||||
show_logs(_mac_config(), topic="test-device-a1b2c3/debug")
|
||||
|
||||
assert mock_initialize.call_args.args[1] == [
|
||||
"test-device-a1b2c3/debug",
|
||||
"+/status",
|
||||
]
|
||||
on_message = mock_initialize.call_args.args[2]
|
||||
client = MagicMock()
|
||||
_online(on_message, client, "test-device-d4e5f6/status")
|
||||
client.publish.assert_not_called()
|
||||
_online(on_message, client, "test-device-a1b2c3/status")
|
||||
client.publish.assert_called_once_with(
|
||||
"esphome/logs/test-device-a1b2c3", None, retain=False
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("cli_topic", "asked"),
|
||||
[
|
||||
pytest.param(None, True, id="no command line topic"),
|
||||
pytest.param("custom/debug", True, id="the shared custom topic"),
|
||||
pytest.param(
|
||||
"test-device-a1b2c3/debug", False, id="a topic the board never uses"
|
||||
),
|
||||
],
|
||||
)
|
||||
@patch("esphome.mqtt.initialize")
|
||||
def test_show_logs_matches_a_command_line_topic_to_the_custom_log_topic(
|
||||
mock_initialize: MagicMock, cli_topic: str | None, asked: bool
|
||||
) -> None:
|
||||
"""Every board's crash replay goes to the custom log topic, so --topic must show it for a board to be asked."""
|
||||
show_logs(_mac_config(log_topic="custom/debug"), topic=cli_topic)
|
||||
|
||||
assert mock_initialize.call_args.args[1] == [
|
||||
cli_topic or "custom/debug",
|
||||
"+/status",
|
||||
]
|
||||
on_message = mock_initialize.call_args.args[2]
|
||||
client = MagicMock()
|
||||
_online(on_message, client, "test-device-a1b2c3/status")
|
||||
if asked:
|
||||
client.publish.assert_called_once_with(
|
||||
"esphome/logs/test-device-a1b2c3", None, retain=False
|
||||
)
|
||||
else:
|
||||
client.publish.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mqtt_config", "message"),
|
||||
[
|
||||
pytest.param(None, "MQTT isn't setup", id="no mqtt"),
|
||||
pytest.param({CONF_LOG_TOPIC: None}, "set to null", id="null log topic"),
|
||||
pytest.param(
|
||||
{CONF_LOG_TOPIC: {}}, "not available", id="log topic without topic"
|
||||
),
|
||||
],
|
||||
)
|
||||
@patch("esphome.mqtt.initialize")
|
||||
def test_show_logs_without_a_log_topic(
|
||||
mock_initialize: MagicMock,
|
||||
mqtt_config: dict | None,
|
||||
message: str,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Without a log topic there is nothing to follow."""
|
||||
config = _discovery_config()
|
||||
if mqtt_config is None:
|
||||
del config[CONF_MQTT]
|
||||
else:
|
||||
config[CONF_MQTT].update(mqtt_config)
|
||||
|
||||
assert show_logs(config) == 1
|
||||
assert message in caplog.text
|
||||
mock_initialize.assert_not_called()
|
||||
|
||||
|
||||
@patch("esphome.mqtt.initialize")
|
||||
def test_show_logs_skips_crash_log_requests_no_suffixed_board_hears(
|
||||
mock_initialize: MagicMock,
|
||||
) -> None:
|
||||
"""A board with the MAC suffix only hears requests named after its own birth message."""
|
||||
show_logs(_mac_config(birth_topic="custom/status", retain=False))
|
||||
|
||||
on_message = mock_initialize.call_args.args[2]
|
||||
on_connect = mock_initialize.call_args.args[3]
|
||||
client = MagicMock()
|
||||
on_connect(client, None, None, 0, None)
|
||||
_online(on_message, client, "custom/status")
|
||||
client.publish.assert_not_called()
|
||||
|
||||
|
||||
def test_get_esphome_device_ip_empty_broker() -> None:
|
||||
"""Test that get_esphome_device_ip raises EsphomeError when broker is empty."""
|
||||
config = {
|
||||
@@ -404,34 +218,6 @@ def test_get_esphome_device_ip_missing_name() -> None:
|
||||
get_esphome_device_ip(config)
|
||||
|
||||
|
||||
def _mac_config(
|
||||
log_topic: str | None = None,
|
||||
birth_topic: str | None = "test-device/status",
|
||||
retain: bool = True,
|
||||
prefix: str | None = None,
|
||||
) -> dict:
|
||||
"""A config with name_add_mac_suffix on and the given mqtt topics."""
|
||||
config = _discovery_config()
|
||||
config[CONF_ESPHOME][CONF_NAME_ADD_MAC_SUFFIX] = True
|
||||
mqtt_conf = config[CONF_MQTT]
|
||||
if prefix is not None:
|
||||
mqtt_conf[CONF_TOPIC_PREFIX] = prefix
|
||||
if log_topic is not None:
|
||||
mqtt_conf[CONF_LOG_TOPIC] = {CONF_TOPIC: log_topic}
|
||||
if birth_topic is not None:
|
||||
mqtt_conf[CONF_BIRTH_MESSAGE] = {
|
||||
CONF_TOPIC: birth_topic,
|
||||
CONF_PAYLOAD: "online",
|
||||
CONF_RETAIN: retain,
|
||||
}
|
||||
return config
|
||||
|
||||
|
||||
def _online(on_message: Callable[..., None], client: MagicMock, topic: str) -> None:
|
||||
"""Deliver the birth payload on a topic."""
|
||||
on_message(client, None, MagicMock(topic=topic, payload=b"online"))
|
||||
|
||||
|
||||
def _discovery_config() -> dict:
|
||||
return {
|
||||
CONF_MQTT: {
|
||||
|
||||
Reference in new issue
Block a user