Compare commits

..
140 changed files with 703 additions and 5418 deletions

No files matched your search

+4 -3
View File
@@ -216,9 +216,10 @@ file does, and it is the authority when they disagree. The most useful starting
the helper's arguments instead of writing a loop. 7-byte BCD RTC calendars use `time::decode_bcd_calendar<LAYOUT>`
and `encode_bcd_calendar<LAYOUT>` with `EPSON_BCD_CALENDAR`, `DS_BCD_CALENDAR` or `PCF_BCD_CALENDAR`.
* **Bus helpers:** UART drains with `read_available(on_byte)` and `discard_available()`, text lines with
`LineReader<N>`; I2C has `update_register`, `write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id`
and `read_bytes_16_le`. `if (!this->status_comm_result(ok)) return;` sets or clears the warning.
A child sensor that skips repeated values is `SUB_SENSOR_WITH_DEDUP(name, type)`.
`LineReader<N>`, fixed-length binary frames with `FrameReader<N, HEADER...>`; I2C has `update_register`,
`write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id` and `read_bytes_16_le`.
`if (!this->status_comm_result(ok)) return;` sets or clears the warning. A child sensor that skips repeated
values is `SUB_SENSOR_WITH_DEDUP(name, type)`.
* **An unset float is `NAN`**, not `optional<float>`; the optional costs 4 to 8 bytes of padding per member.
* **`PollingComponent` first runs `update()`** after a random share of the interval; call `this->update()` at the
end of `setup()` when the entities need a state at boot.
+1 -10
View File
@@ -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
+5 -2
View File
@@ -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_();
}
+5 -2
View File
@@ -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_();
}
-87
View File
@@ -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)
-273
View File
@@ -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
-113
View File
@@ -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])
-26
View File
@@ -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
-18
View File
@@ -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])
-47
View File
@@ -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
-18
View File
@@ -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
-156
View File
@@ -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])
-78
View File
@@ -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
-34
View File
@@ -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
+8 -64
View File
@@ -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
+4 -27
View File
@@ -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) {
+5 -2
View File
@@ -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);
+10 -3
View File
@@ -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, &reg06));
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);
}
+5 -2
View File
@@ -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);
+5 -2
View File
@@ -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_() {
+10 -12
View File
@@ -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
+55 -15
View File
@@ -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
+7 -4
View File
@@ -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:
-1
View File
@@ -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 -1
View File
@@ -1,2 +1,2 @@
CODEOWNERS = ["@B48D81EFCC", "@ryansch"]
CODEOWNERS = ["@B48D81EFCC"]
DOMAIN = "max17048"
+19 -5
View File
@@ -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;
+4 -2
View File
@@ -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;
};
-16
View File
@@ -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()")
)
-3
View File
@@ -1,3 +0,0 @@
CODEOWNERS = ["@ryansch"]
DEPENDENCIES = ["i2c"]
DOMAIN = "max170xx"
-87
View File
@@ -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
-61
View File
@@ -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
+5 -2
View File
@@ -1,5 +1,4 @@
#include "mcp23016.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cstdio>
@@ -80,7 +79,11 @@ void MCP23016::update_reg_(uint8_t pin, bool pin_value, uint8_t reg_addr) {
this->read_reg_(reg_addr, &reg_value);
}
assign_bits(reg_value, 1 << pin, pin_value);
if (pin_value) {
reg_value |= 1 << pin;
} else {
reg_value &= ~(1 << pin);
}
this->write_reg_(reg_addr, reg_value);
@@ -1,5 +1,4 @@
#include "mcp23x08_base.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::mcp23x08_base {
@@ -79,7 +78,11 @@ void MCP23X08Base::update_reg(uint8_t pin, bool pin_value, uint8_t reg_addr) {
this->read_reg(reg_addr, &reg_value);
}
assign_bits(reg_value, 1 << bit, pin_value);
if (pin_value) {
reg_value |= 1 << bit;
} else {
reg_value &= ~(1 << bit);
}
this->write_reg(reg_addr, reg_value);
@@ -92,7 +92,11 @@ void MCP23X17Base::update_reg(uint8_t pin, bool pin_value, uint8_t reg_addr) {
this->read_reg(reg_addr, &reg_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);
@@ -1,6 +1,5 @@
#include "download_buffer.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <cstring>
namespace esphome::online_image {
@@ -35,46 +34,6 @@ size_t DownloadBuffer::read(size_t len) {
return this->unread_;
}
void DownloadSource::compact() {
if (this->keep_read_ || this->pos_ == this->start_)
return;
this->buffer_.read(this->pos_ - this->start_);
this->start_ = this->pos_;
}
int32_t DownloadSource::read(uint8_t *buf, uint32_t len) {
const size_t offset = this->pos_ - this->start_;
const size_t n = std::min<size_t>(len, this->buffer_.unread() - offset);
memcpy(buf, this->buffer_.data(offset), n);
this->pos_ += n;
return static_cast<int32_t>(n);
}
bool DownloadSource::seek(uint32_t position) {
// Only what is still in the buffer can be read again; a decoder never skips ahead of the download
if (position < this->start_ || position > this->start_ + this->buffer_.unread())
return false;
this->pos_ = position;
return true;
}
bool DownloadBuffer::grow(size_t size) {
if (this->size_ >= size)
return true;
RAMAllocator<uint8_t> allocator;
uint8_t *buffer = allocator.allocate(size);
if (buffer == nullptr) {
ESP_LOGE(TAG, "allocation of %zu bytes failed. Biggest block in heap: %zu Bytes", size,
allocator.get_max_free_block_size());
return false;
}
memcpy(buffer, this->buffer_, this->unread_);
allocator.deallocate(this->buffer_, this->size_);
this->buffer_ = buffer;
this->size_ = size;
return true;
}
size_t DownloadBuffer::resize(size_t size) {
if (this->size_ >= size) {
// Avoid useless reallocations; if the buffer is big enough, don't reallocate.
@@ -1,6 +1,5 @@
#pragma once
#include "esphome/components/runtime_image/image_decoder.h"
#include "esphome/core/helpers.h"
#include <cstddef>
#include <cstdint>
@@ -36,8 +35,6 @@ class DownloadBuffer {
void reset() { this->unread_ = 0; }
size_t resize(size_t size);
/// Make the buffer at least size bytes, keeping the unread data. Returns false if the allocation failed.
bool grow(size_t size);
protected:
uint8_t *buffer_;
@@ -46,35 +43,4 @@ class DownloadBuffer {
size_t unread_;
};
/**
* @brief Lets a decoder read a download as it arrives, from the download buffer.
*
* The decoder reads at its own position in the buffer. Bytes it has read stay there until compact() drops
* them, and compact() keeps everything while set_keep_read(true) is in effect, so the decoder can seek back
* while it reads the headers.
*/
class DownloadSource : public runtime_image::DecodeSource {
public:
explicit DownloadSource(DownloadBuffer &buffer) : buffer_(buffer) {}
/// Start again at the beginning of a new download, keeping what is read
void reset() {
this->start_ = 0;
this->pos_ = 0;
this->keep_read_ = true;
}
void set_keep_read(bool keep) { this->keep_read_ = keep; }
/// Drop the bytes the decoder has read, to make room for more of the download
void compact();
int32_t read(uint8_t *buf, uint32_t len) override;
bool seek(uint32_t position) override;
protected:
DownloadBuffer &buffer_;
size_t start_{0}; // Position in the download of the first byte in the buffer
size_t pos_{0}; // Position in the download the decoder reads next
bool keep_read_{true};
};
} // namespace esphome::online_image
-12
View File
@@ -22,8 +22,6 @@ DEPENDENCIES = ["http_request"]
CODEOWNERS = ["@guillempages", "@clydebarrow"]
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
CONF_ON_DOWNLOAD_STARTED = "on_download_started"
CONF_ON_DOWNLOAD_PROGRESS = "on_download_progress"
CONF_UPDATE = "update"
online_image_ns = cg.esphome_ns.namespace("online_image")
@@ -45,9 +43,7 @@ ONLINE_IMAGE_SCHEMA = (
cv.Optional(CONF_REQUEST_HEADERS): cv.All(
cv.Schema({cv.string: cv.templatable(cv.string)})
),
cv.Optional(CONF_ON_DOWNLOAD_STARTED): automation.validate_automation({}),
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation({}),
cv.Optional(CONF_ON_DOWNLOAD_PROGRESS): automation.validate_automation({}),
cv.Optional(CONF_ON_ERROR): automation.validate_automation({}),
}
)
@@ -100,17 +96,9 @@ automation.register_apply_action(
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_DOWNLOAD_STARTED, "add_on_download_started_callback"
),
automation.CallbackAutomation(
CONF_ON_DOWNLOAD_FINISHED, "add_on_finished_callback", [(bool, "cached")]
),
automation.CallbackAutomation(
CONF_ON_DOWNLOAD_PROGRESS,
"add_on_progress_callback",
[(float, "progress"), (cg.size_t, "bytes_read")],
),
automation.CallbackAutomation(CONF_ON_ERROR, "add_on_error_callback"),
)
+10 -144
View File
@@ -4,7 +4,6 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
ESPHOME_LOG_TAG(TAG, "online_image");
@@ -16,11 +15,6 @@ static const char *const IF_MODIFIED_SINCE_HEADER_NAME = "if-modified-since";
namespace esphome::online_image {
// Time one loop pass may spend reading, and decoding a JPEG as it downloads
static constexpr uint32_t PASS_BUDGET_MS = 20;
// Shortest time between two on_download_progress calls
static constexpr uint32_t PROGRESS_INTERVAL_MS = 100;
OnlineImage::OnlineImage(const std::string &url, int width, int height, runtime_image::ImageFormat format,
image::ImageType type, image::Transparency transparency, image::Image *placeholder,
uint32_t buffer_size, bool is_big_endian)
@@ -146,12 +140,8 @@ void OnlineImage::update() {
return;
}
// A JPEG of known size is decoded as it arrives, so the download buffer only has to hold its headers.
// Otherwise the decoder needs the complete image in the buffer.
this->streaming_ = total_size > 0 && total_size <= INT32_MAX && this->decoder_supports_source();
this->stream_started_ = false;
this->source_.reset();
if (!this->streaming_ && format == runtime_image::JPEG && total_size > this->download_buffer_.size() &&
// JPEG requires the complete image in the download buffer before decoding
if (format == runtime_image::JPEG && total_size > this->download_buffer_.size() &&
this->download_buffer_.resize(total_size) == 0) {
// resize() logged the failed allocation; the next update() allocates the configured buffer again
this->end_connection_();
@@ -162,8 +152,6 @@ void OnlineImage::update() {
ESP_LOGI(TAG, "Downloading image (Size: %zu)", total_size);
this->start_time_ = millis();
this->last_data_time_ = this->start_time_;
this->download_started_callback_.call();
this->report_progress_(true);
this->enable_loop();
}
@@ -181,11 +169,6 @@ void OnlineImage::loop() {
return;
}
if (this->streaming_) {
this->loop_stream_();
return;
}
// Check if download is complete — use decoder's format-specific completion check
// to handle both known content-length and chunked transfer encoding
if (this->is_decode_finished() || (this->downloader_->content_length > 0 &&
@@ -196,22 +179,15 @@ void OnlineImage::loop() {
}
// Download and decode more data
if (this->download_buffer_.free_capacity() > 0) {
const uint32_t now = App.get_loop_component_start_time();
size_t received = 0;
int len;
do {
len = this->downloader_->read(this->download_buffer_.append(),
std::min(this->download_buffer_.free_capacity(), READ_CHUNK));
if (len <= 0)
break;
this->download_buffer_.write(len);
received += len;
} while (this->download_buffer_.free_capacity() != 0 && millis() - now < PASS_BUDGET_MS);
size_t available = this->download_buffer_.free_capacity();
if (available > 0) {
// Download in chunks to avoid blocking
available = std::min(available, this->download_buffer_initial_size_);
auto len = this->downloader_->read(this->download_buffer_.append(), available);
if (received > 0) {
this->last_data_time_ = now;
this->report_progress_(false);
if (len > 0) {
this->last_data_time_ = App.get_loop_component_start_time();
this->download_buffer_.write(len);
// Feed data to decoder
auto consumed = this->feed_data(this->download_buffer_.data(), this->download_buffer_.unread());
@@ -279,114 +255,6 @@ void OnlineImage::loop() {
}
}
void OnlineImage::loop_stream_() {
const uint32_t now = App.get_loop_component_start_time();
const size_t total = this->downloader_->content_length;
if (this->stream_started_)
this->source_.compact();
if (this->downloader_->get_bytes_read() < total) {
if (this->download_buffer_.free_capacity() == 0) {
// compact() already ran, so a full buffer means the decoder read nothing in the last pass
if (now - this->last_data_time_ >= this->parent_->get_timeout()) {
ESP_LOGE(TAG, "Image does not fit in the %zu byte download buffer; increase buffer_size",
this->download_buffer_.size());
this->fail_download_();
return;
}
} else {
int len;
do {
len = this->downloader_->read(this->download_buffer_.append(),
std::min(this->download_buffer_.free_capacity(), READ_CHUNK));
if (len <= 0)
break;
this->last_data_time_ = now;
this->download_buffer_.write(len);
this->report_progress_(false);
} while (this->downloader_->get_bytes_read() < total && this->download_buffer_.free_capacity() != 0 &&
millis() - now < PASS_BUDGET_MS);
if (len < 0) {
ESP_LOGE(TAG, "Error downloading image: %d", len);
this->fail_download_();
return;
} else if (now - this->last_data_time_ >= this->parent_->get_timeout()) {
ESP_LOGE(TAG, "No data received for %" PRIu32 " ms", this->parent_->get_timeout());
this->fail_download_();
return;
}
}
}
const bool arrived = this->downloader_->get_bytes_read() >= total;
if (!this->stream_started_) {
// The decoder reads the headers when it opens the image, so wait until they are in the buffer
if (!arrived && this->download_buffer_.free_capacity() != 0)
return;
const int err = this->begin_source(&this->source_);
if (err != 0) {
// Headers cut short by a full buffer fail to parse, or make the decoder seek past the buffer. Such an image
// still decodes once it is all in memory, as it did before JPEG images were decoded as they download.
if (!arrived && (err == runtime_image::DECODE_ERROR_INVALID_TYPE || err == runtime_image::DECODE_ERROR_READ)) {
ESP_LOGW(TAG,
"The image headers do not fit the %zu byte download buffer, so the whole image is downloaded "
"before it is decoded; increase buffer_size to decode it as it downloads",
this->download_buffer_.size());
// grow() logs a failed allocation
if (this->download_buffer_.grow(total)) {
this->streaming_ = false;
} else {
this->fail_download_();
}
return;
}
ESP_LOGE(TAG, "Error decoding image: %s", runtime_image::decode_error_to_string(err));
this->fail_download_();
return;
}
this->source_.set_keep_read(false);
this->stream_started_ = true;
// Opening the image and allocating its buffer is enough for one pass
return;
}
do {
const int result = this->decode_step();
if (result == runtime_image::DECODE_STEP_MORE)
continue;
if (result == runtime_image::DECODE_STEP_DONE) {
this->finish_download_();
} else if (result == runtime_image::DECODE_STEP_NEED_DATA) {
if (!arrived)
return;
// JPEGDEC only waits for bytes short of the size it was given, so this guards against a hang
ESP_LOGE(TAG, "Download ended before the image was complete");
this->fail_download_();
} else {
ESP_LOGE(TAG, "Error decoding image: %s", runtime_image::decode_error_to_string(result));
this->fail_download_();
}
return;
} while (millis() - now < PASS_BUDGET_MS);
}
void OnlineImage::report_progress_(bool force) {
const uint32_t now = App.get_loop_component_start_time();
const size_t bytes = this->downloader_->get_bytes_read();
const size_t total = this->downloader_->content_length;
// The last bytes are always reported, so a progress bar reaches 100%
if (!force && now - this->last_progress_time_ < PROGRESS_INTERVAL_MS && (total == 0 || bytes < total))
return;
this->last_progress_time_ = now;
const float progress = total > 0 ? std::min(100.0f, bytes * 100.0f / total) : NAN;
this->download_progress_callback_.call(progress, bytes);
}
void OnlineImage::fail_download_() {
this->end_connection_();
this->download_error_callback_.call();
}
void OnlineImage::finish_download_() {
// Must be read before end_decode() ends the decoding session
bool decoder_finished = this->is_decode_finished();
@@ -420,8 +288,6 @@ void OnlineImage::end_connection_() {
this->downloader_ = nullptr;
}
this->download_buffer_.reset();
this->streaming_ = false;
this->stream_started_ = false;
this->disable_loop();
}
@@ -16,10 +16,6 @@ using t_http_codes = enum {
HTTP_CODE_NOT_FOUND = 404,
};
/// Largest download read(). A read() blocks until it has every byte it asked for, so this bounds how long one loop
/// pass waits on a slow link.
static constexpr size_t READ_CHUNK = 1460;
/**
* @brief Download an image from a given URL, and decode it using the specified decoder.
* The image will then be stored in a buffer, so that it can be re-displayed without the
@@ -74,17 +70,9 @@ class OnlineImage final : public PollingComponent,
*/
void release();
/// Called when the server has accepted the request and the image starts to download
template<typename F> void add_on_download_started_callback(F &&callback) {
this->download_started_callback_.add(std::forward<F>(callback));
}
template<typename F> void add_on_finished_callback(F &&callback) {
this->download_finished_callback_.add(std::forward<F>(callback));
}
/// Called with the percentage downloaded (NAN when the server did not send the size) and the bytes read so far
template<typename F> void add_on_progress_callback(F &&callback) {
this->download_progress_callback_.add(std::forward<F>(callback));
}
template<typename F> void add_on_error_callback(F &&callback) {
this->download_error_callback_.add(std::forward<F>(callback));
}
@@ -93,20 +81,12 @@ class OnlineImage final : public PollingComponent,
bool validate_url_(const std::string &url);
void end_connection_();
void finish_download_();
/// loop() for a JPEG that is decoded as it downloads
void loop_stream_();
void fail_download_();
/// Reports download progress, at most every PROGRESS_INTERVAL_MS unless force is set
void report_progress_(bool force);
CallbackManager<void(bool)> download_finished_callback_{};
CallbackManager<void()> download_error_callback_{};
LazyCallbackManager<void()> download_started_callback_{};
LazyCallbackManager<void(float, size_t)> download_progress_callback_{};
std::shared_ptr<http_request::HttpContainer> downloader_{nullptr};
DownloadBuffer download_buffer_;
DownloadSource source_{download_buffer_};
/**
* This is the *initial* size of the download buffer, not the current size.
* The download buffer can be resized at runtime; the download_buffer_initial_size_
@@ -129,9 +109,6 @@ class OnlineImage final : public PollingComponent,
uint32_t start_time_{0};
uint32_t last_data_time_{0};
uint32_t last_progress_time_{0};
bool streaming_{false}; // The decoder reads the download itself, as it arrives
bool stream_started_{false}; // The decoder has read the headers
};
} // namespace esphome::online_image
+16 -16
View File
@@ -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; }
+8
View File
@@ -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);
+5 -2
View File
@@ -1,5 +1,4 @@
#include "pca6416a.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::pca6416a {
@@ -171,7 +170,11 @@ void PCA6416AComponent::update_register_(uint8_t pin, bool pin_value, uint8_t re
this->read_register_(reg_addr, &reg_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);
+5 -2
View File
@@ -1,5 +1,4 @@
#include "pca9505.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::pca9505 {
@@ -75,7 +74,11 @@ bool PCA9505Component::digital_read_cache(uint8_t pin) { return this->input_[pin
void PCA9505Component::digital_write_hw(uint8_t pin, bool value) {
const uint8_t bank = pin / 8;
const uint8_t mask = 1 << (pin % 8);
assign_bits(this->output_[bank], mask, value);
if (value) {
this->output_[bank] |= mask;
} else {
this->output_[bank] &= ~mask;
}
this->write_bank_(OUTPUT_REG, bank, this->output_[bank]);
}
+5 -2
View File
@@ -1,5 +1,4 @@
#include "pca9554.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::pca9554 {
@@ -100,7 +99,11 @@ bool PCA9554Component::digital_read_cache(uint8_t pin) {
}
void PCA9554Component::digital_write_hw(uint8_t pin, bool value) {
assign_bits(this->output_mask_, 1 << pin, value);
if (value) {
this->output_mask_ |= (1 << pin);
} else {
this->output_mask_ &= ~(1 << pin);
}
#ifdef USE_PCA9554_PCAL
if (this->write_register_(OUTPUT_REG, this->output_mask_))
this->retry_pcal_shadows_(); // an output-only hub never reads, so it retries here
+5 -1
View File
@@ -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() {
+5 -1
View File
@@ -60,7 +60,11 @@ bool PCF8574Component::digital_read_hw(uint8_t pin) {
bool PCF8574Component::digital_read_cache(uint8_t pin) { return this->input_mask_ & (1 << pin); }
void PCF8574Component::digital_write_hw(uint8_t pin, bool value) {
assign_bits(this->output_mask_, 1 << pin, value);
if (value) {
this->output_mask_ |= (1 << pin);
} else {
this->output_mask_ &= ~(1 << pin);
}
this->write_gpio_();
}
void PCF8574Component::pin_mode(uint8_t pin, gpio::Flags flags) {
@@ -1,5 +1,4 @@
#include "pi4ioe5v6408.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::pi4ioe5v6408 {
@@ -130,7 +129,11 @@ void PI4IOE5V6408Component::digital_write_hw(uint8_t pin, bool value) {
if (this->is_failed())
return;
assign_bits(this->output_mask_, 1 << pin, value);
if (value) {
this->output_mask_ |= (1 << pin);
} else {
this->output_mask_ &= ~(1 << pin);
}
if (!this->status_comm_result(this->write_byte(PI4IOE5V6408_REGISTER_OUT_SET, this->output_mask_),
LOG_STR("Failed to write output register")))
return;
@@ -138,7 +138,7 @@ int HOT JpegDecoder::decode(uint8_t *buffer, size_t size) {
const int err = this->start_();
if (err != 0)
return err;
if (!this->jpeg_.decode(0, 0, this->scale_option_()))
if (!this->jpeg_.decode(0, 0, 0))
return this->fail_();
this->decoded_bytes_ = size;
this->jpeg_.close();
@@ -165,28 +165,10 @@ int JpegDecoder::start_() {
} else {
this->jpeg_.setPixelType(RGB8888);
}
const int width = this->jpeg_.getWidth();
const int height = this->jpeg_.getHeight();
if (!this->set_size(width, height)) {
if (!this->set_size(this->jpeg_.getWidth(), this->jpeg_.getHeight())) {
return DECODE_ERROR_OUT_OF_MEMORY;
}
// JPEGDEC can decode at 1/2, 1/4 or 1/8 size for much less work. Use the smallest size that still has at least as
// many pixels as the buffer, so a large image shown small is not decoded in full only to be thrown away.
const int buffer_width = this->image_->get_buffer_width();
const int buffer_height = this->image_->get_buffer_height();
this->scale_ = 1;
while (this->scale_ < JPEG_SCALE_EIGHTH && width / (this->scale_ * 2) >= buffer_width &&
height / (this->scale_ * 2) >= buffer_height) {
this->scale_ *= 2;
}
const int decoded_width = (width + this->scale_ - 1) / this->scale_;
const int decoded_height = (height + this->scale_ - 1) / this->scale_;
this->x_scale_ = static_cast<double>(buffer_width) / decoded_width;
this->y_scale_ = static_cast<double>(buffer_height) / decoded_height;
if (this->scale_ != 1) {
ESP_LOGD(TAG, "Decoding at 1/%d size, %d x %d", this->scale_, decoded_width, decoded_height);
}
if (this->direct_rgb565_ && !this->prepare_rgb565_scaling_(decoded_width, decoded_height)) {
if (this->direct_rgb565_ && !this->prepare_rgb565_scaling_(this->jpeg_.getWidth(), this->jpeg_.getHeight())) {
return DECODE_ERROR_OUT_OF_MEMORY;
}
return 0;
@@ -270,7 +252,7 @@ int JpegDecoder::begin_source(DecodeSource *source) {
this->source_ = nullptr;
return err;
}
if (!this->jpeg_.decodeBegin(0, 0, this->scale_option_()))
if (!this->jpeg_.decodeBegin(0, 0, 0))
return this->fail_();
return 0;
}
@@ -31,16 +31,12 @@ class JpegDecoder : public ImageDecoder {
int HOT decode_step() override;
bool uses_direct_rgb565() const { return this->direct_rgb565_; }
/// JPEGDEC decodes the image at 1/get_scale() of its size: 1, 2, 4 or 8
int get_scale() const { return this->scale_; }
void draw_rgb565(int x, int y, int w, int h, const uint8_t *pixels);
protected:
bool prepare_rgb565_scaling_(int width, int height);
/// Set up an opened image for decoding. Returns 0, or a DecodeError.
int start_();
/// The JPEGDEC option for scale_: JPEG_SCALE_HALF, _QUARTER and _EIGHTH are the divisors themselves
int scale_option_() const { return this->scale_ == 1 ? 0 : this->scale_; }
/// The DecodeError for a failed read of the source or JPEGDEC's last error, closing the image
int fail_();
@@ -53,7 +49,6 @@ class JpegDecoder : public ImageDecoder {
FixedVector<uint16_t> y_boundaries_{};
int source_width_{0};
int source_height_{0};
int scale_{1}; // JPEGDEC decodes at 1/scale_ of the image size
DecodeSource *source_{nullptr}; // Set while a begin_source() decode is in progress
bool direct_rgb565_{false};
bool source_failed_{false};
@@ -1,6 +1,5 @@
#include "runtime_image.h"
#include "image_decoder.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <cstdint>
@@ -92,7 +91,11 @@ void RuntimeImage::draw_pixel(int x, int y, const Color &color) {
auto on = is_color_on(color);
if (this->has_transparency() && color.w < 0x80)
on = false;
assign_bits(this->buffer_[pos], bitno, on);
if (on) {
this->buffer_[pos] |= bitno;
} else {
this->buffer_[pos] &= ~bitno;
}
break;
}
case image::IMAGE_TYPE_RGB111: {
@@ -342,7 +342,11 @@ void HOT SSD1306::draw_absolute_pixel_internal(int x, int y, Color color) {
uint16_t pos = x + (y / 8) * this->get_width_internal();
uint8_t subpos = y & 0x07;
assign_bits(this->buffer_[pos], 1 << subpos, color.is_on());
if (color.is_on()) {
this->buffer_[pos] |= (1 << subpos);
} else {
this->buffer_[pos] &= ~(1 << subpos);
}
}
void SSD1306::fill(Color color) {
// If clipping is active, fall back to base implementation
+26 -15
View File
@@ -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."""
+7 -2
View File
@@ -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) {
+24 -5
View File
@@ -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
+4 -49
View File
@@ -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) {}
+5 -2
View File
@@ -1,5 +1,4 @@
#include "tca9555.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
static const uint8_t TCA9555_INPUT_PORT_REGISTER_0 = 0x00;
@@ -109,7 +108,11 @@ void TCA9555Component::digital_write_hw(uint8_t pin, bool value) {
if (this->is_failed())
return;
assign_bits(this->output_mask_, 1 << pin, value);
if (value) {
this->output_mask_ |= (1 << pin);
} else {
this->output_mask_ &= ~(1 << pin);
}
this->status_comm_result(this->write_byte_16_le(TCA9555_OUTPUT_PORT_REGISTER_0, this->output_mask_),
LOG_STR("Failed to write output register"));
@@ -1,36 +0,0 @@
import esphome.codegen as cg
from esphome.components import packet_interface, uart
from esphome.components.packet_interface import (
CONF_MAX_PACKET_SIZE,
packet_interface_schema,
)
from esphome.const import CONF_ID
from esphome.types import ConfigType
from .. import uart_ns
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["uart"]
UartPacketInterface = uart_ns.class_(
"UartPacketInterface",
packet_interface.PacketInterface,
uart.UARTDevice,
)
CONFIG_SCHEMA = packet_interface_schema(UartPacketInterface, 508).extend(
uart.UART_DEVICE_SCHEMA
)
async def to_code(config: ConfigType) -> None:
# The receive buffer's size is known here, so it is a static array rather than allocated at run time
rx_buffer = f"{config[CONF_ID].id}__rx_buffer"
cg.add_global(
cg.RawStatement(f"static uint8_t {rx_buffer}[{config[CONF_MAX_PACKET_SIZE]}];")
)
var = await packet_interface.new_packet_interface(
config, cg.RawExpression(rx_buffer)
)
await uart.register_uart_device(var, config)
@@ -1,82 +0,0 @@
#include "esphome/core/log.h"
#include "uart_packet_interface.h"
namespace esphome::uart {
using packet_interface::PacketBuffer;
using packet_interface::PacketMetaData;
ESPHOME_LOG_TAG(TAG, "uart.packet_interface");
static constexpr uint8_t FLAG_BYTE = 0x7E;
static constexpr uint8_t CONTROL_BYTE = 0x7D;
static constexpr uint8_t ESCAPE_XOR = 0x20;
// Raw bytes escaped and written per UART write
static constexpr size_t TX_CHUNK_SIZE = 32;
void UartPacketInterface::loop() {
this->read_available([this](uint8_t byte) { this->handle_rx_byte_(byte); });
}
void UartPacketInterface::handle_rx_byte_(uint8_t byte) {
if (byte == FLAG_BYTE) {
// A flag that ends a frame also starts the next, so an unfinished frame cannot hide the one after it
if (this->rx_started_ && this->rx_length_ != 0)
this->on_receive_from_interface_(PacketBuffer(this->rx_buffer_.data(), this->rx_length_), {});
this->rx_started_ = true;
this->rx_length_ = 0;
this->rx_control_ = false;
return;
}
if (!this->rx_started_)
return;
if (byte == CONTROL_BYTE) {
this->rx_control_ = true;
return;
}
if (this->rx_control_) {
byte ^= ESCAPE_XOR;
this->rx_control_ = false;
}
if (this->rx_length_ >= this->rx_buffer_.size()) {
ESP_LOGD(TAG, "Packet too large, discarding");
this->rx_started_ = false;
this->rx_length_ = 0;
return;
}
this->rx_buffer_[this->rx_length_++] = byte;
}
bool UartPacketInterface::send_to_interface(const PacketBuffer &data, const PacketMetaData &meta_data) {
size_t size = data.size();
if (size == 0)
return true;
if (size > this->max_packet_size_) {
ESP_LOGW(TAG, "Packet of %zu bytes is larger than %zu", size, this->max_packet_size_);
return false;
}
uint8_t raw[TX_CHUNK_SIZE]{};
// Every byte may be escaped, plus a flag byte at each end
uint8_t out[TX_CHUNK_SIZE * 2 + 2];
size_t out_len = 0;
out[out_len++] = FLAG_BYTE;
for (size_t offset = 0; offset < size;) {
size_t count = data.copy_to(raw, TX_CHUNK_SIZE, offset);
offset += count;
for (size_t i = 0; i != count; i++) {
uint8_t byte = raw[i];
if (byte == FLAG_BYTE || byte == CONTROL_BYTE) {
out[out_len++] = CONTROL_BYTE;
byte ^= ESCAPE_XOR;
}
out[out_len++] = byte;
}
if (offset == size)
out[out_len++] = FLAG_BYTE;
this->write_array(out, out_len);
out_len = 0;
}
return true;
}
} // namespace esphome::uart
@@ -1,36 +0,0 @@
#pragma once
#include <span>
#include "esphome/components/packet_interface/packet_interface.h"
#include "esphome/components/uart/uart.h"
namespace esphome::uart {
/**
* A packet interface for sending and receiving framed packets over a UART connection.
* Uses simple byte-stuffing framing with FLAG_BYTE and CONTROL_BYTE.
* The protocol wraps data between FLAG_BYTEs.
* Any occurrence of FLAG_BYTE or CONTROL_BYTE in the data is escaped by emitting CONTROL_BYTE followed by the byte
* XORed with 0x20.
*/
class UartPacketInterface : public packet_interface::PacketInterface, public UARTDevice {
public:
/// @param rx_buffer Holds the unescaped data being received; its size is the largest packet, and it must outlive
/// the link
explicit UartPacketInterface(std::span<uint8_t> rx_buffer)
: packet_interface::PacketInterface(rx_buffer.size()), rx_buffer_(rx_buffer) {}
void loop() override;
bool send_to_interface(const packet_interface::PacketBuffer &data,
const packet_interface::PacketMetaData &meta_data) override;
protected:
void handle_rx_byte_(uint8_t byte);
const std::span<uint8_t> rx_buffer_;
size_t rx_length_{0};
bool rx_started_{};
bool rx_control_{};
};
} // namespace esphome::uart
@@ -6,10 +6,6 @@ namespace esphome::uart {
ESPHOME_LOG_TAG(TAG, "uart_transport");
static constexpr uint16_t MAX_PACKET_SIZE = 508;
static constexpr uint8_t FLAG_BYTE = 0x7E;
static constexpr uint8_t CONTROL_BYTE = 0x7D;
void UARTTransport::loop() {
PacketTransport::loop();
@@ -82,5 +78,4 @@ void UARTTransport::send_packet(const std::vector<uint8_t> &buf) const {
this->parent_->write_byte(FLAG_BYTE);
}
size_t UARTTransport::get_max_packet_size() { return MAX_PACKET_SIZE; }
} // namespace esphome::uart
@@ -16,6 +16,9 @@ namespace esphome::uart {
* Any occurrence of FLAG_BYTE or CONTROL_BYTE in the data is escaped by emitting CONTROL_BYTE followed by the byte
* XORed with 0x20.
*/
static const uint16_t MAX_PACKET_SIZE = 508;
static const uint8_t FLAG_BYTE = 0x7E;
static const uint8_t CONTROL_BYTE = 0x7D;
class UARTTransport final : public packet_transport::PacketTransport, public UARTDevice {
public:
@@ -26,7 +29,7 @@ class UARTTransport final : public packet_transport::PacketTransport, public UAR
void write_byte_(uint8_t byte) const;
void send_packet(const std::vector<uint8_t> &buf) const override;
bool should_send() override { return true; };
size_t get_max_packet_size() override;
size_t get_max_packet_size() override { return MAX_PACKET_SIZE; }
std::vector<uint8_t> receive_buffer_{};
bool rx_started_{};
bool rx_control_{};
@@ -1,5 +1,4 @@
#include "waveshare_io_ch32v003.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::waveshare_io_ch32v003 {
@@ -111,7 +110,11 @@ void WaveshareIOCH32V003Component::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);
}
uint8_t data = this->output_mask_;
this->status_comm_result(this->write_byte(IO_EXTENSION_IO_OUTPUT_ADDR, data),
+5 -1
View File
@@ -210,7 +210,11 @@ bool WeikaiComponent::read_pin_val_(uint8_t pin) {
}
void WeikaiComponent::write_pin_val_(uint8_t pin, bool value) {
assign_bits(this->output_state_, 1 << pin, value);
if (value) {
this->output_state_ |= (1 << pin);
} else {
this->output_state_ &= ~(1 << pin);
}
char bin_buf[9];
ESP_LOGVV(TAG, "writing output pin %d with %d out_state %s", pin, uint8_t(value),
format_bin_to(bin_buf, this->output_state_));
-1
View File
@@ -75,7 +75,6 @@
#define DFROBOT_SEN0465_COMPENSATION_RANGES 3
#define USE_DISPLAY
#define DISPLAY_PAGE_CHANGE_CALLBACK_COUNT 1
#define DUCO_DEVICE_COUNT 4
#define USE_EMONTX_VERSION
#define USE_EMONTX_VERSION_TEXT_SENSOR
#define USE_ENTITY_DEVICE_CLASS
+67
View File
@@ -0,0 +1,67 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <type_traits>
namespace esphome {
/// Collects fixed length binary frames of N bytes that start with the header bytes HEADER...; the binary twin of
/// LineReader. Bytes before a header are skipped; checksums are left to the caller.
template<size_t N, uint8_t... HEADER> class FrameReader {
using pos_t = std::conditional_t<(N <= 0xFF), uint8_t, uint16_t>;
static constexpr size_t HEADER_LEN = sizeof...(HEADER);
static_assert(HEADER_LEN >= 1 && HEADER_LEN <= 8, "FrameReader takes 1 to 8 header bytes"); // bounds replay_() depth
static_assert(HEADER_LEN < N && N <= 0xFFFF, "FrameReader size out of range");
public:
/// Feed one byte; returns the completed frame, header included, valid until the next call, or nullptr.
const uint8_t *feed(uint8_t c) {
pos_t pos = this->pos_;
if (pos < HEADER_LEN && c != header_byte(pos)) {
// A later byte of the broken header, or c, may start the real one
this->buf_[pos] = c;
this->replay_(pos);
return nullptr;
}
this->buf_[pos++] = c;
if (pos < N) {
this->pos_ = pos;
return nullptr;
}
this->pos_ = 0;
return this->buf_;
}
/// Number of bytes of the partial frame; 0 after a frame is returned and while looking for a header.
size_t length() const { return this->pos_; }
/// Forget the partial frame and look for a header from the next byte.
void reset() { this->pos_ = 0; }
/// For a frame the caller rejected (bad checksum): drop its first byte and realign on a header in the rest.
/// Call only right after feed() returned that frame; use reset() otherwise.
void resync() { this->replay_(N - 1); }
protected:
// A fold, not an array, so no header table lands in RAM on ESP8266
static constexpr uint8_t header_byte(size_t i) {
size_t j = 0;
uint8_t r = 0;
((r = (j++ == i) ? HEADER : r), ...);
return r;
}
// Start over and feed buf_[1..last] again; never completes a frame as last < N
void replay_(size_t last) {
this->pos_ = 0;
for (size_t i = 1; i <= last; i++) {
this->feed(this->buf_[i]);
}
}
uint8_t buf_[N];
pos_t pos_{0};
};
} // namespace esphome
-15
View File
@@ -1,5 +1,4 @@
#include "esphome/core/gpio.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -19,18 +18,4 @@ void log_pin(const char *tag, const char *prefix, GPIOPin *pin) {
}
#endif
namespace internal {
void pulse_reset_ms(GPIOPin *pin, uint32_t high_before_ms, uint32_t low_ms, uint32_t high_after_ms) {
if (pin == nullptr)
return;
pin->setup();
const uint32_t waits[] = {high_before_ms, low_ms, high_after_ms};
// Level for each step is high, low, high; a zero high_before_ms skips the first step
for (size_t i = high_before_ms == 0; i < 3; i++) {
pin->digital_write(i != 1);
delay(waits[i]);
}
}
} // namespace internal
} // namespace esphome
-20
View File
@@ -144,24 +144,4 @@ void log_pin(const char *tag, const __FlashStringHelper *prefix, GPIOPin *pin);
void log_pin(const char *tag, const char *prefix, GPIOPin *pin);
#endif
/// Waveform for pulse_reset(): high, low, then high again. Name the fields at the call site, e.g.
/// pulse_reset(this->reset_pin_, {.high_before_ms = 1, .low_ms = 10}).
struct ResetPulse {
uint32_t high_before_ms{0}; ///< Drive high this long first; skipped when 0
uint32_t low_ms{0}; ///< Hold the pin low (in reset) this long
uint32_t high_after_ms{0}; ///< Wait this long after releasing the pin
};
namespace internal {
/// Out-of-line body of pulse_reset(); call that instead.
void pulse_reset_ms(GPIOPin *pin, uint32_t high_before_ms, uint32_t low_ms, uint32_t high_after_ms);
} // namespace internal
/// Set up an active-low reset pin and drive it through \p pulse. Blocks for the whole sequence;
/// does nothing when pin is null. Inline so the fields reach pulse_reset_ms() in registers
/// (RISC-V would otherwise pass the struct by reference).
ESPHOME_ALWAYS_INLINE inline void pulse_reset(GPIOPin *pin, ResetPulse pulse) {
internal::pulse_reset_ms(pin, pulse.high_before_ms, pulse.low_ms, pulse.high_after_ms);
}
} // namespace esphome
+3 -5
View File
@@ -29,14 +29,12 @@ class HashBase {
void get_hex(char *output) { format_hex_to(output, this->get_size() * 2 + 1, this->digest_, this->get_size()); }
/// Compare the hash against a provided byte-encoded hash
bool equals_bytes(const uint8_t *expected) { return constant_time_equals(this->digest_, expected, this->get_size()); }
bool equals_bytes(const uint8_t *expected) { return memcmp(this->digest_, expected, this->get_size()) == 0; }
/// Compare the hash against a provided hex-encoded hash of get_size() * 2 characters, which need not be
/// null-terminated
/// Compare the hash against a provided hex-encoded hash
bool equals_hex(const char *expected) {
uint8_t parsed[32]; // Fixed size for max hash (SHA256 = 32 bytes)
const size_t size = this->get_size();
if (parse_hex(expected, size * 2, parsed, size) != size * 2) {
if (!parse_hex(expected, parsed, this->get_size())) {
return false;
}
return this->equals_bytes(parsed);
-10
View File
@@ -1112,16 +1112,6 @@ template<typename T> constexpr T convert_little_endian(T val) {
#endif
}
/// Compare len bytes of a and b in a time that does not depend on where they first differ.
/// Use this for digests, passwords and tokens, where memcmp's early exit leaks how many bytes matched.
inline bool constant_time_equals(const uint8_t *a, const uint8_t *b, size_t len) {
volatile uint8_t diff = 0;
for (size_t i = 0; i < len; i++) {
diff = diff | (a[i] ^ b[i]);
}
return diff == 0;
}
///@}
/// @name Strings
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff. Show more