1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-09 01:01:56 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
J. Nick Koston
6d1281301f [ld2412] Batch UART reads to reduce loop overhead
Read all available bytes in batches via read_array() instead of
byte-at-a-time read() calls. Each read() internally chains through
read_byte -> read_array(1) -> check_read_timeout_ -> available(),
resulting in 3 UART calls per byte. Batching reduces this
significantly.
2026-02-06 23:36:01 +01:00
15 changed files with 61 additions and 712 deletions

View File

@@ -397,38 +397,47 @@ class MemoryAnalyzer:
return pioenvs_dir
return None
@staticmethod
def _parse_nm_cswtch_output(
output: str,
base_dir: Path | None,
cswtch_map: dict[str, list[tuple[str, int]]],
) -> None:
"""Parse nm output for CSWTCH symbols and add to cswtch_map.
def _scan_cswtch_in_objects(
self, obj_dir: Path
) -> dict[str, list[tuple[str, int]]]:
"""Scan object files for CSWTCH symbols using a single nm invocation.
Handles both ``.o`` files and ``.a`` archives.
nm output formats::
.o files: /path/file.o:hex_addr hex_size type name
.a files: /path/lib.a:member.o:hex_addr hex_size type name
For ``.o`` files, paths are made relative to *base_dir* when possible.
For ``.a`` archives (detected by ``:`` in the file portion), paths are
formatted as ``archive_stem/member.o`` (e.g. ``liblwip2-536-feat/lwip-esp.o``).
Uses ``nm --print-file-name -S`` on all ``.o`` files at once.
Output format: ``/path/to/file.o:address size type name``
Args:
output: Raw stdout from ``nm --print-file-name -S``.
base_dir: Base directory for computing relative paths of ``.o`` files.
Pass ``None`` when scanning archives outside the build tree.
cswtch_map: Dict to populate, mapping ``"CSWTCH$N:size"`` to source list.
obj_dir: Directory containing object files (.pioenvs/<env>/)
Returns:
Dict mapping "CSWTCH$NNN:size" to list of (source_file, size) tuples.
"""
for line in output.splitlines():
cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list)
if not self.nm_path:
return cswtch_map
# Find all .o files recursively, sorted for deterministic output
obj_files = sorted(obj_dir.rglob("*.o"))
if not obj_files:
return cswtch_map
_LOGGER.debug("Scanning %d object files for CSWTCH symbols", len(obj_files))
# Single nm call with --print-file-name for all object files
result = run_tool(
[self.nm_path, "--print-file-name", "-S"] + [str(f) for f in obj_files],
timeout=30,
)
if result is None or result.returncode != 0:
return cswtch_map
for line in result.stdout.splitlines():
if "CSWTCH$" not in line:
continue
# Split on last ":" that precedes a hex address.
# For .o: "filepath.o" : "hex_addr hex_size type name"
# For .a: "filepath.a:member.o" : "hex_addr hex_size type name"
# nm --print-file-name format: filepath:hex_addr hex_size type name
# We split from the right: find the last colon followed by hex digits.
parts_after_colon = line.rsplit(":", 1)
if len(parts_after_colon) != 2:
continue
@@ -448,89 +457,16 @@ class MemoryAnalyzer:
except ValueError:
continue
# Determine readable source path
# Use ".a:" to detect archive format (not bare ":" which matches
# Windows drive letters like "C:\...\file.o").
if ".a:" in file_path:
# Archive format: "archive.a:member.o" → "archive_stem/member.o"
archive_part, member = file_path.rsplit(":", 1)
archive_name = Path(archive_part).stem
rel_path = f"{archive_name}/{member}"
elif base_dir is not None:
try:
rel_path = str(Path(file_path).relative_to(base_dir))
except ValueError:
rel_path = file_path
else:
# Get relative path from obj_dir for readability
try:
rel_path = str(Path(file_path).relative_to(obj_dir))
except ValueError:
rel_path = file_path
key = f"{sym_name}:{size}"
cswtch_map[key].append((rel_path, size))
def _run_nm_cswtch_scan(
self,
files: list[Path],
base_dir: Path | None,
cswtch_map: dict[str, list[tuple[str, int]]],
) -> None:
"""Run nm on *files* and add any CSWTCH symbols to *cswtch_map*.
Args:
files: Object (``.o``) or archive (``.a``) files to scan.
base_dir: Base directory for relative path computation (see
:meth:`_parse_nm_cswtch_output`).
cswtch_map: Dict to populate with results.
"""
if not self.nm_path or not files:
return
_LOGGER.debug("Scanning %d files for CSWTCH symbols", len(files))
result = run_tool(
[self.nm_path, "--print-file-name", "-S"] + [str(f) for f in files],
timeout=30,
)
if result is None or result.returncode != 0:
_LOGGER.debug(
"nm failed or timed out scanning %d files for CSWTCH symbols",
len(files),
)
return
self._parse_nm_cswtch_output(result.stdout, base_dir, cswtch_map)
def _scan_cswtch_in_sdk_archives(
self, cswtch_map: dict[str, list[tuple[str, int]]]
) -> None:
"""Scan SDK library archives (.a) for CSWTCH symbols.
Prebuilt SDK libraries (e.g. lwip, bearssl) are not compiled from source,
so their CSWTCH symbols only exist inside ``.a`` archives. Results are
merged into *cswtch_map* for keys not already found in ``.o`` files.
The same source file (e.g. ``lwip-esp.o``) often appears in multiple
library variants (``liblwip2-536.a``, ``liblwip2-1460-feat.a``, etc.),
so results are deduplicated by member name.
"""
sdk_dirs = self._find_sdk_library_dirs()
if not sdk_dirs:
return
sdk_archives = sorted(a for sdk_dir in sdk_dirs for a in sdk_dir.glob("*.a"))
sdk_map: dict[str, list[tuple[str, int]]] = defaultdict(list)
self._run_nm_cswtch_scan(sdk_archives, None, sdk_map)
# Merge SDK results, deduplicating by member name.
for key, sources in sdk_map.items():
if key in cswtch_map:
continue
seen: dict[str, tuple[str, int]] = {}
for path, sz in sources:
member = Path(path).name
if member not in seen:
seen[member] = (path, sz)
cswtch_map[key] = list(seen.values())
return cswtch_map
def _source_file_to_component(self, source_file: str) -> str:
"""Map a source object file path to its component name.
@@ -569,25 +505,17 @@ class MemoryAnalyzer:
CSWTCH symbols are compiler-generated lookup tables for switch statements.
They are local symbols, so the same name can appear in different object files.
This method scans .o files and SDK archives to attribute them to their
source components.
This method scans .o files to attribute them to their source components.
"""
obj_dir = self._find_object_files_dir()
if obj_dir is None:
_LOGGER.debug("No object files directory found, skipping CSWTCH analysis")
return
# Scan build-dir object files for CSWTCH symbols
cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list)
self._run_nm_cswtch_scan(sorted(obj_dir.rglob("*.o")), obj_dir, cswtch_map)
# Also scan SDK library archives (.a) for CSWTCH symbols.
# Prebuilt SDK libraries (e.g. lwip, bearssl) are not compiled from source
# so their symbols only exist inside .a archives, not as loose .o files.
self._scan_cswtch_in_sdk_archives(cswtch_map)
# Scan object files for CSWTCH symbols
cswtch_map = self._scan_cswtch_in_objects(obj_dir)
if not cswtch_map:
_LOGGER.debug("No CSWTCH symbols found in object files or SDK archives")
_LOGGER.debug("No CSWTCH symbols found in object files")
return
# Collect CSWTCH symbols from the ELF (already parsed in sections)

View File

@@ -1,67 +0,0 @@
#pragma once
#include <cstdint>
#include <algorithm>
#include "esphome/core/color.h"
/* Utility for converting internal \a Color RGB representation to supported IC hardware color keys
*
* Focus in driver layer is on efficiency.
* For optimum output quality on RGB inputs consider offline color keying/dithering.
* Also see e.g. Image component.
*/
namespace esphome::epaper_spi {
/** Delta for when to regard as gray */
static constexpr uint8_t COLORCONV_GRAY_THRESHOLD = 50;
/** Map RGB color to discrete BWYR hex 4 color key
*
* @tparam NATIVE_COLOR Type of native hardware color values
* @param color RGB color to convert from
* @param hw_black Native value for black
* @param hw_white Native value for white
* @param hw_yellow Native value for yellow
* @param hw_red Native value for red
* @return Converted native hardware color value
* @internal Constexpr. Does not depend on side effects ("pure").
*/
template<typename NATIVE_COLOR>
constexpr NATIVE_COLOR color_to_bwyr(Color color, NATIVE_COLOR hw_black, NATIVE_COLOR hw_white, NATIVE_COLOR hw_yellow,
NATIVE_COLOR hw_red) {
// --- Step 1: Check for Grayscale (Black or White) ---
// We define "grayscale" as a color where the min and max components
// are close to each other.
const auto [min_rgb, max_rgb] = std::minmax({color.r, color.g, color.b});
if ((max_rgb - min_rgb) < COLORCONV_GRAY_THRESHOLD) {
// It's a shade of gray. Map to BLACK or WHITE.
// We split the luminance at the halfway point (382 = (255*3)/2)
if ((static_cast<int>(color.r) + color.g + color.b) > 382) {
return hw_white;
}
return hw_black;
}
// --- Step 2: Check for Primary/Secondary Colors ---
// If it's not gray, it's a color. We check which components are
// "on" (over 128) vs "off". This divides the RGB cube into 8 corners.
const bool r_on = (color.r > 128);
const bool g_on = (color.g > 128);
const bool b_on = (color.b > 128);
if (r_on) {
if (!b_on) {
return g_on ? hw_yellow : hw_red;
}
// At least red+blue high (but not gray) -> White
return hw_white;
} else {
return (b_on && g_on) ? hw_white : hw_black;
}
}
} // namespace esphome::epaper_spi

View File

@@ -1,227 +0,0 @@
#include "epaper_spi_jd79660.h"
#include "colorconv.h"
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.jd79660";
/** Pixel color as 2bpp. Must match IC LUT values. */
enum JD79660Color : uint8_t {
BLACK = 0b00,
WHITE = 0b01,
YELLOW = 0b10,
RED = 0b11,
};
/** Map RGB color to JD79660 BWYR hex color keys */
static JD79660Color HOT color_to_hex(Color color) {
return color_to_bwyr(color, JD79660Color::BLACK, JD79660Color::WHITE, JD79660Color::YELLOW, JD79660Color::RED);
}
void EPaperJD79660::fill(Color color) {
// If clipping is active, fall back to base implementation
if (this->get_clipping().is_set()) {
EPaperBase::fill(color);
return;
}
const auto pixel_color = color_to_hex(color);
// We store 4 pixels per byte
this->buffer_.fill(pixel_color | (pixel_color << 2) | (pixel_color << 4) | (pixel_color << 6));
}
void HOT EPaperJD79660::draw_pixel_at(int x, int y, Color color) {
if (!this->rotate_coordinates_(x, y))
return;
const auto pixel_bits = color_to_hex(color);
const uint32_t pixel_position = x + y * this->get_width_internal();
// We store 4 pixels per byte at LSB offsets 6, 4, 2, 0
const uint32_t byte_position = pixel_position / 4;
const uint32_t bit_offset = 6 - ((pixel_position % 4) * 2);
const auto original = this->buffer_[byte_position];
this->buffer_[byte_position] = (original & (~(0b11 << bit_offset))) | // mask old 2bpp
(pixel_bits << bit_offset); // add new 2bpp
}
bool EPaperJD79660::reset() {
// On entry state RESET set step, next state will be RESET_END
if (this->state_ == EPaperState::RESET) {
this->step_ = FSMState::RESET_STEP0_H;
}
switch (this->step_) {
case FSMState::RESET_STEP0_H:
// Step #0: Reset H for some settle time.
ESP_LOGVV(TAG, "reset #0");
this->reset_pin_->digital_write(true);
this->reset_duration_ = SLEEP_MS_RESET0;
this->step_ = FSMState::RESET_STEP1_L;
return false; // another loop: step #1 below
case FSMState::RESET_STEP1_L:
// Step #1: Reset L pulse for slightly >1.5ms.
// This is actual reset trigger.
ESP_LOGVV(TAG, "reset #1");
// As commented on SLEEP_MS_RESET1: Reset pulse must happen within time window.
// So do not use FSM loop, and avoid other calls/logs during pulse below.
this->reset_pin_->digital_write(false);
delay(SLEEP_MS_RESET1);
this->reset_pin_->digital_write(true);
this->reset_duration_ = SLEEP_MS_RESET2;
this->step_ = FSMState::RESET_STEP2_IDLECHECK;
return false; // another loop: step #2 below
case FSMState::RESET_STEP2_IDLECHECK:
// Step #2: Basically finished. Check sanity, and move FSM to INITIALISE state
ESP_LOGVV(TAG, "reset #2");
if (!this->is_idle_()) {
// Expectation: Idle after reset + settle time.
// Improperly connected/unexpected hardware?
// Error path reproducable e.g. with disconnected VDD/... pins
// (optimally while busy_pin configured with local pulldown).
// -> Mark failed to avoid followup problems.
this->mark_failed(LOG_STR("Busy after reset"));
}
break; // End state loop below
default:
// Unexpected step = bug?
this->mark_failed();
}
this->step_ = FSMState::INIT_STEP0_REGULARINIT; // reset for initialize state
return true;
}
bool EPaperJD79660::initialise(bool partial) {
switch (this->step_) {
case FSMState::INIT_STEP0_REGULARINIT:
// Step #0: Regular init sequence
ESP_LOGVV(TAG, "init #0");
if (!EPaperBase::initialise(partial)) { // Call parent impl
return false; // If parent should request another loop, do so
}
// Fast init requested + supported?
if (partial && (this->fast_update_length_ > 0)) {
this->step_ = FSMState::INIT_STEP1_FASTINIT;
this->wait_for_idle_(true); // Must wait for idle before fastinit sequence in next loop
return false; // another loop: step #1 below
}
break; // End state loop below
case FSMState::INIT_STEP1_FASTINIT:
// Step #1: Fast init sequence
ESP_LOGVV(TAG, "init #1");
this->write_fastinit_();
break; // End state loop below
default:
// Unexpected step = bug?
this->mark_failed();
}
this->step_ = FSMState::NONE;
return true; // Finished: State transition waits for idle
}
bool EPaperJD79660::transfer_buffer_chunks_() {
size_t buf_idx = 0;
uint8_t bytes_to_send[MAX_TRANSFER_SIZE];
const uint32_t start_time = App.get_loop_component_start_time();
const auto buffer_length = this->buffer_length_;
while (this->current_data_index_ != buffer_length) {
bytes_to_send[buf_idx++] = this->buffer_[this->current_data_index_++];
if (buf_idx == sizeof bytes_to_send) {
this->start_data_();
this->write_array(bytes_to_send, buf_idx);
this->disable();
ESP_LOGVV(TAG, "Wrote %zu bytes at %ums", buf_idx, (unsigned) millis());
buf_idx = 0;
if (millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop
return false;
}
}
}
// Finished the entire dataset
if (buf_idx != 0) {
this->start_data_();
this->write_array(bytes_to_send, buf_idx);
this->disable();
ESP_LOGVV(TAG, "Wrote %zu bytes at %ums", buf_idx, (unsigned) millis());
}
// Cleanup for next transfer
this->current_data_index_ = 0;
// Finished with all buffer chunks
return true;
}
void EPaperJD79660::write_fastinit_() {
// Undocumented register sequence in vendor register range.
// Related to Fast Init/Update.
// Should likely happen after regular init seq and power on, but before refresh.
// Might only work for some models with certain factory MTP.
// Please do not change without knowledge to avoid breakage.
this->send_init_sequence_(this->fast_update_, this->fast_update_length_);
}
bool EPaperJD79660::transfer_data() {
// For now always send full frame buffer in chunks.
// JD79660 might support partial window transfers. But sample code missing.
// And likely minimal impact, solely on SPI transfer time into RAM.
if (this->current_data_index_ == 0) {
this->command(CMD_TRANSFER);
}
return this->transfer_buffer_chunks_();
}
void EPaperJD79660::refresh_screen([[maybe_unused]] bool partial) {
ESP_LOGV(TAG, "Refresh");
this->cmd_data(CMD_REFRESH, {(uint8_t) 0x00});
}
void EPaperJD79660::power_off() {
ESP_LOGV(TAG, "Power off");
this->cmd_data(CMD_POWEROFF, {(uint8_t) 0x00});
}
void EPaperJD79660::deep_sleep() {
ESP_LOGV(TAG, "Deep sleep");
// "Deepsleep between update": Ensure EPD sleep to avoid early hardware wearout!
this->cmd_data(CMD_DEEPSLEEP, {(uint8_t) 0xA5});
// Notes:
// - VDD: Some boards (Waveshare) with "clever reset logic" would allow switching off
// EPD VDD by pulling reset pin low for longer time.
// However, a) not all boards have this, b) reliable sequence timing is difficult,
// c) saving is not worth it after deepsleep command above.
// If needed: Better option is to drive VDD via MOSFET with separate enable pin.
//
// - Possible safe shutdown:
// EPaperBase::on_safe_shutdown() may also trigger deep_sleep() again.
// Regularly, in IDLE state, this does not make sense for this "deepsleep between update" model,
// but SPI sequence should simply be ignored by sleeping receiver.
// But if triggering during lengthy update, this quick SPI sleep sequence may have benefit.
// Optimally, EPDs should even be set all white for longer storage.
// But full sequence (>15s) not possible w/o app logic.
}
} // namespace esphome::epaper_spi

View File

@@ -1,145 +0,0 @@
#pragma once
#include "epaper_spi.h"
namespace esphome::epaper_spi {
/**
* JD7966x IC driver implementation
*
* Currently tested with:
* - JD79660 (max res: 200x200)
*
* May also work for other JD7966x chipset family members with minimal adaptations.
*
* Capabilities:
* - HW frame buffer layout:
* 4 colors (gray0..3, commonly BWYR). Bytes consist of 4px/2bpp.
* Width must be rounded to multiple of 4.
* - Fast init/update (shorter wave forms): Yes. Controlled by CONF_FULL_UPDATE_EVERY.
* Needs undocumented fastinit sequence, based on likely vendor specific MTP content.
* - Partial transfer (transfer only changed window): No. Maybe possible by HW.
* - Partial refresh (refresh only changed window): No. Likely HW limit.
*
* @internal \c final saves few bytes by devirtualization. Remove \c final when subclassing.
*/
class EPaperJD79660 final : public EPaperBase {
public:
EPaperJD79660(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length, const uint8_t *fast_update, uint16_t fast_update_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR),
fast_update_(fast_update),
fast_update_length_(fast_update_length) {
this->row_width_ = (width + 3) / 4; // Fix base class calc (2bpp instead of 1bpp)
this->buffer_length_ = this->row_width_ * height;
}
void fill(Color color) override;
protected:
/** Draw colored pixel into frame buffer */
void draw_pixel_at(int x, int y, Color color) override;
/** Reset (multistep sequence)
* @pre this->reset_pin_ != nullptr // cv.Required check
* @post Should be idle on successful reset. Can mark failures.
*/
bool reset() override;
/** Initialise (multistep sequence) */
bool initialise(bool partial) override;
/** Buffer transfer */
bool transfer_data() override;
/** Power on: Already part of init sequence (likely needed there before transferring buffers).
* So nothing to do in FSM state.
*/
void power_on() override {}
/** Refresh screen
* @param partial Ignored: Needed earlier in \a ::initialize
* @pre Must be idle.
* @post Should return to idle later after processing.
*/
void refresh_screen([[maybe_unused]] bool partial) override;
/** Power off
* @pre Must be idle.
* @post Should return to idle later after processing.
* (latter will take long period like ~15-20s on actual refresh!)
*/
void power_off() override;
/** Deepsleep: Must be used to avoid hardware wearout!
* @pre Must be idle.
* @post Will go busy, and not return idle till ::reset!
*/
void deep_sleep() override;
/** Internal: Send fast init sequence via undocumented vendor registers
* @pre Must be directly after regular ::initialise sequence, before ::transfer_data
* @pre Must be idle.
* @post Should return to idle later after processing.
*/
void write_fastinit_();
/** Internal: Send raw buffer in chunks
* \retval true Finished
* \retval false Loop time elapsed. Need to call again next loop.
*/
bool transfer_buffer_chunks_();
/** @name IC commands @{ */
static constexpr uint8_t CMD_POWEROFF = 0x02;
static constexpr uint8_t CMD_DEEPSLEEP = 0x07;
static constexpr uint8_t CMD_TRANSFER = 0x10;
static constexpr uint8_t CMD_REFRESH = 0x12;
/** @} */
/** State machine constants for \a step_ */
enum class FSMState : uint8_t {
NONE = 0, //!< Initial/default value: Unused
/* Reset state steps */
RESET_STEP0_H,
RESET_STEP1_L,
RESET_STEP2_IDLECHECK,
/* Init state steps */
INIT_STEP0_REGULARINIT,
INIT_STEP1_FASTINIT,
};
/** Wait time (millisec) for first reset phase: High
*
* Wait via FSM loop.
*/
static constexpr uint16_t SLEEP_MS_RESET0 = 200;
/** Wait time (millisec) for second reset phase: Low
*
* Holding Reset Low too long may trigger "clever reset" logic
* of e.g. Waveshare Rev2 boards: VDD is shut down via MOSFET, and IC
* will not report idle anymore!
* FSM loop may spuriously increase delay, e.g. >16ms.
* Therefore, sync wait below, as allowed (code rule "delays > 10ms not permitted"),
* yet only slightly exceeding known IC min req of >1.5ms.
*/
static constexpr uint16_t SLEEP_MS_RESET1 = 2;
/** Wait time (millisec) for third reset phase: High
*
* Wait via FSM loop.
*/
static constexpr uint16_t SLEEP_MS_RESET2 = 200;
// properties initialised in the constructor
const uint8_t *const fast_update_{};
const uint16_t fast_update_length_{};
/** Counter for tracking substeps within FSM state */
FSMState step_{FSMState::NONE};
};
} // namespace esphome::epaper_spi

View File

@@ -1,86 +0,0 @@
import esphome.codegen as cg
from esphome.components.mipi import flatten_sequence
import esphome.config_validation as cv
from esphome.const import CONF_BUSY_PIN, CONF_RESET_PIN
from esphome.core import ID
from ..display import CONF_INIT_SEQUENCE_ID
from . import EpaperModel
class JD79660(EpaperModel):
def __init__(self, name, class_name="EPaperJD79660", fast_update=None, **kwargs):
super().__init__(name, class_name, **kwargs)
self.fast_update = fast_update
def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required:
# Validate required pins, as C++ code will assume existence
if name in (CONF_RESET_PIN, CONF_BUSY_PIN):
return cv.Required(name)
# Delegate to parent
return super().option(name, fallback)
def get_constructor_args(self, config) -> tuple:
# Resembles init_sequence handling for fast_update config
if self.fast_update is None:
fast_update = cg.nullptr, 0
else:
flat_fast_update = flatten_sequence(self.fast_update)
fast_update = (
cg.static_const_array(
ID(
config[CONF_INIT_SEQUENCE_ID].id + "_fast_update", type=cg.uint8
),
flat_fast_update,
),
len(flat_fast_update),
)
return (*fast_update,)
jd79660 = JD79660(
"jd79660",
# Specified refresh times are ~20s (full) or ~15s (fast) due to BWRY.
# So disallow low update intervals (with safety margin), to avoid e.g. FSM update loops.
# Even less frequent intervals (min/h) highly recommended to optimize lifetime!
minimum_update_interval="30s",
# SPI rate: From spec comparisons, IC should allow SCL write cycles up to 10MHz rate.
# Existing code samples also prefer 10MHz. So justifies as default.
# Decrease value further in user config if needed (e.g. poor cabling).
data_rate="10MHz",
# No need to set optional reset_duration:
# Code requires multistep reset sequence with precise timings
# according to data sheet or samples.
)
# Waveshare 1.54-G
#
# Device may have specific factory provisioned MTP content to facilitate vendor register features like fast init.
# Vendor specific init derived from vendor sample code
# <https://github.com/waveshareteam/e-Paper/blob/master/E-paper_Separate_Program/1in54_e-Paper_G/ESP32/EPD_1in54g.cpp>
# Compatible MIT license, see esphome/LICENSE file.
#
# fmt: off
jd79660.extend(
"Waveshare-1.54in-G",
width=200,
height=200,
initsequence=(
(0x4D, 0x78,),
(0x00, 0x0F, 0x29,),
(0x06, 0x0d, 0x12, 0x30, 0x20, 0x19, 0x2a, 0x22,),
(0x50, 0x37,),
(0x61, 200 // 256, 200 % 256, 200 // 256, 200 % 256,), # RES: 200x200 fixed
(0xE9, 0x01,),
(0x30, 0x08,),
# Power On (0x04): Must be early part of init seq = Disabled later!
(0x04,),
),
fast_update=(
(0xE0, 0x02,),
(0xE6, 0x5D,),
(0xA5, 0x00,),
),
)

View File

@@ -310,8 +310,23 @@ void LD2412Component::restart_and_read_all_info() {
}
void LD2412Component::loop() {
while (this->available()) {
this->readline_(this->read());
int avail = this->available();
if (avail == 0) {
return;
}
// Read all available bytes in batches to reduce UART call overhead.
uint8_t buf[MAX_LINE_LENGTH];
while (avail > 0) {
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) {
break;
}
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
this->readline_(buf[i]);
}
}
}

View File

@@ -46,7 +46,6 @@ CONFIG_SCHEMA = (
RESTORE_MODES, upper=True
),
cv.Optional(CONF_CURRENT_TEMPERATURE): cv.returning_lambda,
cv.Optional(CONF_TARGET_TEMPERATURE): cv.returning_lambda,
cv.Optional(CONF_MODE): cv.returning_lambda,
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
water_heater.validate_water_heater_mode
@@ -79,14 +78,6 @@ async def to_code(config: ConfigType) -> None:
)
cg.add(var.set_current_temperature_lambda(template_))
if CONF_TARGET_TEMPERATURE in config:
template_ = await cg.process_lambda(
config[CONF_TARGET_TEMPERATURE],
[],
return_type=cg.optional.template(cg.float_),
)
cg.add(var.set_target_temperature_lambda(template_))
if CONF_MODE in config:
template_ = await cg.process_lambda(
config[CONF_MODE],

View File

@@ -16,8 +16,7 @@ void TemplateWaterHeater::setup() {
restore->perform();
}
}
if (!this->current_temperature_f_.has_value() && !this->target_temperature_f_.has_value() &&
!this->mode_f_.has_value())
if (!this->current_temperature_f_.has_value() && !this->mode_f_.has_value())
this->disable_loop();
}
@@ -29,9 +28,6 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() {
}
traits.set_supports_current_temperature(true);
if (this->target_temperature_f_.has_value()) {
traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE);
}
return traits;
}
@@ -46,14 +42,6 @@ void TemplateWaterHeater::loop() {
}
}
auto target_temp = this->target_temperature_f_.call();
if (target_temp.has_value()) {
if (*target_temp != this->target_temperature_) {
this->target_temperature_ = *target_temp;
changed = true;
}
}
auto new_mode = this->mode_f_.call();
if (new_mode.has_value()) {
if (*new_mode != this->mode_) {

View File

@@ -20,9 +20,6 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
template<typename F> void set_current_temperature_lambda(F &&f) {
this->current_temperature_f_.set(std::forward<F>(f));
}
template<typename F> void set_target_temperature_lambda(F &&f) {
this->target_temperature_f_.set(std::forward<F>(f));
}
template<typename F> void set_mode_lambda(F &&f) { this->mode_f_.set(std::forward<F>(f)); }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
@@ -47,7 +44,6 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater {
// Ordered to minimize padding on 32-bit: 4-byte members first, then smaller
Trigger<> set_trigger_;
TemplateLambda<float> current_temperature_f_;
TemplateLambda<float> target_temperature_f_;
TemplateLambda<water_heater::WaterHeaterMode> mode_f_;
TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE};
water_heater::WaterHeaterModeMask supported_modes_;

View File

@@ -317,7 +317,6 @@ class EsphomeCommandWebSocket(CheckOriginMixin, tornado.websocket.WebSocketHandl
# Check if the proc was not forcibly closed
_LOGGER.info("Process exited with return code %s", returncode)
self.write_message({"event": "exit", "code": returncode})
self.close()
def on_close(self) -> None:
# Check if proc exists (if 'start' has been run)

View File

@@ -25,22 +25,6 @@ display:
lambda: |-
it.circle(64, 64, 50, Color::BLACK);
- platform: epaper_spi
spi_id: spi_bus
model: waveshare-1.54in-G
cs_pin:
allow_other_uses: true
number: GPIO5
dc_pin:
allow_other_uses: true
number: GPIO17
reset_pin:
allow_other_uses: true
number: GPIO16
busy_pin:
allow_other_uses: true
number: GPIO4
- platform: epaper_spi
spi_id: spi_bus
model: waveshare-2.13in-v3

View File

@@ -412,7 +412,6 @@ water_heater:
name: "Template Water Heater"
optimistic: true
current_temperature: !lambda "return 42.0f;"
target_temperature: !lambda "return 60.0f;"
mode: !lambda "return water_heater::WATER_HEATER_MODE_ECO;"
supported_modes:
- "OFF"

View File

@@ -29,7 +29,7 @@ from esphome.dashboard.entries import (
bool_to_entry_state,
)
from esphome.dashboard.models import build_importable_device_dict
from esphome.dashboard.web_server import DashboardSubscriber, EsphomeCommandWebSocket
from esphome.dashboard.web_server import DashboardSubscriber
from esphome.zeroconf import DiscoveredImport
from .common import get_fixture_path
@@ -1654,25 +1654,3 @@ async def test_websocket_check_origin_multiple_trusted_domains(
assert data["event"] == "initial_state"
finally:
ws.close()
def test_proc_on_exit_calls_close() -> None:
"""Test _proc_on_exit sends exit event and closes the WebSocket."""
handler = Mock(spec=EsphomeCommandWebSocket)
handler._is_closed = False
EsphomeCommandWebSocket._proc_on_exit(handler, 0)
handler.write_message.assert_called_once_with({"event": "exit", "code": 0})
handler.close.assert_called_once()
def test_proc_on_exit_skips_when_already_closed() -> None:
"""Test _proc_on_exit does nothing when WebSocket is already closed."""
handler = Mock(spec=EsphomeCommandWebSocket)
handler._is_closed = True
EsphomeCommandWebSocket._proc_on_exit(handler, 0)
handler.write_message.assert_not_called()
handler.close.assert_not_called()

View File

@@ -10,7 +10,6 @@ water_heater:
name: Test Boiler
optimistic: true
current_temperature: !lambda "return 45.0f;"
target_temperature: !lambda "return 60.0f;"
# Note: No mode lambda - we want optimistic mode changes to stick
# A mode lambda would override mode changes in loop()
supported_modes:

View File

@@ -85,9 +85,6 @@ async def test_water_heater_template(
assert initial_state.current_temperature == 45.0, (
f"Expected current temp 45.0, got {initial_state.current_temperature}"
)
assert initial_state.target_temperature == 60.0, (
f"Expected target temp 60.0, got {initial_state.target_temperature}"
)
# Test changing to GAS mode
client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.GAS)