mirror of
https://github.com/esphome/esphome.git
synced 2026-10-23 14:38:52 +00:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
470ed7cc06 | ||
|
|
195c8285d7 | ||
|
|
e5b5dd52ef | ||
|
|
014e6938c3 | ||
|
|
67a56da25c | ||
|
|
bf36f7532d | ||
|
|
10c32a5cc7 | ||
|
|
69f6429728 | ||
|
|
4ae6ec545c | ||
|
|
263de44633 | ||
|
|
bab99748f0 |
No files matched your search
+1
-10
@@ -175,11 +175,6 @@ esphome/components/ds2484/* @mrk-its
|
||||
esphome/components/ds248x/* @tomwellnitz
|
||||
esphome/components/ds3231/* @linkedupbits
|
||||
esphome/components/dsmr/* @glmnet @PolarGoose
|
||||
esphome/components/duco/* @kokx
|
||||
esphome/components/duco/number/* @kokx
|
||||
esphome/components/duco/select/* @kokx
|
||||
esphome/components/duco/sensor/* @kokx
|
||||
esphome/components/duco/text_sensor/* @kokx
|
||||
esphome/components/duty_time/* @dudanov
|
||||
esphome/components/ecocomfort2/* @gledian
|
||||
esphome/components/ee895/* @Stock-M
|
||||
@@ -379,8 +374,7 @@ esphome/components/m5stack_8angle/* @rnauber
|
||||
esphome/components/mapping/* @clydebarrow
|
||||
esphome/components/matrix_keypad/* @ssieb
|
||||
esphome/components/max17043/* @blacknell
|
||||
esphome/components/max17048/* @B48D81EFCC @ryansch
|
||||
esphome/components/max170xx/* @ryansch
|
||||
esphome/components/max17048/* @B48D81EFCC
|
||||
esphome/components/max31865/* @DAVe3283
|
||||
esphome/components/max31888/* @JiriPrchal
|
||||
esphome/components/max44009/* @berfenger
|
||||
@@ -468,8 +462,6 @@ esphome/components/openthread/* @mrene
|
||||
esphome/components/opt3001/* @ccutrer
|
||||
esphome/components/ota/* @esphome/core
|
||||
esphome/components/output/* @esphome/core
|
||||
esphome/components/packet_interface/* @clydebarrow
|
||||
esphome/components/packet_interface/packet_transport/* @clydebarrow
|
||||
esphome/components/packet_transport/* @clydebarrow
|
||||
esphome/components/pca6416a/* @Mat931
|
||||
esphome/components/pca9505/* @ajfriesen @ssieb
|
||||
@@ -690,7 +682,6 @@ esphome/components/tuya/water_heater/* @iago-veiga
|
||||
esphome/components/uart/* @esphome/core
|
||||
esphome/components/uart/button/* @ssieb
|
||||
esphome/components/uart/event/* @eoasmxd
|
||||
esphome/components/uart/packet_interface/* @clydebarrow
|
||||
esphome/components/uart/packet_transport/* @clydebarrow
|
||||
esphome/components/uart_mux/* @kbx81
|
||||
esphome/components/uart_tcp/* @Bascht74
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "ch422g.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::ch422g {
|
||||
@@ -60,7 +59,11 @@ bool CH422GComponent::digital_read(uint8_t pin) {
|
||||
}
|
||||
|
||||
void CH422GComponent::digital_write(uint8_t pin, bool value) {
|
||||
assign_bits(this->output_bits_, 1 << pin, value);
|
||||
if (value) {
|
||||
this->output_bits_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_bits_ &= ~(1 << pin);
|
||||
}
|
||||
this->write_outputs_();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "ch423.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
@@ -63,7 +62,11 @@ bool CH423Component::digital_read(uint8_t pin) {
|
||||
}
|
||||
|
||||
void CH423Component::digital_write(uint8_t pin, bool value) {
|
||||
assign_bits(this->output_bits_, 1 << pin, value);
|
||||
if (value) {
|
||||
this->output_bits_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_bits_ &= ~(1 << pin);
|
||||
}
|
||||
this->write_outputs_();
|
||||
}
|
||||
|
||||
|
||||
@@ -181,6 +181,16 @@ def get_all_display_metadata() -> dict[str, DisplayMetaData]:
|
||||
return {id_.id: meta for id_, meta in entries}
|
||||
|
||||
|
||||
def config_has_writer(config: ConfigType) -> bool:
|
||||
"""Return True when the display config draws anything."""
|
||||
return (
|
||||
config.get(CONF_AUTO_CLEAR_ENABLED) is True
|
||||
or config.get(CONF_PAGES) is not None
|
||||
or config.get(CONF_LAMBDA) is not None
|
||||
or config.get(CONF_SHOW_TEST_CARD) is True
|
||||
)
|
||||
|
||||
|
||||
def get_display_metadata(display_id: ID) -> DisplayMetaData:
|
||||
"""Get display metadata by ID object
|
||||
|
||||
@@ -210,10 +220,7 @@ def get_display_metadata(display_id: ID) -> DisplayMetaData:
|
||||
height=dimensions[1],
|
||||
has_hardware_rotation=False,
|
||||
byte_order=disp_config.get(CONF_BYTE_ORDER, cv.UNDEFINED),
|
||||
has_writer=disp_config.get(CONF_AUTO_CLEAR_ENABLED) is True
|
||||
or disp_config.get(CONF_PAGES) is not None
|
||||
or disp_config.get(CONF_LAMBDA) is not None
|
||||
or disp_config.get(CONF_SHOW_TEST_CARD) is True,
|
||||
has_writer=config_has_writer(disp_config),
|
||||
rotation=disp_config.get(CONF_ROTATION, 0),
|
||||
draw_rounding=disp_config.get(CONF_DRAW_ROUNDING, 0),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#include "init_sequence.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::display {
|
||||
|
||||
InitSequenceStep InitSequenceReader::next(InitSequenceEntry &entry, uint8_t max_count) {
|
||||
const size_t remaining = this->length_ - this->index_;
|
||||
if (remaining == 0)
|
||||
return INIT_SEQUENCE_STEP_END;
|
||||
const uint8_t *pos = this->sequence_ + this->index_;
|
||||
if (remaining >= 2) {
|
||||
entry.value = progmem_read_byte(pos);
|
||||
entry.count = progmem_read_byte(pos + 1);
|
||||
entry.args = pos + 2;
|
||||
if (entry.count == INIT_SEQUENCE_DELAY_FLAG) {
|
||||
this->index_ += 2;
|
||||
return INIT_SEQUENCE_STEP_DELAY;
|
||||
}
|
||||
if (entry.count <= max_count && remaining - 2 >= entry.count) {
|
||||
this->index_ += 2 + entry.count;
|
||||
return INIT_SEQUENCE_STEP_COMMAND;
|
||||
}
|
||||
}
|
||||
this->index_ = this->length_;
|
||||
return INIT_SEQUENCE_STEP_MALFORMED;
|
||||
}
|
||||
|
||||
} // namespace esphome::display
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::display {
|
||||
|
||||
/// Count byte that marks a delay entry: the byte before it is the delay in ms.
|
||||
static constexpr uint8_t INIT_SEQUENCE_DELAY_FLAG = 0xFF;
|
||||
|
||||
/// Unscoped so `while (auto step = reader.next(entry))` stops at END.
|
||||
enum InitSequenceStep : uint8_t {
|
||||
INIT_SEQUENCE_STEP_END = 0,
|
||||
INIT_SEQUENCE_STEP_COMMAND,
|
||||
INIT_SEQUENCE_STEP_DELAY,
|
||||
INIT_SEQUENCE_STEP_MALFORMED,
|
||||
};
|
||||
|
||||
/// `value` is the command or the delay in ms; `args` points into the (possibly PROGMEM) sequence.
|
||||
struct InitSequenceEntry {
|
||||
const uint8_t *args;
|
||||
uint8_t value;
|
||||
uint8_t count;
|
||||
};
|
||||
|
||||
/// Walks a mipi.flatten_sequence table (`cmd, count, args...` or `ms, 0xFF`) with progmem reads; resumable.
|
||||
class InitSequenceReader {
|
||||
public:
|
||||
InitSequenceReader(const uint8_t *sequence, size_t length) : sequence_(sequence), length_(length) {}
|
||||
|
||||
/// Fill `entry` with the next entry; a command with more than `max_count` args is MALFORMED.
|
||||
InitSequenceStep next(InitSequenceEntry &entry, uint8_t max_count = 0xFE);
|
||||
|
||||
protected:
|
||||
const uint8_t *sequence_;
|
||||
size_t length_;
|
||||
size_t index_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::display
|
||||
@@ -1,87 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import time, uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DISCOVERY, CONF_ID, CONF_TIME, CONF_TIME_ID
|
||||
from esphome.core import ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@kokx"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
DOMAIN = "duco"
|
||||
MULTI_CONF = True
|
||||
|
||||
CONF_DISABLE_CRC = "disable_crc"
|
||||
CONF_DUCO_ID = "duco_id"
|
||||
|
||||
duco_ns = cg.esphome_ns.namespace("duco")
|
||||
Duco = duco_ns.class_("Duco", cg.Component, uart.UARTDevice)
|
||||
# Base of every class that sends requests through the hub
|
||||
DucoDevice = duco_ns.class_(
|
||||
"DucoDevice", cg.PollingComponent, cg.Parented.template(Duco)
|
||||
)
|
||||
DucoDiscovery = duco_ns.class_("DucoDiscovery", DucoDevice)
|
||||
DucoTime = duco_ns.class_("DucoTime", cg.PollingComponent, cg.Parented.template(Duco))
|
||||
|
||||
# Entities of the platforms take the hub with this
|
||||
DUCO_COMPONENT_SCHEMA = cv.Schema({cv.GenerateID(CONF_DUCO_ID): cv.use_id(Duco)})
|
||||
|
||||
DISCOVERY_SCHEMA = cv.Schema({cv.GenerateID(): cv.declare_id(DucoDiscovery)}).extend(
|
||||
cv.COMPONENT_SCHEMA
|
||||
)
|
||||
|
||||
TIME_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DucoTime),
|
||||
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
|
||||
}
|
||||
).extend(cv.polling_component_schema("15min"))
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Duco),
|
||||
cv.Optional(CONF_DISABLE_CRC, default=False): cv.boolean,
|
||||
cv.Optional(CONF_DISCOVERY): DISCOVERY_SCHEMA,
|
||||
cv.Optional(CONF_TIME): TIME_SCHEMA,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
DOMAIN,
|
||||
baud_rate=57600,
|
||||
require_tx=True,
|
||||
require_rx=True,
|
||||
data_bits=8,
|
||||
parity="NONE",
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
_request_device_slot = cg.slot_counter("DUCO_DEVICE_COUNT")
|
||||
|
||||
|
||||
async def register_duco_device(var: cg.MockObj, config: ConfigType, hub_id: ID) -> None:
|
||||
"""Register a device that sends requests, and give it an open request slot on its hub."""
|
||||
await cg.register_component(var, config)
|
||||
hub = await cg.get_variable(hub_id)
|
||||
await cg.register_parented(var, hub)
|
||||
_request_device_slot(str(hub_id))
|
||||
cg.add(hub.register_device(var))
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await uart.new_uart_component(config)
|
||||
cg.add(var.set_disable_crc(config[CONF_DISABLE_CRC]))
|
||||
|
||||
if (discovery := config.get(CONF_DISCOVERY)) is not None:
|
||||
discovery_var = cg.new_Pvariable(discovery[CONF_ID])
|
||||
await register_duco_device(discovery_var, discovery, config[CONF_ID])
|
||||
|
||||
if (time_config := config.get(CONF_TIME)) is not None:
|
||||
clock = await cg.get_variable(time_config[CONF_TIME_ID])
|
||||
time_var = cg.new_Pvariable(time_config[CONF_ID], clock)
|
||||
await cg.register_component(time_var, time_config)
|
||||
await cg.register_parented(time_var, var)
|
||||
@@ -1,273 +0,0 @@
|
||||
#include "duco.h"
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::duco {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "duco");
|
||||
|
||||
static constexpr uint8_t HEADER_START = 0xAA;
|
||||
static constexpr uint8_t HEADER_END = 0x55;
|
||||
static constexpr uint8_t STUFFING = 0x01; // follows an 0xAA inside a message, not part of it
|
||||
static constexpr uint32_t FRAME_GAP_MS = 50;
|
||||
|
||||
static constexpr uint32_t DISCOVERY_START_ID = 0;
|
||||
static constexpr uint32_t DISCOVERY_STEP_ID = 1;
|
||||
static constexpr uint32_t DISCOVERY_START_MS = 16000;
|
||||
static constexpr uint32_t DISCOVERY_STEP_MS = 1600;
|
||||
static constexpr uint8_t DISCOVERY_MAX_ATTEMPTS = 3;
|
||||
|
||||
void Duco::loop() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
// Read first: after a slow loop pass the rest of a message is already waiting, which is not silence
|
||||
this->read_available([this, now](uint8_t byte) {
|
||||
if (this->parse_byte_(byte)) {
|
||||
this->last_byte_ = now;
|
||||
} else {
|
||||
this->reset_frame_();
|
||||
}
|
||||
});
|
||||
|
||||
// The last message of a burst has no header after it, so it ends on silence
|
||||
if (this->rx_size_ != 0 && now - this->last_byte_ > FRAME_GAP_MS) {
|
||||
this->finalize_message_();
|
||||
this->reset_frame_();
|
||||
}
|
||||
}
|
||||
|
||||
void Duco::reset_frame_() {
|
||||
this->rx_size_ = 0;
|
||||
this->escaped_ = false;
|
||||
}
|
||||
|
||||
void Duco::close_request(DucoDevice *device) {
|
||||
#ifdef DUCO_DEVICE_COUNT
|
||||
for (auto &pending : this->pending_) {
|
||||
if (pending.device == device) {
|
||||
pending.function = 0;
|
||||
pending.missed = false;
|
||||
this->update_warning_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool Duco::parse_byte_(uint8_t byte) {
|
||||
const size_t at = this->rx_size_;
|
||||
if (at >= MAX_FRAME_SIZE) {
|
||||
ESP_LOGV(TAG, "Message longer than %u bytes dropped", static_cast<unsigned>(MAX_FRAME_SIZE));
|
||||
return false;
|
||||
}
|
||||
const uint8_t prev = at > 0 ? this->rx_buffer_[at - 1] : 0;
|
||||
|
||||
if (at == 1 && prev == HEADER_START && byte == HEADER_END)
|
||||
return false; // header of the next message
|
||||
|
||||
if (at >= 2) {
|
||||
const bool escaped = this->escaped_;
|
||||
this->escaped_ = false;
|
||||
if (prev == HEADER_START && !escaped) {
|
||||
if (byte == STUFFING) {
|
||||
this->escaped_ = true;
|
||||
return true;
|
||||
}
|
||||
if (at >= 3 && byte == HEADER_END) {
|
||||
this->rx_size_--; // drop the 0xAA of the header
|
||||
this->finalize_message_();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->rx_buffer_[this->rx_size_++] = byte;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Duco::finalize_message_() {
|
||||
const uint8_t *raw = this->rx_buffer_.data();
|
||||
// length (function, id and data), function, id, data, CRC low, CRC high
|
||||
if (this->rx_size_ < 5 || raw[0] < 2 || this->rx_size_ < raw[0] + 3u) {
|
||||
ESP_LOGV(TAG, "Malformed message dropped: %u bytes, length byte %u", this->rx_size_, raw[0]);
|
||||
return;
|
||||
}
|
||||
const uint8_t length = raw[0];
|
||||
|
||||
const uint16_t computed_crc = crc16(raw, length + 1);
|
||||
const uint16_t remote_crc = encode_uint16(raw[length + 2], raw[length + 1]);
|
||||
if (computed_crc != remote_crc) {
|
||||
if (!this->disable_crc_) {
|
||||
ESP_LOGV(TAG, "CRC check failed: %04X != %04X", computed_crc, remote_crc);
|
||||
return;
|
||||
}
|
||||
ESP_LOGV(TAG, "CRC check failed, ignored: %04X != %04X", computed_crc, remote_crc);
|
||||
}
|
||||
|
||||
#ifdef DUCO_DEVICE_COUNT
|
||||
// The box first acknowledges a request with function + 1, then answers with function + 2
|
||||
const DucoMessage message{raw[2], raw + 3, static_cast<uint8_t>(length - 2)};
|
||||
for (auto &pending : this->pending_) {
|
||||
if (pending.function != 0 && pending.id == message.id && pending.function + 2 == raw[1]) {
|
||||
pending.function = 0;
|
||||
pending.missed = false;
|
||||
this->update_warning_();
|
||||
pending.device->receive_response(message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Acknowledgements (odd function codes) are expected and never match
|
||||
if ((raw[1] & 1) == 0) {
|
||||
ESP_LOGV(TAG, "Answer %02X with id %u matches no open request", raw[1], message.id);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef DUCO_DEVICE_COUNT
|
||||
void Duco::update_warning_() {
|
||||
for (const auto &pending : this->pending_) {
|
||||
if (pending.missed) {
|
||||
this->status_set_warning(LOG_STR("No answer from the box"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
this->status_clear_warning();
|
||||
}
|
||||
#endif
|
||||
|
||||
void Duco::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Duco:\n"
|
||||
" CRC Disabled: %s",
|
||||
YESNO(this->disable_crc_));
|
||||
}
|
||||
|
||||
uint8_t Duco::next_id_() {
|
||||
do {
|
||||
this->last_id_++;
|
||||
} while (this->last_id_ == HEADER_START || this->last_id_ == HEADER_END);
|
||||
return this->last_id_;
|
||||
}
|
||||
|
||||
void Duco::send(DucoDevice *device, uint8_t function, std::initializer_list<uint8_t> data) {
|
||||
if (data.size() > MAX_REQUEST_DATA) {
|
||||
ESP_LOGE(TAG, "Request of %u bytes is too long", static_cast<unsigned>(data.size()));
|
||||
return;
|
||||
}
|
||||
// Not byte stuffed, as in the author's original and no capture shows a request with 0xAA. An 0xAA followed by any
|
||||
// byte but 0x01 or 0x55 reads the same with or without unstuffing on the box, so only those rare pairs are
|
||||
// unverified.
|
||||
const size_t size = data.size();
|
||||
const uint8_t id = this->next_id_();
|
||||
uint8_t frame[5 + MAX_REQUEST_DATA + 2] = {HEADER_START, HEADER_END, static_cast<uint8_t>(size + 2), function, id};
|
||||
std::copy_n(data.begin(), size, frame + 5);
|
||||
const uint16_t crc = crc16(frame + 2, size + 3);
|
||||
frame[size + 5] = crc & 0xFF;
|
||||
frame[size + 6] = crc >> 8;
|
||||
this->write_array(frame, size + 7);
|
||||
this->flush();
|
||||
|
||||
#ifdef DUCO_DEVICE_COUNT
|
||||
for (auto &pending : this->pending_) {
|
||||
if (pending.device == device) {
|
||||
pending.missed = pending.function != 0;
|
||||
if (pending.missed) {
|
||||
ESP_LOGV(TAG, "No answer to request %02X with id %u", pending.function, pending.id);
|
||||
this->update_warning_();
|
||||
}
|
||||
pending.id = id;
|
||||
pending.function = function;
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
ESP_LOGV_HEX(TAG, frame + 2, size + 5, sizeof(frame), "Sent %s");
|
||||
}
|
||||
|
||||
static const LogString *node_type_name(uint8_t type) {
|
||||
switch (type) {
|
||||
case 8:
|
||||
return LOG_STR("UCBAT");
|
||||
case 9:
|
||||
return LOG_STR("UC");
|
||||
case 10:
|
||||
return LOG_STR("UCRH");
|
||||
case 12:
|
||||
return LOG_STR("UCCO2");
|
||||
case 13:
|
||||
return LOG_STR("VLV");
|
||||
case 17:
|
||||
return LOG_STR("BOX");
|
||||
case 18:
|
||||
return LOG_STR("SWITCH");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
void DucoDiscovery::setup() {
|
||||
// Give the box time to start before walking its node addresses
|
||||
this->set_timeout(DISCOVERY_START_ID, DISCOVERY_START_MS, [this]() {
|
||||
this->set_interval(DISCOVERY_STEP_ID, DISCOVERY_STEP_MS, [this]() { this->update(); });
|
||||
});
|
||||
}
|
||||
|
||||
void DucoDiscovery::update() {
|
||||
if (this->attempts_ == DISCOVERY_MAX_ATTEMPTS) {
|
||||
ESP_LOGV(TAG, "No answer for node %u", this->next_node_);
|
||||
this->next_node_++;
|
||||
this->attempts_ = 0;
|
||||
}
|
||||
if (this->next_node_ >= NODE_COUNT) {
|
||||
this->cancel_interval(DISCOVERY_STEP_ID);
|
||||
this->parent_->close_request(this);
|
||||
return;
|
||||
}
|
||||
// Without an answer by the next step the same node is asked again; a retry is not a missed answer
|
||||
if (this->attempts_ > 0)
|
||||
this->parent_->close_request(this);
|
||||
ESP_LOGV(TAG, "Discovering node %u", this->next_node_);
|
||||
this->attempts_++;
|
||||
this->parent_->send(this, FUNCTION_NODE, {0x01, this->next_node_});
|
||||
}
|
||||
|
||||
void DucoDiscovery::receive_response(const DucoMessage &message) {
|
||||
// Only the latest attempt is open, so a late answer to an earlier one never arrives here
|
||||
if (message.size >= 1 && this->next_node_ < NODE_COUNT) {
|
||||
if (message.data[0] != 0) {
|
||||
ESP_LOGV(TAG, "Found node %u: type %u", this->next_node_, message.data[0]);
|
||||
}
|
||||
this->node_types_[this->next_node_++] = message.data[0];
|
||||
this->attempts_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void DucoDiscovery::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Discovered nodes:");
|
||||
for (uint8_t node = 0; node < NODE_COUNT; node++) {
|
||||
const uint8_t type = this->node_types_[node];
|
||||
if (type != 0) {
|
||||
ESP_LOGCONFIG(TAG, " Node %u: type %u (%s)", node, type, LOG_STR_ARG(node_type_name(type)));
|
||||
}
|
||||
}
|
||||
if (this->next_node_ < NODE_COUNT) {
|
||||
ESP_LOGCONFIG(TAG, " Still discovering, at node %u", this->next_node_);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_TIME
|
||||
void DucoTime::update() {
|
||||
const ESPTime now = this->clock_->now();
|
||||
if (!now.is_valid()) {
|
||||
ESP_LOGV(TAG, "Time is not valid yet");
|
||||
return;
|
||||
}
|
||||
const uint32_t ts = now.timestamp;
|
||||
this->parent_->send(nullptr, FUNCTION_PARAMETER,
|
||||
{0x05, static_cast<uint8_t>(ts), static_cast<uint8_t>(ts >> 8), static_cast<uint8_t>(ts >> 16),
|
||||
static_cast<uint8_t>(ts >> 24), 0x00, 0x00, 0x00, 0x00});
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::duco
|
||||
@@ -1,113 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <initializer_list>
|
||||
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_TIME
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::duco {
|
||||
|
||||
// Function codes of the requests; the box answers a request with function + 2
|
||||
static constexpr uint8_t FUNCTION_NODE = 0x0c; // discovery, status and mode of a node
|
||||
static constexpr uint8_t FUNCTION_NODE_DATA = 0x10; // a block of a node's data (sensors, serial number)
|
||||
static constexpr uint8_t FUNCTION_PARAMETER = 0x24; // read or write a box parameter, set the clock
|
||||
|
||||
/// A received message; data points into the receive buffer and is valid only during the callback.
|
||||
struct DucoMessage {
|
||||
uint8_t id;
|
||||
const uint8_t *data;
|
||||
uint8_t size;
|
||||
};
|
||||
|
||||
class DucoDevice;
|
||||
|
||||
/// The open request of one device: the answer to it is function + 2 with the same message id.
|
||||
struct DucoPendingRequest {
|
||||
DucoDevice *device;
|
||||
uint8_t id;
|
||||
uint8_t function; // 0 while nothing is open
|
||||
bool missed; // the request before this one got no answer; sits in padding
|
||||
};
|
||||
|
||||
class Duco : public uart::UARTDevice, public Component {
|
||||
public:
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_disable_crc(bool disable_crc) { this->disable_crc_ = disable_crc; }
|
||||
|
||||
#ifdef DUCO_DEVICE_COUNT
|
||||
void register_device(DucoDevice *device) { this->pending_.push_back({device, 0, 0, false}); }
|
||||
#endif
|
||||
|
||||
/// Forget device's open request, for a device that will not send again.
|
||||
void close_request(DucoDevice *device);
|
||||
|
||||
/// Send a request; its answer goes to device, or nowhere if nullptr. A device's new request replaces its
|
||||
/// open one, so an answer is never lost to another device's traffic.
|
||||
void send(DucoDevice *device, uint8_t function, std::initializer_list<uint8_t> data);
|
||||
|
||||
protected:
|
||||
// The longest answer to a request sent here is 25 bytes (node status), the longest request 9
|
||||
static constexpr size_t MAX_FRAME_SIZE = 32;
|
||||
static constexpr size_t MAX_REQUEST_DATA = 9;
|
||||
|
||||
bool parse_byte_(uint8_t byte);
|
||||
void reset_frame_();
|
||||
#ifdef DUCO_DEVICE_COUNT
|
||||
void update_warning_(); // warn while any device's previous request went unanswered
|
||||
#endif
|
||||
void finalize_message_();
|
||||
uint8_t next_id_();
|
||||
|
||||
#ifdef DUCO_DEVICE_COUNT
|
||||
StaticVector<DucoPendingRequest, DUCO_DEVICE_COUNT> pending_;
|
||||
#endif
|
||||
std::array<uint8_t, MAX_FRAME_SIZE> rx_buffer_{};
|
||||
uint32_t last_byte_{0};
|
||||
uint8_t rx_size_{0};
|
||||
uint8_t last_id_{10};
|
||||
bool escaped_{false}; // the last 0xAA was followed by a stuffing byte, so it starts no header
|
||||
bool disable_crc_{false};
|
||||
};
|
||||
|
||||
/// Every entity polls the box; deriving from PollingComponent shares its vtable.
|
||||
class DucoDevice : public PollingComponent, public Parented<Duco> {
|
||||
public:
|
||||
virtual void receive_response(const DucoMessage &message) = 0;
|
||||
};
|
||||
|
||||
/// Walks the node addresses once after boot; the nodes found are listed in the config dump.
|
||||
class DucoDiscovery : public DucoDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override; // one discovery step, run by the step interval
|
||||
void receive_response(const DucoMessage &message) override;
|
||||
|
||||
protected:
|
||||
static constexpr uint8_t NODE_COUNT = 0x45;
|
||||
|
||||
std::array<uint8_t, NODE_COUNT> node_types_{}; // 0 when no node answered at that address
|
||||
uint8_t next_node_{0};
|
||||
uint8_t attempts_{0};
|
||||
};
|
||||
|
||||
#ifdef USE_TIME
|
||||
class DucoTime : public PollingComponent, public Parented<Duco> {
|
||||
public:
|
||||
explicit DucoTime(time::RealTimeClock *clock) : clock_(clock) {}
|
||||
void update() override;
|
||||
|
||||
protected:
|
||||
time::RealTimeClock *clock_;
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace esphome::duco
|
||||
@@ -1,34 +0,0 @@
|
||||
from esphome.components import number
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import DEVICE_CLASS_TEMPERATURE, UNIT_CELSIUS
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
CONF_DUCO_ID,
|
||||
DUCO_COMPONENT_SCHEMA,
|
||||
DucoDevice,
|
||||
duco_ns,
|
||||
register_duco_device,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["duco"]
|
||||
CODEOWNERS = ["@kokx"]
|
||||
|
||||
DucoComfortTemperature = duco_ns.class_(
|
||||
"DucoComfortTemperature", DucoDevice, number.Number
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
number.number_schema(
|
||||
DucoComfortTemperature,
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
)
|
||||
.extend(DUCO_COMPONENT_SCHEMA)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await number.new_number(config, min_value=10.0, max_value=25.5, step=0.1)
|
||||
await register_duco_device(var, config, config[CONF_DUCO_ID])
|
||||
@@ -1,26 +0,0 @@
|
||||
#include "number.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::duco {
|
||||
|
||||
static constexpr uint8_t PARAMETER_COMFORT_TEMPERATURE = 0x12;
|
||||
static constexpr uint8_t GROUP_COMFORT_TEMPERATURE = 0x0a;
|
||||
|
||||
void DucoComfortTemperature::update() {
|
||||
this->parent_->send(this, FUNCTION_PARAMETER, {0x00, PARAMETER_COMFORT_TEMPERATURE, GROUP_COMFORT_TEMPERATURE});
|
||||
}
|
||||
|
||||
void DucoComfortTemperature::receive_response(const DucoMessage &message) {
|
||||
// Reads and writes get the same answer: one byte in tenths of a degree, below 10.0 is invalid
|
||||
if (message.size >= 4 && message.data[3] >= 100)
|
||||
this->publish_state(message.data[3] / 10.0f);
|
||||
}
|
||||
|
||||
void DucoComfortTemperature::control(float value) {
|
||||
const auto tenths = static_cast<uint8_t>(lroundf(value * 10.0f));
|
||||
this->parent_->send(this, FUNCTION_PARAMETER,
|
||||
{0x01, PARAMETER_COMFORT_TEMPERATURE, GROUP_COMFORT_TEMPERATURE, tenths, 0x00, 0x00, 0x00});
|
||||
}
|
||||
|
||||
} // namespace esphome::duco
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/number/number.h"
|
||||
|
||||
#include "../duco.h"
|
||||
|
||||
namespace esphome::duco {
|
||||
|
||||
class DucoComfortTemperature : public DucoDevice, public number::Number {
|
||||
public:
|
||||
void update() override;
|
||||
void receive_response(const DucoMessage &message) override;
|
||||
|
||||
protected:
|
||||
void control(float value) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::duco
|
||||
@@ -1,45 +0,0 @@
|
||||
from esphome.components import select
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
CONF_DUCO_ID,
|
||||
DUCO_COMPONENT_SCHEMA,
|
||||
DucoDevice,
|
||||
duco_ns,
|
||||
register_duco_device,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["duco"]
|
||||
CODEOWNERS = ["@kokx"]
|
||||
|
||||
# Same order as MODE_CODES in select.cpp
|
||||
DUCO_MODE_OPTIONS = [
|
||||
"AUTO",
|
||||
"MAN1",
|
||||
"MAN2",
|
||||
"MAN3",
|
||||
"EMPT",
|
||||
"CNT1",
|
||||
"CNT2",
|
||||
"CNT3",
|
||||
"MAN1x2",
|
||||
"MAN2x2",
|
||||
"MAN3x2",
|
||||
"MAN1x3",
|
||||
"MAN2x3",
|
||||
"MAN3x3",
|
||||
]
|
||||
|
||||
DucoSelect = duco_ns.class_("DucoSelect", DucoDevice, select.Select)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
select.select_schema(DucoSelect)
|
||||
.extend(cv.polling_component_schema("3s"))
|
||||
.extend(DUCO_COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await select.new_select(config, options=DUCO_MODE_OPTIONS)
|
||||
await register_duco_device(var, config, config[CONF_DUCO_ID])
|
||||
@@ -1,47 +0,0 @@
|
||||
#include "select.h"
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::duco {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "duco.select");
|
||||
|
||||
// Box code of each option, in the order of DUCO_MODE_OPTIONS in __init__.py
|
||||
static constexpr uint8_t MODE_CODES[] PROGMEM = {
|
||||
0x00, // AUTO
|
||||
0x04, // MAN1
|
||||
0x05, // MAN2
|
||||
0x06, // MAN3
|
||||
0x07, // EMPT
|
||||
0x08, // CNT1
|
||||
0x09, // CNT2
|
||||
0x0a, // CNT3
|
||||
0x84, // MAN1x2
|
||||
0x85, // MAN2x2
|
||||
0x86, // MAN3x2
|
||||
0xc4, // MAN1x3
|
||||
0xc5, // MAN2x3
|
||||
0xc6, // MAN3x3
|
||||
};
|
||||
|
||||
void DucoSelect::update() { this->parent_->send(this, FUNCTION_NODE, {0x02, 0x01}); }
|
||||
|
||||
void DucoSelect::receive_response(const DucoMessage &message) {
|
||||
if (message.size < 1)
|
||||
return;
|
||||
for (size_t i = 0; i < sizeof(MODE_CODES); i++) {
|
||||
if (progmem_read_byte(&MODE_CODES[i]) == message.data[0]) {
|
||||
this->publish_state(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ESP_LOGV(TAG, "Unknown mode %02X", message.data[0]);
|
||||
}
|
||||
|
||||
void DucoSelect::control(size_t index) {
|
||||
// The next update reads the new mode back
|
||||
this->parent_->send(nullptr, FUNCTION_NODE, {0x04, 0x01, progmem_read_byte(&MODE_CODES[index])});
|
||||
}
|
||||
|
||||
} // namespace esphome::duco
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/select/select.h"
|
||||
|
||||
#include "../duco.h"
|
||||
|
||||
namespace esphome::duco {
|
||||
|
||||
class DucoSelect : public DucoDevice, public select::Select {
|
||||
public:
|
||||
void update() override;
|
||||
void receive_response(const DucoMessage &message) override;
|
||||
|
||||
protected:
|
||||
void control(size_t index) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::duco
|
||||
@@ -1,156 +0,0 @@
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_CO2,
|
||||
CONF_HUMIDITY,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
DEVICE_CLASS_DURATION,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PARTS_PER_MILLION,
|
||||
UNIT_PERCENT,
|
||||
UNIT_SECOND,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
CONF_DUCO_ID,
|
||||
DUCO_COMPONENT_SCHEMA,
|
||||
DucoDevice,
|
||||
duco_ns,
|
||||
register_duco_device,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["duco"]
|
||||
CODEOWNERS = ["@kokx"]
|
||||
|
||||
UNIT_DAYS = "d"
|
||||
|
||||
CONF_BYPASS = "bypass"
|
||||
CONF_FILTER_REMAINING = "filter_remaining"
|
||||
CONF_FLOW_LEVEL = "flow_level"
|
||||
CONF_TEMPERATURE_EHA = "temperature_eha"
|
||||
CONF_TEMPERATURE_ETA = "temperature_eta"
|
||||
CONF_TEMPERATURE_ODA = "temperature_oda"
|
||||
CONF_TEMPERATURE_SUP = "temperature_sup"
|
||||
CONF_TIME_REMAINING = "time_remaining"
|
||||
|
||||
DucoSensor = duco_ns.class_("DucoSensor", DucoDevice, sensor.Sensor)
|
||||
DucoReading = duco_ns.enum("DucoReading", is_class=True)
|
||||
|
||||
BOX_NODE = 1
|
||||
|
||||
|
||||
def _schema(update_interval: str = "60s", **kwargs) -> cv.Schema:
|
||||
return sensor.sensor_schema(
|
||||
DucoSensor, state_class=STATE_CLASS_MEASUREMENT, **kwargs
|
||||
).extend(cv.polling_component_schema(update_interval))
|
||||
|
||||
|
||||
def _node_schema(**kwargs) -> cv.All:
|
||||
"""A list of sensors, one per node address."""
|
||||
return cv.ensure_list(
|
||||
_schema(**kwargs).extend({cv.Required(CONF_ADDRESS): cv.int_range(0, 68)})
|
||||
)
|
||||
|
||||
|
||||
_TEMPERATURE = {
|
||||
"unit_of_measurement": UNIT_CELSIUS,
|
||||
"accuracy_decimals": 1,
|
||||
"device_class": DEVICE_CLASS_TEMPERATURE,
|
||||
}
|
||||
|
||||
# key: (what is read, request argument or None for the node address, schema)
|
||||
SENSORS = {
|
||||
CONF_CO2: (
|
||||
DucoReading.DUCO_READING_CO2,
|
||||
None,
|
||||
_node_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
|
||||
),
|
||||
),
|
||||
CONF_TEMPERATURE: (
|
||||
DucoReading.DUCO_READING_NODE_TEMPERATURE,
|
||||
None,
|
||||
_node_schema(**_TEMPERATURE),
|
||||
),
|
||||
CONF_HUMIDITY: (
|
||||
DucoReading.DUCO_READING_HUMIDITY,
|
||||
None,
|
||||
_node_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
),
|
||||
),
|
||||
CONF_FILTER_REMAINING: (
|
||||
DucoReading.DUCO_READING_FILTER_REMAINING,
|
||||
0x30,
|
||||
_schema(
|
||||
"1200s",
|
||||
unit_of_measurement=UNIT_DAYS,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
),
|
||||
),
|
||||
CONF_FLOW_LEVEL: (
|
||||
DucoReading.DUCO_READING_FLOW_LEVEL,
|
||||
BOX_NODE,
|
||||
_schema(unit_of_measurement=UNIT_PERCENT, accuracy_decimals=0),
|
||||
),
|
||||
CONF_TIME_REMAINING: (
|
||||
DucoReading.DUCO_READING_TIME_REMAINING,
|
||||
BOX_NODE,
|
||||
_schema(
|
||||
unit_of_measurement=UNIT_SECOND,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_DURATION,
|
||||
),
|
||||
),
|
||||
CONF_TEMPERATURE_ODA: (
|
||||
DucoReading.DUCO_READING_BOX_TEMPERATURE,
|
||||
0x00,
|
||||
_schema(**_TEMPERATURE),
|
||||
),
|
||||
CONF_TEMPERATURE_SUP: (
|
||||
DucoReading.DUCO_READING_BOX_TEMPERATURE,
|
||||
0x01,
|
||||
_schema(**_TEMPERATURE),
|
||||
),
|
||||
CONF_TEMPERATURE_ETA: (
|
||||
DucoReading.DUCO_READING_BOX_TEMPERATURE,
|
||||
0x02,
|
||||
_schema(**_TEMPERATURE),
|
||||
),
|
||||
CONF_TEMPERATURE_EHA: (
|
||||
DucoReading.DUCO_READING_BOX_TEMPERATURE,
|
||||
0x03,
|
||||
_schema(**_TEMPERATURE),
|
||||
),
|
||||
CONF_BYPASS: (
|
||||
DucoReading.DUCO_READING_BYPASS,
|
||||
0x10,
|
||||
_schema(unit_of_measurement=UNIT_PERCENT, accuracy_decimals=0),
|
||||
),
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = DUCO_COMPONENT_SCHEMA.extend(
|
||||
{cv.Optional(key): schema for key, (_, _, schema) in SENSORS.items()}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
for key, (reading, argument, _) in SENSORS.items():
|
||||
if (confs := config.get(key)) is None:
|
||||
continue
|
||||
for conf in confs if isinstance(confs, list) else [confs]:
|
||||
var = await sensor.new_sensor(
|
||||
conf, reading, conf.get(CONF_ADDRESS, argument)
|
||||
)
|
||||
await register_duco_device(var, conf, config[CONF_DUCO_ID])
|
||||
@@ -1,78 +0,0 @@
|
||||
#include "sensor.h"
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::duco {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "duco.sensor");
|
||||
|
||||
enum class DucoValueFormat : uint8_t {
|
||||
DUCO_VALUE_FORMAT_UINT8,
|
||||
DUCO_VALUE_FORMAT_UINT16,
|
||||
DUCO_VALUE_FORMAT_INT16,
|
||||
};
|
||||
|
||||
/// A little endian field at offset in the answer. Values outside [minimum, maximum] are dropped;
|
||||
/// the box reports those for sensors that are missing or still starting.
|
||||
struct ReadingLayout {
|
||||
uint8_t function;
|
||||
uint8_t offset;
|
||||
DucoValueFormat format;
|
||||
uint8_t divisor;
|
||||
int16_t minimum;
|
||||
uint16_t maximum;
|
||||
};
|
||||
|
||||
// Indexed by DucoReading
|
||||
static constexpr ReadingLayout READINGS[] PROGMEM = {
|
||||
{FUNCTION_NODE_DATA, 4, DucoValueFormat::DUCO_VALUE_FORMAT_UINT16, 1, 300, 10000}, // CO2
|
||||
{FUNCTION_NODE_DATA, 2, DucoValueFormat::DUCO_VALUE_FORMAT_UINT16, 10, 0, 1000}, // node temperature
|
||||
{FUNCTION_NODE_DATA, 6, DucoValueFormat::DUCO_VALUE_FORMAT_UINT16, 100, 0, 10000}, // humidity
|
||||
{FUNCTION_PARAMETER, 3, DucoValueFormat::DUCO_VALUE_FORMAT_UINT8, 1, 0, 255}, // filter remaining
|
||||
{FUNCTION_NODE, 2, DucoValueFormat::DUCO_VALUE_FORMAT_UINT8, 1, 0, 255}, // flow level
|
||||
{FUNCTION_NODE, 12, DucoValueFormat::DUCO_VALUE_FORMAT_UINT16, 1, 0, 65535}, // time remaining
|
||||
{FUNCTION_PARAMETER, 3, DucoValueFormat::DUCO_VALUE_FORMAT_INT16, 10, -999, 1000}, // box temperature
|
||||
{FUNCTION_PARAMETER, 3, DucoValueFormat::DUCO_VALUE_FORMAT_UINT8, 1, 0, 100}, // bypass
|
||||
};
|
||||
|
||||
static ReadingLayout layout_of(DucoReading reading) {
|
||||
ReadingLayout layout;
|
||||
progmem_memcpy(&layout, &READINGS[static_cast<uint8_t>(reading)], sizeof(layout));
|
||||
return layout;
|
||||
}
|
||||
|
||||
void DucoSensor::update() {
|
||||
switch (layout_of(this->reading_).function) {
|
||||
case FUNCTION_NODE_DATA:
|
||||
this->parent_->send(this, FUNCTION_NODE_DATA, {0x01, this->argument_, 0x00, 0x49, 0x04});
|
||||
break;
|
||||
case FUNCTION_PARAMETER:
|
||||
this->parent_->send(this, FUNCTION_PARAMETER, {0x00, this->argument_, 0x09});
|
||||
break;
|
||||
case FUNCTION_NODE:
|
||||
this->parent_->send(this, FUNCTION_NODE, {0x02, this->argument_});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void DucoSensor::receive_response(const DucoMessage &message) {
|
||||
const ReadingLayout layout = layout_of(this->reading_);
|
||||
const bool wide = layout.format != DucoValueFormat::DUCO_VALUE_FORMAT_UINT8;
|
||||
if (message.size < layout.offset + (wide ? 2 : 1)) {
|
||||
ESP_LOGV(TAG, "Answer of %u bytes is too short", message.size);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t *field = message.data + layout.offset;
|
||||
int32_t value = field[0];
|
||||
if (layout.format == DucoValueFormat::DUCO_VALUE_FORMAT_UINT16) {
|
||||
value = encode_value_le<uint16_t>(field);
|
||||
} else if (layout.format == DucoValueFormat::DUCO_VALUE_FORMAT_INT16) {
|
||||
value = encode_value_le<int16_t>(field);
|
||||
}
|
||||
if (value >= layout.minimum && value <= layout.maximum)
|
||||
this->publish_state(static_cast<float>(value) / layout.divisor);
|
||||
}
|
||||
|
||||
} // namespace esphome::duco
|
||||
@@ -1,34 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
#include "../duco.h"
|
||||
|
||||
namespace esphome::duco {
|
||||
|
||||
/// What a sensor reads; the request and where the value is in the answer are in READINGS in sensor.cpp.
|
||||
enum class DucoReading : uint8_t {
|
||||
DUCO_READING_CO2,
|
||||
DUCO_READING_NODE_TEMPERATURE,
|
||||
DUCO_READING_HUMIDITY,
|
||||
DUCO_READING_FILTER_REMAINING,
|
||||
DUCO_READING_FLOW_LEVEL,
|
||||
DUCO_READING_TIME_REMAINING,
|
||||
DUCO_READING_BOX_TEMPERATURE,
|
||||
DUCO_READING_BYPASS,
|
||||
};
|
||||
|
||||
class DucoSensor : public DucoDevice, public sensor::Sensor {
|
||||
public:
|
||||
/// argument is the node address, or the parameter number for a box parameter.
|
||||
DucoSensor(DucoReading reading, uint8_t argument) : reading_(reading), argument_(argument) {}
|
||||
|
||||
void update() override;
|
||||
void receive_response(const DucoMessage &message) override;
|
||||
|
||||
protected:
|
||||
DucoReading reading_;
|
||||
uint8_t argument_;
|
||||
};
|
||||
|
||||
} // namespace esphome::duco
|
||||
@@ -1,27 +0,0 @@
|
||||
from esphome.components import text_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
CONF_DUCO_ID,
|
||||
DUCO_COMPONENT_SCHEMA,
|
||||
DucoDevice,
|
||||
duco_ns,
|
||||
register_duco_device,
|
||||
)
|
||||
|
||||
DEPENDENCIES = ["duco"]
|
||||
CODEOWNERS = ["@kokx"]
|
||||
|
||||
DucoSerial = duco_ns.class_("DucoSerial", DucoDevice, text_sensor.TextSensor)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
text_sensor.text_sensor_schema(DucoSerial)
|
||||
.extend(cv.polling_component_schema("300s"))
|
||||
.extend(DUCO_COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await text_sensor.new_text_sensor(config)
|
||||
await register_duco_device(var, config, config[CONF_DUCO_ID])
|
||||
@@ -1,17 +0,0 @@
|
||||
#include "serial.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::duco {
|
||||
|
||||
void DucoSerial::update() { this->parent_->send(this, FUNCTION_NODE_DATA, {0x01, 0x01, 0x00, 0x1a, 0x10}); }
|
||||
|
||||
void DucoSerial::receive_response(const DucoMessage &message) {
|
||||
if (message.size > 2) {
|
||||
// The serial number starts at byte 2 and is padded with zeros
|
||||
const char *serial = reinterpret_cast<const char *>(message.data + 2);
|
||||
this->publish_state(serial, strnlen(serial, message.size - 2));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::duco
|
||||
@@ -1,15 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
|
||||
#include "../duco.h"
|
||||
|
||||
namespace esphome::duco {
|
||||
|
||||
class DucoSerial : public DucoDevice, public text_sensor::TextSensor {
|
||||
public:
|
||||
void update() override;
|
||||
void receive_response(const DucoMessage &message) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::duco
|
||||
@@ -15,7 +15,6 @@ from esphome.components.mipi import (
|
||||
import esphome.config_validation as cv
|
||||
from esphome.config_validation import update_interval
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_BUSY_PIN,
|
||||
CONF_CS_PIN,
|
||||
CONF_DATA_RATE,
|
||||
@@ -189,10 +188,7 @@ def customise_schema(config):
|
||||
height,
|
||||
has_hardware_rotation=True,
|
||||
byte_order=cv.UNDEFINED,
|
||||
has_writer=config.get(CONF_AUTO_CLEAR_ENABLED) is True
|
||||
or config.get(CONF_PAGES) is not None
|
||||
or config.get(CONF_LAMBDA) is not None
|
||||
or config.get(CONF_SHOW_TEST_CARD) is True,
|
||||
has_writer=display.config_has_writer(config),
|
||||
rotation=config.get(CONF_ROTATION, 0),
|
||||
draw_rounding=0,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "epaper_spi.h"
|
||||
#include <algorithm>
|
||||
#include "esphome/components/display/init_sequence.h"
|
||||
#include <cinttypes>
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -38,42 +38,6 @@ bool EPaperBase::init_buffer_(size_t buffer_length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EPaperBase::init_sent_frame_(size_t length) {
|
||||
if (!this->is_using_partial_update_())
|
||||
return false;
|
||||
if (this->sent_.init(length))
|
||||
return true;
|
||||
ESP_LOGW(TAG, "No memory for the previous frame; partial updates will send the whole drawn area");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t EPaperBase::buffer_hash_() const {
|
||||
uint32_t hash = FNV1_OFFSET_BASIS;
|
||||
size_t index = 0;
|
||||
while (index != this->buffer_.size()) {
|
||||
size_t length;
|
||||
const uint8_t *data = this->buffer_.get_span(index, length);
|
||||
for (size_t i = 0; i != length; i++)
|
||||
hash = fnv1a_hash_extend(hash, data[i]);
|
||||
index += length;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
void EPaperBase::set_full_bounds_() {
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
this->x_high_ = this->width_;
|
||||
this->y_high_ = this->height_;
|
||||
}
|
||||
|
||||
void EPaperBase::reset_bounds_() {
|
||||
this->x_low_ = this->width_;
|
||||
this->x_high_ = 0;
|
||||
this->y_low_ = this->height_;
|
||||
this->y_high_ = 0;
|
||||
}
|
||||
|
||||
void EPaperBase::setup_pins_() const {
|
||||
for (auto *pin : this->enable_pins_) {
|
||||
pin->setup();
|
||||
@@ -270,28 +234,17 @@ void EPaperBase::process_state_() {
|
||||
// Refresh the whole panel even if nothing was drawn
|
||||
this->full_update_requested_ = false;
|
||||
this->update_count_ = 0;
|
||||
this->set_full_bounds_();
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
this->x_high_ = this->width_;
|
||||
this->y_high_ = this->height_;
|
||||
}
|
||||
if (this->x_high_ < this->x_low_ || this->y_high_ < this->y_low_) {
|
||||
this->set_state_(EPaperState::IDLE);
|
||||
return;
|
||||
}
|
||||
if (this->is_using_partial_update_()) {
|
||||
const uint32_t frame_hash = this->buffer_hash_();
|
||||
if (this->update_count_ != 0 && this->shown_hash_valid_ && frame_hash == this->shown_hash_) {
|
||||
ESP_LOGD(TAG, "Frame unchanged, refresh skipped");
|
||||
this->reset_bounds_();
|
||||
this->set_state_(EPaperState::IDLE);
|
||||
return;
|
||||
}
|
||||
this->shown_hash_ = frame_hash;
|
||||
}
|
||||
this->drawn_since_hash_ = false;
|
||||
// Valid again only once this frame is on the panel, so an abandoned update leaves nothing to match
|
||||
this->shown_hash_valid_ = false;
|
||||
// A full update resets update_count_ to zero, so a non-zero count marks a partial one
|
||||
this->partial_ = this->update_count_ != 0;
|
||||
this->sent_count_ = 0;
|
||||
this->set_state_(EPaperState::RESET);
|
||||
break;
|
||||
case EPaperState::INITIALISE:
|
||||
@@ -304,15 +257,10 @@ void EPaperBase::process_state_() {
|
||||
if (!this->transfer_data()) {
|
||||
return; // Not done yet, come back next loop
|
||||
}
|
||||
if (this->sent_.is_valid() && this->sent_count_ == this->sent_.size())
|
||||
this->sent_valid_ = true;
|
||||
// A draw during the chunked push may not have reached the panel, so the next update sends the whole frame
|
||||
this->sent_hash_intact_ = this->is_using_partial_update_() && !this->drawn_since_hash_;
|
||||
if (this->drawn_since_hash_) {
|
||||
this->set_full_bounds_();
|
||||
} else {
|
||||
this->reset_bounds_();
|
||||
}
|
||||
this->x_low_ = this->width_;
|
||||
this->x_high_ = 0;
|
||||
this->y_low_ = this->height_;
|
||||
this->y_high_ = 0;
|
||||
this->set_state_(EPaperState::POWER_ON);
|
||||
break;
|
||||
case EPaperState::POWER_ON:
|
||||
@@ -333,7 +281,6 @@ void EPaperBase::process_state_() {
|
||||
break;
|
||||
case EPaperState::DEEP_SLEEP:
|
||||
this->deep_sleep();
|
||||
this->shown_hash_valid_ = this->sent_hash_intact_;
|
||||
this->status_clear_warning();
|
||||
this->set_state_(EPaperState::IDLE);
|
||||
ESP_LOGD(TAG, "Display update took %" PRIu32 " ms", millis() - this->update_start_time_);
|
||||
@@ -369,26 +316,18 @@ void EPaperBase::start_data_() {
|
||||
void EPaperBase::on_safe_shutdown() { this->deep_sleep(); }
|
||||
|
||||
void EPaperBase::send_init_sequence_(const uint8_t *sequence, size_t length) {
|
||||
size_t index = 0;
|
||||
|
||||
while (index != length) {
|
||||
if (length - index < 2) {
|
||||
display::InitSequenceReader reader(sequence, length);
|
||||
display::InitSequenceEntry entry;
|
||||
while (auto step = reader.next(entry)) {
|
||||
if (step == display::INIT_SEQUENCE_STEP_MALFORMED) {
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
const uint8_t cmd = sequence[index++];
|
||||
if (const uint8_t x = sequence[index++]; x == DELAY_FLAG) {
|
||||
ESP_LOGV(TAG, "Delay %dms", cmd);
|
||||
delay(cmd);
|
||||
if (step == display::INIT_SEQUENCE_STEP_DELAY) {
|
||||
ESP_LOGV(TAG, "Delay %dms", entry.value);
|
||||
delay(entry.value);
|
||||
} else {
|
||||
const uint8_t num_args = x & 0x7F;
|
||||
if (length - index < num_args) {
|
||||
ESP_LOGE(TAG, "Malformed init sequence, cmd = %X, num_args = %u", cmd, num_args);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->cmd_data(cmd, sequence + index, num_args);
|
||||
index += num_args;
|
||||
this->cmd_data(entry.value, entry.args, entry.count);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -419,7 +358,6 @@ bool EPaperBase::rotate_coordinates_(int &x, int &y) {
|
||||
this->x_high_ = clamp_at_least(this->x_high_, x + 1);
|
||||
this->y_low_ = clamp_at_most(this->y_low_, y);
|
||||
this->y_high_ = clamp_at_least(this->y_high_, y + 1);
|
||||
this->drawn_since_hash_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -541,7 +479,6 @@ void HOT EPaperBase::draw_rgb565_block_(uint8_t *buffer, int x_start, int y_star
|
||||
this->x_high_ = clamp_at_least(this->x_high_, x_high);
|
||||
this->y_low_ = clamp_at_most(this->y_low_, y_low);
|
||||
this->y_high_ = clamp_at_least(this->y_high_, y_high);
|
||||
this->drawn_since_hash_ = true;
|
||||
}
|
||||
|
||||
// Writes an RGB565 block of pixels, as LVGL sends them, straight into a 1 bit per pixel buffer that is one block (the
|
||||
|
||||
@@ -34,7 +34,6 @@ static constexpr uint32_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to
|
||||
static constexpr size_t MAX_TRANSFER_SIZE = 128;
|
||||
// Longer busy waits are the waveform or a stuck pin, and are polled at the normal loop pace
|
||||
static constexpr uint32_t FAST_BUSY_POLL_MS = 50;
|
||||
static constexpr uint8_t DELAY_FLAG = 0xFF;
|
||||
|
||||
class EPaperBase : public Display,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||
@@ -99,7 +98,10 @@ class EPaperBase : public Display,
|
||||
|
||||
// We store 8 pixels per byte
|
||||
this->buffer_.fill(pixel_color);
|
||||
this->mark_all_drawn_();
|
||||
this->x_high_ = this->width_;
|
||||
this->y_high_ = this->height_;
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
}
|
||||
|
||||
void clear() override {
|
||||
@@ -136,25 +138,6 @@ class EPaperBase : public Display,
|
||||
void wait_for_idle_(bool should_wait);
|
||||
void abandon_update_();
|
||||
bool init_buffer_(size_t buffer_length);
|
||||
// Allocates the copy of the previous frame; only drivers that diff against it (needs_previous_frame) call this
|
||||
bool init_sent_frame_(size_t length);
|
||||
void record_sent_(size_t index, const uint8_t *data, size_t length) {
|
||||
if (this->sent_.is_valid()) {
|
||||
this->sent_.write(index, data, length);
|
||||
this->sent_count_ += length;
|
||||
}
|
||||
}
|
||||
// Whether the driver reads the previous frame (sent_) for its differential refresh
|
||||
virtual bool needs_previous_frame() const { return false; }
|
||||
// FNV-1a hash of the current frame buffer, to detect an unchanged frame without a full copy
|
||||
uint32_t buffer_hash_() const;
|
||||
void reset_bounds_();
|
||||
void set_full_bounds_();
|
||||
// For a draw that writes the whole buffer without going through rotate_coordinates_()
|
||||
void mark_all_drawn_() {
|
||||
this->set_full_bounds_();
|
||||
this->drawn_since_hash_ = true;
|
||||
}
|
||||
void update_effective_transform_();
|
||||
bool rotate_coordinates_(int &x, int &y);
|
||||
void draw_rgb565_block_(uint8_t *buffer, int x_start, int y_start, int w, int h, const uint8_t *ptr, bool big_endian,
|
||||
@@ -216,13 +199,6 @@ class EPaperBase : public Display,
|
||||
size_t buffer_length_;
|
||||
size_t current_data_index_{}; // used by data transfer to track progress
|
||||
split_buffer::SplitBuffer buffer_{};
|
||||
split_buffer::SplitBuffer sent_{}; // the previous frame, kept only by drivers that diff against it
|
||||
bool sent_valid_{}; // sent_ holds a whole frame
|
||||
size_t sent_count_{}; // bytes recorded into sent_ by the update in progress
|
||||
uint32_t shown_hash_{}; // hash of the frame on the panel, to skip an unchanged update
|
||||
bool shown_hash_valid_{}; // shown_hash_ holds the hash of a frame whose refresh completed
|
||||
bool sent_hash_intact_{}; // the buffer was unchanged by the end of the push of shown_hash_
|
||||
bool drawn_since_hash_{}; // something was drawn after shown_hash_ was taken
|
||||
GPIOPin *dc_pin_{};
|
||||
GPIOPin *busy_pin_{};
|
||||
GPIOPin *reset_pin_{};
|
||||
|
||||
@@ -18,7 +18,11 @@ void EPaper4bpp::fill(Color color) {
|
||||
// We store 2 pixels per byte
|
||||
this->buffer_.fill(pixel_color + (pixel_color << 4));
|
||||
|
||||
this->mark_all_drawn_();
|
||||
// Whole buffer just changed; mark the entire canvas dirty.
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
this->x_high_ = this->width_;
|
||||
this->y_high_ = this->height_;
|
||||
}
|
||||
|
||||
void EPaper4bpp::clear() {
|
||||
|
||||
@@ -71,7 +71,10 @@ void EPaperInkplate2::fill(Color color) {
|
||||
for (size_t i = half_buffer; i < this->buffer_length_; i++)
|
||||
this->buffer_[i] = red_byte;
|
||||
|
||||
this->mark_all_drawn_();
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
this->x_high_ = this->width_;
|
||||
this->y_high_ = this->height_;
|
||||
}
|
||||
|
||||
void EPaperInkplate2::clear() { this->fill(COLOR_ON); }
|
||||
|
||||
@@ -30,7 +30,6 @@ void EPaperJD79660::fill(Color color) {
|
||||
|
||||
// We store 4 pixels per byte
|
||||
this->buffer_.fill(pixel_color | (pixel_color << 2) | (pixel_color << 4) | (pixel_color << 6));
|
||||
this->drawn_since_hash_ = true;
|
||||
}
|
||||
|
||||
void HOT EPaperJD79660::draw_pixel_at(int x, int y, Color color) {
|
||||
|
||||
@@ -7,12 +7,6 @@
|
||||
namespace esphome::epaper_spi {
|
||||
ESPHOME_LOG_TAG(TAG, "epaper_spi.mono");
|
||||
|
||||
void EPaperMono::setup() {
|
||||
EPaperBase::setup();
|
||||
if (!this->is_failed() && this->needs_previous_frame())
|
||||
this->init_sent_frame_(this->sent_frame_length());
|
||||
}
|
||||
|
||||
void EPaperMono::refresh_screen(bool partial) {
|
||||
ESP_LOGV(TAG, "Refresh screen");
|
||||
this->cmd_data(0x22, {partial ? (uint8_t) 0xFF : (uint8_t) 0xF7});
|
||||
@@ -72,8 +66,7 @@ bool HOT EPaperMono::transfer_data() {
|
||||
ESP_LOGV(TAG, "Writing %u bytes at line %zu at %ums", row_length, this->current_data_index_, (unsigned) millis());
|
||||
this->start_data_();
|
||||
while (this->current_data_index_ != this->y_high_) {
|
||||
const size_t row_start = this->current_data_index_ * this->row_width_ + this->x_low_ / 8;
|
||||
size_t data_idx = row_start;
|
||||
size_t data_idx = this->current_data_index_ * this->row_width_ + this->x_low_ / 8;
|
||||
for (size_t i = 0; i != row_length; i++) {
|
||||
bytes_to_send[i] = this->send_red_ && !this->send_red_as_image_ ? 0 : this->buffer_[data_idx++];
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ class EPaperMono : public EPaperBase {
|
||||
size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY, uint8_t bits_per_pixel = 1)
|
||||
: EPaperBase(name, width, height, init_sequence, init_sequence_length, display_type, bits_per_pixel) {}
|
||||
|
||||
void setup() override;
|
||||
|
||||
protected:
|
||||
void refresh_screen(bool partial) override;
|
||||
void power_on() override {}
|
||||
@@ -21,8 +19,6 @@ class EPaperMono : public EPaperBase {
|
||||
void deep_sleep() override;
|
||||
bool reset() override;
|
||||
virtual void set_window();
|
||||
// Bytes in the comparison frame; the buffer's size unless a subclass sends the frame in another layout
|
||||
virtual size_t sent_frame_length() const { return this->buffer_length_; }
|
||||
bool transfer_data() override;
|
||||
bool send_red_{true};
|
||||
bool software_reset_{true}; // send 0x12 after the hardware reset
|
||||
|
||||
@@ -6,6 +6,21 @@
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
ESPHOME_LOG_TAG(TAG, "epaper_spi.ssd1677");
|
||||
|
||||
void EPaperSSD1677::setup() {
|
||||
EPaperMono::setup();
|
||||
if (!this->is_failed())
|
||||
this->init_comparison_frame_();
|
||||
}
|
||||
|
||||
void EPaperSSD1677::init_comparison_frame_() {
|
||||
if (!this->is_using_partial_update_())
|
||||
return;
|
||||
if (!this->sent_.init(this->plane_row_length_() * this->height_)) {
|
||||
ESP_LOGW(TAG, "No memory for the comparison frame; partial updates will degrade unchanged areas");
|
||||
}
|
||||
}
|
||||
|
||||
void EPaperSSD1677::plane_row(size_t y, uint8_t *out) {
|
||||
const size_t row_length = this->plane_row_length_();
|
||||
@@ -67,7 +82,7 @@ bool HOT EPaperSSD1677::transfer_data() {
|
||||
}
|
||||
this->write_array(data, length);
|
||||
if (this->plane_ == 1)
|
||||
this->record_sent_(this->current_data_index_, data, length);
|
||||
this->sent_.write(this->current_data_index_, data, length);
|
||||
this->current_data_index_ += length;
|
||||
if (this->current_data_index_ != plane_length && millis() - start_time > MAX_TRANSFER_TIME) {
|
||||
// Let the main loop run and come back next loop
|
||||
|
||||
@@ -26,12 +26,12 @@ class EPaperSSD1677 : public EPaperMono {
|
||||
size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY, uint8_t bits_per_pixel = 1)
|
||||
: EPaperMono(name, width, height, init_sequence, init_sequence_length, display_type, bits_per_pixel) {}
|
||||
|
||||
void setup() override;
|
||||
|
||||
protected:
|
||||
// Reads the previous frame to copy it into the other RAM bank on a full refresh
|
||||
bool needs_previous_frame() const override { return true; }
|
||||
// Allocates the comparison frame as setup() would, for tests that run without a bus.
|
||||
void init_comparison_frame_() { this->init_sent_frame_(this->sent_frame_length()); }
|
||||
size_t sent_frame_length() const override { return this->plane_row_length_() * this->height_; }
|
||||
// Allocates the comparison frame when partial updates are enabled. Separate from setup() so it
|
||||
// can run without a bus.
|
||||
void init_comparison_frame_();
|
||||
// Bytes in one row of a RAM plane: 8 pixels per byte, whatever the buffer holds.
|
||||
size_t plane_row_length_() const { return (this->width_ + 7) / 8; }
|
||||
// Row y of the frame as a RAM plane holds it, 1 bit per pixel with 1 = white.
|
||||
@@ -39,7 +39,8 @@ class EPaperSSD1677 : public EPaperMono {
|
||||
bool reset() override;
|
||||
bool transfer_data() override;
|
||||
|
||||
uint8_t plane_{0}; // 0 while sending 0x26, 1 while sending 0x24
|
||||
split_buffer::SplitBuffer sent_{}; // the frame last sent to 0x24, i.e. what the panel shows
|
||||
uint8_t plane_{0}; // 0 while sending 0x26, 1 while sending 0x24
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -29,7 +29,10 @@ void EPaperSSD1677Gray4::fill(Color color) {
|
||||
}
|
||||
const uint8_t level = color_to_gray4(color);
|
||||
this->buffer_.fill((uint8_t) (level | (level << 2) | (level << 4) | (level << 6)));
|
||||
this->mark_all_drawn_();
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
this->x_high_ = this->width_;
|
||||
this->y_high_ = this->height_;
|
||||
}
|
||||
|
||||
void HOT EPaperSSD1677Gray4::draw_pixel_at(int x, int y, Color color) {
|
||||
@@ -80,14 +83,14 @@ bool HOT EPaperSSD1677Gray4::transfer_data() {
|
||||
this->start_data_();
|
||||
while (this->current_data_index_ != this->y_high_) {
|
||||
const size_t src_row = this->current_data_index_ * this->row_width_ + this->x_low_ / 4;
|
||||
for (size_t i = 0; i != plane_row_length; i++)
|
||||
bytes_to_send[i] = plane_byte(this->buffer_[src_row + 2 * i], this->buffer_[src_row + 2 * i + 1], first_pass);
|
||||
// What the next partial update compares against: the high bits, as a black-and-white frame.
|
||||
if (first_pass)
|
||||
this->record_sent_(this->current_data_index_ * plane_row_length, bytes_to_send, plane_row_length);
|
||||
// The OTP grayscale waveform treats data as inverted relative to monochrome
|
||||
for (size_t i = 0; i != plane_row_length; i++)
|
||||
bytes_to_send[i] = (uint8_t) ~bytes_to_send[i];
|
||||
for (size_t i = 0; i != plane_row_length; i++) {
|
||||
const uint8_t plane = plane_byte(this->buffer_[src_row + 2 * i], this->buffer_[src_row + 2 * i + 1], first_pass);
|
||||
// The OTP grayscale waveform treats data as inverted relative to monochrome
|
||||
bytes_to_send[i] = (uint8_t) ~plane;
|
||||
// What the next partial update compares against: the high bits, as a black-and-white frame.
|
||||
if (first_pass && this->sent_.is_valid())
|
||||
this->sent_[this->current_data_index_ * plane_row_length + i] = plane;
|
||||
}
|
||||
++this->current_data_index_;
|
||||
this->write_array(bytes_to_send, plane_row_length);
|
||||
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
||||
|
||||
@@ -1,50 +1,10 @@
|
||||
#include "epaper_spi_ssd1681.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
ESPHOME_LOG_TAG(TAG, "epaper_spi.ssd1681");
|
||||
|
||||
void EPaperSSD1681::bounds_from_changes_() {
|
||||
if (!this->sent_valid_ || this->sent_.size() != this->buffer_.size())
|
||||
return;
|
||||
bool changed = false;
|
||||
uint16_t row_low = this->height_, row_high = 0, col_low = this->row_width_, col_high = 0;
|
||||
for (uint16_t row = 0; row != this->height_; row++) {
|
||||
const size_t base = row * this->row_width_;
|
||||
const size_t end = base + this->row_width_;
|
||||
size_t index = base;
|
||||
while (index != end) {
|
||||
size_t new_length, old_length;
|
||||
const uint8_t *new_bytes = this->buffer_.get_span(index, new_length);
|
||||
const uint8_t *old_bytes = this->sent_.get_span(index, old_length);
|
||||
const size_t length = std::min({new_length, old_length, end - index});
|
||||
if (std::memcmp(new_bytes, old_bytes, length) != 0) {
|
||||
for (size_t i = 0; i != length; i++) {
|
||||
if (new_bytes[i] == old_bytes[i])
|
||||
continue;
|
||||
const auto col = static_cast<uint16_t>(index + i - base);
|
||||
changed = true;
|
||||
row_low = std::min(row_low, row);
|
||||
row_high = std::max<uint16_t>(row_high, row + 1);
|
||||
col_low = std::min(col_low, col);
|
||||
col_high = std::max<uint16_t>(col_high, col + 1);
|
||||
}
|
||||
}
|
||||
index += length;
|
||||
}
|
||||
}
|
||||
if (!changed)
|
||||
return;
|
||||
this->x_low_ = col_low * 8;
|
||||
this->x_high_ = std::min<uint16_t>(col_high * 8, this->width_);
|
||||
this->y_low_ = row_low;
|
||||
this->y_high_ = row_high;
|
||||
}
|
||||
|
||||
bool HOT EPaperSSD1681::transfer_data() {
|
||||
if (this->current_data_index_ == 0 && this->send_red_) {
|
||||
if (!this->partial_) {
|
||||
@@ -55,7 +15,6 @@ bool HOT EPaperSSD1681::transfer_data() {
|
||||
this->y_high_ = this->height_;
|
||||
} else {
|
||||
// 0x26 already holds the image on the panel: the controller copies 0x24 into it after each refresh
|
||||
this->bounds_from_changes_();
|
||||
this->set_window();
|
||||
this->send_red_ = false;
|
||||
}
|
||||
|
||||
@@ -16,10 +16,6 @@ class EPaperSSD1681 : public EPaperSSD1683 {
|
||||
}
|
||||
|
||||
protected:
|
||||
// Reads the previous frame to compute the changed window for a partial refresh
|
||||
bool needs_previous_frame() const override { return true; }
|
||||
// Shrinks the update bounds to the bytes that differ from the frame last sent
|
||||
void bounds_from_changes_();
|
||||
bool transfer_data() override;
|
||||
void refresh_screen(bool partial) override;
|
||||
};
|
||||
|
||||
@@ -71,15 +71,12 @@ bool HOT EPaperSSD1683::transfer_data() {
|
||||
ESP_LOGV(TAG, "Writing %u bytes at line %zu at %ums", row_length, this->current_data_index_, (unsigned) millis());
|
||||
this->start_data_();
|
||||
while (this->current_data_index_ != this->y_high_) {
|
||||
const size_t row_start = this->current_data_index_ * this->row_width_ + this->x_low_;
|
||||
size_t data_idx = row_start;
|
||||
size_t data_idx = this->current_data_index_ * this->row_width_ + this->x_low_;
|
||||
for (size_t i = 0; i != row_length; i++) {
|
||||
bytes_to_send[i] = this->buffer_[data_idx++];
|
||||
}
|
||||
++this->current_data_index_;
|
||||
this->write_array(&bytes_to_send.front(), row_length); // NOLINT
|
||||
if (!this->send_red_)
|
||||
this->record_sent_(row_start, &bytes_to_send.front(), row_length);
|
||||
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
||||
// Let the main loop run and come back next loop
|
||||
this->disable();
|
||||
|
||||
@@ -232,7 +232,6 @@ void EPaperT133A01::fill(Color color) {
|
||||
}
|
||||
auto pixel_color = color_to_index(color);
|
||||
this->buffer_.fill(pixel_color + (pixel_color << 4));
|
||||
this->drawn_since_hash_ = true;
|
||||
}
|
||||
|
||||
void EPaperT133A01::draw_pixel_at(int x, int y, Color color) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "epaper_waveshare_bwr.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
@@ -37,9 +36,17 @@ void EPaperWaveshareBWR::draw_pixel_at(int x, int y, Color color) {
|
||||
|
||||
const auto bwr = color_to_bwr(color);
|
||||
|
||||
assign_bits(this->buffer_[pos], bit, bwr == BwrState::BWR_BLACK);
|
||||
if (bwr == BwrState::BWR_BLACK) {
|
||||
this->buffer_[pos] |= bit;
|
||||
} else {
|
||||
this->buffer_[pos] &= ~bit;
|
||||
}
|
||||
|
||||
assign_bits(this->buffer_[red_offset + pos], bit, bwr == BwrState::BWR_RED);
|
||||
if (bwr == BwrState::BWR_RED) {
|
||||
this->buffer_[red_offset + pos] |= bit;
|
||||
} else {
|
||||
this->buffer_[red_offset + pos] &= ~bit;
|
||||
}
|
||||
}
|
||||
|
||||
void EPaperWaveshareBWR::fill(Color color) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "es8156.h"
|
||||
#include "es8156_const.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
|
||||
@@ -101,7 +100,11 @@ bool ES8156::set_mute_state_(bool mute_state) {
|
||||
|
||||
ESP_LOGV(TAG, "Read ES8156_REG13_DAC_MUTE: %u", reg13);
|
||||
|
||||
assign_bits(reg13, BIT(1) | BIT(2), mute_state);
|
||||
if (mute_state) {
|
||||
reg13 |= BIT(1) | BIT(2);
|
||||
} else {
|
||||
reg13 &= ~(BIT(1) | BIT(2));
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Setting ES8156_REG13_DAC_MUTE to %u (muted: %s)", reg13, YESNO(mute_state));
|
||||
return this->write_byte(ES8156_REG13_DAC_MUTE, reg13);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "es8311.h"
|
||||
#include "es8311_const.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
|
||||
@@ -155,7 +154,11 @@ bool ES8311::configure_clock_() {
|
||||
// Register 0x06
|
||||
uint8_t reg06;
|
||||
ES8311_ERROR_CHECK(this->read_byte(ES8311_REG06_CLK_MANAGER, ®06));
|
||||
assign_bits(reg06, BIT(5), this->sclk_inverted_);
|
||||
if (this->sclk_inverted_) {
|
||||
reg06 |= BIT(5);
|
||||
} else {
|
||||
reg06 &= ~BIT(5);
|
||||
}
|
||||
reg06 &= 0xE0;
|
||||
if (coefficient->bclk_div < 19) {
|
||||
reg06 |= (coefficient->bclk_div - 1) << 0;
|
||||
@@ -220,7 +223,11 @@ bool ES8311::set_mute_state_(bool mute_state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
assign_bits(reg31, BIT(6) | BIT(5), mute_state);
|
||||
if (mute_state) {
|
||||
reg31 |= BIT(6) | BIT(5);
|
||||
} else {
|
||||
reg31 &= ~(BIT(6) | BIT(5));
|
||||
}
|
||||
|
||||
return this->write_byte(ES8311_REG31_DAC, reg31);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <cinttypes>
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::es8388 {
|
||||
@@ -185,7 +184,11 @@ bool ES8388::set_mute_state_(bool mute_state) {
|
||||
// DAC settings that must be preserved. Previously muting overwrote the whole
|
||||
// register with 0x3C and unmuting never cleared the bit, so once muted the DAC
|
||||
// could not be unmuted again.
|
||||
assign_bits(value, ES8388_DACCONTROL3_DAC_MUTE, mute_state);
|
||||
if (mute_state) {
|
||||
value |= ES8388_DACCONTROL3_DAC_MUTE;
|
||||
} else {
|
||||
value &= ~ES8388_DACCONTROL3_DAC_MUTE;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Setting ES8388_DACCONTROL3 to 0x%02X (muted: %s)", value, YESNO(mute_state));
|
||||
return this->write_byte(ES8388_DACCONTROL3, value);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "gree.h"
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::gree {
|
||||
|
||||
@@ -25,7 +24,11 @@ void GreeClimate::set_model(Model model) {
|
||||
}
|
||||
|
||||
void GreeClimate::set_mode_bit(uint8_t bit_mask, bool enabled) {
|
||||
assign_bits(this->mode_bits_, bit_mask, enabled);
|
||||
if (enabled) {
|
||||
this->mode_bits_ |= bit_mask;
|
||||
} else {
|
||||
this->mode_bits_ &= ~bit_mask;
|
||||
}
|
||||
this->transmit_state();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "hitachi_ac344.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::hitachi_ac344 {
|
||||
|
||||
@@ -17,6 +16,15 @@ void set_bits(uint8_t *const dst, const uint8_t offset, const uint8_t nbits, con
|
||||
*dst |= ((data & mask) << offset);
|
||||
}
|
||||
|
||||
void set_bit(uint8_t *const data, const uint8_t position, const bool on) {
|
||||
uint8_t mask = 1 << position;
|
||||
if (on) {
|
||||
*data |= mask;
|
||||
} else {
|
||||
*data &= ~mask;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t *invert_byte_pairs(uint8_t *ptr, const uint16_t length) {
|
||||
for (uint16_t i = 1; i < length; i += 2) {
|
||||
// Code done this way to avoid a compiler warning bug.
|
||||
@@ -116,7 +124,7 @@ bool HitachiClimate::get_swing_v_toggle_() { return get_button_() == HITACHI_AC3
|
||||
|
||||
void HitachiClimate::set_swing_v_(bool on) {
|
||||
set_swing_v_toggle_(on); // Set the button value.
|
||||
assign_bits(this->remote_state_[HITACHI_AC344_SWINGV_BYTE], 1 << HITACHI_AC344_SWINGV_OFFSET, on);
|
||||
set_bit(&remote_state_[HITACHI_AC344_SWINGV_BYTE], HITACHI_AC344_SWINGV_OFFSET, on);
|
||||
}
|
||||
|
||||
bool HitachiClimate::get_swing_v_() {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "hitachi_ac424.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::hitachi_ac424 {
|
||||
|
||||
@@ -17,6 +16,15 @@ void set_bits(uint8_t *const dst, const uint8_t offset, const uint8_t nbits, con
|
||||
*dst |= ((data & mask) << offset);
|
||||
}
|
||||
|
||||
void set_bit(uint8_t *const data, const uint8_t position, const bool on) {
|
||||
uint8_t mask = 1 << position;
|
||||
if (on) {
|
||||
*data |= mask;
|
||||
} else {
|
||||
*data &= ~mask;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t *invert_byte_pairs(uint8_t *ptr, const uint16_t length) {
|
||||
for (uint16_t i = 1; i < length; i += 2) {
|
||||
// Code done this way to avoid a compiler warning bug.
|
||||
@@ -116,7 +124,7 @@ bool HitachiClimate::get_swing_v_toggle_() { return get_button_() == HITACHI_AC4
|
||||
|
||||
void HitachiClimate::set_swing_v_(bool on) {
|
||||
set_swing_v_toggle_(on); // Set the button value.
|
||||
assign_bits(this->remote_state_[HITACHI_AC424_SWINGV_BYTE], 1 << HITACHI_AC424_SWINGV_OFFSET, on);
|
||||
set_bit(&remote_state_[HITACHI_AC424_SWINGV_BYTE], HITACHI_AC424_SWINGV_OFFSET, on);
|
||||
}
|
||||
|
||||
bool HitachiClimate::get_swing_v_() {
|
||||
|
||||
@@ -45,15 +45,14 @@ void HmacSHA256::get_hex(char *output) {
|
||||
}
|
||||
|
||||
bool HmacSHA256::equals_bytes(const uint8_t *expected) {
|
||||
return constant_time_equals(this->digest_, expected, SHA256_DIGEST_SIZE);
|
||||
return memcmp(this->digest_, expected, SHA256_DIGEST_SIZE) == 0;
|
||||
}
|
||||
|
||||
bool HmacSHA256::equals_hex(const char *expected) {
|
||||
uint8_t parsed[SHA256_DIGEST_SIZE];
|
||||
if (parse_hex(expected, SHA256_DIGEST_SIZE * 2, parsed, SHA256_DIGEST_SIZE) != SHA256_DIGEST_SIZE * 2) {
|
||||
return false;
|
||||
}
|
||||
return this->equals_bytes(parsed);
|
||||
char hex_output[SHA256_DIGEST_SIZE * 2 + 1];
|
||||
this->get_hex(hex_output);
|
||||
hex_output[SHA256_DIGEST_SIZE * 2] = '\0';
|
||||
return strncmp(hex_output, expected, SHA256_DIGEST_SIZE * 2) == 0;
|
||||
}
|
||||
|
||||
#elif defined(USE_HMAC_SHA256_MBEDTLS)
|
||||
@@ -78,15 +77,14 @@ void HmacSHA256::get_hex(char *output) {
|
||||
}
|
||||
|
||||
bool HmacSHA256::equals_bytes(const uint8_t *expected) {
|
||||
return constant_time_equals(this->digest_, expected, SHA256_DIGEST_SIZE);
|
||||
return memcmp(this->digest_, expected, SHA256_DIGEST_SIZE) == 0;
|
||||
}
|
||||
|
||||
bool HmacSHA256::equals_hex(const char *expected) {
|
||||
uint8_t parsed[SHA256_DIGEST_SIZE];
|
||||
if (parse_hex(expected, SHA256_DIGEST_SIZE * 2, parsed, SHA256_DIGEST_SIZE) != SHA256_DIGEST_SIZE * 2) {
|
||||
return false;
|
||||
}
|
||||
return this->equals_bytes(parsed);
|
||||
char hex_output[SHA256_DIGEST_SIZE * 2 + 1];
|
||||
this->get_hex(hex_output);
|
||||
hex_output[SHA256_DIGEST_SIZE * 2] = '\0';
|
||||
return strncmp(hex_output, expected, SHA256_DIGEST_SIZE * 2) == 0;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
@@ -3,36 +3,67 @@
|
||||
|
||||
namespace esphome::max17043 {
|
||||
|
||||
// MAX17043 is a 1-Cell Fuel Gauge with ModelGauge and Low-Battery Alert
|
||||
// MAX174043 is a 1-Cell Fuel Gauge with ModelGauge and Low-Battery Alert
|
||||
// Consult the datasheet at https://www.analog.com/en/products/max17043.html
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "max17043");
|
||||
|
||||
void MAX17043Component::update() {
|
||||
if (!this->ready_for_update_())
|
||||
return;
|
||||
static const uint8_t MAX17043_VCELL = 0x02;
|
||||
static const uint8_t MAX17043_SOC = 0x04;
|
||||
static const uint8_t MAX17043_CONFIG = 0x0c;
|
||||
|
||||
uint16_t raw;
|
||||
bool ok = true;
|
||||
static const uint16_t MAX17043_CONFIG_POWER_UP_DEFAULT = 0x971C;
|
||||
static const uint16_t MAX17043_CONFIG_SAFE_MASK = 0xFF1F; // mask out sleep bit (7), unused bit (6) and alert bit (4)
|
||||
static const uint16_t MAX17043_CONFIG_SLEEP_MASK = 0x0080;
|
||||
|
||||
void MAX17043Component::update() {
|
||||
uint16_t raw_voltage, raw_percent;
|
||||
|
||||
if (this->voltage_sensor_ != nullptr) {
|
||||
if (this->read_byte_16(max170xx::REG_VCELL, &raw)) {
|
||||
// 12 bits in the top of the word at 1.25 mV
|
||||
this->voltage_sensor_->publish_state((raw >> 4) * 1.25e-3f);
|
||||
if (!this->read_byte_16(MAX17043_VCELL, &raw_voltage)) {
|
||||
this->status_set_warning(LOG_STR("Unable to read MAX17043_VCELL"));
|
||||
} else {
|
||||
ok = false;
|
||||
float voltage = (1.25f * (float) (raw_voltage >> 4)) / 1000.0f;
|
||||
this->voltage_sensor_->publish_state(voltage);
|
||||
this->status_clear_warning();
|
||||
}
|
||||
}
|
||||
if (this->battery_remaining_sensor_ != nullptr) {
|
||||
if (this->read_byte_16(max170xx::REG_SOC, &raw)) {
|
||||
float percent = (float) ((raw >> 8) + 0.003906f * (raw & 0x00ff));
|
||||
this->battery_remaining_sensor_->publish_state(percent);
|
||||
if (!this->read_byte_16(MAX17043_SOC, &raw_percent)) {
|
||||
this->status_set_warning(LOG_STR("Unable to read MAX17043_SOC"));
|
||||
} else {
|
||||
ok = false;
|
||||
float percent = (float) ((raw_percent >> 8) + 0.003906f * (raw_percent & 0x00ff));
|
||||
this->battery_remaining_sensor_->publish_state(percent);
|
||||
this->status_clear_warning();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->status_comm_result(ok, LOG_STR("Register read failed"));
|
||||
void MAX17043Component::setup() {
|
||||
uint16_t config_reg;
|
||||
if (this->write(&MAX17043_CONFIG, 1) != i2c::ERROR_OK) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->read(reinterpret_cast<uint8_t *>(&config_reg), 2) != i2c::ERROR_OK) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
config_reg = i2c::i2ctohs(config_reg) & MAX17043_CONFIG_SAFE_MASK;
|
||||
ESP_LOGV(TAG, "MAX17043 CONFIG register reads 0x%X", config_reg);
|
||||
|
||||
if (config_reg != MAX17043_CONFIG_POWER_UP_DEFAULT) {
|
||||
this->mark_failed(LOG_STR("Device does not appear to be a MAX17043"));
|
||||
return;
|
||||
}
|
||||
|
||||
// need to write back to config register to reset the sleep bit
|
||||
if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT)) {
|
||||
this->mark_failed(LOG_STR("sleep reset failed"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void MAX17043Component::dump_config() {
|
||||
@@ -43,4 +74,13 @@ void MAX17043Component::dump_config() {
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_remaining_sensor_);
|
||||
}
|
||||
|
||||
void MAX17043Component::sleep_mode() {
|
||||
if (!this->is_failed()) {
|
||||
if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT | MAX17043_CONFIG_SLEEP_MASK)) {
|
||||
ESP_LOGW(TAG, "Unable to write the sleep bit to config register");
|
||||
this->status_set_warning();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::max17043
|
||||
@@ -1,18 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/max170xx/max170xx.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome::max17043 {
|
||||
|
||||
class MAX17043Component final : public max170xx::MAX170xxComponent<max170xx::Model::MODEL_MAX17043> {
|
||||
class MAX17043Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
void sleep_mode();
|
||||
|
||||
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; }
|
||||
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
|
||||
void set_battery_remaining_sensor(sensor::Sensor *battery_remaining_sensor) {
|
||||
this->battery_remaining_sensor_ = battery_remaining_sensor;
|
||||
battery_remaining_sensor_ = battery_remaining_sensor;
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
@@ -16,7 +16,6 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["max170xx"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
max17043_ns = cg.esphome_ns.namespace("max17043")
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
CODEOWNERS = ["@B48D81EFCC", "@ryansch"]
|
||||
CODEOWNERS = ["@B48D81EFCC"]
|
||||
DOMAIN = "max17048"
|
||||
@@ -8,27 +8,41 @@ namespace esphome::max17048 {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "max17048");
|
||||
|
||||
static constexpr uint8_t REG_CRATE = 0x16; // charge rate, signed
|
||||
static constexpr uint8_t REG_VCELL = 0x02; // cell voltage
|
||||
static constexpr uint8_t REG_SOC = 0x04; // state of charge
|
||||
static constexpr uint8_t REG_VERSION = 0x08; // 0x001x
|
||||
static constexpr uint8_t REG_CRATE = 0x16; // charge rate, signed
|
||||
|
||||
static constexpr uint16_t VERSION_MASK = 0xFFF0;
|
||||
static constexpr uint16_t VERSION_FAMILY = 0x0010;
|
||||
|
||||
static constexpr float VCELL_VOLTS_PER_LSB = 78.125e-6f;
|
||||
static constexpr float SOC_PERCENT_PER_LSB = 1.0f / 256.0f;
|
||||
static constexpr float CRATE_PERCENT_PER_HOUR_PER_LSB = 0.208f;
|
||||
|
||||
void MAX17048Component::update() {
|
||||
if (!this->ready_for_update_())
|
||||
void MAX17048Component::setup() {
|
||||
uint16_t version;
|
||||
if (!this->read_byte_16(REG_VERSION, &version)) {
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if ((version & VERSION_MASK) != VERSION_FAMILY) {
|
||||
ESP_LOGW(TAG, "Unexpected version 0x%04X", version);
|
||||
}
|
||||
}
|
||||
|
||||
void MAX17048Component::update() {
|
||||
bool ok = true;
|
||||
uint16_t raw;
|
||||
if (this->battery_voltage_sensor_ != nullptr) {
|
||||
if (this->read_byte_16(max170xx::REG_VCELL, &raw)) {
|
||||
if (this->read_byte_16(REG_VCELL, &raw)) {
|
||||
this->battery_voltage_sensor_->publish_state(raw * VCELL_VOLTS_PER_LSB);
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (this->battery_level_sensor_ != nullptr) {
|
||||
if (this->read_byte_16(max170xx::REG_SOC, &raw)) {
|
||||
if (this->read_byte_16(REG_SOC, &raw)) {
|
||||
this->battery_level_sensor_->publish_state(raw * SOC_PERCENT_PER_LSB);
|
||||
} else {
|
||||
ok = false;
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/max170xx/max170xx.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome::max17048 {
|
||||
|
||||
class MAX17048Component final : public max170xx::MAX170xxComponent<max170xx::Model::MODEL_MAX17048> {
|
||||
class MAX17048Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
SUB_SENSOR(battery_voltage)
|
||||
SUB_SENSOR(battery_level)
|
||||
SUB_SENSOR(rate)
|
||||
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
};
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
from esphome import automation
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BATTERY_LEVEL,
|
||||
CONF_BATTERY_VOLTAGE,
|
||||
CONF_ID,
|
||||
CONF_RATE,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
@@ -17,7 +14,6 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["max170xx"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
UNIT_PERCENT_PER_HOUR = "%/h"
|
||||
@@ -65,15 +61,3 @@ async def to_code(config: ConfigType) -> None:
|
||||
await sensors(CONF_BATTERY_VOLTAGE, var.set_battery_voltage_sensor)
|
||||
await sensors(CONF_BATTERY_LEVEL, var.set_battery_level_sensor)
|
||||
await sensors(CONF_RATE, var.set_rate_sensor)
|
||||
|
||||
|
||||
MAX17048_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(MAX17048Component),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
automation.register_apply_action(
|
||||
"max17048.sleep_mode", MAX17048_ACTION_SCHEMA, automation.ApplyCall("sleep_mode()")
|
||||
)
|
||||
@@ -1,3 +0,0 @@
|
||||
CODEOWNERS = ["@ryansch"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
DOMAIN = "max170xx"
|
||||
@@ -1,87 +0,0 @@
|
||||
#include "max170xx.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::max170xx {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "max170xx");
|
||||
|
||||
template<Model MODEL> void MAX170xxComponent<MODEL>::setup() {
|
||||
// A silent bus is retried from update(): some boards gate the I2C rail until after setup.
|
||||
// An early on_boot may already have configured the gauge and put it to sleep, so this must not configure again.
|
||||
this->ready_for_update_();
|
||||
}
|
||||
|
||||
template<Model MODEL> bool MAX170xxComponent<MODEL>::ready_for_update_() {
|
||||
if (this->configured_ || this->configure_())
|
||||
return true;
|
||||
this->status_set_warning(LOG_STR("Gauge did not answer"));
|
||||
return false;
|
||||
}
|
||||
|
||||
template<Model MODEL> bool MAX170xxComponent<MODEL>::configure_() {
|
||||
uint16_t config;
|
||||
if (!this->read_byte_16(REG_CONFIG, &config))
|
||||
return false;
|
||||
|
||||
const bool was_asleep = (config & CONFIG_SLEEP) != 0;
|
||||
// RCOMP and ATHD survive on the battery, so an earlier firmware's values are reset here, not treated as a fault
|
||||
if ((config & CONFIG_SAFE_MASK) != CONFIG_POWER_UP_DEFAULT) {
|
||||
ESP_LOGW(TAG, "CONFIG reads 0x%04X, expected 0x%04X; resetting it", config & CONFIG_SAFE_MASK,
|
||||
CONFIG_POWER_UP_DEFAULT);
|
||||
}
|
||||
|
||||
uint16_t version;
|
||||
if (this->read_byte_16(REG_VERSION, &version)) {
|
||||
ESP_LOGCONFIG(TAG, "IC version: 0x%04X", version);
|
||||
}
|
||||
|
||||
// Write back the power-up value to clear a stale sleep bit; a failed write is retried like a silent bus
|
||||
if (!this->write_byte_16(REG_CONFIG, CONFIG_POWER_UP_DEFAULT))
|
||||
return false;
|
||||
|
||||
this->configured_ = true;
|
||||
// Waking from sleep raises a false low-battery alert; a real one at an ordinary boot is left alone
|
||||
if (was_asleep)
|
||||
this->set_timeout(WAKE_ALERT_CLEAR_TIMEOUT_ID, WAKE_ALERT_CLEAR_DELAY_MS, [this]() { this->clear_wake_alert_(); });
|
||||
return true;
|
||||
}
|
||||
|
||||
template<Model MODEL> void MAX170xxComponent<MODEL>::clear_wake_alert_() {
|
||||
uint16_t config;
|
||||
if (!this->read_byte_16(REG_CONFIG, &config) || !this->write_byte_16(REG_CONFIG, config & ~CONFIG_ALRT)) {
|
||||
this->status_set_warning(LOG_STR("Wake alert clear failed"));
|
||||
return;
|
||||
}
|
||||
if constexpr (MODEL == Model::MODEL_MAX17048) {
|
||||
uint16_t status;
|
||||
if (!this->read_byte_16(REG_STATUS, &status) || !this->write_byte_16(REG_STATUS, status & ~STATUS_HD))
|
||||
this->status_set_warning(LOG_STR("Wake alert clear failed"));
|
||||
}
|
||||
}
|
||||
|
||||
template<Model MODEL> void MAX170xxComponent<MODEL>::sleep_mode() {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
// The deferred configuration clears the sleep bit, so it has to run before the gauge is put to sleep
|
||||
if (!this->ready_for_update_())
|
||||
return;
|
||||
|
||||
// A clear still pending would otherwise run against a sleeping gauge
|
||||
if (this->cancel_timeout(WAKE_ALERT_CLEAR_TIMEOUT_ID))
|
||||
this->clear_wake_alert_();
|
||||
|
||||
if constexpr (MODEL == Model::MODEL_MAX17048) {
|
||||
if (!this->write_byte_16(REG_MODE, MODE_EN_SLEEP)) {
|
||||
this->status_set_warning(LOG_STR("Unable to enable sleep mode in the mode register"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->write_byte_16(REG_CONFIG, CONFIG_POWER_UP_DEFAULT | CONFIG_SLEEP))
|
||||
this->status_set_warning(LOG_STR("Unable to write the sleep bit to config register"));
|
||||
}
|
||||
|
||||
template class MAX170xxComponent<Model::MODEL_MAX17043>;
|
||||
template class MAX170xxComponent<Model::MODEL_MAX17048>;
|
||||
|
||||
} // namespace esphome::max170xx
|
||||
@@ -1,61 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome::max170xx {
|
||||
|
||||
// Shared base for the ModelGauge fuel gauges.
|
||||
// MAX17043: https://www.analog.com/en/products/max17043.html
|
||||
// MAX17048: https://www.analog.com/en/products/max17048.html
|
||||
// The MAX17048 is register-compatible with the MAX17043, and adds a STATUS register and a MODE.EnSleep gate in
|
||||
// front of CONFIG.SLEEP.
|
||||
|
||||
static constexpr uint8_t REG_VCELL = 0x02;
|
||||
static constexpr uint8_t REG_SOC = 0x04;
|
||||
static constexpr uint8_t REG_MODE = 0x06;
|
||||
static constexpr uint8_t REG_VERSION = 0x08;
|
||||
static constexpr uint8_t REG_CONFIG = 0x0C;
|
||||
static constexpr uint8_t REG_STATUS = 0x1A;
|
||||
|
||||
static constexpr uint16_t CONFIG_POWER_UP_DEFAULT = 0x971C;
|
||||
// Mask out SLEEP (7), ALRT (5), and bit 6, which is a don't-care on the MAX17043 but ALSC on the MAX17048.
|
||||
static constexpr uint16_t CONFIG_SAFE_MASK = 0xFF1F;
|
||||
static constexpr uint16_t CONFIG_SLEEP = 0x0080;
|
||||
static constexpr uint16_t CONFIG_ALRT = 0x0020;
|
||||
|
||||
// CONFIG.SLEEP is ignored unless MODE.EnSleep (bit 13) is set first.
|
||||
static constexpr uint16_t MODE_EN_SLEEP = 0x2000;
|
||||
static constexpr uint16_t STATUS_HD = 0x1000;
|
||||
|
||||
static constexpr uint32_t WAKE_ALERT_CLEAR_TIMEOUT_ID = 0;
|
||||
// The gauge raises its false wake alert some time after the wake write; on hardware it rose 0.8 to 1.0 s later
|
||||
static constexpr uint32_t WAKE_ALERT_CLEAR_DELAY_MS = 2000;
|
||||
|
||||
enum class Model : uint8_t {
|
||||
MODEL_MAX17043,
|
||||
MODEL_MAX17048,
|
||||
};
|
||||
|
||||
/// Defined in max170xx.cpp, which instantiates both models.
|
||||
template<Model MODEL> class MAX170xxComponent : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void sleep_mode();
|
||||
|
||||
protected:
|
||||
/// True once the gauge is configured; otherwise retries the configuration and warns if the gauge did not answer.
|
||||
/// Call first in update().
|
||||
bool ready_for_update_();
|
||||
/// Verify and wake the gauge; false if it did not answer.
|
||||
bool configure_();
|
||||
/// Clear the false alert raised on waking, leaving every other CONFIG and STATUS bit as it is.
|
||||
void clear_wake_alert_();
|
||||
|
||||
bool configured_{false};
|
||||
};
|
||||
|
||||
extern template class MAX170xxComponent<Model::MODEL_MAX17043>;
|
||||
extern template class MAX170xxComponent<Model::MODEL_MAX17048>;
|
||||
|
||||
} // namespace esphome::max170xx
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "mcp23016.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cstdio>
|
||||
|
||||
@@ -80,7 +79,11 @@ void MCP23016::update_reg_(uint8_t pin, bool pin_value, uint8_t reg_addr) {
|
||||
this->read_reg_(reg_addr, ®_value);
|
||||
}
|
||||
|
||||
assign_bits(reg_value, 1 << pin, pin_value);
|
||||
if (pin_value) {
|
||||
reg_value |= 1 << pin;
|
||||
} else {
|
||||
reg_value &= ~(1 << pin);
|
||||
}
|
||||
|
||||
this->write_reg_(reg_addr, reg_value);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "mcp23x08_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::mcp23x08_base {
|
||||
@@ -79,7 +78,11 @@ void MCP23X08Base::update_reg(uint8_t pin, bool pin_value, uint8_t reg_addr) {
|
||||
this->read_reg(reg_addr, ®_value);
|
||||
}
|
||||
|
||||
assign_bits(reg_value, 1 << bit, pin_value);
|
||||
if (pin_value) {
|
||||
reg_value |= 1 << bit;
|
||||
} else {
|
||||
reg_value &= ~(1 << bit);
|
||||
}
|
||||
|
||||
this->write_reg(reg_addr, reg_value);
|
||||
|
||||
|
||||
@@ -92,7 +92,11 @@ void MCP23X17Base::update_reg(uint8_t pin, bool pin_value, uint8_t reg_addr) {
|
||||
this->read_reg(reg_addr, ®_value);
|
||||
}
|
||||
|
||||
assign_bits(reg_value, 1 << bit, pin_value);
|
||||
if (pin_value) {
|
||||
reg_value |= 1 << bit;
|
||||
} else {
|
||||
reg_value &= ~(1 << bit);
|
||||
}
|
||||
|
||||
this->write_reg(reg_addr, reg_value);
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ from esphome.components.mipi import (
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_COLOR_ORDER,
|
||||
CONF_DIMENSIONS,
|
||||
CONF_ENABLE_PIN,
|
||||
@@ -167,8 +166,7 @@ def _config_schema(config: ConfigType) -> ConfigType:
|
||||
height,
|
||||
has_hardware_rotation=False,
|
||||
byte_order=config[CONF_BYTE_ORDER],
|
||||
has_writer=requires_buffer(config)
|
||||
or config.get(CONF_AUTO_CLEAR_ENABLED) is True,
|
||||
has_writer=display.config_has_writer(config),
|
||||
rotation=config.get(CONF_ROTATION, 0),
|
||||
draw_rounding=config.get(CONF_DRAW_ROUNDING, 0),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifdef USE_ESP32_VARIANT_ESP32P4
|
||||
#include <utility>
|
||||
#include "mipi_dsi.h"
|
||||
#include "esphome/components/display/init_sequence.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -123,47 +124,38 @@ void MipiDsi::setup() {
|
||||
this->smark_failed(LOG_STR("esp_lcd_init failed"), err);
|
||||
return;
|
||||
}
|
||||
size_t index = 0;
|
||||
const uint8_t *seq = this->init_sequence_;
|
||||
const size_t len = this->init_sequence_len_;
|
||||
while (index != len) {
|
||||
if (len - index < 2) {
|
||||
display::InitSequenceReader reader(this->init_sequence_, this->init_sequence_len_);
|
||||
display::InitSequenceEntry entry;
|
||||
while (auto step = reader.next(entry)) {
|
||||
if (step == display::INIT_SEQUENCE_STEP_MALFORMED) {
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
uint8_t cmd = seq[index++];
|
||||
uint8_t x = seq[index++];
|
||||
if (x == DELAY_FLAG) {
|
||||
ESP_LOGD(TAG, "Delay %dms", cmd);
|
||||
delay(cmd);
|
||||
} else {
|
||||
uint8_t num_args = x & 0x7F;
|
||||
if (len - index < num_args) {
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
if (cmd == SLEEP_OUT) {
|
||||
// are we ready, boots?
|
||||
int duration = when - millis();
|
||||
if (duration > 0) {
|
||||
delay(duration);
|
||||
}
|
||||
}
|
||||
const auto *ptr = seq + index;
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MIPI_DSI_MAX_CMD_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGVV(TAG, "Command %02X, length %d, byte(s) %s", cmd, num_args,
|
||||
format_hex_pretty_to(hex_buf, ptr, num_args, '.'));
|
||||
err = esp_lcd_panel_io_tx_param(this->io_handle_, cmd, ptr, num_args);
|
||||
if (err != ESP_OK) {
|
||||
this->smark_failed(LOG_STR("lcd_panel_io_tx_param failed"), err);
|
||||
return;
|
||||
}
|
||||
index += num_args;
|
||||
if (cmd == SLEEP_OUT)
|
||||
delay(10);
|
||||
if (step == display::INIT_SEQUENCE_STEP_DELAY) {
|
||||
ESP_LOGD(TAG, "Delay %dms", entry.value);
|
||||
delay(entry.value);
|
||||
continue;
|
||||
}
|
||||
const uint8_t cmd = entry.value;
|
||||
if (cmd == SLEEP_OUT) {
|
||||
// are we ready, boots?
|
||||
int duration = when - millis();
|
||||
if (duration > 0) {
|
||||
delay(duration);
|
||||
}
|
||||
}
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MIPI_DSI_MAX_CMD_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGVV(TAG, "Command %02X, length %d, byte(s) %s", cmd, entry.count,
|
||||
format_hex_pretty_to(hex_buf, entry.args, entry.count, '.'));
|
||||
err = esp_lcd_panel_io_tx_param(this->io_handle_, cmd, entry.args, entry.count);
|
||||
if (err != ESP_OK) {
|
||||
this->smark_failed(LOG_STR("lcd_panel_io_tx_param failed"), err);
|
||||
return;
|
||||
}
|
||||
if (cmd == SLEEP_OUT)
|
||||
delay(10);
|
||||
}
|
||||
this->io_lock_ = xSemaphoreCreateBinary();
|
||||
esp_lcd_dpi_panel_event_callbacks_t cbs = {
|
||||
|
||||
@@ -31,7 +31,6 @@ const uint8_t INVERT_OFF = 0x20;
|
||||
const uint8_t INVERT_ON = 0x21;
|
||||
const uint8_t DISPLAY_ON = 0x29;
|
||||
const uint8_t CMD2_BKSEL = 0xFF;
|
||||
const uint8_t DELAY_FLAG = 0xFF;
|
||||
const uint8_t MADCTL_BGR = 0x08;
|
||||
const uint8_t MADCTL_MX = 0x40;
|
||||
const uint8_t MADCTL_MY = 0x80;
|
||||
|
||||
@@ -50,7 +50,6 @@ from esphome.components.spi import (
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_BLUE,
|
||||
CONF_COLOR_ORDER,
|
||||
CONF_CS_PIN,
|
||||
@@ -243,8 +242,7 @@ def _config_schema(config: ConfigType) -> ConfigType:
|
||||
height,
|
||||
has_hardware_rotation=True,
|
||||
byte_order=config[CONF_BYTE_ORDER],
|
||||
has_writer=requires_buffer(config)
|
||||
or config.get(CONF_AUTO_CLEAR_ENABLED) is True,
|
||||
has_writer=display.config_has_writer(config),
|
||||
rotation=config.get(CONF_ROTATION, 0),
|
||||
draw_rounding=config.get(CONF_DRAW_ROUNDING, 0),
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S31)
|
||||
#include "mipi_rgb.h"
|
||||
#include "esphome/components/display/init_sequence.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -13,8 +14,6 @@
|
||||
|
||||
namespace esphome::mipi_rgb {
|
||||
|
||||
static const uint8_t DELAY_FLAG = 0xFF;
|
||||
|
||||
// Maximum bytes to log for init commands (truncated if larger)
|
||||
static constexpr size_t MIPI_RGB_MAX_CMD_LOG_BYTES = 64;
|
||||
static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
|
||||
@@ -75,44 +74,30 @@ void MipiRgbSpi::write_data_(uint8_t value) {
|
||||
this->disable();
|
||||
}
|
||||
|
||||
/**
|
||||
* this relies upon the init sequence being well-formed, which is guaranteed by the Python init code.
|
||||
*/
|
||||
|
||||
void MipiRgbSpi::write_init_sequence_() {
|
||||
size_t index = 0;
|
||||
const uint8_t *seq = this->init_sequence_;
|
||||
const size_t len = this->init_sequence_len_;
|
||||
while (index != len) {
|
||||
if (len - index < 2) {
|
||||
display::InitSequenceReader reader(this->init_sequence_, this->init_sequence_len_);
|
||||
display::InitSequenceEntry entry;
|
||||
while (auto step = reader.next(entry)) {
|
||||
if (step == display::INIT_SEQUENCE_STEP_MALFORMED) {
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
uint8_t cmd = seq[index++];
|
||||
uint8_t x = seq[index++];
|
||||
if (x == DELAY_FLAG) {
|
||||
ESP_LOGD(TAG, "Delay %dms", cmd);
|
||||
delay(cmd);
|
||||
} else {
|
||||
uint8_t num_args = x & 0x7F;
|
||||
if (len - index < num_args) {
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
if (cmd == SLEEP_OUT) {
|
||||
delay(120); // NOLINT
|
||||
}
|
||||
const auto *ptr = seq + index;
|
||||
char hex_buf[format_hex_pretty_size(MIPI_RGB_MAX_CMD_LOG_BYTES)];
|
||||
ESP_LOGD(TAG, "Write command %02X, length %d, byte(s) %s", cmd, num_args,
|
||||
format_hex_pretty_to(hex_buf, ptr, num_args, '.'));
|
||||
index += num_args;
|
||||
this->write_command_(cmd);
|
||||
while (num_args-- != 0)
|
||||
this->write_data_(*ptr++);
|
||||
if (cmd == SLEEP_OUT)
|
||||
delay(10);
|
||||
if (step == display::INIT_SEQUENCE_STEP_DELAY) {
|
||||
ESP_LOGD(TAG, "Delay %dms", entry.value);
|
||||
delay(entry.value);
|
||||
continue;
|
||||
}
|
||||
const uint8_t cmd = entry.value;
|
||||
if (cmd == SLEEP_OUT)
|
||||
delay(120); // NOLINT
|
||||
char hex_buf[format_hex_pretty_size(MIPI_RGB_MAX_CMD_LOG_BYTES)];
|
||||
ESP_LOGD(TAG, "Write command %02X, length %d, byte(s) %s", cmd, entry.count,
|
||||
format_hex_pretty_to(hex_buf, entry.args, entry.count, '.'));
|
||||
this->write_command_(cmd);
|
||||
for (uint8_t i = 0; i < entry.count; i++)
|
||||
this->write_data_(entry.args[i]);
|
||||
if (cmd == SLEEP_OUT)
|
||||
delay(10);
|
||||
}
|
||||
// this->spi_teardown(); // SPI not needed after this
|
||||
delay(10);
|
||||
|
||||
@@ -33,7 +33,6 @@ from esphome.components.spi import CONF_SPI_MODE, TYPE_OCTAL, TYPE_QUAD, TYPE_SI
|
||||
import esphome.config_validation as cv
|
||||
from esphome.config_validation import ALLOW_EXTRA
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_BRIGHTNESS,
|
||||
CONF_BUFFER_SIZE,
|
||||
CONF_COLOR_ORDER,
|
||||
@@ -73,6 +72,10 @@ Model = mipi_spi_ns.enum("Model")
|
||||
PixelMode = mipi_spi_ns.enum("PixelMode")
|
||||
BusType = mipi_spi_ns.enum("BusType")
|
||||
|
||||
# Matches INIT_SEQUENCE_MAX_ARGS, the flash copy buffer in mipi_spi.h
|
||||
INIT_SEQUENCE_MAX_ARGS = 128
|
||||
|
||||
|
||||
COLOR_ORDERS = {
|
||||
MODE_RGB: ColorOrder.COLOR_ORDER_RGB,
|
||||
MODE_BGR: ColorOrder.COLOR_ORDER_BGR,
|
||||
@@ -216,7 +219,15 @@ def model_schema(config: ConfigType) -> cv.All | cv.Schema:
|
||||
bus_mode, lower=True
|
||||
),
|
||||
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
|
||||
iseqconf: cv.ensure_list(map_sequence),
|
||||
iseqconf: cv.ensure_list(
|
||||
cv.All(
|
||||
map_sequence,
|
||||
cv.Length(
|
||||
max=INIT_SEQUENCE_MAX_ARGS + 1,
|
||||
msg=f"An init sequence command can have at most {INIT_SEQUENCE_MAX_ARGS} data bytes",
|
||||
),
|
||||
)
|
||||
),
|
||||
cv.Optional(CONF_BUFFER_SIZE): cv.All(
|
||||
cv.percentage, cv.Range(0.12, 1.0)
|
||||
),
|
||||
@@ -290,8 +301,7 @@ def customise_schema(config: ConfigType) -> ConfigType:
|
||||
height,
|
||||
has_hardware_transform,
|
||||
byte_order=config[CONF_BYTE_ORDER],
|
||||
has_writer=requires_buffer(config)
|
||||
or config.get(CONF_AUTO_CLEAR_ENABLED) is True,
|
||||
has_writer=display.config_has_writer(config),
|
||||
rotation=config.get(CONF_ROTATION, 0),
|
||||
draw_rounding=config.get(CONF_DRAW_ROUNDING, 0),
|
||||
)
|
||||
|
||||
@@ -7,12 +7,16 @@
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
#include "esphome/components/display/init_sequence.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::mipi_spi {
|
||||
|
||||
/// Largest argument count in one init command; display.py rejects longer ones.
|
||||
static constexpr size_t INIT_SEQUENCE_MAX_ARGS = 128;
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "display.mipi_spi");
|
||||
|
||||
// Maximum bytes to log for commands (truncated if larger)
|
||||
@@ -48,7 +52,6 @@ static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horiz
|
||||
static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
|
||||
static constexpr uint16_t MADCTL_FLIP_FLAG = 0x100; // controller uses axis flip bits
|
||||
|
||||
static constexpr uint8_t DELAY_FLAG = 0xFF;
|
||||
// store a 16 bit value in a buffer, big endian.
|
||||
static inline void put16_be(uint8_t *buf, uint16_t value) {
|
||||
buf[0] = value >> 8;
|
||||
@@ -163,36 +166,23 @@ class MipiSpi : public display::Display,
|
||||
|
||||
// need to know when the display is ready for SLPOUT command - will be 120ms after reset
|
||||
auto when = millis() + 120;
|
||||
size_t index = 0;
|
||||
const uint8_t *seq = this->init_sequence_;
|
||||
const size_t len = this->init_sequence_len_;
|
||||
while (index != len) {
|
||||
if (len - index < 2) {
|
||||
esph_log_e(TAG, "Malformed init sequence");
|
||||
this->mark_failed();
|
||||
display::InitSequenceReader reader(this->init_sequence_, this->init_sequence_len_);
|
||||
display::InitSequenceEntry entry;
|
||||
// The sequence is in flash, which SPI DMA (and ESP8266 byte loads) cannot read
|
||||
uint8_t args[INIT_SEQUENCE_MAX_ARGS];
|
||||
while (auto step = reader.next(entry, sizeof(args))) {
|
||||
if (step == display::INIT_SEQUENCE_STEP_MALFORMED) {
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
uint8_t cmd = progmem_read_byte(seq + index++);
|
||||
uint8_t x = progmem_read_byte(seq + index++);
|
||||
if (x == DELAY_FLAG) {
|
||||
if (cmd == 0) {
|
||||
cmd = clamp_at_least((int) (when - millis()), 0);
|
||||
}
|
||||
esph_log_d(TAG, "Delay %dms", cmd);
|
||||
delay(cmd);
|
||||
} else {
|
||||
uint8_t num_args = x & 0x7F;
|
||||
if (len - index < num_args) {
|
||||
esph_log_e(TAG, "Malformed init sequence");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
// The sequence is in flash, which SPI DMA (and ESP8266 byte loads) cannot read
|
||||
uint8_t args[0x80];
|
||||
progmem_memcpy(args, seq + index, num_args);
|
||||
this->write_command_(cmd, args, num_args);
|
||||
index += num_args;
|
||||
if (step == display::INIT_SEQUENCE_STEP_DELAY) {
|
||||
const uint8_t ms = entry.value != 0 ? entry.value : clamp_at_least((int) (when - millis()), 0);
|
||||
esph_log_d(TAG, "Delay %dms", ms);
|
||||
delay(ms);
|
||||
continue;
|
||||
}
|
||||
progmem_memcpy(args, entry.args, entry.count);
|
||||
this->write_command_(entry.value, args, entry.count);
|
||||
}
|
||||
this->reset_params_();
|
||||
// Marks init as done, so later commands log at verbose level instead of debug
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "download_buffer.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::online_image {
|
||||
@@ -35,46 +34,6 @@ size_t DownloadBuffer::read(size_t len) {
|
||||
return this->unread_;
|
||||
}
|
||||
|
||||
void DownloadSource::compact() {
|
||||
if (this->keep_read_ || this->pos_ == this->start_)
|
||||
return;
|
||||
this->buffer_.read(this->pos_ - this->start_);
|
||||
this->start_ = this->pos_;
|
||||
}
|
||||
|
||||
int32_t DownloadSource::read(uint8_t *buf, uint32_t len) {
|
||||
const size_t offset = this->pos_ - this->start_;
|
||||
const size_t n = std::min<size_t>(len, this->buffer_.unread() - offset);
|
||||
memcpy(buf, this->buffer_.data(offset), n);
|
||||
this->pos_ += n;
|
||||
return static_cast<int32_t>(n);
|
||||
}
|
||||
|
||||
bool DownloadSource::seek(uint32_t position) {
|
||||
// Only what is still in the buffer can be read again; a decoder never skips ahead of the download
|
||||
if (position < this->start_ || position > this->start_ + this->buffer_.unread())
|
||||
return false;
|
||||
this->pos_ = position;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DownloadBuffer::grow(size_t size) {
|
||||
if (this->size_ >= size)
|
||||
return true;
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
uint8_t *buffer = allocator.allocate(size);
|
||||
if (buffer == nullptr) {
|
||||
ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", size,
|
||||
allocator.get_max_free_block_size());
|
||||
return false;
|
||||
}
|
||||
memcpy(buffer, this->buffer_, this->unread_);
|
||||
allocator.deallocate(this->buffer_, this->size_);
|
||||
this->buffer_ = buffer;
|
||||
this->size_ = size;
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t DownloadBuffer::resize(size_t size) {
|
||||
if (this->size_ >= size) {
|
||||
// Avoid useless reallocations; if the buffer is big enough, don't reallocate.
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/runtime_image/image_decoder.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -36,8 +35,6 @@ class DownloadBuffer {
|
||||
|
||||
void reset() { this->unread_ = 0; }
|
||||
size_t resize(size_t size);
|
||||
/// Make the buffer at least size bytes, keeping the unread data. Returns false if the allocation failed.
|
||||
bool grow(size_t size);
|
||||
|
||||
protected:
|
||||
uint8_t *buffer_;
|
||||
@@ -46,35 +43,4 @@ class DownloadBuffer {
|
||||
size_t unread_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Lets a decoder read a download as it arrives, from the download buffer.
|
||||
*
|
||||
* The decoder reads at its own position in the buffer. Bytes it has read stay there until compact() drops
|
||||
* them, and compact() keeps everything while set_keep_read(true) is in effect, so the decoder can seek back
|
||||
* while it reads the headers.
|
||||
*/
|
||||
class DownloadSource : public runtime_image::DecodeSource {
|
||||
public:
|
||||
explicit DownloadSource(DownloadBuffer &buffer) : buffer_(buffer) {}
|
||||
|
||||
/// Start again at the beginning of a new download, keeping what is read
|
||||
void reset() {
|
||||
this->start_ = 0;
|
||||
this->pos_ = 0;
|
||||
this->keep_read_ = true;
|
||||
}
|
||||
void set_keep_read(bool keep) { this->keep_read_ = keep; }
|
||||
/// Drop the bytes the decoder has read, to make room for more of the download
|
||||
void compact();
|
||||
|
||||
int32_t read(uint8_t *buf, uint32_t len) override;
|
||||
bool seek(uint32_t position) override;
|
||||
|
||||
protected:
|
||||
DownloadBuffer &buffer_;
|
||||
size_t start_{0}; // Position in the download of the first byte in the buffer
|
||||
size_t pos_{0}; // Position in the download the decoder reads next
|
||||
bool keep_read_{true};
|
||||
};
|
||||
|
||||
} // namespace esphome::online_image
|
||||
@@ -22,8 +22,6 @@ DEPENDENCIES = ["http_request"]
|
||||
CODEOWNERS = ["@guillempages", "@clydebarrow"]
|
||||
|
||||
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
|
||||
CONF_ON_DOWNLOAD_STARTED = "on_download_started"
|
||||
CONF_ON_DOWNLOAD_PROGRESS = "on_download_progress"
|
||||
CONF_UPDATE = "update"
|
||||
|
||||
online_image_ns = cg.esphome_ns.namespace("online_image")
|
||||
@@ -45,9 +43,7 @@ ONLINE_IMAGE_SCHEMA = (
|
||||
cv.Optional(CONF_REQUEST_HEADERS): cv.All(
|
||||
cv.Schema({cv.string: cv.templatable(cv.string)})
|
||||
),
|
||||
cv.Optional(CONF_ON_DOWNLOAD_STARTED): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_DOWNLOAD_PROGRESS): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_ERROR): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
@@ -100,17 +96,9 @@ automation.register_apply_action(
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_DOWNLOAD_STARTED, "add_on_download_started_callback"
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_DOWNLOAD_FINISHED, "add_on_finished_callback", [(bool, "cached")]
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_DOWNLOAD_PROGRESS,
|
||||
"add_on_progress_callback",
|
||||
[(float, "progress"), (cg.size_t, "bytes_read")],
|
||||
),
|
||||
automation.CallbackAutomation(CONF_ON_ERROR, "add_on_error_callback"),
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "online_image");
|
||||
@@ -16,11 +15,6 @@ static const char *const IF_MODIFIED_SINCE_HEADER_NAME = "if-modified-since";
|
||||
|
||||
namespace esphome::online_image {
|
||||
|
||||
// Time one loop pass may spend reading, and decoding a JPEG as it downloads
|
||||
static constexpr uint32_t PASS_BUDGET_MS = 20;
|
||||
// Shortest time between two on_download_progress calls
|
||||
static constexpr uint32_t PROGRESS_INTERVAL_MS = 100;
|
||||
|
||||
OnlineImage::OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format,
|
||||
image::ImageType type, image::Transparency transparency, image::Image *placeholder,
|
||||
uint32_t buffer_size, bool is_big_endian)
|
||||
@@ -146,12 +140,8 @@ void OnlineImage::update() {
|
||||
return;
|
||||
}
|
||||
|
||||
// A JPEG of known size is decoded as it arrives, so the download buffer only has to hold its headers.
|
||||
// Otherwise the decoder needs the complete image in the buffer.
|
||||
this->streaming_ = total_size > 0 && total_size <= INT32_MAX && this->decoder_supports_source();
|
||||
this->stream_started_ = false;
|
||||
this->source_.reset();
|
||||
if (!this->streaming_ && format == runtime_image::JPEG && total_size > this->download_buffer_.size() &&
|
||||
// JPEG requires the complete image in the download buffer before decoding
|
||||
if (format == runtime_image::JPEG && total_size > this->download_buffer_.size() &&
|
||||
this->download_buffer_.resize(total_size) == 0) {
|
||||
// resize() logged the failed allocation; the next update() allocates the configured buffer again
|
||||
this->end_connection_();
|
||||
@@ -162,8 +152,6 @@ void OnlineImage::update() {
|
||||
ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size);
|
||||
this->start_time_ = millis();
|
||||
this->last_data_time_ = this->start_time_;
|
||||
this->download_started_callback_.call();
|
||||
this->report_progress_(true);
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
@@ -181,11 +169,6 @@ void OnlineImage::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->streaming_) {
|
||||
this->loop_stream_();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if download is complete — use decoder's format-specific completion check
|
||||
// to handle both known content-length and chunked transfer encoding
|
||||
if (this->is_decode_finished() || (this->downloader_->content_length > 0 &&
|
||||
@@ -196,22 +179,15 @@ void OnlineImage::loop() {
|
||||
}
|
||||
|
||||
// Download and decode more data
|
||||
if (this->download_buffer_.free_capacity() > 0) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
size_t received = 0;
|
||||
int len;
|
||||
do {
|
||||
len = this->downloader_->read(this->download_buffer_.append(),
|
||||
std::min(this->download_buffer_.free_capacity(), READ_CHUNK));
|
||||
if (len <= 0)
|
||||
break;
|
||||
this->download_buffer_.write(len);
|
||||
received += len;
|
||||
} while (this->download_buffer_.free_capacity() != 0 && millis() - now < PASS_BUDGET_MS);
|
||||
size_t available = this->download_buffer_.free_capacity();
|
||||
if (available > 0) {
|
||||
// Download in chunks to avoid blocking
|
||||
available = std::min(available, this->download_buffer_initial_size_);
|
||||
auto len = this->downloader_->read(this->download_buffer_.append(), available);
|
||||
|
||||
if (received > 0) {
|
||||
this->last_data_time_ = now;
|
||||
this->report_progress_(false);
|
||||
if (len > 0) {
|
||||
this->last_data_time_ = App.get_loop_component_start_time();
|
||||
this->download_buffer_.write(len);
|
||||
|
||||
// Feed data to decoder
|
||||
auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread());
|
||||
@@ -279,114 +255,6 @@ void OnlineImage::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
void OnlineImage::loop_stream_() {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
const size_t total = this->downloader_->content_length;
|
||||
if (this->stream_started_)
|
||||
this->source_.compact();
|
||||
|
||||
if (this->downloader_->get_bytes_read() < total) {
|
||||
if (this->download_buffer_.free_capacity() == 0) {
|
||||
// compact() already ran, so a full buffer means the decoder read nothing in the last pass
|
||||
if (now - this->last_data_time_ >= this->parent_->get_timeout()) {
|
||||
ESP_LOGE(TAG, "Image does not fit in the %zu byte download buffer; increase buffer_size",
|
||||
this->download_buffer_.size());
|
||||
this->fail_download_();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
int len;
|
||||
do {
|
||||
len = this->downloader_->read(this->download_buffer_.append(),
|
||||
std::min(this->download_buffer_.free_capacity(), READ_CHUNK));
|
||||
if (len <= 0)
|
||||
break;
|
||||
this->last_data_time_ = now;
|
||||
this->download_buffer_.write(len);
|
||||
this->report_progress_(false);
|
||||
} while (this->downloader_->get_bytes_read() < total && this->download_buffer_.free_capacity() != 0 &&
|
||||
millis() - now < PASS_BUDGET_MS);
|
||||
if (len < 0) {
|
||||
ESP_LOGE(TAG, "Error downloading image: %d", len);
|
||||
this->fail_download_();
|
||||
return;
|
||||
} else if (now - this->last_data_time_ >= this->parent_->get_timeout()) {
|
||||
ESP_LOGE(TAG, "No data received for %" PRIu32 " ms", this->parent_->get_timeout());
|
||||
this->fail_download_();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
const bool arrived = this->downloader_->get_bytes_read() >= total;
|
||||
|
||||
if (!this->stream_started_) {
|
||||
// The decoder reads the headers when it opens the image, so wait until they are in the buffer
|
||||
if (!arrived && this->download_buffer_.free_capacity() != 0)
|
||||
return;
|
||||
const int err = this->begin_source(&this->source_);
|
||||
if (err != 0) {
|
||||
// Headers cut short by a full buffer fail to parse, or make the decoder seek past the buffer. Such an image
|
||||
// still decodes once it is all in memory, as it did before JPEG images were decoded as they download.
|
||||
if (!arrived && (err == runtime_image::DECODE_ERROR_INVALID_TYPE || err == runtime_image::DECODE_ERROR_READ)) {
|
||||
ESP_LOGW(TAG,
|
||||
"The image headers do not fit the %zu byte download buffer, so the whole image is downloaded "
|
||||
"before it is decoded; increase buffer_size to decode it as it downloads",
|
||||
this->download_buffer_.size());
|
||||
// grow() logs a failed allocation
|
||||
if (this->download_buffer_.grow(total)) {
|
||||
this->streaming_ = false;
|
||||
} else {
|
||||
this->fail_download_();
|
||||
}
|
||||
return;
|
||||
}
|
||||
ESP_LOGE(TAG, "Error decoding image: %s", runtime_image::decode_error_to_string(err));
|
||||
this->fail_download_();
|
||||
return;
|
||||
}
|
||||
this->source_.set_keep_read(false);
|
||||
this->stream_started_ = true;
|
||||
// Opening the image and allocating its buffer is enough for one pass
|
||||
return;
|
||||
}
|
||||
|
||||
do {
|
||||
const int result = this->decode_step();
|
||||
if (result == runtime_image::DECODE_STEP_MORE)
|
||||
continue;
|
||||
if (result == runtime_image::DECODE_STEP_DONE) {
|
||||
this->finish_download_();
|
||||
} else if (result == runtime_image::DECODE_STEP_NEED_DATA) {
|
||||
if (!arrived)
|
||||
return;
|
||||
// JPEGDEC only waits for bytes short of the size it was given, so this guards against a hang
|
||||
ESP_LOGE(TAG, "Download ended before the image was complete");
|
||||
this->fail_download_();
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Error decoding image: %s", runtime_image::decode_error_to_string(result));
|
||||
this->fail_download_();
|
||||
}
|
||||
return;
|
||||
} while (millis() - now < PASS_BUDGET_MS);
|
||||
}
|
||||
|
||||
void OnlineImage::report_progress_(bool force) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
const size_t bytes = this->downloader_->get_bytes_read();
|
||||
const size_t total = this->downloader_->content_length;
|
||||
// The last bytes are always reported, so a progress bar reaches 100%
|
||||
if (!force && now - this->last_progress_time_ < PROGRESS_INTERVAL_MS && (total == 0 || bytes < total))
|
||||
return;
|
||||
this->last_progress_time_ = now;
|
||||
const float progress = total > 0 ? std::min(100.0f, bytes * 100.0f / total) : NAN;
|
||||
this->download_progress_callback_.call(progress, bytes);
|
||||
}
|
||||
|
||||
void OnlineImage::fail_download_() {
|
||||
this->end_connection_();
|
||||
this->download_error_callback_.call();
|
||||
}
|
||||
|
||||
void OnlineImage::finish_download_() {
|
||||
// Must be read before end_decode() ends the decoding session
|
||||
bool decoder_finished = this->is_decode_finished();
|
||||
@@ -420,8 +288,6 @@ void OnlineImage::end_connection_() {
|
||||
this->downloader_ = nullptr;
|
||||
}
|
||||
this->download_buffer_.reset();
|
||||
this->streaming_ = false;
|
||||
this->stream_started_ = false;
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,6 @@ using t_http_codes = enum {
|
||||
HTTP_CODE_NOT_FOUND = 404,
|
||||
};
|
||||
|
||||
/// Largest download read(). A read() blocks until it has every byte it asked for, so this bounds how long one loop
|
||||
/// pass waits on a slow link.
|
||||
static constexpr size_t READ_CHUNK = 1460;
|
||||
|
||||
/**
|
||||
* @brief Download an image from a given URL, and decode it using the specified decoder.
|
||||
* The image will then be stored in a buffer, so that it can be re-displayed without the
|
||||
@@ -74,17 +70,9 @@ class OnlineImage final : public PollingComponent,
|
||||
*/
|
||||
void release();
|
||||
|
||||
/// Called when the server has accepted the request and the image starts to download
|
||||
template<typename F> void add_on_download_started_callback(F &&callback) {
|
||||
this->download_started_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
template<typename F> void add_on_finished_callback(F &&callback) {
|
||||
this->download_finished_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
/// Called with the percentage downloaded (NAN when the server did not send the size) and the bytes read so far
|
||||
template<typename F> void add_on_progress_callback(F &&callback) {
|
||||
this->download_progress_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
template<typename F> void add_on_error_callback(F &&callback) {
|
||||
this->download_error_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
@@ -93,20 +81,12 @@ class OnlineImage final : public PollingComponent,
|
||||
bool validate_url_(const std::string &url);
|
||||
void end_connection_();
|
||||
void finish_download_();
|
||||
/// loop() for a JPEG that is decoded as it downloads
|
||||
void loop_stream_();
|
||||
void fail_download_();
|
||||
/// Reports download progress, at most every PROGRESS_INTERVAL_MS unless force is set
|
||||
void report_progress_(bool force);
|
||||
|
||||
CallbackManager<void(bool)> download_finished_callback_{};
|
||||
CallbackManager<void()> download_error_callback_{};
|
||||
LazyCallbackManager<void()> download_started_callback_{};
|
||||
LazyCallbackManager<void(float, size_t)> download_progress_callback_{};
|
||||
|
||||
std::shared_ptr<http_request::HttpContainer> downloader_{nullptr};
|
||||
DownloadBuffer download_buffer_;
|
||||
DownloadSource source_{download_buffer_};
|
||||
/**
|
||||
* This is the *initial* size of the download buffer, not the current size.
|
||||
* The download buffer can be resized at runtime; the download_buffer_initial_size_
|
||||
@@ -129,9 +109,6 @@ class OnlineImage final : public PollingComponent,
|
||||
|
||||
uint32_t start_time_{0};
|
||||
uint32_t last_data_time_{0};
|
||||
uint32_t last_progress_time_{0};
|
||||
bool streaming_{false}; // The decoder reads the download itself, as it arrives
|
||||
bool stream_started_{false}; // The decoder has read the headers
|
||||
};
|
||||
|
||||
} // namespace esphome::online_image
|
||||
@@ -33,22 +33,22 @@ uint16_t parse_u8_hb_60(OpenthermData &data) { return data.valueHB * 60; }
|
||||
int16_t parse_s16(OpenthermData &data) { return data.get_s16(); }
|
||||
float parse_f88(OpenthermData &data) { return data.get_f88(); }
|
||||
|
||||
void write_flag8_lb_0(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 0, value); }
|
||||
void write_flag8_lb_1(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 1, value); }
|
||||
void write_flag8_lb_2(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 2, value); }
|
||||
void write_flag8_lb_3(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 3, value); }
|
||||
void write_flag8_lb_4(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 4, value); }
|
||||
void write_flag8_lb_5(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 5, value); }
|
||||
void write_flag8_lb_6(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 6, value); }
|
||||
void write_flag8_lb_7(const bool value, OpenthermData &data) { assign_bits(data.valueLB, 1 << 7, value); }
|
||||
void write_flag8_hb_0(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 0, value); }
|
||||
void write_flag8_hb_1(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 1, value); }
|
||||
void write_flag8_hb_2(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 2, value); }
|
||||
void write_flag8_hb_3(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 3, value); }
|
||||
void write_flag8_hb_4(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 4, value); }
|
||||
void write_flag8_hb_5(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 5, value); }
|
||||
void write_flag8_hb_6(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 6, value); }
|
||||
void write_flag8_hb_7(const bool value, OpenthermData &data) { assign_bits(data.valueHB, 1 << 7, value); }
|
||||
void write_flag8_lb_0(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 0, value); }
|
||||
void write_flag8_lb_1(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 1, value); }
|
||||
void write_flag8_lb_2(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 2, value); }
|
||||
void write_flag8_lb_3(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 3, value); }
|
||||
void write_flag8_lb_4(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 4, value); }
|
||||
void write_flag8_lb_5(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 5, value); }
|
||||
void write_flag8_lb_6(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 6, value); }
|
||||
void write_flag8_lb_7(const bool value, OpenthermData &data) { data.valueLB = write_bit(data.valueLB, 7, value); }
|
||||
void write_flag8_hb_0(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 0, value); }
|
||||
void write_flag8_hb_1(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 1, value); }
|
||||
void write_flag8_hb_2(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 2, value); }
|
||||
void write_flag8_hb_3(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 3, value); }
|
||||
void write_flag8_hb_4(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 4, value); }
|
||||
void write_flag8_hb_5(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 5, value); }
|
||||
void write_flag8_hb_6(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 6, value); }
|
||||
void write_flag8_hb_7(const bool value, OpenthermData &data) { data.valueHB = write_bit(data.valueHB, 7, value); }
|
||||
void write_u8_lb(const uint8_t value, OpenthermData &data) { data.valueLB = value; }
|
||||
void write_u8_hb(const uint8_t value, OpenthermData &data) { data.valueHB = value; }
|
||||
void write_s8_lb(const int8_t value, OpenthermData &data) { data.valueLB = (uint8_t) value; }
|
||||
|
||||
@@ -20,6 +20,14 @@ namespace esphome::opentherm {
|
||||
|
||||
template<class T> constexpr T read_bit(T value, uint8_t bit) { return (value >> bit) & 0x01; }
|
||||
|
||||
template<class T> constexpr T set_bit(T value, uint8_t bit) { return value |= (1UL << bit); }
|
||||
|
||||
template<class T> constexpr T clear_bit(T value, uint8_t bit) { return value &= ~(1UL << bit); }
|
||||
|
||||
template<class T> constexpr T write_bit(T value, uint8_t bit, uint8_t bit_value) {
|
||||
return bit_value ? set_bit(value, bit) : clear_bit(value, bit);
|
||||
}
|
||||
|
||||
enum OperationMode {
|
||||
IDLE = 0, // no operation
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_MAX_PACKET_SIZE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
DOMAIN = "packet_interface"
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
CONF_PACKET_INTERFACE = "packet_interface"
|
||||
|
||||
packet_interface_ns = cg.esphome_ns.namespace("packet_interface")
|
||||
PacketInterface = packet_interface_ns.class_("PacketInterface", cg.Component)
|
||||
|
||||
__all__ = [
|
||||
"CONF_MAX_PACKET_SIZE",
|
||||
"CONF_PACKET_INTERFACE",
|
||||
"PacketInterface",
|
||||
"new_packet_interface",
|
||||
"packet_interface_ns",
|
||||
"packet_interface_schema",
|
||||
]
|
||||
|
||||
|
||||
def packet_interface_schema(
|
||||
class_: MockObjClass,
|
||||
default_max_packet_size: int,
|
||||
max_packet_size_limit: int = 4096,
|
||||
) -> cv.Schema:
|
||||
"""Schema for a link platform."""
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(class_),
|
||||
cv.Optional(
|
||||
CONF_MAX_PACKET_SIZE, default=default_max_packet_size
|
||||
): cv.int_range(16, max_packet_size_limit),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def new_packet_interface(config: ConfigType, *args: Any) -> MockObj:
|
||||
"""Create the link layer.
|
||||
|
||||
The link's constructor takes its maximum packet size, usually as a std::span over a buffer that code generation
|
||||
sized from the configuration, so the two cannot differ.
|
||||
|
||||
Returns the link layer; applications reach the top layer with PacketInterface::top().
|
||||
"""
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
cg.add(var.set_name(config[CONF_ID].id))
|
||||
await cg.register_component(var, config)
|
||||
return var
|
||||
@@ -1,173 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
/**
|
||||
* A buffer class that efficiently represents a composite buffer with three sections:
|
||||
* - pre_buffer: A pointer to uint8_t data with a length
|
||||
* - mid_buffer: A reference to another PacketBuffer (optional)
|
||||
* - post_buffer: A pointer to uint8_t data with a length
|
||||
*
|
||||
* This allows efficient zero-copy composition of buffers without allocating new memory.
|
||||
*
|
||||
* WARNING: PacketBuffer holds raw pointers to data. The user must ensure that the data remains valid
|
||||
* for the lifetime of the PacketBuffer instance. Storing PacketBuffer instances beyond the scope of the data they
|
||||
* reference will lead to dangling pointers and undefined behavior, so to keep a packet, copy it with copy_to() into a
|
||||
* buffer allocated in setup().
|
||||
*/
|
||||
class PacketBuffer {
|
||||
public:
|
||||
PacketBuffer() = default;
|
||||
|
||||
// Disable copy semantics - PacketBuffer contains pointers and copying would create dangling references
|
||||
PacketBuffer(const PacketBuffer &) = delete;
|
||||
PacketBuffer &operator=(const PacketBuffer &) = delete;
|
||||
|
||||
// Enable move semantics
|
||||
PacketBuffer(PacketBuffer &&) = default;
|
||||
PacketBuffer &operator=(PacketBuffer &&) = default;
|
||||
|
||||
/**
|
||||
* Construct from a single buffer.
|
||||
*/
|
||||
PacketBuffer(const uint8_t *data, size_t length) : pre_buffer_(data), pre_length_(length) {}
|
||||
|
||||
/**
|
||||
* Construct from a vector.
|
||||
*/
|
||||
explicit PacketBuffer(const std::vector<uint8_t> &vec) : pre_buffer_(vec.data()), pre_length_(vec.size()) {}
|
||||
// A temporary vector would leave the buffer dangling
|
||||
explicit PacketBuffer(std::vector<uint8_t> &&) = delete;
|
||||
|
||||
/**
|
||||
* Construct with pre and post buffers.
|
||||
*/
|
||||
PacketBuffer(const uint8_t *pre_data, size_t pre_length, const uint8_t *post_data, size_t post_length)
|
||||
: pre_buffer_(pre_data), pre_length_(pre_length), post_buffer_(post_data), post_length_(post_length) {}
|
||||
|
||||
/**
|
||||
* Construct with pre, mid, and post buffers.
|
||||
*/
|
||||
PacketBuffer(const uint8_t *pre_data, size_t pre_length, const PacketBuffer *mid_buffer, const uint8_t *post_data,
|
||||
size_t post_length)
|
||||
: pre_buffer_(pre_data),
|
||||
pre_length_(pre_length),
|
||||
mid_buffer_(mid_buffer),
|
||||
post_buffer_(post_data),
|
||||
post_length_(post_length) {}
|
||||
|
||||
/**
|
||||
* Get the total size of the buffer.
|
||||
*/
|
||||
size_t size() const {
|
||||
size_t total = this->pre_length_ + this->post_length_;
|
||||
if (this->mid_buffer_ != nullptr) {
|
||||
total += this->mid_buffer_->size();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the buffer is empty.
|
||||
*/
|
||||
bool empty() const { return this->size() == 0; }
|
||||
|
||||
/**
|
||||
* Check if all the data is held in the pre-buffer, so it can be read as one span.
|
||||
*/
|
||||
bool is_contiguous() const { return this->mid_buffer_ == nullptr && this->post_length_ == 0; }
|
||||
|
||||
/**
|
||||
* Get the data as one span. Only valid when is_contiguous() is true.
|
||||
*/
|
||||
std::span<const uint8_t> as_span() const { return {this->pre_buffer_, this->pre_length_}; }
|
||||
|
||||
/**
|
||||
* Access a byte at a specific position.
|
||||
* @param index The position to access
|
||||
* @return The byte at the specified position, or 0 past the end
|
||||
*/
|
||||
uint8_t operator[](size_t index) const {
|
||||
if (index < this->pre_length_) {
|
||||
return this->pre_buffer_[index];
|
||||
}
|
||||
index -= this->pre_length_;
|
||||
|
||||
if (this->mid_buffer_ != nullptr) {
|
||||
size_t mid_size = this->mid_buffer_->size();
|
||||
if (index < mid_size) {
|
||||
return (*this->mid_buffer_)[index];
|
||||
}
|
||||
index -= mid_size;
|
||||
}
|
||||
|
||||
return index < this->post_length_ ? this->post_buffer_[index] : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy data from the buffer to a destination pointer.
|
||||
* @param dest Destination pointer
|
||||
* @param count Number of bytes to copy
|
||||
* @param offset Offset in the source buffer to start copying from
|
||||
* @return Number of bytes actually copied
|
||||
*/
|
||||
size_t copy_to(uint8_t *dest, size_t count, size_t offset = 0) const {
|
||||
size_t total_size = this->size();
|
||||
if (offset >= total_size) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t available = total_size - offset;
|
||||
size_t to_copy = count < available ? count : available;
|
||||
size_t copied = 0;
|
||||
|
||||
// Copy from pre_buffer
|
||||
if (offset < this->pre_length_) {
|
||||
size_t pre_offset = offset;
|
||||
size_t pre_available = this->pre_length_ - pre_offset;
|
||||
size_t pre_copy = to_copy < pre_available ? to_copy : pre_available;
|
||||
std::memcpy(dest, this->pre_buffer_ + pre_offset, pre_copy);
|
||||
copied += pre_copy;
|
||||
dest += pre_copy;
|
||||
offset += pre_copy;
|
||||
}
|
||||
|
||||
// Copy from mid_buffer
|
||||
size_t mid_size = this->mid_buffer_ != nullptr ? this->mid_buffer_->size() : 0;
|
||||
if (copied < to_copy && offset < this->pre_length_ + mid_size) {
|
||||
size_t mid_offset = offset - this->pre_length_;
|
||||
size_t mid_available = mid_size - mid_offset;
|
||||
size_t mid_copy = (to_copy - copied) < mid_available ? (to_copy - copied) : mid_available;
|
||||
this->mid_buffer_->copy_to(dest, mid_copy, mid_offset);
|
||||
copied += mid_copy;
|
||||
dest += mid_copy;
|
||||
offset += mid_copy;
|
||||
}
|
||||
|
||||
// Copy from post_buffer
|
||||
if ((copied < to_copy) && (this->post_length_ > 0)) {
|
||||
size_t post_offset = offset - this->pre_length_ - mid_size;
|
||||
size_t post_available = this->post_length_ - post_offset;
|
||||
size_t post_copy = (to_copy - copied) < post_available ? (to_copy - copied) : post_available;
|
||||
std::memcpy(dest, this->post_buffer_ + post_offset, post_copy);
|
||||
copied += post_copy;
|
||||
}
|
||||
|
||||
return copied;
|
||||
}
|
||||
|
||||
private:
|
||||
const uint8_t *pre_buffer_{nullptr};
|
||||
size_t pre_length_{0};
|
||||
const PacketBuffer *mid_buffer_{nullptr};
|
||||
const uint8_t *post_buffer_{nullptr};
|
||||
size_t post_length_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::packet_interface
|
||||
@@ -1,39 +0,0 @@
|
||||
#include "packet_interface.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "packet_interface");
|
||||
|
||||
// Packets in a row that cannot be understood before the mismatch is logged
|
||||
static constexpr uint8_t MISMATCH_COUNT = 10;
|
||||
|
||||
void PacketInterface::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Packet Interface '%s':\n"
|
||||
" Max Packet Size: %zu",
|
||||
this->name_, this->get_max_packet_size());
|
||||
}
|
||||
|
||||
void PacketInterface::on_receive_from_interface_(const PacketBuffer &data, const PacketMetaData &meta_data) {
|
||||
#ifdef ESPHOME_LOG_HAS_VERBOSE
|
||||
static constexpr size_t LOG_BYTES = 24;
|
||||
uint8_t head[LOG_BYTES];
|
||||
size_t len = data.copy_to(head, LOG_BYTES);
|
||||
ESP_LOGV_HEX(TAG, head, len, LOG_BYTES, "%s: received %zu bytes %s", this->name_, data.size());
|
||||
#endif
|
||||
this->callback_.call(data, meta_data);
|
||||
}
|
||||
|
||||
void PacketInterface::note_received_(bool understood) {
|
||||
if (understood) {
|
||||
this->not_understood_count_ = 0;
|
||||
} else if (this->not_understood_count_ < MISMATCH_COUNT) {
|
||||
this->not_understood_count_++;
|
||||
if (this->not_understood_count_ == MISMATCH_COUNT) {
|
||||
ESP_LOGW(TAG, "%s: %u packets in a row could not be understood; check that both ends have the same layers",
|
||||
this->name_, MISMATCH_COUNT);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace esphome::packet_interface
|
||||
@@ -1,149 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "packet_buffer.h"
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
/// The room left for a packet of \p size bytes after \p used bytes, or 0 if it is too small.
|
||||
constexpr size_t remaining_packet_size(size_t size, size_t used) { return size > used ? size - used : 0; }
|
||||
|
||||
/// What a stack of layers provides, as bits of PacketInterface::get_capabilities().
|
||||
enum Capability : uint8_t {
|
||||
CAPABILITY_ENCRYPTION = 1 << 0,
|
||||
CAPABILITY_RELIABILITY = 1 << 1,
|
||||
CAPABILITY_SESSION = 1 << 2,
|
||||
CAPABILITY_ADDRESSING = 1 << 3,
|
||||
CAPABILITY_FRAGMENTATION = 1 << 4,
|
||||
};
|
||||
|
||||
/**
|
||||
* A struct to hold metadata about a packet.
|
||||
* @param info Additional information about the packet
|
||||
* @param source_address With an addressing layer, the sender of a received packet; 0 if not known
|
||||
* @param destination_address With an addressing layer, the destination of a packet; when sending, 0 uses the
|
||||
* configured destination
|
||||
* @param rssi The RSSI of the packet
|
||||
* @param snr The SNR of the packet
|
||||
* @param mac_address The MAC address of the sender/receiver
|
||||
*/
|
||||
class PacketMetaData {
|
||||
public:
|
||||
StringRef info{};
|
||||
uint16_t source_address{0};
|
||||
uint16_t destination_address{0};
|
||||
float rssi{NAN};
|
||||
float snr{NAN};
|
||||
uint8_t mac_address[MAC_ADDRESS_SIZE]{};
|
||||
};
|
||||
/**
|
||||
* A class that will transport data to and from an abstract channel.
|
||||
*
|
||||
* The operations defined are:
|
||||
*
|
||||
* Transmit: the function send_to_interface() takes a PacketBuffer of bytes, and returns when all data is
|
||||
* written or buffered for transmission.
|
||||
*
|
||||
* Receive: The callbacks for an instance of this class are called with currently received data.
|
||||
*/
|
||||
class PacketInterface : public Component {
|
||||
public:
|
||||
/**
|
||||
*
|
||||
* Method to be implemented by child classes to send data to the interface.
|
||||
*
|
||||
* @param data The data to be sent
|
||||
* @param meta_data Metadata about the packet to be sent
|
||||
* @return True if the data was queued for transmission, false if it could not be sent
|
||||
*/
|
||||
virtual bool send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) = 0;
|
||||
|
||||
/// The largest packet this layer accepts; fixed when the layer is constructed.
|
||||
size_t get_max_packet_size() const { return this->max_packet_size_; }
|
||||
/// How many packets send_to_interface() would accept now, so a layer above can tell before sending a series.
|
||||
virtual size_t get_send_capacity() const { return SIZE_MAX; }
|
||||
/// The capabilities of this layer and every layer below it.
|
||||
uint8_t get_capabilities() const { return this->capabilities_; }
|
||||
bool has_capability(Capability capability) const { return (this->capabilities_ & capability) != 0; }
|
||||
/**
|
||||
* With CAPABILITY_SESSION, the callback is called with true when the peer can be reached, including after it
|
||||
* restarts, and with false when it is lost. The session layer is always the top layer, so register on top().
|
||||
* A reliability layer calls it with false when it resets the link and discards data, which the session layer above
|
||||
* reports as the session lost.
|
||||
*/
|
||||
template<typename F> void add_on_session_state_callback(F &&callback) {
|
||||
this->session_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
/// The topmost layer stacked on this one, or this one if nothing is stacked on it.
|
||||
PacketInterface *top() {
|
||||
PacketInterface *layer = this;
|
||||
while (layer->upper_ != nullptr)
|
||||
layer = layer->upper_;
|
||||
return layer;
|
||||
}
|
||||
/// Name shown in log messages; a layer takes the name of the link it is stacked on.
|
||||
void set_name(const char *name) { this->name_ = name; }
|
||||
const char *get_name() const { return this->name_; }
|
||||
|
||||
/**
|
||||
* Add a listener that will be called when data is received.
|
||||
*
|
||||
* @param callback The callback to be called when data is received.
|
||||
*/
|
||||
template<typename F> void add_packet_interface_listener(F &&callback) {
|
||||
this->callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
/// @param max_packet_size The largest packet the layer accepts, set from the layer below or the layer's own buffer
|
||||
explicit PacketInterface(size_t max_packet_size) : max_packet_size_(max_packet_size) {}
|
||||
|
||||
virtual ~PacketInterface() = default;
|
||||
/**
|
||||
* This function should be called by implementing classes with received data to be passed to any listeners.
|
||||
*
|
||||
* @param data The received data
|
||||
* @param meta_data Metadata about the received packet
|
||||
*/
|
||||
void on_receive_from_interface_(const PacketBuffer &data, const PacketMetaData &meta_data);
|
||||
/**
|
||||
* Called by a layer for each received packet it understood, or could not make sense of. A run of packets that
|
||||
* cannot be understood usually means the peer has different layers configured, which is logged once per run.
|
||||
*/
|
||||
void note_received_(bool understood);
|
||||
/// Called by a layer's constructor to record itself as the layer above \p lower, adding its own capability.
|
||||
void stack_on_(PacketInterface *lower, Capability capability) {
|
||||
lower->upper_ = this;
|
||||
this->inherit_from_(lower, capability);
|
||||
}
|
||||
/// Takes the name and capabilities of `lower` without becoming its top, for one of several stacks on a link.
|
||||
void inherit_from_(PacketInterface *lower, Capability capability) {
|
||||
this->name_ = lower->name_;
|
||||
this->capabilities_ = lower->capabilities_ | capability;
|
||||
// A link that has a session of its own, such as TCP, reports it to whatever is stacked above, as does a
|
||||
// reliability layer that discards data; the session layer handles the latter itself
|
||||
if ((lower->has_capability(CAPABILITY_SESSION) || lower->has_capability(CAPABILITY_RELIABILITY)) &&
|
||||
capability != CAPABILITY_SESSION) {
|
||||
lower->add_on_session_state_callback([this](bool connected) { this->session_callback_.call(connected); });
|
||||
}
|
||||
}
|
||||
|
||||
LazyCallbackManager<void(const PacketBuffer &, const PacketMetaData &)> callback_;
|
||||
LazyCallbackManager<void(bool)> session_callback_;
|
||||
PacketInterface *upper_{nullptr};
|
||||
const char *name_{""};
|
||||
const size_t max_packet_size_;
|
||||
uint8_t capabilities_{0};
|
||||
uint8_t not_understood_count_{0};
|
||||
};
|
||||
} // namespace esphome::packet_interface
|
||||
@@ -1,28 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.packet_transport import (
|
||||
PacketTransport,
|
||||
new_packet_transport,
|
||||
transport_schema,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_PACKET_INTERFACE, PacketInterface, packet_interface_ns
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
DEPENDENCIES = ["packet_interface"]
|
||||
|
||||
PacketInterfaceTransport = packet_interface_ns.class_(
|
||||
"PacketInterfaceTransport", PacketTransport
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = transport_schema(PacketInterfaceTransport).extend(
|
||||
{
|
||||
cv.GenerateID(CONF_PACKET_INTERFACE): cv.use_id(PacketInterface),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
interface = await cg.get_variable(config[CONF_PACKET_INTERFACE])
|
||||
await new_packet_transport(config, interface)
|
||||
@@ -1,29 +0,0 @@
|
||||
#include "packet_interface_transport.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "packet_interface.transport");
|
||||
|
||||
void PacketInterfaceTransport::setup() {
|
||||
PacketTransport::setup();
|
||||
this->interface_ = this->interface_->top();
|
||||
this->interface_->add_packet_interface_listener(
|
||||
[this](const PacketBuffer &data, const PacketMetaData &) { this->receive_(data); });
|
||||
}
|
||||
|
||||
void PacketInterfaceTransport::receive_(const PacketBuffer &data) {
|
||||
// Packet interface layers deliver packets in one piece
|
||||
if (!data.is_contiguous()) {
|
||||
ESP_LOGW(TAG, "Packet split across buffers is not supported");
|
||||
return;
|
||||
}
|
||||
this->process_(data.as_span());
|
||||
}
|
||||
|
||||
void PacketInterfaceTransport::send_packet(const std::vector<uint8_t> &buf) const {
|
||||
if (!this->interface_->send_to_interface(PacketBuffer(buf), {}))
|
||||
ESP_LOGD(TAG, "Failed to send packet of %zu bytes", buf.size());
|
||||
}
|
||||
|
||||
} // namespace esphome::packet_interface
|
||||
@@ -1,29 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/packet_interface/packet_interface.h"
|
||||
#include "esphome/components/packet_transport/packet_transport.h"
|
||||
|
||||
namespace esphome::packet_interface {
|
||||
|
||||
/**
|
||||
* A transport protocol adapter that bridges PacketInterface to PacketTransport.
|
||||
* This allows packet_transport functionality to be used with any PacketInterface implementation.
|
||||
*/
|
||||
class PacketInterfaceTransport : public packet_transport::PacketTransport {
|
||||
public:
|
||||
explicit PacketInterfaceTransport(PacketInterface *interface) : interface_(interface) {}
|
||||
|
||||
void setup() override;
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
|
||||
|
||||
protected:
|
||||
// implementing PacketTransport virtual methods
|
||||
size_t get_max_packet_size() override { return this->interface_->top()->get_max_packet_size(); }
|
||||
void send_packet(const std::vector<uint8_t> &buf) const override;
|
||||
void receive_(const PacketBuffer &data);
|
||||
|
||||
PacketInterface *interface_;
|
||||
};
|
||||
|
||||
} // namespace esphome::packet_interface
|
||||
@@ -205,10 +205,8 @@ async def register_packet_transport(var: MockObj, config: ConfigType) -> set[str
|
||||
return providers
|
||||
|
||||
|
||||
async def new_packet_transport(
|
||||
config: ConfigType, *args: Any
|
||||
) -> tuple[MockObj, set[str]]:
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
async def new_packet_transport(config: ConfigType) -> tuple[MockObj, set[str]]:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add(var.set_platform_name(config[CONF_PLATFORM]))
|
||||
providers = await register_packet_transport(var, config)
|
||||
return var, providers
|
||||
@@ -547,11 +547,9 @@ void PacketTransport::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Packet Transport:\n"
|
||||
" Platform: %s\n"
|
||||
" Max Packet Size: %zu\n"
|
||||
" Encrypted: %s\n"
|
||||
" Ping-pong: %s",
|
||||
this->platform_name_, this->get_max_packet_size(), YESNO(this->is_encrypted_()),
|
||||
YESNO(this->ping_pong_enable_));
|
||||
this->platform_name_, YESNO(this->is_encrypted_()), YESNO(this->ping_pong_enable_));
|
||||
#ifdef USE_SENSOR
|
||||
for (const auto &sensor : this->sensors_) {
|
||||
ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.id);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "pca6416a.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pca6416a {
|
||||
@@ -171,7 +170,11 @@ void PCA6416AComponent::update_register_(uint8_t pin, bool pin_value, uint8_t re
|
||||
this->read_register_(reg_addr, ®_value);
|
||||
}
|
||||
|
||||
assign_bits(reg_value, 1 << bit, pin_value);
|
||||
if (pin_value) {
|
||||
reg_value |= 1 << bit;
|
||||
} else {
|
||||
reg_value &= ~(1 << bit);
|
||||
}
|
||||
|
||||
this->write_register_(reg_addr, reg_value);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "pca9505.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pca9505 {
|
||||
@@ -75,7 +74,11 @@ bool PCA9505Component::digital_read_cache(uint8_t pin) { return this->input_[pin
|
||||
void PCA9505Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
const uint8_t bank = pin / 8;
|
||||
const uint8_t mask = 1 << (pin % 8);
|
||||
assign_bits(this->output_[bank], mask, value);
|
||||
if (value) {
|
||||
this->output_[bank] |= mask;
|
||||
} else {
|
||||
this->output_[bank] &= ~mask;
|
||||
}
|
||||
this->write_bank_(OUTPUT_REG, bank, this->output_[bank]);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "pca9554.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pca9554 {
|
||||
@@ -100,7 +99,11 @@ bool PCA9554Component::digital_read_cache(uint8_t pin) {
|
||||
}
|
||||
|
||||
void PCA9554Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
assign_bits(this->output_mask_, 1 << pin, value);
|
||||
if (value) {
|
||||
this->output_mask_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_mask_ &= ~(1 << pin);
|
||||
}
|
||||
#ifdef USE_PCA9554_PCAL
|
||||
if (this->write_register_(OUTPUT_REG, this->output_mask_))
|
||||
this->retry_pcal_shadows_(); // an output-only hub never reads, so it retries here
|
||||
|
||||
@@ -90,7 +90,11 @@ void HOT PCD8544::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
|
||||
uint16_t pos = x + (y / 8) * this->get_width_internal();
|
||||
uint8_t subpos = y % 8;
|
||||
assign_bits(this->buffer_[pos], 1 << subpos, color.is_on());
|
||||
if (color.is_on()) {
|
||||
this->buffer_[pos] |= (1 << subpos);
|
||||
} else {
|
||||
this->buffer_[pos] &= ~(1 << subpos);
|
||||
}
|
||||
}
|
||||
|
||||
void PCD8544::dump_config() {
|
||||
|
||||
@@ -60,7 +60,11 @@ bool PCF8574Component::digital_read_hw(uint8_t pin) {
|
||||
bool PCF8574Component::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
|
||||
|
||||
void PCF8574Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
assign_bits(this->output_mask_, 1 << pin, value);
|
||||
if (value) {
|
||||
this->output_mask_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_mask_ &= ~(1 << pin);
|
||||
}
|
||||
this->write_gpio_();
|
||||
}
|
||||
void PCF8574Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "pi4ioe5v6408.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pi4ioe5v6408 {
|
||||
@@ -130,7 +129,11 @@ void PI4IOE5V6408Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
|
||||
assign_bits(this->output_mask_, 1 << pin, value);
|
||||
if (value) {
|
||||
this->output_mask_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_mask_ &= ~(1 << pin);
|
||||
}
|
||||
if (!this->status_comm_result(this->write_byte(PI4IOE5V6408_REGISTER_OUT_SET, this->output_mask_),
|
||||
LOG_STR("Failed to write output register")))
|
||||
return;
|
||||
|
||||
@@ -138,7 +138,7 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
|
||||
const int err = this->start_();
|
||||
if (err != 0)
|
||||
return err;
|
||||
if (!this->jpeg_.decode(0, 0, this->scale_option_()))
|
||||
if (!this->jpeg_.decode(0, 0, 0))
|
||||
return this->fail_();
|
||||
this->decoded_bytes_ = size;
|
||||
this->jpeg_.close();
|
||||
@@ -165,28 +165,10 @@ int JpegDecoder::start_() {
|
||||
} else {
|
||||
this->jpeg_.setPixelType(RGB8888);
|
||||
}
|
||||
const int width = this->jpeg_.getWidth();
|
||||
const int height = this->jpeg_.getHeight();
|
||||
if (!this->set_size(width, height)) {
|
||||
if (!this->set_size(this->jpeg_.getWidth(), this->jpeg_.getHeight())) {
|
||||
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
|
||||
// many pixels as the buffer, so a large image shown small is not decoded in full only to be thrown away.
|
||||
const int buffer_width = this->image_->get_buffer_width();
|
||||
const int buffer_height = this->image_->get_buffer_height();
|
||||
this->scale_ = 1;
|
||||
while (this->scale_ < JPEG_SCALE_EIGHTH && width / (this->scale_ * 2) >= buffer_width &&
|
||||
height / (this->scale_ * 2) >= buffer_height) {
|
||||
this->scale_ *= 2;
|
||||
}
|
||||
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);
|
||||
}
|
||||
if (this->direct_rgb565_ && !this->prepare_rgb565_scaling_(decoded_width, decoded_height)) {
|
||||
if (this->direct_rgb565_ && !this->prepare_rgb565_scaling_(this->jpeg_.getWidth(), this->jpeg_.getHeight())) {
|
||||
return DECODE_ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
return 0;
|
||||
@@ -270,7 +252,7 @@ int JpegDecoder::begin_source(DecodeSource *source) {
|
||||
this->source_ = nullptr;
|
||||
return err;
|
||||
}
|
||||
if (!this->jpeg_.decodeBegin(0, 0, this->scale_option_()))
|
||||
if (!this->jpeg_.decodeBegin(0, 0, 0))
|
||||
return this->fail_();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -31,16 +31,12 @@ class JpegDecoder : public ImageDecoder {
|
||||
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
|
||||
int scale_option_() const { return this->scale_ == 1 ? 0 : this->scale_; }
|
||||
/// The DecodeError for a failed read of the source or JPEGDEC's last error, closing the image
|
||||
int fail_();
|
||||
|
||||
@@ -53,7 +49,6 @@ class JpegDecoder : public ImageDecoder {
|
||||
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};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "runtime_image.h"
|
||||
#include "image_decoder.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -92,7 +91,11 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
|
||||
auto on = is_color_on(color);
|
||||
if (this->has_transparency() && color.w < 0x80)
|
||||
on = false;
|
||||
assign_bits(this->buffer_[pos], bitno, on);
|
||||
if (on) {
|
||||
this->buffer_[pos] |= bitno;
|
||||
} else {
|
||||
this->buffer_[pos] &= ~bitno;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case image::IMAGE_TYPE_RGB111: {
|
||||
|
||||
@@ -342,7 +342,11 @@ void HOT SSD1306::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
|
||||
uint16_t pos = x + (y / 8) * this->get_width_internal();
|
||||
uint8_t subpos = y & 0x07;
|
||||
assign_bits(this->buffer_[pos], 1 << subpos, color.is_on());
|
||||
if (color.is_on()) {
|
||||
this->buffer_[pos] |= (1 << subpos);
|
||||
} else {
|
||||
this->buffer_[pos] &= ~(1 << subpos);
|
||||
}
|
||||
}
|
||||
void SSD1306::fill(Color color) {
|
||||
// If clipping is active, fall back to base implementation
|
||||
|
||||
@@ -4,7 +4,6 @@ import pkgutil
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import display, spi
|
||||
from esphome.components.display import CONF_SHOW_TEST_CARD
|
||||
from esphome.components.mipi import (
|
||||
DriverChip,
|
||||
flatten_sequence,
|
||||
@@ -13,16 +12,13 @@ from esphome.components.mipi import (
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
CONF_CS_PIN,
|
||||
CONF_DATA_RATE,
|
||||
CONF_DC_PIN,
|
||||
CONF_HEIGHT,
|
||||
CONF_ID,
|
||||
CONF_INIT_SEQUENCE,
|
||||
CONF_LAMBDA,
|
||||
CONF_MODEL,
|
||||
CONF_PAGES,
|
||||
CONF_RESET_PIN,
|
||||
CONF_ROTATION,
|
||||
CONF_WIDTH,
|
||||
@@ -53,13 +49,6 @@ MODELS = DriverChip.get_models()
|
||||
DEFAULT_MODEL = "WAVESHARE-400X300"
|
||||
|
||||
|
||||
def _model_key(model: DriverChip, key: str) -> cv.Optional | cv.Required:
|
||||
"""Default the key from the model, or require it when the model has none."""
|
||||
if model.get_default(key, None) is None:
|
||||
return cv.Required(key)
|
||||
return model.option(key)
|
||||
|
||||
|
||||
def _model_schema(model: DriverChip) -> cv.Schema:
|
||||
iseqconf = (
|
||||
cv.Required(CONF_INIT_SEQUENCE)
|
||||
@@ -79,12 +68,12 @@ def _model_schema(model: DriverChip) -> cv.Schema:
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ST7305),
|
||||
cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True),
|
||||
_model_key(model, CONF_CS_PIN): pins.gpio_output_pin_schema,
|
||||
_model_key(model, CONF_DC_PIN): pins.gpio_output_pin_schema,
|
||||
model.option(CONF_CS_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema,
|
||||
model.option(CONF_DC_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema,
|
||||
model.option(CONF_RESET_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema,
|
||||
_model_key(model, CONF_WIDTH): cv.int_range(min=8, max=1024),
|
||||
_model_key(model, CONF_HEIGHT): cv.int_range(min=8, max=1024),
|
||||
_model_key(model, CONF_PORTRAIT): cv.boolean,
|
||||
cv.Optional(CONF_WIDTH): cv.int_range(min=8, max=1024),
|
||||
cv.Optional(CONF_HEIGHT): cv.int_range(min=8, max=1024),
|
||||
cv.Optional(CONF_PORTRAIT): cv.boolean,
|
||||
cv.Optional(CONF_COLUMN_START): cv.int_range(min=0, max=255),
|
||||
cv.Optional(CONF_COLUMN_END): cv.int_range(min=0, max=255),
|
||||
cv.Optional(CONF_ROW_START): cv.int_range(min=0, max=255),
|
||||
@@ -96,9 +85,23 @@ def _model_schema(model: DriverChip) -> cv.Schema:
|
||||
|
||||
|
||||
def _resolve(config: ConfigType, model: DriverChip) -> None:
|
||||
"""Derive and validate the address window, and default the init sequence."""
|
||||
width = config[CONF_WIDTH]
|
||||
height = config[CONF_HEIGHT]
|
||||
"""Fill in model defaults and validate the geometry and address window."""
|
||||
if (width := config.get(CONF_WIDTH)) is None:
|
||||
if (width := model.get_default(CONF_WIDTH, None)) is None:
|
||||
raise cv.Invalid("'width' is required for this model", path=[CONF_WIDTH])
|
||||
config[CONF_WIDTH] = width
|
||||
if (height := config.get(CONF_HEIGHT)) is None:
|
||||
if (height := model.get_default(CONF_HEIGHT, None)) is None:
|
||||
raise cv.Invalid("'height' is required for this model", path=[CONF_HEIGHT])
|
||||
config[CONF_HEIGHT] = height
|
||||
|
||||
if config.get(CONF_PORTRAIT) is None:
|
||||
if (default := model.get_default(CONF_PORTRAIT, None)) is None:
|
||||
raise cv.Invalid(
|
||||
"'portrait' is required for this model", path=[CONF_PORTRAIT]
|
||||
)
|
||||
config[CONF_PORTRAIT] = default
|
||||
|
||||
if config[CONF_PORTRAIT]:
|
||||
if width % 12 or height % 2:
|
||||
raise cv.Invalid(
|
||||
@@ -145,6 +148,10 @@ def _resolve(config: ConfigType, model: DriverChip) -> None:
|
||||
if config.get(CONF_INIT_SEQUENCE) is None:
|
||||
config[CONF_INIT_SEQUENCE] = model.initsequence
|
||||
|
||||
for key, label in ((CONF_CS_PIN, "cs_pin"), (CONF_DC_PIN, "dc_pin")):
|
||||
if key not in config:
|
||||
raise cv.Invalid(f"'{label}' is required for this model", path=[key])
|
||||
|
||||
|
||||
def model_schema(config: ConfigType) -> cv.Schema:
|
||||
"""Return the schema for the model selected in the config."""
|
||||
@@ -171,10 +178,7 @@ def customise_schema(config: ConfigType) -> ConfigType:
|
||||
config[CONF_HEIGHT],
|
||||
has_hardware_rotation=False,
|
||||
byte_order=cv.UNDEFINED,
|
||||
has_writer=config.get(CONF_AUTO_CLEAR_ENABLED) is True
|
||||
or config.get(CONF_PAGES) is not None
|
||||
or config.get(CONF_LAMBDA) is not None
|
||||
or config.get(CONF_SHOW_TEST_CARD) is True,
|
||||
has_writer=display.config_has_writer(config),
|
||||
rotation=config.get(CONF_ROTATION, 0),
|
||||
)
|
||||
return config
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "st7305.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::st7305 {
|
||||
@@ -27,6 +27,7 @@ bool ST7305::init_buffer_() {
|
||||
this->mark_failed(LOG_STR("Buffer allocation failed"));
|
||||
return false;
|
||||
}
|
||||
memset(this->buffer_, 0x00, this->buffer_length_);
|
||||
this->clear_dirty_();
|
||||
return true;
|
||||
}
|
||||
@@ -55,33 +56,26 @@ void ST7305::reset_() {
|
||||
}
|
||||
|
||||
void ST7305::send_init_sequence_() {
|
||||
while (this->init_index_ < this->init_sequence_length_) {
|
||||
if (this->init_sequence_length_ - this->init_index_ < 2) {
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
const uint8_t command = progmem_read_byte(&this->init_sequence_[this->init_index_++]);
|
||||
const uint8_t count = progmem_read_byte(&this->init_sequence_[this->init_index_++]);
|
||||
if (count == 0xFF) {
|
||||
// Resume the sequence after the delay instead of blocking the main loop.
|
||||
this->set_timeout(STARTUP_TIMEOUT_ID, command, [this]() { this->send_init_sequence_(); });
|
||||
return;
|
||||
}
|
||||
if (this->init_sequence_length_ - this->init_index_ < count) {
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
return;
|
||||
}
|
||||
display::InitSequenceEntry entry;
|
||||
display::InitSequenceStep step;
|
||||
while ((step = this->init_reader_.next(entry)) == display::INIT_SEQUENCE_STEP_COMMAND) {
|
||||
this->enable();
|
||||
this->send_command_(command);
|
||||
if (count != 0) {
|
||||
this->send_command_(entry.value);
|
||||
if (entry.count != 0) {
|
||||
this->dc_pin_->digital_write(true);
|
||||
for (uint8_t i = 0; i < count; i++)
|
||||
this->write_byte(progmem_read_byte(&this->init_sequence_[this->init_index_ + i]));
|
||||
for (uint8_t i = 0; i < entry.count; i++)
|
||||
this->write_byte(progmem_read_byte(entry.args + i));
|
||||
}
|
||||
this->disable();
|
||||
this->init_index_ += count;
|
||||
}
|
||||
this->finish_init_();
|
||||
if (step == display::INIT_SEQUENCE_STEP_DELAY) {
|
||||
// Resume the sequence after the delay instead of blocking the main loop.
|
||||
this->set_timeout(STARTUP_TIMEOUT_ID, entry.value, [this]() { this->send_init_sequence_(); });
|
||||
} else if (step == display::INIT_SEQUENCE_STEP_MALFORMED) {
|
||||
this->mark_failed(LOG_STR("Malformed init sequence"));
|
||||
} else {
|
||||
this->finish_init_();
|
||||
}
|
||||
}
|
||||
|
||||
void ST7305::finish_init_() {
|
||||
@@ -190,7 +184,11 @@ void HOT ST7305::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
// The init sequence enables INVON, so a set bit is white and a clear bit is black.
|
||||
uint8_t &value = this->buffer_[index];
|
||||
const uint8_t previous = value;
|
||||
assign_bits(value, mask, color.is_on());
|
||||
if (color.is_on()) {
|
||||
value |= mask;
|
||||
} else {
|
||||
value &= ~mask;
|
||||
}
|
||||
if (value == previous)
|
||||
return;
|
||||
this->extend_dirty_(x, y, x, y);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/components/display/display_buffer.h"
|
||||
#include "esphome/components/display/init_sequence.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
@@ -35,8 +36,7 @@ class ST7305 : public display::DisplayBuffer,
|
||||
row_start_(row_start),
|
||||
row_end_(row_end),
|
||||
buffer_length_(static_cast<size_t>(width) * static_cast<size_t>(height) / 8),
|
||||
init_sequence_(init_sequence),
|
||||
init_sequence_length_(init_sequence_length) {}
|
||||
init_reader_(init_sequence, init_sequence_length) {}
|
||||
|
||||
void setup() override;
|
||||
void update() override;
|
||||
@@ -113,9 +113,7 @@ class ST7305 : public display::DisplayBuffer,
|
||||
uint8_t row_start_;
|
||||
uint8_t row_end_;
|
||||
size_t buffer_length_;
|
||||
const uint8_t *init_sequence_;
|
||||
size_t init_sequence_length_;
|
||||
size_t init_index_{0};
|
||||
display::InitSequenceReader init_reader_;
|
||||
bool ready_{false};
|
||||
|
||||
int x_low_{0};
|
||||
|
||||
@@ -123,7 +123,11 @@ void HOT ST7567::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
|
||||
uint16_t pos = x + (y / 8) * this->get_width_internal();
|
||||
uint8_t subpos = y & 0x07;
|
||||
assign_bits(this->buffer_[pos], 1 << subpos, color.is_on());
|
||||
if (color.is_on()) {
|
||||
this->buffer_[pos] |= (1 << subpos);
|
||||
} else {
|
||||
this->buffer_[pos] &= ~(1 << subpos);
|
||||
}
|
||||
}
|
||||
|
||||
void ST7567::fill(Color color) {
|
||||
|
||||
@@ -92,9 +92,20 @@ void SX1509Component::digital_write_hw(uint8_t pin, bool bit_value) {
|
||||
// If the pin is an output, write high/low
|
||||
uint16_t temp_reg_data = 0;
|
||||
this->read_byte_16(REG_DATA_B, &temp_reg_data);
|
||||
assign_bits(this->output_state_, 1 << pin, bit_value);
|
||||
// Outputs take the shadow state, inputs keep what was read
|
||||
temp_reg_data = (temp_reg_data & this->ddr_mask_) | (this->output_state_ & ~this->ddr_mask_);
|
||||
if (bit_value) {
|
||||
output_state_ |= (1 << pin); // set bit in shadow register
|
||||
} else {
|
||||
output_state_ &= ~(1 << pin); // reset bit shadow register
|
||||
}
|
||||
for (uint16_t b = 0x8000; b; b >>= 1) {
|
||||
if ((~ddr_mask_) & b) { // transfer bits of outputs, but don't mess with inputs
|
||||
if (output_state_ & b) {
|
||||
temp_reg_data |= b;
|
||||
} else {
|
||||
temp_reg_data &= ~b;
|
||||
}
|
||||
}
|
||||
}
|
||||
this->write_byte_16(REG_DATA_B, temp_reg_data);
|
||||
}
|
||||
}
|
||||
@@ -137,12 +148,20 @@ void SX1509Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
|
||||
// Pullup
|
||||
this->read_byte_16(REG_PULL_UP_B, &temp_word);
|
||||
assign_bits(temp_word, 1 << pin, flags & gpio::FLAG_PULLUP);
|
||||
if (flags & gpio::FLAG_PULLUP) {
|
||||
temp_word |= (1 << pin);
|
||||
} else {
|
||||
temp_word &= ~(1 << pin);
|
||||
}
|
||||
this->write_byte_16(REG_PULL_UP_B, temp_word);
|
||||
|
||||
// Pulldown
|
||||
this->read_byte_16(REG_PULL_DOWN_B, &temp_word);
|
||||
assign_bits(temp_word, 1 << pin, flags & gpio::FLAG_PULLDOWN);
|
||||
if (flags & gpio::FLAG_PULLDOWN) {
|
||||
temp_word |= (1 << pin);
|
||||
} else {
|
||||
temp_word &= ~(1 << pin);
|
||||
}
|
||||
this->write_byte_16(REG_PULL_DOWN_B, temp_word);
|
||||
|
||||
// Set direction to input
|
||||
|
||||
@@ -19,52 +19,6 @@ static constexpr uint8_t LOG_LEVEL_TO_SYSLOG_SEVERITY[] PROGMEM = {
|
||||
7 // VERY_VERBOSE
|
||||
};
|
||||
|
||||
static constexpr char SECRET_BEGIN[] = ESPHOME_LOG_SECRET_BEGIN;
|
||||
static constexpr char SECRET_END[] = ESPHOME_LOG_SECRET_END;
|
||||
static constexpr size_t SECRET_BEGIN_LEN = sizeof(SECRET_BEGIN) - 1;
|
||||
static constexpr size_t SECRET_END_LEN = sizeof(SECRET_END) - 1;
|
||||
|
||||
static bool starts_with_marker(const char *message, size_t remaining, const char *marker, size_t marker_len) {
|
||||
return message[0] == '\033' && remaining >= marker_len && memcmp(message, marker, marker_len) == 0;
|
||||
}
|
||||
|
||||
static size_t append_bytes(char *buf, size_t size, size_t pos, const char *data, size_t len) {
|
||||
if (pos >= size) {
|
||||
return size;
|
||||
}
|
||||
size_t n = std::min(len, size - pos - 1);
|
||||
memcpy(buf + pos, data, n);
|
||||
pos += n;
|
||||
buf[pos] = '\0';
|
||||
return pos;
|
||||
}
|
||||
|
||||
size_t append_redacted_message(char *buf, size_t size, size_t pos, const char *message, size_t len) {
|
||||
if (memchr(message, '\033', len) == nullptr) {
|
||||
return append_bytes(buf, size, pos, message, len);
|
||||
}
|
||||
size_t i = 0;
|
||||
while (i < len) {
|
||||
if (starts_with_marker(message + i, len - i, SECRET_BEGIN, SECRET_BEGIN_LEN)) {
|
||||
pos = buf_append_str(buf, size, pos, "[redacted]");
|
||||
i += SECRET_BEGIN_LEN;
|
||||
while (i < len && !starts_with_marker(message + i, len - i, SECRET_END, SECRET_END_LEN)) {
|
||||
i++;
|
||||
}
|
||||
if (i < len) {
|
||||
i += SECRET_END_LEN;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
size_t start = i;
|
||||
while (i < len && !starts_with_marker(message + i, len - i, SECRET_BEGIN, SECRET_BEGIN_LEN)) {
|
||||
i++;
|
||||
}
|
||||
pos = append_bytes(buf, size, pos, message + start, i - start);
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
void Syslog::setup() {
|
||||
logger::global_logger->add_log_callback(
|
||||
this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) {
|
||||
@@ -117,15 +71,16 @@ void Syslog::log_(const int level, const char *tag, const char *message, size_t
|
||||
} else {
|
||||
offset = buf_append_str(packet, sizeof(packet), offset, "-");
|
||||
}
|
||||
offset = buf_append_printf(packet, sizeof(packet), offset, " %s %s - - - ", App.get_name().c_str(), tag);
|
||||
offset = buf_append_printf(packet, sizeof(packet), offset, " %s %s - - - %.*s", App.get_name().c_str(), tag,
|
||||
(int) len, message);
|
||||
} else {
|
||||
// RFC 3164 has no NILVALUE. If the clock is invalid, omit TIMESTAMP so a relay can add it.
|
||||
if (now.is_valid()) {
|
||||
offset += now.strftime(packet + offset, sizeof(packet) - offset, "%b %e %H:%M:%S ");
|
||||
}
|
||||
offset = buf_append_printf(packet, sizeof(packet), offset, "%s %s: ", App.get_name().c_str(), tag);
|
||||
offset = buf_append_printf(packet, sizeof(packet), offset, "%s %s: %.*s", App.get_name().c_str(), tag, (int) len,
|
||||
message);
|
||||
}
|
||||
offset = append_redacted_message(packet, sizeof(packet), offset, message, len);
|
||||
// Clamp to exclude null terminator position if buffer was filled
|
||||
if (offset >= sizeof(packet)) {
|
||||
offset = sizeof(packet) - 1;
|
||||
|
||||
@@ -12,10 +12,6 @@ enum SyslogFormat : uint8_t {
|
||||
SYSLOG_FORMAT_RFC5424,
|
||||
};
|
||||
|
||||
/// Append len bytes of message to buf at pos, replacing every LOG_SECRET() region with "[redacted]".
|
||||
/// Returns the new position, capped at size like the other buf_append helpers.
|
||||
size_t append_redacted_message(char *buf, size_t size, size_t pos, const char *message, size_t len);
|
||||
|
||||
class Syslog final : public Component, public Parented<udp::UDPComponent> {
|
||||
public:
|
||||
Syslog(int level, time::RealTimeClock *time) : log_level_(level), time_(time) {}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "tca9555.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
static const uint8_t TCA9555_INPUT_PORT_REGISTER_0 = 0x00;
|
||||
@@ -109,7 +108,11 @@ void TCA9555Component::digital_write_hw(uint8_t pin, bool value) {
|
||||
if (this->is_failed())
|
||||
return;
|
||||
|
||||
assign_bits(this->output_mask_, 1 << pin, value);
|
||||
if (value) {
|
||||
this->output_mask_ |= (1 << pin);
|
||||
} else {
|
||||
this->output_mask_ &= ~(1 << pin);
|
||||
}
|
||||
|
||||
this->status_comm_result(this->write_byte_16_le(TCA9555_OUTPUT_PORT_REGISTER_0, this->output_mask_),
|
||||
LOG_STR("Failed to write output register"));
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import packet_interface, uart
|
||||
from esphome.components.packet_interface import (
|
||||
CONF_MAX_PACKET_SIZE,
|
||||
packet_interface_schema,
|
||||
)
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import uart_ns
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
UartPacketInterface = uart_ns.class_(
|
||||
"UartPacketInterface",
|
||||
packet_interface.PacketInterface,
|
||||
uart.UARTDevice,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = packet_interface_schema(UartPacketInterface, 508).extend(
|
||||
uart.UART_DEVICE_SCHEMA
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
# The receive buffer's size is known here, so it is a static array rather than allocated at run time
|
||||
rx_buffer = f"{config[CONF_ID].id}__rx_buffer"
|
||||
cg.add_global(
|
||||
cg.RawStatement(f"static uint8_t {rx_buffer}[{config[CONF_MAX_PACKET_SIZE]}];")
|
||||
)
|
||||
var = await packet_interface.new_packet_interface(
|
||||
config, cg.RawExpression(rx_buffer)
|
||||
)
|
||||
await uart.register_uart_device(var, config)
|
||||
@@ -1,82 +0,0 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "uart_packet_interface.h"
|
||||
|
||||
namespace esphome::uart {
|
||||
|
||||
using packet_interface::PacketBuffer;
|
||||
using packet_interface::PacketMetaData;
|
||||
|
||||
ESPHOME_LOG_TAG(TAG, "uart.packet_interface");
|
||||
|
||||
static constexpr uint8_t FLAG_BYTE = 0x7E;
|
||||
static constexpr uint8_t CONTROL_BYTE = 0x7D;
|
||||
static constexpr uint8_t ESCAPE_XOR = 0x20;
|
||||
// Raw bytes escaped and written per UART write
|
||||
static constexpr size_t TX_CHUNK_SIZE = 32;
|
||||
|
||||
void UartPacketInterface::loop() {
|
||||
this->read_available([this](uint8_t byte) { this->handle_rx_byte_(byte); });
|
||||
}
|
||||
|
||||
void UartPacketInterface::handle_rx_byte_(uint8_t byte) {
|
||||
if (byte == FLAG_BYTE) {
|
||||
// A flag that ends a frame also starts the next, so an unfinished frame cannot hide the one after it
|
||||
if (this->rx_started_ && this->rx_length_ != 0)
|
||||
this->on_receive_from_interface_(PacketBuffer(this->rx_buffer_.data(), this->rx_length_), {});
|
||||
this->rx_started_ = true;
|
||||
this->rx_length_ = 0;
|
||||
this->rx_control_ = false;
|
||||
return;
|
||||
}
|
||||
if (!this->rx_started_)
|
||||
return;
|
||||
if (byte == CONTROL_BYTE) {
|
||||
this->rx_control_ = true;
|
||||
return;
|
||||
}
|
||||
if (this->rx_control_) {
|
||||
byte ^= ESCAPE_XOR;
|
||||
this->rx_control_ = false;
|
||||
}
|
||||
if (this->rx_length_ >= this->rx_buffer_.size()) {
|
||||
ESP_LOGD(TAG, "Packet too large, discarding");
|
||||
this->rx_started_ = false;
|
||||
this->rx_length_ = 0;
|
||||
return;
|
||||
}
|
||||
this->rx_buffer_[this->rx_length_++] = byte;
|
||||
}
|
||||
|
||||
bool UartPacketInterface::send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) {
|
||||
size_t size = data.size();
|
||||
if (size == 0)
|
||||
return true;
|
||||
if (size > this->max_packet_size_) {
|
||||
ESP_LOGW(TAG, "Packet of %zu bytes is larger than %zu", size, this->max_packet_size_);
|
||||
return false;
|
||||
}
|
||||
uint8_t raw[TX_CHUNK_SIZE]{};
|
||||
// Every byte may be escaped, plus a flag byte at each end
|
||||
uint8_t out[TX_CHUNK_SIZE * 2 + 2];
|
||||
size_t out_len = 0;
|
||||
out[out_len++] = FLAG_BYTE;
|
||||
for (size_t offset = 0; offset < size;) {
|
||||
size_t count = data.copy_to(raw, TX_CHUNK_SIZE, offset);
|
||||
offset += count;
|
||||
for (size_t i = 0; i != count; i++) {
|
||||
uint8_t byte = raw[i];
|
||||
if (byte == FLAG_BYTE || byte == CONTROL_BYTE) {
|
||||
out[out_len++] = CONTROL_BYTE;
|
||||
byte ^= ESCAPE_XOR;
|
||||
}
|
||||
out[out_len++] = byte;
|
||||
}
|
||||
if (offset == size)
|
||||
out[out_len++] = FLAG_BYTE;
|
||||
this->write_array(out, out_len);
|
||||
out_len = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::uart
|
||||
Loaded 100 of 149 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user