mirror of
https://github.com/esphome/esphome.git
synced 2026-10-23 14:38:52 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c43f039631 | ||
|
|
8d1476d585 | ||
|
|
25b8c93ad6 | ||
|
|
5b1642b721 | ||
|
|
b62e12007f | ||
|
|
68d24cb215 |
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_();
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "epaper_spi.h"
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -38,42 +37,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 +233,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 +256,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 +280,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_);
|
||||
@@ -419,7 +365,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 +486,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
|
||||
|
||||
@@ -99,7 +99,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 +139,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 +200,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);
|
||||
|
||||
@@ -22,9 +22,3 @@ def validate_interrupt_pin(value: ConfigType) -> ConfigType:
|
||||
f"Remove the '{CONF_INTERRUPT_PIN}' to fall back to polling."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
# Schema of a CachedGpioExpanderComponent; to_code sets the pin with cg.add_pin().
|
||||
INTERRUPT_EXPANDER_SCHEMA = cv.COMPONENT_SCHEMA.extend(
|
||||
{cv.Optional(CONF_INTERRUPT_PIN): validate_interrupt_pin}
|
||||
)
|
||||
@@ -1,28 +0,0 @@
|
||||
#include "cached_gpio_component.h"
|
||||
|
||||
namespace esphome::gpio_expander {
|
||||
|
||||
// Not a member of the template: GCC 10 (ESP8266) ignores section attributes on template instantiations.
|
||||
static void IRAM_ATTR interrupt_pin_isr(Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
// Like the per-chip code this replaces, an INT line already low at attach gives no falling edge, so
|
||||
// nothing runs until the next edge.
|
||||
bool InterruptExpanderComponent::attach_interrupt_pin_() {
|
||||
// Polling mode enables the loop in pin_mode() for the first input; interrupt mode from the ISR.
|
||||
this->disable_loop();
|
||||
if (this->interrupt_pin_ == nullptr)
|
||||
return false;
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->attach_interrupt(&interrupt_pin_isr, static_cast<Component *>(this),
|
||||
gpio::INTERRUPT_FALLING_EDGE);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Input changes that straddle the read leave INT asserted without a new falling edge, so keep looping
|
||||
// until the line is released or the expander would keep stale state forever.
|
||||
void InterruptExpanderComponent::disable_loop_when_released_() {
|
||||
if (this->interrupt_pin_->digital_read())
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
} // namespace esphome::gpio_expander
|
||||
@@ -1,50 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/gpio_expander/cached_gpio.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::gpio_expander {
|
||||
|
||||
/// Component side of a cached GPIO expander with an optional active-low INT line. Not a template, so the
|
||||
/// ISR and the shared code are emitted once for every expander.
|
||||
class InterruptExpanderComponent : public Component {
|
||||
public:
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
float get_setup_priority() const override { return setup_priority::IO; }
|
||||
|
||||
protected:
|
||||
/// Attach the falling-edge ISR when there is an interrupt pin, then disable the loop until an input
|
||||
/// needs it. Returns whether the interrupt pin is in use.
|
||||
bool attach_interrupt_pin_();
|
||||
/// The loop runs again after the INT line falls; stop it once the line is released (HIGH).
|
||||
/// Only called with an interrupt pin.
|
||||
void disable_loop_when_released_();
|
||||
/// Call from pin_mode() for an input pin: without an interrupt pin the loop polls instead.
|
||||
void enable_polling_loop_() {
|
||||
if (this->interrupt_pin_ == nullptr)
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
};
|
||||
|
||||
/// Base for expanders that cache reads per loop. With an interrupt pin the cache stays valid until INT fires.
|
||||
template<typename T, uint16_t N>
|
||||
class CachedGpioExpanderComponent : public InterruptExpanderComponent, public CachedGpioExpander<T, N> {
|
||||
public:
|
||||
void loop() override {
|
||||
this->reset_pin_cache_();
|
||||
if (this->interrupt_pin_ != nullptr)
|
||||
this->disable_loop_when_released_();
|
||||
}
|
||||
|
||||
protected:
|
||||
/// Call at the end of setup().
|
||||
void setup_interrupt_pin_() {
|
||||
if (this->attach_interrupt_pin_())
|
||||
this->set_invalidate_on_read_(false);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::gpio_expander
|
||||
@@ -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
|
||||
|
||||
@@ -322,9 +322,8 @@ class I2CDevice {
|
||||
bool update_register(uint8_t a_register, uint8_t mask, uint8_t value);
|
||||
|
||||
protected:
|
||||
I2CBus *bus_{nullptr}; ///< pointer to I2CBus instance
|
||||
// Last so a derived class can place its byte-sized members in the padding after it
|
||||
uint8_t address_{0x00}; ///< store the address of the device on the bus
|
||||
I2CBus *bus_{nullptr}; ///< pointer to I2CBus instance
|
||||
};
|
||||
|
||||
} // namespace esphome::i2c
|
||||
@@ -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
|
||||
@@ -20,9 +20,10 @@ CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(MCP23016),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
|
||||
}
|
||||
)
|
||||
.extend(gpio_expander.INTERRUPT_EXPANDER_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x20))
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "mcp23016.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cstdio>
|
||||
|
||||
@@ -25,9 +24,25 @@ void MCP23016::setup() {
|
||||
// all pins input
|
||||
this->write_reg_(MCP23016_IODIR1, 0xFFFF);
|
||||
|
||||
this->setup_interrupt_pin_();
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->attach_interrupt(&MCP23016::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
this->set_invalidate_on_read_(false);
|
||||
}
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void IRAM_ATTR MCP23016::gpio_intr(MCP23016 *arg) { arg->enable_loop_soon_any_context(); }
|
||||
void MCP23016::loop() {
|
||||
// Invalidate cache at the start of each loop
|
||||
this->reset_pin_cache_();
|
||||
// Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the
|
||||
// I2C read leave INT asserted without re-firing a falling edge, which would strand us with
|
||||
// stale state forever; keep looping until the line is released so we self-heal.
|
||||
if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
bool MCP23016::digital_read_hw(uint8_t pin) { return this->read_reg_(MCP23016_GP1, &this->input_mask_); }
|
||||
|
||||
bool MCP23016::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
|
||||
@@ -35,11 +50,14 @@ void MCP23016::digital_write_hw(uint8_t pin, bool value) { this->update_reg_(pin
|
||||
void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
if (flags == gpio::FLAG_INPUT) {
|
||||
this->update_reg_(pin, true, MCP23016_IODIR1);
|
||||
this->enable_polling_loop_();
|
||||
if (this->interrupt_pin_ == nullptr) {
|
||||
this->enable_loop();
|
||||
}
|
||||
} else if (flags == gpio::FLAG_OUTPUT) {
|
||||
this->update_reg_(pin, false, MCP23016_IODIR1);
|
||||
}
|
||||
}
|
||||
float MCP23016::get_setup_priority() const { return setup_priority::IO; }
|
||||
bool MCP23016::read_reg_(uint8_t reg, uint16_t *value) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
@@ -61,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);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/gpio_expander/cached_gpio_component.h"
|
||||
#include "esphome/components/gpio_expander/cached_gpio.h"
|
||||
#include "esphome/components/gpio_expander/expander_gpio_pin.h"
|
||||
|
||||
namespace esphome::mcp23016 {
|
||||
@@ -25,14 +25,20 @@ enum MCP23016GPIORegisters {
|
||||
MCP23016_IOCON1 = 0x0B,
|
||||
};
|
||||
|
||||
class MCP23016 final : public gpio_expander::CachedGpioExpanderComponent<uint16_t, 16>, public i2c::I2CDevice {
|
||||
class MCP23016 final : public Component, public i2c::I2CDevice, public gpio_expander::CachedGpioExpander<uint16_t, 16> {
|
||||
public:
|
||||
MCP23016() = default;
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
static void gpio_intr(MCP23016 *arg);
|
||||
// Virtual methods from CachedGpioExpander
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
@@ -48,6 +54,7 @@ class MCP23016 final : public gpio_expander::CachedGpioExpanderComponent<uint16_
|
||||
uint16_t olat_{0x0000};
|
||||
// Cache for input values (16-bit combined for both banks)
|
||||
uint16_t input_mask_{0x0000};
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
};
|
||||
|
||||
using MCP23016GPIOPin = gpio_expander::ExpanderGPIOPin<MCP23016>;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "mcp23x08_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::mcp23x08_base {
|
||||
@@ -32,11 +31,15 @@ void MCP23X08Base::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
} else if (flags == gpio::FLAG_OUTPUT) {
|
||||
this->update_reg(pin, false, iodir);
|
||||
}
|
||||
if (flags & gpio::FLAG_INPUT) {
|
||||
// With an interrupt pin, INT fires on any change of an input
|
||||
if (this->interrupt_pin_ != nullptr)
|
||||
this->pin_interrupt_mode(pin, mcp23xxx_base::MCP23XXX_CHANGE);
|
||||
this->enable_polling_loop_();
|
||||
// When interrupt_pin is configured, auto-enable CHANGE interrupt for input pins
|
||||
// so the chip's INT output fires on any input state change
|
||||
if (this->interrupt_pin_ != nullptr && (flags & gpio::FLAG_INPUT)) {
|
||||
this->pin_interrupt_mode(pin, mcp23xxx_base::MCP23XXX_CHANGE);
|
||||
}
|
||||
// Enable polling loop for input pins (not needed for interrupt-driven mode
|
||||
// where the ISR handles re-enabling loop)
|
||||
if (this->interrupt_pin_ == nullptr && (flags & gpio::FLAG_INPUT)) {
|
||||
this->enable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,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);
|
||||
|
||||
|
||||
@@ -43,11 +43,15 @@ void MCP23X17Base::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
} else if (flags == gpio::FLAG_OUTPUT) {
|
||||
this->update_reg(pin, false, iodir);
|
||||
}
|
||||
if (flags & gpio::FLAG_INPUT) {
|
||||
// With an interrupt pin, INT fires on any change of an input
|
||||
if (this->interrupt_pin_ != nullptr)
|
||||
this->pin_interrupt_mode(pin, mcp23xxx_base::MCP23XXX_CHANGE);
|
||||
this->enable_polling_loop_();
|
||||
// When interrupt_pin is configured, auto-enable CHANGE interrupt for input pins
|
||||
// so the chip's INT output fires on any input state change
|
||||
if (this->interrupt_pin_ != nullptr && (flags & gpio::FLAG_INPUT)) {
|
||||
this->pin_interrupt_mode(pin, mcp23xxx_base::MCP23XXX_CHANGE);
|
||||
}
|
||||
// Enable polling loop for input pins (not needed for interrupt-driven mode
|
||||
// where the ISR handles re-enabling loop)
|
||||
if (this->interrupt_pin_ == nullptr && (flags & gpio::FLAG_INPUT)) {
|
||||
this->enable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,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);
|
||||
|
||||
|
||||
@@ -32,8 +32,9 @@ MCP23XXX_INTERRUPT_MODES = MCP23XXXInterruptMode.options(
|
||||
MCP23XXX_CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_OPEN_DRAIN_INTERRUPT, default=False): cv.boolean,
|
||||
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
|
||||
}
|
||||
).extend(gpio_expander.INTERRUPT_EXPANDER_SCHEMA)
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
@coroutine
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
namespace esphome::mcp23xxx_base {
|
||||
|
||||
void IRAM_ATTR mcp23xxx_gpio_intr(Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
template<uint8_t N> void MCP23XXXGPIOPin<N>::setup() {
|
||||
this->pin_mode(flags_);
|
||||
// When interrupt_pin is configured, pin_mode() already auto-enables CHANGE
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/gpio_expander/cached_gpio_component.h"
|
||||
#include "esphome/components/gpio_expander/cached_gpio.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
@@ -8,15 +8,47 @@ namespace esphome::mcp23xxx_base {
|
||||
|
||||
enum MCP23XXXInterruptMode : uint8_t { MCP23XXX_NO_INTERRUPT = 0, MCP23XXX_CHANGE, MCP23XXX_RISING, MCP23XXX_FALLING };
|
||||
|
||||
template<uint8_t N> class MCP23XXXBase : public gpio_expander::CachedGpioExpanderComponent<uint8_t, N> {
|
||||
// Not a template member: GCC 10 (ESP8266) ignores section attributes on template instantiations, leaving the
|
||||
// ISR in flash.
|
||||
void mcp23xxx_gpio_intr(Component *arg);
|
||||
|
||||
template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander::CachedGpioExpander<uint8_t, N> {
|
||||
public:
|
||||
virtual void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||
virtual void pin_interrupt_mode(uint8_t pin, MCP23XXXInterruptMode interrupt_mode);
|
||||
|
||||
void set_open_drain_ints(const bool value) { this->open_drain_ints_ = value; }
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
InternalGPIOPin *get_interrupt_pin() const { return this->interrupt_pin_; }
|
||||
float get_setup_priority() const override { return setup_priority::IO; }
|
||||
|
||||
void loop() override {
|
||||
this->reset_pin_cache_();
|
||||
// Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the
|
||||
// I2C read leave INT asserted without re-firing a falling edge, which would strand us with
|
||||
// stale state forever; keep looping until the line is released so we self-heal.
|
||||
if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
// No need to clear latched interrupts before attaching the ISR — if INT is
|
||||
// already low the ISR fires immediately, loop runs, cache invalidates, and
|
||||
// the GPIO read clears the latch. One harmless extra read at most.
|
||||
void setup_interrupt_pin_() {
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->attach_interrupt(&mcp23xxx_gpio_intr, static_cast<Component *>(this),
|
||||
gpio::INTERRUPT_FALLING_EDGE);
|
||||
this->set_invalidate_on_read_(false);
|
||||
}
|
||||
// Disable loop until an input pin is configured via pin_mode()
|
||||
// For interrupt-driven mode, loop is re-enabled by the ISR
|
||||
// For polling mode, loop is re-enabled when pin_mode() registers an input pin
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
// read a given register
|
||||
virtual bool read_reg(uint8_t reg, uint8_t *value) = 0;
|
||||
// write a value to a given register
|
||||
@@ -25,6 +57,7 @@ template<uint8_t N> class MCP23XXXBase : public gpio_expander::CachedGpioExpande
|
||||
virtual void update_reg(uint8_t pin, bool pin_value, uint8_t reg_a) = 0;
|
||||
|
||||
bool open_drain_ints_;
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
};
|
||||
|
||||
template<uint8_t N> class MCP23XXXGPIOPin final : public GPIOPin {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -29,9 +29,10 @@ CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(PCA6416AComponent),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
|
||||
}
|
||||
)
|
||||
.extend(gpio_expander.INTERRUPT_EXPANDER_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x21))
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "pca6416a.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pca6416a {
|
||||
@@ -50,7 +49,24 @@ void PCA6416AComponent::setup() {
|
||||
ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
|
||||
this->status_has_error());
|
||||
|
||||
this->setup_interrupt_pin_();
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->attach_interrupt(&PCA6416AComponent::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
this->set_invalidate_on_read_(false);
|
||||
}
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void IRAM_ATTR PCA6416AComponent::gpio_intr(PCA6416AComponent *arg) { arg->enable_loop_soon_any_context(); }
|
||||
void PCA6416AComponent::loop() {
|
||||
// Invalidate cache at the start of each loop
|
||||
this->reset_pin_cache_();
|
||||
// Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the
|
||||
// I2C read leave INT asserted without re-firing a falling edge, which would strand us with
|
||||
// stale state forever; keep looping until the line is released so we self-heal.
|
||||
if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void PCA6416AComponent::dump_config() {
|
||||
@@ -96,6 +112,9 @@ void PCA6416AComponent::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
this->update_register_(pin, true, pull_dir);
|
||||
this->update_register_(pin, false, pull_en);
|
||||
}
|
||||
if (this->interrupt_pin_ == nullptr) {
|
||||
this->enable_loop();
|
||||
}
|
||||
} else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) {
|
||||
this->update_register_(pin, true, io_dir);
|
||||
if (has_pullup_) {
|
||||
@@ -104,11 +123,12 @@ void PCA6416AComponent::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Your PCA6416A does not support pull-up resistors");
|
||||
}
|
||||
if (this->interrupt_pin_ == nullptr) {
|
||||
this->enable_loop();
|
||||
}
|
||||
} else if (flags == gpio::FLAG_OUTPUT) {
|
||||
this->update_register_(pin, false, io_dir);
|
||||
}
|
||||
if (flags & gpio::FLAG_INPUT)
|
||||
this->enable_polling_loop_();
|
||||
}
|
||||
|
||||
bool PCA6416AComponent::read_register_(uint8_t reg, uint8_t *value) {
|
||||
@@ -150,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);
|
||||
|
||||
@@ -161,6 +185,8 @@ void PCA6416AComponent::update_register_(uint8_t pin, bool pin_value, uint8_t re
|
||||
}
|
||||
}
|
||||
|
||||
float PCA6416AComponent::get_setup_priority() const { return setup_priority::IO; }
|
||||
|
||||
} // namespace esphome::pca6416a
|
||||
|
||||
ESPHOME_EXPANDER_PIN_SUMMARY(esphome::pca6416a::PCA6416AComponent, "%u via PCA6416A")
|
||||
@@ -3,23 +3,31 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/gpio_expander/cached_gpio_component.h"
|
||||
#include "esphome/components/gpio_expander/cached_gpio.h"
|
||||
#include "esphome/components/gpio_expander/expander_gpio_pin.h"
|
||||
|
||||
namespace esphome::pca6416a {
|
||||
|
||||
class PCA6416AComponent final : public gpio_expander::CachedGpioExpanderComponent<uint8_t, 16>, public i2c::I2CDevice {
|
||||
class PCA6416AComponent final : public Component,
|
||||
public i2c::I2CDevice,
|
||||
public gpio_expander::CachedGpioExpander<uint8_t, 16> {
|
||||
public:
|
||||
PCA6416AComponent() = default;
|
||||
|
||||
/// Check i2c availability and setup masks
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
/// Helper function to set the pin mode of a pin.
|
||||
void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
static void gpio_intr(PCA6416AComponent *arg);
|
||||
// Virtual methods from CachedGpioExpander
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
@@ -38,6 +46,7 @@ class PCA6416AComponent final : public gpio_expander::CachedGpioExpanderComponen
|
||||
esphome::i2c::ErrorCode last_error_;
|
||||
/// Only the PCAL6416A has pull-up resistors
|
||||
bool has_pullup_{false};
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
};
|
||||
|
||||
using PCA6416AGPIOPin = gpio_expander::ExpanderGPIOPin<PCA6416AComponent>;
|
||||
|
||||
@@ -24,9 +24,10 @@ CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(PCA9505Component),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
|
||||
}
|
||||
)
|
||||
.extend(gpio_expander.INTERRUPT_EXPANDER_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x20))
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "pca9505.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pca9505 {
|
||||
@@ -38,7 +37,28 @@ void PCA9505Component::setup() {
|
||||
}
|
||||
this->write_all_banks_(MASK_REG, 0xFF);
|
||||
|
||||
this->setup_interrupt_pin_();
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->attach_interrupt(&PCA9505Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
// Don't invalidate cache on read - only invalidate when interrupt fires
|
||||
this->set_invalidate_on_read_(false);
|
||||
}
|
||||
|
||||
// Nothing to poll until pin_mode() registers an input pin.
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void IRAM_ATTR PCA9505Component::gpio_intr(PCA9505Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
void PCA9505Component::loop() {
|
||||
// Invalidate the cache so the next digital_read() triggers a fresh I2C read
|
||||
this->reset_pin_cache_();
|
||||
// Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the
|
||||
// I2C read leave INT asserted without re-firing a falling edge, which would strand us with
|
||||
// stale state forever; keep looping until the line is released so we self-heal.
|
||||
if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void PCA9505Component::dump_config() {
|
||||
@@ -54,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]);
|
||||
}
|
||||
|
||||
@@ -63,9 +87,11 @@ void PCA9505Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
const uint8_t mask = 1 << (pin % 8);
|
||||
if (flags & gpio::FLAG_INPUT) {
|
||||
this->config_[bank] &= ~mask;
|
||||
// An input may raise INT.
|
||||
// An input may raise INT; the polling loop is only needed when there is no interrupt pin.
|
||||
this->mask_[bank] &= ~mask;
|
||||
this->enable_polling_loop_();
|
||||
if (this->interrupt_pin_ == nullptr) {
|
||||
this->enable_loop();
|
||||
}
|
||||
} else if (flags & gpio::FLAG_OUTPUT) {
|
||||
this->config_[bank] |= mask;
|
||||
// An output must never raise INT.
|
||||
@@ -107,6 +133,8 @@ bool PCA9505Component::write_all_banks_(uint8_t reg, uint8_t value) {
|
||||
LOG_STR("Failed to write register"));
|
||||
}
|
||||
|
||||
float PCA9505Component::get_setup_priority() const { return setup_priority::IO; }
|
||||
|
||||
} // namespace esphome::pca9505
|
||||
|
||||
ESPHOME_EXPANDER_PIN_SUMMARY(esphome::pca9505::PCA9505Component, "%u via PCA9505")
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/gpio_expander/cached_gpio_component.h"
|
||||
#include "esphome/components/gpio_expander/cached_gpio.h"
|
||||
#include "esphome/components/gpio_expander/expander_gpio_pin.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
@@ -14,15 +14,23 @@ static constexpr uint8_t PIN_COUNT = 40;
|
||||
/// The pins are grouped into five ports of eight, each with its own set of registers.
|
||||
static constexpr uint8_t BANK_COUNT = PIN_COUNT / 8;
|
||||
|
||||
class PCA9505Component final : public gpio_expander::CachedGpioExpanderComponent<uint8_t, PIN_COUNT>,
|
||||
public i2c::I2CDevice {
|
||||
class PCA9505Component final : public Component,
|
||||
public i2c::I2CDevice,
|
||||
public gpio_expander::CachedGpioExpander<uint8_t, PIN_COUNT> {
|
||||
public:
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
static void gpio_intr(PCA9505Component *arg);
|
||||
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
void digital_write_hw(uint8_t pin, bool value) override;
|
||||
@@ -39,6 +47,7 @@ class PCA9505Component final : public gpio_expander::CachedGpioExpanderComponent
|
||||
uint8_t input_[BANK_COUNT]{};
|
||||
/// Which pins may pull INT low - 1 means masked, matching the chip
|
||||
uint8_t mask_[BANK_COUNT]{};
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
};
|
||||
|
||||
using PCA9505GPIOPin = gpio_expander::ExpanderGPIOPin<PCA9505Component>;
|
||||
|
||||
@@ -2,8 +2,9 @@ from typing import NamedTuple
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import gpio_expander, i2c
|
||||
from esphome.components import i2c
|
||||
from esphome.components.const import CONF_DRIVE_STRENGTH
|
||||
from esphome.components.gpio_expander import validate_interrupt_pin
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
@@ -89,8 +90,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ADDRESS): cv.i2c_address,
|
||||
cv.Optional(CONF_DEVICE): cv.one_of(*DEVICES, upper=True),
|
||||
cv.Optional(CONF_PIN_COUNT): cv.one_of(4, 8, 16),
|
||||
cv.Optional(CONF_INTERRUPT_PIN): validate_interrupt_pin,
|
||||
}
|
||||
).extend(gpio_expander.INTERRUPT_EXPANDER_SCHEMA),
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
_validate_hub,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "pca9554.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pca9554 {
|
||||
@@ -52,7 +51,29 @@ void PCA9554Component::setup() {
|
||||
ESP_LOGD(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
|
||||
this->status_has_error());
|
||||
|
||||
this->setup_interrupt_pin_();
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->attach_interrupt(&PCA9554Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
// Don't invalidate cache on read — only invalidate when interrupt fires
|
||||
this->set_invalidate_on_read_(false);
|
||||
}
|
||||
// Disable loop until an input pin is configured via pin_mode()
|
||||
// For interrupt-driven mode, loop is re-enabled by the ISR
|
||||
// For polling mode, loop is re-enabled when pin_mode() registers an input pin
|
||||
this->disable_loop();
|
||||
}
|
||||
|
||||
void IRAM_ATTR PCA9554Component::gpio_intr(PCA9554Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
|
||||
void PCA9554Component::loop() {
|
||||
// Invalidate the cache so the next digital_read() triggers a fresh I2C read
|
||||
this->reset_pin_cache_();
|
||||
// Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the
|
||||
// I2C read leave INT asserted without re-firing a falling edge, which would strand us with
|
||||
// stale state forever; keep looping until the line is released so we self-heal.
|
||||
if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void PCA9554Component::dump_config() {
|
||||
@@ -78,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
|
||||
@@ -91,7 +116,11 @@ void PCA9554Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
if (flags & gpio::FLAG_INPUT) {
|
||||
// Clear mode mask bit
|
||||
this->config_mask_ &= ~(1 << pin);
|
||||
this->enable_polling_loop_();
|
||||
// Enable polling loop for input pins (not needed for interrupt-driven mode
|
||||
// where the ISR handles re-enabling loop)
|
||||
if (this->interrupt_pin_ == nullptr) {
|
||||
this->enable_loop();
|
||||
}
|
||||
} else if (flags & gpio::FLAG_OUTPUT) {
|
||||
// Set mode mask bit
|
||||
this->config_mask_ |= 1 << pin;
|
||||
@@ -178,6 +207,8 @@ void PCA9554Component::update_pcal_register_(uint8_t reg, uint16_t &shadow, uint
|
||||
}
|
||||
#endif
|
||||
|
||||
float PCA9554Component::get_setup_priority() const { return setup_priority::IO; }
|
||||
|
||||
} // namespace esphome::pca9554
|
||||
|
||||
ESPHOME_EXPANDER_PIN_SUMMARY(esphome::pca9554::PCA9554Component, "%u via PCA9554")
|
||||
@@ -3,22 +3,28 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/gpio_expander/cached_gpio_component.h"
|
||||
#include "esphome/components/gpio_expander/cached_gpio.h"
|
||||
#include "esphome/components/gpio_expander/expander_gpio_pin.h"
|
||||
|
||||
namespace esphome::pca9554 {
|
||||
|
||||
class PCA9554Component final : public gpio_expander::CachedGpioExpanderComponent<uint16_t, 16>, public i2c::I2CDevice {
|
||||
class PCA9554Component final : public Component,
|
||||
public i2c::I2CDevice,
|
||||
public gpio_expander::CachedGpioExpander<uint16_t, 16> {
|
||||
public:
|
||||
explicit PCA9554Component(uint8_t pin_count) : pin_count_(pin_count), reg_width_((pin_count + 7) / 8) {}
|
||||
|
||||
/// Check i2c availability and setup masks
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
/// Helper function to set the pin mode of a pin.
|
||||
void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
#ifdef USE_PCA9554_PCAL
|
||||
/// The PCAL parts add pull resistors, input latches, an interrupt mask and drive strength.
|
||||
void set_pcal() { this->pcal_ = true; }
|
||||
@@ -32,6 +38,8 @@ class PCA9554Component final : public gpio_expander::CachedGpioExpanderComponent
|
||||
#endif
|
||||
|
||||
protected:
|
||||
static void gpio_intr(PCA9554Component *arg);
|
||||
|
||||
bool read_inputs_();
|
||||
bool write_register_(uint8_t reg, uint16_t value) { return this->write_address_(reg * this->reg_width_, value); }
|
||||
bool write_address_(uint8_t address, uint16_t value);
|
||||
@@ -53,6 +61,7 @@ class PCA9554Component final : public gpio_expander::CachedGpioExpanderComponent
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
void digital_write_hw(uint8_t pin, bool value) override;
|
||||
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
#ifdef USE_PCA9554_PCAL
|
||||
/// Drive strength shadow, two bits per pin; the low reg_width_ bytes go to OUTPUT_DRIVE_0
|
||||
uint32_t drive_{0xFFFFFFFF};
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -36,9 +36,10 @@ CONFIG_SCHEMA = (
|
||||
cv.Required(CONF_ID): cv.declare_id(PCF8574Component),
|
||||
cv.Optional(CONF_PCF8575, default=False): cv.boolean,
|
||||
cv.Optional(CONF_RESET, default=DEFAULT_RESET): cv.boolean,
|
||||
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
|
||||
}
|
||||
)
|
||||
.extend(gpio_expander.INTERRUPT_EXPANDER_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x21))
|
||||
)
|
||||
|
||||
|
||||
@@ -21,7 +21,27 @@ void PCF8574Component::setup() {
|
||||
this->mode_mask_ = ~this->configured_input_pins_;
|
||||
}
|
||||
|
||||
this->setup_interrupt_pin_();
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->attach_interrupt(&PCF8574Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
// Don't invalidate cache on read — only invalidate when interrupt fires
|
||||
this->set_invalidate_on_read_(false);
|
||||
}
|
||||
// Disable loop until an input pin is configured via pin_mode()
|
||||
// For interrupt-driven mode, loop is re-enabled by the ISR
|
||||
// For polling mode, loop is re-enabled when pin_mode() registers an input pin
|
||||
this->disable_loop();
|
||||
}
|
||||
void IRAM_ATTR PCF8574Component::gpio_intr(PCF8574Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
void PCF8574Component::loop() {
|
||||
// Invalidate the cache so the next digital_read() triggers a fresh I2C read
|
||||
this->reset_pin_cache_();
|
||||
// Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the
|
||||
// I2C read leave INT asserted without re-firing a falling edge, which would strand us with
|
||||
// stale state forever; keep looping until the line is released so we self-heal.
|
||||
if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
void PCF8574Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
@@ -40,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) {
|
||||
@@ -49,7 +73,11 @@ void PCF8574Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
this->mode_mask_ &= ~(1 << pin);
|
||||
// Write GPIO to enable input mode
|
||||
this->write_gpio_();
|
||||
this->enable_polling_loop_();
|
||||
// Enable polling loop for input pins (not needed for interrupt-driven mode
|
||||
// where the ISR handles re-enabling loop)
|
||||
if (this->interrupt_pin_ == nullptr) {
|
||||
this->enable_loop();
|
||||
}
|
||||
} else if (flags == gpio::FLAG_OUTPUT) {
|
||||
// Set mode mask bit
|
||||
this->mode_mask_ |= 1 << pin;
|
||||
@@ -79,6 +107,7 @@ bool PCF8574Component::write_gpio_() {
|
||||
data[1] = value >> 8;
|
||||
return this->status_comm_result(this->write(data, this->pcf8575_ ? 2 : 1) == i2c::ERROR_OK);
|
||||
}
|
||||
float PCF8574Component::get_setup_priority() const { return setup_priority::IO; }
|
||||
|
||||
} // namespace esphome::pcf8574
|
||||
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/gpio_expander/cached_gpio_component.h"
|
||||
#include "esphome/components/gpio_expander/cached_gpio.h"
|
||||
#include "esphome/components/gpio_expander/expander_gpio_pin.h"
|
||||
|
||||
namespace esphome::pcf8574 {
|
||||
|
||||
// PCF8574(8 pins)/PCF8575(16 pins) always read/write all pins in a single I2C transaction
|
||||
// so we use uint16_t as bank type to ensure all pins are in one bank and cached together
|
||||
class PCF8574Component final : public gpio_expander::CachedGpioExpanderComponent<uint16_t, 16>, public i2c::I2CDevice {
|
||||
class PCF8574Component final : public Component,
|
||||
public i2c::I2CDevice,
|
||||
public gpio_expander::CachedGpioExpander<uint16_t, 16> {
|
||||
public:
|
||||
PCF8574Component() = default;
|
||||
|
||||
@@ -18,15 +20,21 @@ class PCF8574Component final : public gpio_expander::CachedGpioExpanderComponent
|
||||
void set_reset(bool reset) { this->reset_ = reset; }
|
||||
// Called by pin setters before any component's setup or GPIO writes.
|
||||
void register_input_pin(uint8_t pin) { this->configured_input_pins_ |= 1 << pin; }
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
/// Check i2c availability and setup masks
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
/// Helper function to set the pin mode of a pin.
|
||||
void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
static void gpio_intr(PCF8574Component *arg);
|
||||
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
void digital_write_hw(uint8_t pin, bool value) override;
|
||||
@@ -46,6 +54,7 @@ class PCF8574Component final : public gpio_expander::CachedGpioExpanderComponent
|
||||
// Keep in sync with DEFAULT_RESET in __init__.py; codegen omits the setter for the default.
|
||||
bool reset_{true};
|
||||
bool pcf8575_; ///< TRUE->16-channel PCF8575, FALSE->8-channel PCF8574
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
};
|
||||
|
||||
using PCF8574GPIOPin = gpio_expander::ExpanderGPIOPin<PCF8574Component>;
|
||||
|
||||
@@ -34,9 +34,10 @@ CONFIG_SCHEMA = (
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(PI4IOE5V6408Component),
|
||||
cv.Optional(CONF_RESET, default=True): cv.boolean,
|
||||
cv.Optional(CONF_INTERRUPT_PIN): gpio_expander.validate_interrupt_pin,
|
||||
}
|
||||
)
|
||||
.extend(gpio_expander.INTERRUPT_EXPANDER_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(0x43))
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "pi4ioe5v6408.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::pi4ioe5v6408 {
|
||||
@@ -34,8 +33,20 @@ void PI4IOE5V6408Component::setup() {
|
||||
}
|
||||
}
|
||||
|
||||
this->setup_interrupt_pin_();
|
||||
// No need to clear latched interrupts before attaching the ISR — if INT is
|
||||
// already low the ISR fires immediately, loop runs, cache invalidates, and
|
||||
// the read clears the latch. One harmless extra read at most.
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->interrupt_pin_->attach_interrupt(&PI4IOE5V6408Component::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
|
||||
this->set_invalidate_on_read_(false);
|
||||
}
|
||||
// Disable loop until an input pin is configured via pin_mode()
|
||||
// For interrupt-driven mode, loop is re-enabled by the ISR
|
||||
// For polling mode, loop is re-enabled when pin_mode() registers an input pin
|
||||
this->disable_loop();
|
||||
}
|
||||
void IRAM_ATTR PI4IOE5V6408Component::gpio_intr(PI4IOE5V6408Component *arg) { arg->enable_loop_soon_any_context(); }
|
||||
void PI4IOE5V6408Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "PI4IOE5V6408:");
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
@@ -55,12 +66,26 @@ void PI4IOE5V6408Component::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
this->pull_up_down_mask_ &= ~(1 << pin);
|
||||
this->pull_enable_mask_ |= 1 << pin;
|
||||
}
|
||||
this->enable_polling_loop_();
|
||||
// Enable polling loop for input pins (not needed for interrupt-driven mode
|
||||
// where the ISR handles re-enabling loop)
|
||||
if (this->interrupt_pin_ == nullptr) {
|
||||
this->enable_loop();
|
||||
}
|
||||
}
|
||||
// Write GPIO to enable input mode
|
||||
this->write_gpio_modes_();
|
||||
}
|
||||
|
||||
void PI4IOE5V6408Component::loop() {
|
||||
this->reset_pin_cache_();
|
||||
// Only disable the loop once INT has actually gone HIGH. Input transitions that straddle the
|
||||
// I2C read leave INT asserted without re-firing a falling edge, which would strand us with
|
||||
// stale state forever; keep looping until the line is released so we self-heal.
|
||||
if (this->interrupt_pin_ != nullptr && this->interrupt_pin_->digital_read()) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
bool PI4IOE5V6408Component::read_gpio_outputs_() {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
@@ -104,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;
|
||||
@@ -151,6 +180,8 @@ bool PI4IOE5V6408Component::write_gpio_modes_() {
|
||||
|
||||
bool PI4IOE5V6408Component::digital_read_cache(uint8_t pin) { return (this->input_mask_ & (1 << pin)); }
|
||||
|
||||
float PI4IOE5V6408Component::get_setup_priority() const { return setup_priority::IO; }
|
||||
|
||||
} // namespace esphome::pi4ioe5v6408
|
||||
|
||||
ESPHOME_EXPANDER_PIN_SUMMARY(esphome::pi4ioe5v6408::PI4IOE5V6408Component, "%u via PI4IOE5V6408")
|
||||
@@ -1,26 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/gpio_expander/cached_gpio_component.h"
|
||||
#include "esphome/components/gpio_expander/cached_gpio.h"
|
||||
#include "esphome/components/gpio_expander/expander_gpio_pin.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::pi4ioe5v6408 {
|
||||
class PI4IOE5V6408Component final : public gpio_expander::CachedGpioExpanderComponent<uint8_t, 8>,
|
||||
public i2c::I2CDevice {
|
||||
class PI4IOE5V6408Component final : public Component,
|
||||
public i2c::I2CDevice,
|
||||
public gpio_expander::CachedGpioExpander<uint8_t, 8> {
|
||||
public:
|
||||
PI4IOE5V6408Component() = default;
|
||||
|
||||
void setup() override;
|
||||
void pin_mode(uint8_t pin, gpio::Flags flags);
|
||||
|
||||
float get_setup_priority() const override;
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
|
||||
/// Indicate if the component should reset the state during setup
|
||||
void set_reset(bool reset) { this->reset_ = reset; }
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||
|
||||
protected:
|
||||
static void gpio_intr(PI4IOE5V6408Component *arg);
|
||||
|
||||
bool digital_read_hw(uint8_t pin) override;
|
||||
bool digital_read_cache(uint8_t pin) override;
|
||||
void digital_write_hw(uint8_t pin, bool value) override;
|
||||
@@ -37,6 +43,7 @@ class PI4IOE5V6408Component final : public gpio_expander::CachedGpioExpanderComp
|
||||
uint8_t pull_up_down_mask_{0x00};
|
||||
|
||||
bool reset_{true};
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
|
||||
bool read_gpio_modes_();
|
||||
bool write_gpio_modes_();
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#include "runtime_image.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
@@ -11,8 +10,8 @@ ESPHOME_LOG_TAG(TAG, "image_decoder");
|
||||
bool ImageDecoder::set_size(int width, int height) {
|
||||
bool success = this->image_->resize(width, height) > 0;
|
||||
this->size_valid_ = success;
|
||||
this->x_scale_ = static_cast<double>(this->image_->get_buffer_width()) / width;
|
||||
this->y_scale_ = static_cast<double>(this->image_->get_buffer_height()) / height;
|
||||
this->draw_width_ = width;
|
||||
this->draw_height_ = height;
|
||||
return success;
|
||||
}
|
||||
|
||||
@@ -20,10 +19,24 @@ void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
|
||||
if (!this->size_valid_) {
|
||||
return;
|
||||
}
|
||||
auto width = std::min(this->image_->get_buffer_width(), static_cast<int>(std::ceil((x + w) * this->x_scale_)));
|
||||
auto height = std::min(this->image_->get_buffer_height(), static_cast<int>(std::ceil((y + h) * this->y_scale_)));
|
||||
for (int i = x * this->x_scale_; i < width; i++) {
|
||||
for (int j = y * this->y_scale_; j < height; j++) {
|
||||
// When shrinking, each target pixel is painted once, by the first source pixel that reaches it, rather than by every
|
||||
// pixel that lands on it. Integer maths: the S3 has no double FPU.
|
||||
const int buffer_width = this->image_->get_buffer_width();
|
||||
const int buffer_height = this->image_->get_buffer_height();
|
||||
int x1 = x;
|
||||
int y1 = y;
|
||||
int x2 = x + w;
|
||||
int y2 = y + h;
|
||||
if (buffer_width != this->draw_width_ || buffer_height != this->draw_height_) {
|
||||
x1 = scale_boundary(x, this->draw_width_, buffer_width);
|
||||
y1 = scale_boundary(y, this->draw_height_, buffer_height);
|
||||
x2 = scale_boundary(x + w, this->draw_width_, buffer_width);
|
||||
y2 = scale_boundary(y + h, this->draw_height_, buffer_height);
|
||||
}
|
||||
x2 = std::min(buffer_width, x2);
|
||||
y2 = std::min(buffer_height, y2);
|
||||
for (int i = x1; i < x2; i++) {
|
||||
for (int j = y1; j < y2; j++) {
|
||||
this->image_->draw_pixel(i, j, color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,15 @@
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
/// The first of `target` pixels that pixel n of `source` paints, rounded up: ceil(n * target / source). The spans of
|
||||
/// neighbouring pixels meet without overlapping, so a shrunk image paints each target pixel once.
|
||||
inline int scale_boundary(int n, int source, int target) {
|
||||
if (source <= 0)
|
||||
return 0;
|
||||
return static_cast<int>((static_cast<uint32_t>(n) * static_cast<uint32_t>(target) + source - 1) /
|
||||
static_cast<uint32_t>(source));
|
||||
}
|
||||
|
||||
enum DecodeError : int {
|
||||
DECODE_ERROR_INVALID_TYPE = -1,
|
||||
DECODE_ERROR_UNSUPPORTED_FORMAT = -2,
|
||||
@@ -83,8 +92,8 @@ class ImageDecoder {
|
||||
this->expected_size_ = 0;
|
||||
this->decoded_bytes_ = 0;
|
||||
this->size_valid_ = true;
|
||||
this->x_scale_ = 1.0;
|
||||
this->y_scale_ = 1.0;
|
||||
this->draw_width_ = 0;
|
||||
this->draw_height_ = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,8 +193,9 @@ class ImageDecoder {
|
||||
uint8_t *image_buffer_() const;
|
||||
bool image_is_big_endian_() const;
|
||||
|
||||
double x_scale_ = 1.0;
|
||||
double y_scale_ = 1.0;
|
||||
// Size of the image as the decoder draws it, which draw() spreads over the buffer
|
||||
int draw_width_ = 0;
|
||||
int draw_height_ = 0;
|
||||
RuntimeImage *image_;
|
||||
size_t expected_size_ = 0; // Expected data size (0 if unknown)
|
||||
size_t decoded_bytes_ = 0; // Bytes processed so far
|
||||
|
||||
@@ -28,16 +28,14 @@ bool JpegDecoder::prepare_rgb565_scaling_(int width, int height) {
|
||||
this->source_height_ = height;
|
||||
this->x_boundaries_.clear();
|
||||
this->y_boundaries_.clear();
|
||||
const uint32_t target_width = this->image_->get_buffer_width();
|
||||
const uint32_t target_height = this->image_->get_buffer_height();
|
||||
// Boundary n is the first target pixel source pixel n covers, rounded up. When shrinking, the source
|
||||
// pixel whose span lands on a target pixel paints it, so an 8 to 4 scale keeps columns 0, 2, 4 and 6;
|
||||
// the generic draw() path rounds down and keeps the odd ones instead
|
||||
for (uint32_t x = 0; x <= static_cast<uint32_t>(width); x++) {
|
||||
this->x_boundaries_.push_back(static_cast<uint16_t>((x * target_width + width - 1) / width));
|
||||
const int target_width = this->image_->get_buffer_width();
|
||||
const int target_height = this->image_->get_buffer_height();
|
||||
// The same boundaries as the generic draw() path, so an 8 to 4 scale keeps columns 0, 2, 4 and 6 in both
|
||||
for (int x = 0; x <= width; x++) {
|
||||
this->x_boundaries_.push_back(static_cast<uint16_t>(scale_boundary(x, width, target_width)));
|
||||
}
|
||||
for (uint32_t y = 0; y <= static_cast<uint32_t>(height); y++) {
|
||||
this->y_boundaries_.push_back(static_cast<uint16_t>((y * target_height + height - 1) / height));
|
||||
for (int y = 0; y <= height; y++) {
|
||||
this->y_boundaries_.push_back(static_cast<uint16_t>(scale_boundary(y, height, target_height)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -181,8 +179,8 @@ int JpegDecoder::start_() {
|
||||
}
|
||||
const int decoded_width = (width + this->scale_ - 1) / this->scale_;
|
||||
const int decoded_height = (height + this->scale_ - 1) / this->scale_;
|
||||
this->x_scale_ = static_cast<double>(buffer_width) / decoded_width;
|
||||
this->y_scale_ = static_cast<double>(buffer_height) / decoded_height;
|
||||
this->draw_width_ = decoded_width;
|
||||
this->draw_height_ = decoded_height;
|
||||
if (this->scale_ != 1) {
|
||||
ESP_LOGD(TAG, "Decoding at 1/%d size, %d x %d", this->scale_, decoded_width, decoded_height);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -53,13 +53,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 +72,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 +89,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 +152,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."""
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -190,7 +191,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);
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
Loaded 100 of 146 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user