Compare commits

...
14 changed files with 166 additions and 63 deletions
@@ -15,6 +15,8 @@ on:
# base branch checkout, so fork code never runs with the token.
pull_request_target:
types: [opened, synchronize, reopened]
branches:
- dev
paths:
- requirements_dev.txt
- requirements_test.txt
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.10.0b1
PROJECT_NUMBER = 2026.11.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+32 -8
View File
@@ -56,24 +56,48 @@ inline constexpr int64_t decode_zigzag64(uint64_t value) {
return (value & 1) ? static_cast<int64_t>(~(value >> 1)) : static_cast<int64_t>(value >> 1);
}
/// Count varints in a packed buffer: len minus bytes with the continuation bit, summed a word at a time.
/// Word is a template parameter so tests can cover the 32-bit path on a 64-bit host.
/// Count number of varints in a packed buffer
///
/// A varint always ends on the first byte whose continuation bit (0x80) is clear, so the number
/// of varints is the number of bytes without that bit, i.e. len minus the number of bytes with
/// it. A trailing truncated varint (continuation bit set through the end of the buffer) is not
/// counted, matching the wire format.
///
/// Continuation bits are counted a machine word at a time: for a word w, (w >> 7) & 0x01..01
/// leaves 1 in every byte lane whose high bit was set. The lanes are accumulated in parallel and
/// only folded together once per chunk of 255 words, the most a byte lane can hold without
/// overflowing. Word loads are aligned first: unaligned word access is unsupported (Xtensa) or
/// slow on the platforms ESPHome targets.
template<typename Word = size_t> inline uint16_t count_packed_varints(const uint8_t *data, size_t len) {
constexpr size_t word_size = sizeof(Word);
constexpr Word lane_ones = ~Word{0} / 0xFF; // 0x01..01
constexpr size_t max_chunk_words = 255; // a byte lane counts at most one per word
const uint8_t *end = data + len;
size_t continuations = 0;
while (data != end) {
// Unaligned word loads fault on Xtensa
if ((reinterpret_cast<uintptr_t>(data) & (word_size - 1)) == 0 && static_cast<size_t>(end - data) >= word_size) {
// Leading bytes before the first word boundary
while (data != end && (reinterpret_cast<uintptr_t>(data) & (word_size - 1)) != 0) {
continuations += *data++ >> 7;
}
while (static_cast<size_t>(end - data) >= word_size) {
size_t words = static_cast<size_t>(end - data) / word_size;
if (words > max_chunk_words)
words = max_chunk_words;
const uint8_t *chunk_end = data + words * word_size;
Word lanes = 0;
do {
Word word;
memcpy(&word, __builtin_assume_aligned(data, word_size), word_size);
continuations += (((word >> 7) & lane_ones) * lane_ones) >> (word_size * 8 - 8);
lanes += (word >> 7) & lane_ones;
data += word_size;
} else {
continuations += *data++ >> 7;
} while (data != chunk_end);
for (size_t shift = 0; shift < word_size * 8; shift += 8) {
continuations += static_cast<size_t>(lanes >> shift) & 0xFFu;
}
}
// Trailing bytes after the last full word
while (data != end) {
continuations += *data++ >> 7;
}
return static_cast<uint16_t>(len - continuations);
}
+11 -13
View File
@@ -13,7 +13,6 @@ from esphome.const import (
CONF_VALUE,
CONF_WAIT_TIME,
)
from esphome.core import ID
CODEOWNERS = ["@lygris", "@gabest11"]
DEPENDENCIES = ["spi"]
@@ -384,7 +383,10 @@ def validate_raw_data(value):
SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(CC1101Component),
cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
# TX FIFO size
cv.Required(CONF_DATA): cv.templatable(
cv.All(validate_raw_data, cv.Length(max=64))
),
},
key=CONF_DATA,
)
@@ -399,17 +401,13 @@ SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
async def send_packet_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
data = config[CONF_DATA]
if isinstance(data, bytes):
data = list(data)
if cg.is_template(data):
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
cg.add(var.set_data_template(templ))
else:
# Generate static array in flash to avoid RAM copy
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
cg.add(var.set_data_static(arr, len(data)))
await automation.templatable_bytes(
config[CONF_DATA],
args,
var.set_data_template,
var.set_data_static,
"cc1101_data",
)
return var
+24 -4
View File
@@ -3,10 +3,16 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cmath>
#include <cstring>
#ifdef USE_ESP32
#include <esp_memory_utils.h>
#endif
namespace esphome::cc1101 {
static const char *const TAG = "cc1101";
static constexpr size_t TX_FIFO_SIZE = 64;
static void split_float(float value, int mbits, uint8_t &e, uint32_t &m) {
int e_tmp;
@@ -379,18 +385,32 @@ void CC1101Component::read_(Register reg, uint8_t *buffer, size_t length) {
this->disable();
}
CC1101Error CC1101Component::transmit_packet(const std::vector<uint8_t> &packet) {
CC1101Error CC1101Component::transmit_packet(const uint8_t *data, size_t len) {
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
return CC1101Error::PARAMS;
}
const bool variable = this->state_.LENGTH_CONFIG == static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE);
// In variable length mode the FIFO also holds the length byte
if (len + (variable ? 1 : 0) > TX_FIFO_SIZE) {
ESP_LOGE(TAG, "Packet of %u bytes does not fit the TX FIFO", static_cast<unsigned>(len));
return CC1101Error::PARAMS;
}
// Write packet
this->enter_idle_();
this->strobe_(Command::FTX);
if (this->state_.LENGTH_CONFIG == static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE)) {
this->write_(Register::FIFO, static_cast<uint8_t>(packet.size()));
if (variable) {
this->write_(Register::FIFO, static_cast<uint8_t>(len));
}
this->write_(Register::FIFO, packet.data(), packet.size());
#ifdef USE_ESP32
// SPI DMA cannot read flash or unaligned buffers; stage them here so the driver does not allocate a copy
alignas(4) uint8_t staged[TX_FIFO_SIZE];
if (len != 0 && (!esp_ptr_dma_capable(data) || (reinterpret_cast<uintptr_t>(data) & 3) != 0)) {
std::memcpy(staged, data, len);
data = staged;
}
#endif
this->write_(Register::FIFO, data, len);
// Calibrate PLL
if (!this->enter_calibrated_(State::FSTXON, Command::FSTXON)) {
+6 -19
View File
@@ -89,7 +89,10 @@ class CC1101Component final : public Component,
void set_whitening(bool value);
// Packet mode operations
CC1101Error transmit_packet(const std::vector<uint8_t> &packet);
CC1101Error transmit_packet(const std::vector<uint8_t> &packet) {
return this->transmit_packet(packet.data(), packet.size());
}
CC1101Error transmit_packet(const uint8_t *data, size_t len);
void register_listener(CC1101Listener *listener) { this->listeners_.push_back(listener); }
Trigger<std::vector<uint8_t>, float, float, uint8_t> *get_packet_trigger() { return &this->packet_trigger_; }
@@ -131,27 +134,11 @@ class CC1101Component final : public Component,
// Action Wrappers
template<typename... Ts> class SendPacketAction final : public Action<Ts...>, public Parented<CC1101Component> {
public:
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) { this->data_func_ = func; }
void set_data_static(const uint8_t *data, size_t len) {
this->data_static_ = data;
this->data_static_len_ = len;
}
TEMPLATABLE_BYTES(data)
void play(const Ts &...x) override {
if (this->data_func_) {
auto data = this->data_func_(x...);
this->parent_->transmit_packet(data);
} else if (this->data_static_ != nullptr) {
std::vector<uint8_t> data(this->data_static_, this->data_static_ + this->data_static_len_);
this->parent_->transmit_packet(data);
}
this->data_.visit([this](const uint8_t *data, size_t len) { this->parent_->transmit_packet(data, len); }, x...);
}
protected:
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
const uint8_t *data_static_{nullptr};
size_t data_static_len_{0};
};
} // namespace esphome::cc1101
@@ -15,9 +15,12 @@ static const uint8_t STARTUP_SEQUENCE[][2] PROGMEM = {
{0x60, 0x01}, // ADR_PIN_CTRL: output
{0x7D, 0x11},
{0x7E, 0xFF},
{0x00, 0x01},
{0x00, 0x01}, // Page 1
{0x51, 0x05},
{0x00, 0x00},
{0x00, 0x00}, // Page 0
// Additional register configuration
{0x53, 0x60}, // ANA_CTRL: for high audio performance use 175kHz bandwidth with Fsw=768kHz
};
static const LogString *model_name() { return LOG_STR("TAS5805M"); }
+13 -14
View File
@@ -4,13 +4,11 @@
namespace esphome::tas58xx {
// Remainder of the startup sequence, run after the reset. Register 0x00 selects the page.
// All writes except starting and last three register writes are exactly the startup sequence from TI PurePath Console.
// Registers 0x46, 0x7D, 0x7E, page 1 register 0x51 and Page 2 registers 0x1D, 0x19 are not documented in the datasheet.
// clang-format off
// Remainder of the startup sequence, run after the reset in TAS58xx::init_(). Register 0x00 selects the page.
// Based on the TI PurePath Console (PPC) export for "TAS5825M 2-Band DRC&AGL 2.0 96k".
// Registers 0x46, 0x7D, 0x7E, page 1 register 0x51 and page 2 registers 0x1D, 0x19 are not documented in datasheet.
static const uint8_t STARTUP_SEQUENCE[][2] PROGMEM = {
{0x03, 0x00}, // Deep Sleep
{0x03, 0x00}, // DEVICE_CTRL_2: deep sleep, the state the PPC sequence starts from
{0x7D, 0x11},
{0x7E, 0xFF},
{0x00, 0x01}, // Page 1
@@ -20,15 +18,16 @@ static const uint8_t STARTUP_SEQUENCE[][2] PROGMEM = {
{0x19, 0x80},
{0x00, 0x00}, // Page 0
{0x46, 0x11},
{0x02, 0x00}, // DEVICE_CTRL_1 - BD MODE, Damp to PBL MODE
{0x53, 0x01}, // ANA_CTRL PWM Phase Control - in phase
{0x54, 0x00}, // AGAIN 0dB
{0x03, 0x02}, // Hi-Z
{0x61, 0x0B}, // GPIO0_SEL - GPIO0 as FAULTZ output
{0x60, 0x01}, // GPIO0_OE - Enable GPIO0 as output
{0x77, 0x07}, // CBC_CONTROL enabling CBC function for warnings and faults
// PPC also writes 0x02 (DEVICE_CTRL_1) and 0x54 (AGAIN) here; init_() writes both from YAML
{0x53, 0x01}, // ANA_CTRL: in phase, 100kHz bandwidth for high audio performance with Fsw=384kHz
{0x03, 0x02}, // DEVICE_CTRL_2: Hi-Z
// Additional register configuration
// GPIO1 is a dedicated pin on TAS5825M (separate from ADR), used as FAULTZ
{0x62, 0x0B}, // GPIO1_SEL: GPIO1 as FAULTZ
{0x60, 0x02}, // GPIO_CTRL: GPIO1 is output
{0x77, 0x07}, // CBC_CONTROL: enable cycle-by-cycle current limit for warnings and faults
};
// clang-format on
static const LogString *model_name() { return LOG_STR("TAS5825M"); }
+1 -1
View File
@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2026.10.0b1"
__version__ = "2026.11.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (
+4
View File
@@ -405,10 +405,14 @@ template<typename... X> class TemplatableValue<std::string, X...> {
/// Check if this holds a static string (const char* stored without allocation)
/// The pointer is always directly readable (RAM or flash-mapped).
/// Returns false for FLASH_STRING (PROGMEM on ESP8266, requires _P functions).
// Remove before 2027.4.0
ESPDEPRECATED("Read the value with ref_or_copy_to() instead. Removed in 2027.4.0", "2026.10.0")
bool is_static_string() const { return this->type_ == STATIC_STRING; }
/// Get the static string pointer (only valid if is_static_string() returns true)
/// The pointer is always directly readable — FLASH_STRING uses a separate type.
// Remove before 2027.4.0
ESPDEPRECATED("Use ref_or_copy_to() instead. Removed in 2027.4.0", "2026.10.0")
const char *get_static_string() const { return this->static_str_; }
/// Check if the string value is empty without allocating.
+1 -1
View File
@@ -12,7 +12,7 @@ pyserial==3.5
platformio==6.2.0
esptool==5.4.0
click==8.5.0
aioesphomeapi==46.6.0
aioesphomeapi==46.7.0
aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi
zeroconf==0.151.5
puremagic==2.2.0
@@ -0,0 +1,25 @@
---
esphome:
name: test
on_boot:
then:
- cc1101.send_packet: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]
- cc1101.send_packet:
data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]
- cc1101.send_packet: !lambda return {0x09};
esp32:
board: esp32dev
spi:
clk_pin: 18
mosi_pin: 23
miso_pin: 19
cc1101:
id: transceiver
cs_pin: 5
gdo0_pin: 4
frequency: 433.92MHz
packet_mode: true
packet_length: 8
@@ -0,0 +1,41 @@
"""Tests for cc1101 constant payloads in shared PROGMEM tables."""
from collections.abc import Callable
from pathlib import Path
import re
import pytest
from esphome.components.cc1101 import SEND_PACKET_ACTION_SCHEMA
import esphome.config_validation as cv
def test_constant_payloads_share_progmem_tables(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Equal send_packet payloads share one table; lambdas stay templates."""
main_cpp = generate_main(component_config_path("payload_tables.yaml"))
tables = re.findall(
r"static constexpr uint8_t (\w+)\[\] PROGMEM = (\{[^}]*\});", main_cpp
)
assert [v for _, v in tables] == [
"{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}"
]
assert main_cpp.count(f"set_data_static({tables[0][0]}, 8);") == 2
assert "set_data_template(" in main_cpp
@pytest.mark.parametrize("size", [64, 65])
@pytest.mark.parametrize("make", [lambda n: [0x01] * n, lambda n: "a" * n])
def test_send_packet_tx_fifo_limit(
size: int, make: Callable[[int], list[int] | str]
) -> None:
"""Payloads up to the 64 byte TX FIFO are accepted, longer ones rejected."""
config = {"id": "transceiver", "data": make(size)}
if size <= 64:
assert len(SEND_PACKET_ACTION_SCHEMA(config)["data"]) == size
else:
with pytest.raises(cv.Invalid, match="at most 64"):
SEND_PACKET_ACTION_SCHEMA(config)