mirror of
https://github.com/esphome/esphome.git
synced 2025-09-06 05:12:21 +01:00
Merge branch 'bluetooth_connection_churn' into integration
This commit is contained in:
@@ -1 +1 @@
|
||||
f84518ea4140c194b21cc516aae05aaa0cf876ab866f89e22e91842df46333ed
|
||||
6af8b429b94191fe8e239fcb3b73f7982d0266cb5b05ffbc81edaeac1bc8c273
|
||||
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -281,7 +281,7 @@ jobs:
|
||||
pio_cache_key: tidyesp32-idf
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ZEPHYR
|
||||
options: --environment nrf52-tidy --grep USE_ZEPHYR
|
||||
options: --environment nrf52-tidy --grep USE_ZEPHYR --grep USE_NRF52
|
||||
pio_cache_key: tidy-zephyr
|
||||
ignore_errors: false
|
||||
|
||||
|
@@ -155,6 +155,7 @@ esphome/components/esp32_rmt/* @jesserockz
|
||||
esphome/components/esp32_rmt_led_strip/* @jesserockz
|
||||
esphome/components/esp8266/* @esphome/core
|
||||
esphome/components/esp_ldo/* @clydebarrow
|
||||
esphome/components/espnow/* @jesserockz
|
||||
esphome/components/ethernet_info/* @gtjadsonsantos
|
||||
esphome/components/event/* @nohat
|
||||
esphome/components/event_emitter/* @Rapsssito
|
||||
|
@@ -267,6 +267,11 @@ def validate_adc_pin(value):
|
||||
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True
|
||||
)(value)
|
||||
|
||||
if CORE.is_nrf52:
|
||||
return pins.gpio_pin_schema(
|
||||
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True
|
||||
)(value)
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -283,5 +288,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
PlatformFramework.RTL87XX_ARDUINO,
|
||||
PlatformFramework.LN882X_ARDUINO,
|
||||
},
|
||||
"adc_sensor_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
|
||||
}
|
||||
)
|
||||
|
@@ -13,6 +13,10 @@
|
||||
#include "hal/adc_types.h" // This defines ADC_CHANNEL_MAX
|
||||
#endif // USE_ESP32
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
#include <zephyr/drivers/adc.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace adc {
|
||||
|
||||
@@ -38,15 +42,15 @@ enum class SamplingMode : uint8_t {
|
||||
|
||||
const LogString *sampling_mode_to_str(SamplingMode mode);
|
||||
|
||||
class Aggregator {
|
||||
template<typename T> class Aggregator {
|
||||
public:
|
||||
Aggregator(SamplingMode mode);
|
||||
void add_sample(uint32_t value);
|
||||
uint32_t aggregate();
|
||||
void add_sample(T value);
|
||||
T aggregate();
|
||||
|
||||
protected:
|
||||
uint32_t aggr_{0};
|
||||
uint32_t samples_{0};
|
||||
T aggr_{0};
|
||||
uint8_t samples_{0};
|
||||
SamplingMode mode_{SamplingMode::AVG};
|
||||
};
|
||||
|
||||
@@ -69,6 +73,11 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
|
||||
/// @return A float representing the setup priority.
|
||||
float get_setup_priority() const override;
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
/// Set the ADC channel to be used by the ADC sensor.
|
||||
/// @param channel Pointer to an adc_dt_spec structure representing the ADC channel.
|
||||
void set_adc_channel(const adc_dt_spec *channel) { this->channel_ = channel; }
|
||||
#endif
|
||||
/// Set the GPIO pin to be used by the ADC sensor.
|
||||
/// @param pin Pointer to an InternalGPIOPin representing the ADC input pin.
|
||||
void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
|
||||
@@ -151,6 +160,10 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
|
||||
#ifdef USE_RP2040
|
||||
bool is_temperature_{false};
|
||||
#endif // USE_RP2040
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
const struct adc_dt_spec *channel_ = nullptr;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace adc
|
||||
|
@@ -18,15 +18,15 @@ const LogString *sampling_mode_to_str(SamplingMode mode) {
|
||||
return LOG_STR("unknown");
|
||||
}
|
||||
|
||||
Aggregator::Aggregator(SamplingMode mode) {
|
||||
template<typename T> Aggregator<T>::Aggregator(SamplingMode mode) {
|
||||
this->mode_ = mode;
|
||||
// set to max uint if mode is "min"
|
||||
if (mode == SamplingMode::MIN) {
|
||||
this->aggr_ = UINT32_MAX;
|
||||
this->aggr_ = std::numeric_limits<T>::max();
|
||||
}
|
||||
}
|
||||
|
||||
void Aggregator::add_sample(uint32_t value) {
|
||||
template<typename T> void Aggregator<T>::add_sample(T value) {
|
||||
this->samples_ += 1;
|
||||
|
||||
switch (this->mode_) {
|
||||
@@ -47,7 +47,7 @@ void Aggregator::add_sample(uint32_t value) {
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t Aggregator::aggregate() {
|
||||
template<typename T> T Aggregator<T>::aggregate() {
|
||||
if (this->mode_ == SamplingMode::AVG) {
|
||||
if (this->samples_ == 0) {
|
||||
return this->aggr_;
|
||||
@@ -59,6 +59,12 @@ uint32_t Aggregator::aggregate() {
|
||||
return this->aggr_;
|
||||
}
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
template class Aggregator<int32_t>;
|
||||
#else
|
||||
template class Aggregator<uint32_t>;
|
||||
#endif
|
||||
|
||||
void ADCSensor::update() {
|
||||
float value_v = this->sample();
|
||||
ESP_LOGV(TAG, "'%s': Voltage=%.4fV", this->get_name().c_str(), value_v);
|
||||
|
@@ -152,7 +152,7 @@ float ADCSensor::sample() {
|
||||
}
|
||||
|
||||
float ADCSensor::sample_fixed_attenuation_() {
|
||||
auto aggr = Aggregator(this->sampling_mode_);
|
||||
auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
|
||||
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
int raw;
|
||||
|
@@ -37,7 +37,7 @@ void ADCSensor::dump_config() {
|
||||
}
|
||||
|
||||
float ADCSensor::sample() {
|
||||
auto aggr = Aggregator(this->sampling_mode_);
|
||||
auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
|
||||
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
uint32_t raw = 0;
|
||||
|
@@ -30,7 +30,7 @@ void ADCSensor::dump_config() {
|
||||
|
||||
float ADCSensor::sample() {
|
||||
uint32_t raw = 0;
|
||||
auto aggr = Aggregator(this->sampling_mode_);
|
||||
auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
|
||||
|
||||
if (this->output_raw_) {
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
|
@@ -41,7 +41,7 @@ void ADCSensor::dump_config() {
|
||||
|
||||
float ADCSensor::sample() {
|
||||
uint32_t raw = 0;
|
||||
auto aggr = Aggregator(this->sampling_mode_);
|
||||
auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
|
||||
|
||||
if (this->is_temperature_) {
|
||||
adc_set_temp_sensor_enabled(true);
|
||||
|
207
esphome/components/adc/adc_sensor_zephyr.cpp
Normal file
207
esphome/components/adc/adc_sensor_zephyr.cpp
Normal file
@@ -0,0 +1,207 @@
|
||||
|
||||
#include "adc_sensor.h"
|
||||
#ifdef USE_ZEPHYR
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "hal/nrf_saadc.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace adc {
|
||||
|
||||
static const char *const TAG = "adc.zephyr";
|
||||
|
||||
void ADCSensor::setup() {
|
||||
if (!adc_is_ready_dt(this->channel_)) {
|
||||
ESP_LOGE(TAG, "ADC controller device %s not ready", this->channel_->dev->name);
|
||||
return;
|
||||
}
|
||||
|
||||
auto err = adc_channel_setup_dt(this->channel_);
|
||||
if (err < 0) {
|
||||
ESP_LOGE(TAG, "Could not setup channel %s (%d)", this->channel_->dev->name, err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
static const LogString *gain_to_str(enum adc_gain gain) {
|
||||
switch (gain) {
|
||||
case ADC_GAIN_1_6:
|
||||
return LOG_STR("1/6");
|
||||
case ADC_GAIN_1_5:
|
||||
return LOG_STR("1/5");
|
||||
case ADC_GAIN_1_4:
|
||||
return LOG_STR("1/4");
|
||||
case ADC_GAIN_1_3:
|
||||
return LOG_STR("1/3");
|
||||
case ADC_GAIN_2_5:
|
||||
return LOG_STR("2/5");
|
||||
case ADC_GAIN_1_2:
|
||||
return LOG_STR("1/2");
|
||||
case ADC_GAIN_2_3:
|
||||
return LOG_STR("2/3");
|
||||
case ADC_GAIN_4_5:
|
||||
return LOG_STR("4/5");
|
||||
case ADC_GAIN_1:
|
||||
return LOG_STR("1");
|
||||
case ADC_GAIN_2:
|
||||
return LOG_STR("2");
|
||||
case ADC_GAIN_3:
|
||||
return LOG_STR("3");
|
||||
case ADC_GAIN_4:
|
||||
return LOG_STR("4");
|
||||
case ADC_GAIN_6:
|
||||
return LOG_STR("6");
|
||||
case ADC_GAIN_8:
|
||||
return LOG_STR("8");
|
||||
case ADC_GAIN_12:
|
||||
return LOG_STR("12");
|
||||
case ADC_GAIN_16:
|
||||
return LOG_STR("16");
|
||||
case ADC_GAIN_24:
|
||||
return LOG_STR("24");
|
||||
case ADC_GAIN_32:
|
||||
return LOG_STR("32");
|
||||
case ADC_GAIN_64:
|
||||
return LOG_STR("64");
|
||||
case ADC_GAIN_128:
|
||||
return LOG_STR("128");
|
||||
}
|
||||
return LOG_STR("undefined gain");
|
||||
}
|
||||
|
||||
static const LogString *reference_to_str(enum adc_reference reference) {
|
||||
switch (reference) {
|
||||
case ADC_REF_VDD_1:
|
||||
return LOG_STR("VDD");
|
||||
case ADC_REF_VDD_1_2:
|
||||
return LOG_STR("VDD/2");
|
||||
case ADC_REF_VDD_1_3:
|
||||
return LOG_STR("VDD/3");
|
||||
case ADC_REF_VDD_1_4:
|
||||
return LOG_STR("VDD/4");
|
||||
case ADC_REF_INTERNAL:
|
||||
return LOG_STR("INTERNAL");
|
||||
case ADC_REF_EXTERNAL0:
|
||||
return LOG_STR("External, input 0");
|
||||
case ADC_REF_EXTERNAL1:
|
||||
return LOG_STR("External, input 1");
|
||||
}
|
||||
return LOG_STR("undefined reference");
|
||||
}
|
||||
|
||||
static const LogString *input_to_str(uint8_t input) {
|
||||
switch (input) {
|
||||
case NRF_SAADC_INPUT_AIN0:
|
||||
return LOG_STR("AIN0");
|
||||
case NRF_SAADC_INPUT_AIN1:
|
||||
return LOG_STR("AIN1");
|
||||
case NRF_SAADC_INPUT_AIN2:
|
||||
return LOG_STR("AIN2");
|
||||
case NRF_SAADC_INPUT_AIN3:
|
||||
return LOG_STR("AIN3");
|
||||
case NRF_SAADC_INPUT_AIN4:
|
||||
return LOG_STR("AIN4");
|
||||
case NRF_SAADC_INPUT_AIN5:
|
||||
return LOG_STR("AIN5");
|
||||
case NRF_SAADC_INPUT_AIN6:
|
||||
return LOG_STR("AIN6");
|
||||
case NRF_SAADC_INPUT_AIN7:
|
||||
return LOG_STR("AIN7");
|
||||
case NRF_SAADC_INPUT_VDD:
|
||||
return LOG_STR("VDD");
|
||||
case NRF_SAADC_INPUT_VDDHDIV5:
|
||||
return LOG_STR("VDDHDIV5");
|
||||
}
|
||||
return LOG_STR("undefined input");
|
||||
}
|
||||
#endif // ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
|
||||
void ADCSensor::dump_config() {
|
||||
LOG_SENSOR("", "ADC Sensor", this);
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
ESP_LOGV(TAG,
|
||||
" Name: %s\n"
|
||||
" Channel: %d\n"
|
||||
" vref_mv: %d\n"
|
||||
" Resolution %d\n"
|
||||
" Oversampling %d",
|
||||
this->channel_->dev->name, this->channel_->channel_id, this->channel_->vref_mv, this->channel_->resolution,
|
||||
this->channel_->oversampling);
|
||||
|
||||
ESP_LOGV(TAG,
|
||||
" Gain: %s\n"
|
||||
" reference: %s\n"
|
||||
" acquisition_time: %d\n"
|
||||
" differential %s",
|
||||
LOG_STR_ARG(gain_to_str(this->channel_->channel_cfg.gain)),
|
||||
LOG_STR_ARG(reference_to_str(this->channel_->channel_cfg.reference)),
|
||||
this->channel_->channel_cfg.acquisition_time, YESNO(this->channel_->channel_cfg.differential));
|
||||
if (this->channel_->channel_cfg.differential) {
|
||||
ESP_LOGV(TAG,
|
||||
" Positive: %s\n"
|
||||
" Negative: %s",
|
||||
LOG_STR_ARG(input_to_str(this->channel_->channel_cfg.input_positive)),
|
||||
LOG_STR_ARG(input_to_str(this->channel_->channel_cfg.input_negative)));
|
||||
} else {
|
||||
ESP_LOGV(TAG, " Positive: %s", LOG_STR_ARG(input_to_str(this->channel_->channel_cfg.input_positive)));
|
||||
}
|
||||
#endif
|
||||
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
float ADCSensor::sample() {
|
||||
auto aggr = Aggregator<int32_t>(this->sampling_mode_);
|
||||
int err;
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
int16_t buf = 0;
|
||||
struct adc_sequence sequence = {
|
||||
.buffer = &buf,
|
||||
/* buffer size in bytes, not number of samples */
|
||||
.buffer_size = sizeof(buf),
|
||||
};
|
||||
int32_t val_raw;
|
||||
|
||||
err = adc_sequence_init_dt(this->channel_, &sequence);
|
||||
if (err < 0) {
|
||||
ESP_LOGE(TAG, "Could sequence init %s (%d)", this->channel_->dev->name, err);
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
err = adc_read(this->channel_->dev, &sequence);
|
||||
if (err < 0) {
|
||||
ESP_LOGE(TAG, "Could not read %s (%d)", this->channel_->dev->name, err);
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
val_raw = (int32_t) buf;
|
||||
if (!this->channel_->channel_cfg.differential) {
|
||||
// https://github.com/adafruit/Adafruit_nRF52_Arduino/blob/0ed4d9ffc674ae407be7cacf5696a02f5e789861/cores/nRF5/wiring_analog_nRF52.c#L222
|
||||
if (val_raw < 0) {
|
||||
val_raw = 0;
|
||||
}
|
||||
}
|
||||
aggr.add_sample(val_raw);
|
||||
}
|
||||
|
||||
int32_t val_mv = aggr.aggregate();
|
||||
|
||||
if (this->output_raw_) {
|
||||
return val_mv;
|
||||
}
|
||||
|
||||
err = adc_raw_to_millivolts_dt(this->channel_, &val_mv);
|
||||
/* conversion to mV may not be supported, skip if not */
|
||||
if (err < 0) {
|
||||
ESP_LOGE(TAG, "Value in mV not available %s (%d)", this->channel_->dev->name, err);
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return val_mv / 1000.0f;
|
||||
}
|
||||
|
||||
} // namespace adc
|
||||
} // namespace esphome
|
||||
#endif
|
@@ -3,6 +3,12 @@ import logging
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor, voltage_sampler
|
||||
from esphome.components.esp32 import get_esp32_variant
|
||||
from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC
|
||||
from esphome.components.zephyr import (
|
||||
zephyr_add_overlay,
|
||||
zephyr_add_prj_conf,
|
||||
zephyr_add_user,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ATTENUATION,
|
||||
@@ -11,6 +17,7 @@ from esphome.const import (
|
||||
CONF_PIN,
|
||||
CONF_RAW,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
PLATFORM_NRF52,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
@@ -60,6 +67,10 @@ ADCSensor = adc_ns.class_(
|
||||
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
|
||||
)
|
||||
|
||||
CONF_NRF_SAADC = "nrf_saadc"
|
||||
|
||||
adc_dt_spec = cg.global_ns.class_("adc_dt_spec")
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
sensor.sensor_schema(
|
||||
ADCSensor,
|
||||
@@ -75,6 +86,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All(
|
||||
cv.only_on_esp32, _attenuation
|
||||
),
|
||||
cv.OnlyWith(CONF_NRF_SAADC, PLATFORM_NRF52): cv.declare_id(adc_dt_spec),
|
||||
cv.Optional(CONF_SAMPLES, default=1): cv.int_range(min=1, max=255),
|
||||
cv.Optional(CONF_SAMPLING_MODE, default="avg"): _sampling_mode,
|
||||
}
|
||||
@@ -83,6 +95,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
validate_config,
|
||||
)
|
||||
|
||||
CONF_ADC_CHANNEL_ID = "adc_channel_id"
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
@@ -93,7 +107,7 @@ async def to_code(config):
|
||||
cg.add_define("USE_ADC_SENSOR_VCC")
|
||||
elif config[CONF_PIN] == "TEMPERATURE":
|
||||
cg.add(var.set_is_temperature())
|
||||
else:
|
||||
elif not CORE.is_nrf52 or config[CONF_PIN][CONF_NUMBER] not in EXTRA_ADC:
|
||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||
cg.add(var.set_pin(pin))
|
||||
|
||||
@@ -122,3 +136,41 @@ async def to_code(config):
|
||||
):
|
||||
chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num]
|
||||
cg.add(var.set_channel(adc_unit_t.ADC_UNIT_2, chan))
|
||||
|
||||
elif CORE.is_nrf52:
|
||||
CORE.data.setdefault(CONF_ADC_CHANNEL_ID, 0)
|
||||
channel_id = CORE.data[CONF_ADC_CHANNEL_ID]
|
||||
CORE.data[CONF_ADC_CHANNEL_ID] = channel_id + 1
|
||||
zephyr_add_prj_conf("ADC", True)
|
||||
nrf_saadc = config[CONF_NRF_SAADC]
|
||||
rhs = cg.RawExpression(
|
||||
f"ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), {channel_id})"
|
||||
)
|
||||
adc = cg.new_Pvariable(nrf_saadc, rhs)
|
||||
cg.add(var.set_adc_channel(adc))
|
||||
gain = "ADC_GAIN_1_6"
|
||||
pin_number = config[CONF_PIN][CONF_NUMBER]
|
||||
if pin_number == "VDDHDIV5":
|
||||
gain = "ADC_GAIN_1_2"
|
||||
if isinstance(pin_number, int):
|
||||
GPIO_TO_AIN = {v: k for k, v in AIN_TO_GPIO.items()}
|
||||
pin_number = GPIO_TO_AIN[pin_number]
|
||||
zephyr_add_user("io-channels", f"<&adc {channel_id}>")
|
||||
zephyr_add_overlay(
|
||||
f"""
|
||||
&adc {{
|
||||
#address-cells = <1>;
|
||||
#size-cells = <0>;
|
||||
|
||||
channel@{channel_id} {{
|
||||
reg = <{channel_id}>;
|
||||
zephyr,gain = "{gain}";
|
||||
zephyr,reference = "ADC_REF_INTERNAL";
|
||||
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
|
||||
zephyr,input-positive = <NRF_SAADC_{pin_number}>;
|
||||
zephyr,resolution = <14>;
|
||||
zephyr,oversampling = <8>;
|
||||
}};
|
||||
}};
|
||||
"""
|
||||
)
|
||||
|
@@ -34,17 +34,20 @@ SetFrameAction = animation_ns.class_(
|
||||
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = espImage.IMAGE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(Animation_),
|
||||
cv.Optional(CONF_LOOP): cv.All(
|
||||
{
|
||||
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
|
||||
cv.Optional(CONF_END_FRAME): cv.positive_int,
|
||||
cv.Optional(CONF_REPEAT): cv.positive_int,
|
||||
}
|
||||
),
|
||||
},
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
espImage.IMAGE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(Animation_),
|
||||
cv.Optional(CONF_LOOP): cv.All(
|
||||
{
|
||||
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
|
||||
cv.Optional(CONF_END_FRAME): cv.positive_int,
|
||||
cv.Optional(CONF_REPEAT): cv.positive_int,
|
||||
}
|
||||
),
|
||||
},
|
||||
),
|
||||
espImage.validate_settings,
|
||||
)
|
||||
|
||||
|
||||
|
@@ -1621,7 +1621,10 @@ message BluetoothConnectionsFreeResponse {
|
||||
|
||||
uint32 free = 1;
|
||||
uint32 limit = 2;
|
||||
repeated uint64 allocated = 3;
|
||||
repeated uint64 allocated = 3 [
|
||||
(fixed_array_size_define) = "BLUETOOTH_PROXY_MAX_CONNECTIONS",
|
||||
(fixed_array_skip_zero) = true
|
||||
];
|
||||
}
|
||||
|
||||
message BluetoothGATTErrorResponse {
|
||||
|
@@ -1105,10 +1105,8 @@ void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg)
|
||||
|
||||
bool APIConnection::send_subscribe_bluetooth_connections_free_response(
|
||||
const SubscribeBluetoothConnectionsFreeRequest &msg) {
|
||||
BluetoothConnectionsFreeResponse resp;
|
||||
resp.free = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_free();
|
||||
resp.limit = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_limit();
|
||||
return this->send_message(resp, BluetoothConnectionsFreeResponse::MESSAGE_TYPE);
|
||||
bluetooth_proxy::global_bluetooth_proxy->send_connections_free();
|
||||
return true;
|
||||
}
|
||||
|
||||
void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) {
|
||||
|
@@ -29,6 +29,7 @@ extend google.protobuf.FieldOptions {
|
||||
optional uint32 fixed_array_size = 50007;
|
||||
optional bool no_zero_copy = 50008 [default=false];
|
||||
optional bool fixed_array_skip_zero = 50009 [default=false];
|
||||
optional string fixed_array_size_define = 50010;
|
||||
|
||||
// container_pointer: Zero-copy optimization for repeated fields.
|
||||
//
|
||||
|
@@ -2073,15 +2073,17 @@ void BluetoothGATTNotifyDataResponse::calculate_size(ProtoSize &size) const {
|
||||
void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_uint32(1, this->free);
|
||||
buffer.encode_uint32(2, this->limit);
|
||||
for (auto &it : this->allocated) {
|
||||
buffer.encode_uint64(3, it, true);
|
||||
for (const auto &it : this->allocated) {
|
||||
if (it != 0) {
|
||||
buffer.encode_uint64(3, it, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
void BluetoothConnectionsFreeResponse::calculate_size(ProtoSize &size) const {
|
||||
size.add_uint32(1, this->free);
|
||||
size.add_uint32(1, this->limit);
|
||||
if (!this->allocated.empty()) {
|
||||
for (const auto &it : this->allocated) {
|
||||
for (const auto &it : this->allocated) {
|
||||
if (it != 0) {
|
||||
size.add_uint64_force(1, it);
|
||||
}
|
||||
}
|
||||
|
@@ -2076,13 +2076,13 @@ class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage {
|
||||
class BluetoothConnectionsFreeResponse : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 81;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 16;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 20;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "bluetooth_connections_free_response"; }
|
||||
#endif
|
||||
uint32_t free{0};
|
||||
uint32_t limit{0};
|
||||
std::vector<uint64_t> allocated{};
|
||||
std::array<uint64_t, BLUETOOTH_PROXY_MAX_CONNECTIONS> allocated{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
|
@@ -87,6 +87,10 @@ async def to_code(config):
|
||||
cg.add(var.set_active(config[CONF_ACTIVE]))
|
||||
await esp32_ble_tracker.register_raw_ble_device(var, config)
|
||||
|
||||
# Define max connections for protobuf fixed array
|
||||
connection_count = len(config.get(CONF_CONNECTIONS, []))
|
||||
cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", connection_count)
|
||||
|
||||
for connection_conf in config.get(CONF_CONNECTIONS, []):
|
||||
connection_var = cg.new_Pvariable(connection_conf[CONF_ID])
|
||||
await cg.register_component(connection_var, connection_conf)
|
||||
|
@@ -46,6 +46,18 @@ static constexpr uint8_t DESC_SIZE_16BIT = 10; // UUID(6) + handle(4
|
||||
static constexpr uint8_t DESC_PER_CHAR = 1; // Assume 1 descriptor per characteristic
|
||||
|
||||
// Helper to estimate service size before fetching all data
|
||||
/**
|
||||
* Estimate the size of a Bluetooth service based on the number of characteristics and UUID format.
|
||||
*
|
||||
* @param char_count The number of characteristics in the service.
|
||||
* @param use_efficient_uuids Whether to use efficient UUIDs (16-bit or 32-bit) for newer APIVersions.
|
||||
* @return The estimated size of the service in bytes.
|
||||
*
|
||||
* This function calculates the size of a Bluetooth service by considering:
|
||||
* - A service overhead, which depends on whether efficient UUIDs are used.
|
||||
* - The size of each characteristic, assuming 128-bit UUIDs for safety.
|
||||
* - The size of descriptors, assuming one 128-bit descriptor per characteristic.
|
||||
*/
|
||||
static size_t estimate_service_size(uint16_t char_count, bool use_efficient_uuids) {
|
||||
size_t service_overhead = use_efficient_uuids ? SERVICE_OVERHEAD_EFFICIENT : SERVICE_OVERHEAD_LEGACY;
|
||||
// Always assume 128-bit UUIDs for characteristics to be safe
|
||||
@@ -66,6 +78,30 @@ void BluetoothConnection::dump_config() {
|
||||
BLEClientBase::dump_config();
|
||||
}
|
||||
|
||||
void BluetoothConnection::update_allocated_slot_(uint64_t find_value, uint64_t set_value) {
|
||||
auto &allocated = this->proxy_->connections_free_response_.allocated;
|
||||
auto it = std::find(allocated.begin(), allocated.end(), find_value);
|
||||
if (it != allocated.end()) {
|
||||
*it = set_value;
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothConnection::set_address(uint64_t address) {
|
||||
// If we're clearing an address (disconnecting), update the pre-allocated message
|
||||
if (address == 0 && this->address_ != 0) {
|
||||
this->proxy_->connections_free_response_.free++;
|
||||
this->update_allocated_slot_(this->address_, 0);
|
||||
}
|
||||
// If we're setting a new address (connecting), update the pre-allocated message
|
||||
else if (address != 0 && this->address_ == 0) {
|
||||
this->proxy_->connections_free_response_.free--;
|
||||
this->update_allocated_slot_(0, address);
|
||||
}
|
||||
|
||||
// Call parent implementation to actually set the address
|
||||
BLEClientBase::set_address(address);
|
||||
}
|
||||
|
||||
void BluetoothConnection::loop() {
|
||||
BLEClientBase::loop();
|
||||
|
||||
|
@@ -24,12 +24,15 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
|
||||
|
||||
esp_err_t notify_characteristic(uint16_t handle, bool enable);
|
||||
|
||||
void set_address(uint64_t address) override;
|
||||
|
||||
protected:
|
||||
friend class BluetoothProxy;
|
||||
|
||||
bool supports_efficient_uuids_() const;
|
||||
void send_service_for_discovery_();
|
||||
void reset_connection_(esp_err_t reason);
|
||||
void update_allocated_slot_(uint64_t find_value, uint64_t set_value);
|
||||
|
||||
// Memory optimized layout for 32-bit systems
|
||||
// Group 1: Pointers (4 bytes each, naturally aligned)
|
||||
|
@@ -35,6 +35,9 @@ void BluetoothProxy::setup() {
|
||||
// Don't pre-allocate pool - let it grow only if needed in busy environments
|
||||
// Many devices in quiet areas will never need the overflow pool
|
||||
|
||||
this->connections_free_response_.limit = this->connections_.size();
|
||||
this->connections_free_response_.free = this->connections_.size();
|
||||
|
||||
this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) {
|
||||
if (this->api_connection_ != nullptr) {
|
||||
this->send_bluetooth_scanner_state_(state);
|
||||
@@ -134,20 +137,6 @@ void BluetoothProxy::dump_config() {
|
||||
YESNO(this->active_), this->connections_.size());
|
||||
}
|
||||
|
||||
int BluetoothProxy::get_bluetooth_connections_free() {
|
||||
int free = 0;
|
||||
for (auto *connection : this->connections_) {
|
||||
if (connection->address_ == 0) {
|
||||
free++;
|
||||
ESP_LOGV(TAG, "[%d] Free connection", connection->get_connection_index());
|
||||
} else {
|
||||
ESP_LOGV(TAG, "[%d] Used connection by [%s]", connection->get_connection_index(),
|
||||
connection->address_str().c_str());
|
||||
}
|
||||
}
|
||||
return free;
|
||||
}
|
||||
|
||||
void BluetoothProxy::loop() {
|
||||
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) {
|
||||
for (auto *connection : this->connections_) {
|
||||
@@ -441,15 +430,9 @@ void BluetoothProxy::send_device_connection(uint64_t address, bool connected, ui
|
||||
void BluetoothProxy::send_connections_free() {
|
||||
if (this->api_connection_ == nullptr)
|
||||
return;
|
||||
api::BluetoothConnectionsFreeResponse call;
|
||||
call.free = this->get_bluetooth_connections_free();
|
||||
call.limit = this->get_bluetooth_connections_limit();
|
||||
for (auto *connection : this->connections_) {
|
||||
if (connection->address_ != 0) {
|
||||
call.allocated.push_back(connection->address_);
|
||||
}
|
||||
}
|
||||
this->api_connection_->send_message(call, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE);
|
||||
|
||||
this->api_connection_->send_message(this->connections_free_response_,
|
||||
api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
void BluetoothProxy::send_gatt_services_done(uint64_t address) {
|
||||
|
@@ -49,6 +49,7 @@ enum BluetoothProxySubscriptionFlag : uint32_t {
|
||||
};
|
||||
|
||||
class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
|
||||
friend class BluetoothConnection; // Allow connection to call free_connection_
|
||||
public:
|
||||
BluetoothProxy();
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
@@ -74,9 +75,6 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
|
||||
void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg);
|
||||
void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg);
|
||||
|
||||
int get_bluetooth_connections_free();
|
||||
int get_bluetooth_connections_limit() { return this->connections_.size(); }
|
||||
|
||||
void subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags);
|
||||
void unsubscribe_api_connection(api::APIConnection *api_connection);
|
||||
api::APIConnection *get_api_connection() { return this->api_connection_; }
|
||||
@@ -149,6 +147,9 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
|
||||
// Group 3: 4-byte types
|
||||
uint32_t last_advertisement_flush_time_{0};
|
||||
|
||||
// Pre-allocated response message - always ready to send
|
||||
api::BluetoothConnectionsFreeResponse connections_free_response_;
|
||||
|
||||
// Group 4: 1-byte types grouped together
|
||||
bool active_;
|
||||
uint8_t advertisement_count_{0};
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.zephyr import zephyr_add_prj_conf
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -10,6 +11,7 @@ from esphome.const import (
|
||||
CONF_LOOP_TIME,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
DEPENDENCIES = ["logger"]
|
||||
@@ -44,6 +46,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
if CORE.using_zephyr:
|
||||
zephyr_add_prj_conf("HWINFO", True)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -62,5 +66,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
PlatformFramework.RTL87XX_ARDUINO,
|
||||
PlatformFramework.LN882X_ARDUINO,
|
||||
},
|
||||
"debug_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
|
||||
}
|
||||
)
|
||||
|
281
esphome/components/debug/debug_zephyr.cpp
Normal file
281
esphome/components/debug/debug_zephyr.cpp
Normal file
@@ -0,0 +1,281 @@
|
||||
#include "debug_component.h"
|
||||
#ifdef USE_ZEPHYR
|
||||
#include <climits>
|
||||
#include "esphome/core/log.h"
|
||||
#include <zephyr/drivers/hwinfo.h>
|
||||
#include <hal/nrf_power.h>
|
||||
#include <cstdint>
|
||||
|
||||
#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0]
|
||||
|
||||
namespace esphome {
|
||||
namespace debug {
|
||||
|
||||
static const char *const TAG = "debug";
|
||||
constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC;
|
||||
constexpr std::uintptr_t MBR_BOOTLOADER_ADDR = 0xFF8;
|
||||
|
||||
static void show_reset_reason(std::string &reset_reason, bool set, const char *reason) {
|
||||
if (!set) {
|
||||
return;
|
||||
}
|
||||
if (!reset_reason.empty()) {
|
||||
reset_reason += ", ";
|
||||
}
|
||||
reset_reason += reason;
|
||||
}
|
||||
|
||||
inline uint32_t read_mem_u32(uintptr_t addr) {
|
||||
return *reinterpret_cast<volatile uint32_t *>(addr); // NOLINT(performance-no-int-to-ptr)
|
||||
}
|
||||
|
||||
std::string DebugComponent::get_reset_reason_() {
|
||||
uint32_t cause;
|
||||
auto ret = hwinfo_get_reset_cause(&cause);
|
||||
if (ret) {
|
||||
ESP_LOGE(TAG, "Unable to get reset cause: %d", ret);
|
||||
return "";
|
||||
}
|
||||
std::string reset_reason;
|
||||
|
||||
show_reset_reason(reset_reason, cause & RESET_PIN, "External pin");
|
||||
show_reset_reason(reset_reason, cause & RESET_SOFTWARE, "Software reset");
|
||||
show_reset_reason(reset_reason, cause & RESET_BROWNOUT, "Brownout (drop in voltage)");
|
||||
show_reset_reason(reset_reason, cause & RESET_POR, "Power-on reset (POR)");
|
||||
show_reset_reason(reset_reason, cause & RESET_WATCHDOG, "Watchdog timer expiration");
|
||||
show_reset_reason(reset_reason, cause & RESET_DEBUG, "Debug event");
|
||||
show_reset_reason(reset_reason, cause & RESET_SECURITY, "Security violation");
|
||||
show_reset_reason(reset_reason, cause & RESET_LOW_POWER_WAKE, "Waking up from low power mode");
|
||||
show_reset_reason(reset_reason, cause & RESET_CPU_LOCKUP, "CPU lock-up detected");
|
||||
show_reset_reason(reset_reason, cause & RESET_PARITY, "Parity error");
|
||||
show_reset_reason(reset_reason, cause & RESET_PLL, "PLL error");
|
||||
show_reset_reason(reset_reason, cause & RESET_CLOCK, "Clock error");
|
||||
show_reset_reason(reset_reason, cause & RESET_HARDWARE, "Hardware reset");
|
||||
show_reset_reason(reset_reason, cause & RESET_USER, "User reset");
|
||||
show_reset_reason(reset_reason, cause & RESET_TEMPERATURE, "Temperature reset");
|
||||
|
||||
ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str());
|
||||
return reset_reason;
|
||||
}
|
||||
|
||||
uint32_t DebugComponent::get_free_heap_() { return INT_MAX; }
|
||||
|
||||
void DebugComponent::get_device_info_(std::string &device_info) {
|
||||
std::string supply = "Main supply status: ";
|
||||
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) {
|
||||
supply += "Normal voltage.";
|
||||
} else {
|
||||
supply += "High voltage.";
|
||||
}
|
||||
ESP_LOGD(TAG, "%s", supply.c_str());
|
||||
device_info += "|" + supply;
|
||||
|
||||
std::string reg0 = "Regulator stage 0: ";
|
||||
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) {
|
||||
reg0 += nrf_power_dcdcen_vddh_get(NRF_POWER) ? "DC/DC" : "LDO";
|
||||
reg0 += ", ";
|
||||
switch (NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) {
|
||||
case (UICR_REGOUT0_VOUT_DEFAULT << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "1.8V (default)";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_1V8 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "1.8V";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_2V1 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "2.1V";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_2V4 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "2.4V";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_2V7 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "2.7V";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_3V0 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "3.0V";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_3V3 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "3.3V";
|
||||
break;
|
||||
default:
|
||||
reg0 += "???V";
|
||||
}
|
||||
} else {
|
||||
reg0 += "disabled";
|
||||
}
|
||||
ESP_LOGD(TAG, "%s", reg0.c_str());
|
||||
device_info += "|" + reg0;
|
||||
|
||||
std::string reg1 = "Regulator stage 1: ";
|
||||
reg1 += nrf_power_dcdcen_get(NRF_POWER) ? "DC/DC" : "LDO";
|
||||
ESP_LOGD(TAG, "%s", reg1.c_str());
|
||||
device_info += "|" + reg1;
|
||||
|
||||
std::string usb_power = "USB power state: ";
|
||||
if (nrf_power_usbregstatus_vbusdet_get(NRF_POWER)) {
|
||||
if (nrf_power_usbregstatus_outrdy_get(NRF_POWER)) {
|
||||
/**< From the power viewpoint, USB is ready for working. */
|
||||
usb_power += "ready";
|
||||
} else {
|
||||
/**< The USB power is detected, but USB power regulator is not ready. */
|
||||
usb_power += "connected (regulator is not ready)";
|
||||
}
|
||||
} else {
|
||||
/**< No power on USB lines detected. */
|
||||
usb_power += "disconected";
|
||||
}
|
||||
ESP_LOGD(TAG, "%s", usb_power.c_str());
|
||||
device_info += "|" + usb_power;
|
||||
|
||||
bool enabled;
|
||||
nrf_power_pof_thr_t pof_thr;
|
||||
|
||||
pof_thr = nrf_power_pofcon_get(NRF_POWER, &enabled);
|
||||
std::string pof = "Power-fail comparator: ";
|
||||
if (enabled) {
|
||||
switch (pof_thr) {
|
||||
case POWER_POFCON_THRESHOLD_V17:
|
||||
pof += "1.7V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V18:
|
||||
pof += "1.8V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V19:
|
||||
pof += "1.9V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V20:
|
||||
pof += "2.0V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V21:
|
||||
pof += "2.1V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V22:
|
||||
pof += "2.2V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V23:
|
||||
pof += "2.3V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V24:
|
||||
pof += "2.4V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V25:
|
||||
pof += "2.5V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V26:
|
||||
pof += "2.6V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V27:
|
||||
pof += "2.7V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V28:
|
||||
pof += "2.8V";
|
||||
break;
|
||||
}
|
||||
|
||||
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) {
|
||||
pof += ", VDDH: ";
|
||||
switch (nrf_power_pofcon_vddh_get(NRF_POWER)) {
|
||||
case NRF_POWER_POFTHRVDDH_V27:
|
||||
pof += "2.7V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V28:
|
||||
pof += "2.8V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V29:
|
||||
pof += "2.9V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V30:
|
||||
pof += "3.0V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V31:
|
||||
pof += "3.1V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V32:
|
||||
pof += "3.2V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V33:
|
||||
pof += "3.3V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V34:
|
||||
pof += "3.4V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V35:
|
||||
pof += "3.5V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V36:
|
||||
pof += "3.6V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V37:
|
||||
pof += "3.7V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V38:
|
||||
pof += "3.8V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V39:
|
||||
pof += "3.9V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V40:
|
||||
pof += "4.0V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V41:
|
||||
pof += "4.1V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V42:
|
||||
pof += "4.2V";
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pof += "disabled";
|
||||
}
|
||||
ESP_LOGD(TAG, "%s", pof.c_str());
|
||||
device_info += "|" + pof;
|
||||
|
||||
auto package = [](uint32_t value) {
|
||||
switch (value) {
|
||||
case 0x2004:
|
||||
return "QIxx - 7x7 73-pin aQFN";
|
||||
case 0x2000:
|
||||
return "QFxx - 6x6 48-pin QFN";
|
||||
case 0x2005:
|
||||
return "CKxx - 3.544 x 3.607 WLCSP";
|
||||
}
|
||||
return "Unspecified";
|
||||
};
|
||||
|
||||
ESP_LOGD(TAG, "Code page size: %u, code size: %u, device id: 0x%08x%08x", NRF_FICR->CODEPAGESIZE, NRF_FICR->CODESIZE,
|
||||
NRF_FICR->DEVICEID[1], NRF_FICR->DEVICEID[0]);
|
||||
ESP_LOGD(TAG, "Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x", NRF_FICR->ER[0],
|
||||
NRF_FICR->ER[1], NRF_FICR->ER[2], NRF_FICR->ER[3], NRF_FICR->IR[0], NRF_FICR->IR[1], NRF_FICR->IR[2],
|
||||
NRF_FICR->IR[3]);
|
||||
ESP_LOGD(TAG, "Device address type: %s, address: %s", (NRF_FICR->DEVICEADDRTYPE & 0x1 ? "Random" : "Public"),
|
||||
get_mac_address_pretty().c_str());
|
||||
ESP_LOGD(TAG, "Part code: nRF%x, version: %c%c%c%c, package: %s", NRF_FICR->INFO.PART,
|
||||
NRF_FICR->INFO.VARIANT >> 24 & 0xFF, NRF_FICR->INFO.VARIANT >> 16 & 0xFF, NRF_FICR->INFO.VARIANT >> 8 & 0xFF,
|
||||
NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE));
|
||||
ESP_LOGD(TAG, "RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH,
|
||||
(NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not "));
|
||||
ESP_LOGD(
|
||||
TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s",
|
||||
YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)),
|
||||
YESNO(((NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) !=
|
||||
(UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos)) ||
|
||||
((NRF_UICR->PSELRESET[1] & UICR_PSELRESET_CONNECT_Msk) !=
|
||||
(UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos))));
|
||||
|
||||
#ifdef USE_BOOTLOADER_MCUBOOT
|
||||
ESP_LOGD(TAG, "bootloader: mcuboot");
|
||||
#else
|
||||
ESP_LOGD(TAG, "bootloader: Adafruit, version %u.%u.%u", (BOOTLOADER_VERSION_REGISTER >> 16) & 0xFF,
|
||||
(BOOTLOADER_VERSION_REGISTER >> 8) & 0xFF, BOOTLOADER_VERSION_REGISTER & 0xFF);
|
||||
ESP_LOGD(TAG, "MBR bootloader addr 0x%08x, UICR bootloader addr 0x%08x", read_mem_u32(MBR_BOOTLOADER_ADDR),
|
||||
NRF_UICR->NRFFW[0]);
|
||||
ESP_LOGD(TAG, "MBR param page addr 0x%08x, UICR param page addr 0x%08x", read_mem_u32(MBR_PARAM_PAGE_ADDR),
|
||||
NRF_UICR->NRFFW[1]);
|
||||
#endif
|
||||
}
|
||||
|
||||
void DebugComponent::update_platform_() {}
|
||||
|
||||
} // namespace debug
|
||||
} // namespace esphome
|
||||
#endif
|
@@ -1,6 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
from esphome.components.esp32 import CONF_CPU_FREQUENCY
|
||||
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BLOCK,
|
||||
@@ -54,7 +55,7 @@ CONFIG_SCHEMA = {
|
||||
),
|
||||
cv.Optional(CONF_PSRAM): cv.All(
|
||||
cv.only_on_esp32,
|
||||
cv.requires_component("psram"),
|
||||
cv.requires_component(PSRAM_DOMAIN),
|
||||
sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_BYTES,
|
||||
icon=ICON_COUNTER,
|
||||
|
@@ -76,6 +76,7 @@ CONF_ASSERTION_LEVEL = "assertion_level"
|
||||
CONF_COMPILER_OPTIMIZATION = "compiler_optimization"
|
||||
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features"
|
||||
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
|
||||
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
|
||||
CONF_RELEASE = "release"
|
||||
|
||||
ASSERTION_LEVELS = {
|
||||
@@ -313,7 +314,7 @@ def _format_framework_espidf_version(
|
||||
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1)
|
||||
# The platform-espressif32 version to use for arduino frameworks
|
||||
# - https://github.com/pioarduino/platform-espressif32/releases
|
||||
ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21, "1")
|
||||
ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21, "2")
|
||||
|
||||
# The default/recommended esp-idf framework version
|
||||
# - https://github.com/espressif/esp-idf/releases
|
||||
@@ -322,7 +323,7 @@ RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 4, 2)
|
||||
# The platformio/espressif32 version to use for esp-idf frameworks
|
||||
# - https://github.com/platformio/platform-espressif32/releases
|
||||
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
|
||||
ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21, "1")
|
||||
ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21, "2")
|
||||
|
||||
# List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions
|
||||
SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
|
||||
@@ -519,32 +520,59 @@ def _detect_variant(value):
|
||||
|
||||
|
||||
def final_validate(config):
|
||||
if not (
|
||||
pio_options := fv.full_config.get()[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS)
|
||||
):
|
||||
# Not specified or empty
|
||||
return config
|
||||
|
||||
pio_flash_size_key = "board_upload.flash_size"
|
||||
pio_partitions_key = "board_build.partitions"
|
||||
if CONF_PARTITIONS in config and pio_partitions_key in pio_options:
|
||||
raise cv.Invalid(
|
||||
f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32"
|
||||
)
|
||||
|
||||
if pio_flash_size_key in pio_options:
|
||||
raise cv.Invalid(
|
||||
f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only"
|
||||
)
|
||||
# Imported locally to avoid circular import issues
|
||||
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
||||
|
||||
errs = []
|
||||
full_config = fv.full_config.get()
|
||||
if pio_options := full_config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS):
|
||||
pio_flash_size_key = "board_upload.flash_size"
|
||||
pio_partitions_key = "board_build.partitions"
|
||||
if CONF_PARTITIONS in config and pio_partitions_key in pio_options:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32"
|
||||
)
|
||||
)
|
||||
if pio_flash_size_key in pio_options:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only"
|
||||
)
|
||||
)
|
||||
if (
|
||||
config[CONF_VARIANT] != VARIANT_ESP32
|
||||
and CONF_ADVANCED in (conf_fw := config[CONF_FRAMEWORK])
|
||||
and CONF_IGNORE_EFUSE_MAC_CRC in conf_fw[CONF_ADVANCED]
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"{CONF_IGNORE_EFUSE_MAC_CRC} is not supported on {config[CONF_VARIANT]}"
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"'{CONF_IGNORE_EFUSE_MAC_CRC}' is not supported on {config[CONF_VARIANT]}",
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_IGNORE_EFUSE_MAC_CRC],
|
||||
)
|
||||
)
|
||||
if (
|
||||
config.get(CONF_FRAMEWORK, {})
|
||||
.get(CONF_ADVANCED, {})
|
||||
.get(CONF_EXECUTE_FROM_PSRAM)
|
||||
):
|
||||
if config[CONF_VARIANT] != VARIANT_ESP32S3:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"'{CONF_EXECUTE_FROM_PSRAM}' is only supported on {VARIANT_ESP32S3} variant",
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM],
|
||||
)
|
||||
)
|
||||
if PSRAM_DOMAIN not in full_config:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"'{CONF_EXECUTE_FROM_PSRAM}' requires PSRAM to be configured",
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM],
|
||||
)
|
||||
)
|
||||
|
||||
if errs:
|
||||
raise cv.MultipleInvalid(errs)
|
||||
|
||||
return config
|
||||
|
||||
@@ -627,6 +655,7 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
|
||||
cv.Optional(
|
||||
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True
|
||||
): cv.boolean,
|
||||
cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
|
||||
@@ -792,6 +821,9 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
|
||||
if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
|
||||
if advanced.get(CONF_EXECUTE_FROM_PSRAM, False):
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
|
||||
|
||||
# Apply LWIP core locking for better socket performance
|
||||
# This is already enabled by default in Arduino framework, where it provides
|
||||
|
@@ -2,6 +2,7 @@ import logging
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
_ESP32H2_SPI_FLASH_PINS = {6, 7, 15, 16, 17, 18, 19, 20, 21}
|
||||
|
||||
@@ -15,13 +16,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def esp32_h2_validate_gpio_pin(value):
|
||||
if value < 0 or value > 27:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-27)")
|
||||
if value in _ESP32H2_STRAPPING_PINS:
|
||||
_LOGGER.warning(
|
||||
"GPIO%d is a Strapping PIN and should be avoided.\n"
|
||||
"Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n"
|
||||
"See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins",
|
||||
value,
|
||||
)
|
||||
if value in _ESP32H2_SPI_FLASH_PINS:
|
||||
_LOGGER.warning(
|
||||
"GPIO%d is reserved for SPI Flash communication on some ESP32-H2 chip variants.\n"
|
||||
@@ -49,4 +43,5 @@ def esp32_h2_validate_supports(value):
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
check_strapping_pin(value, _ESP32H2_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
@@ -2,6 +2,7 @@ import logging
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
_ESP32P4_USB_JTAG_PINS = {24, 25}
|
||||
|
||||
@@ -13,13 +14,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def esp32_p4_validate_gpio_pin(value):
|
||||
if value < 0 or value > 54:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)")
|
||||
if value in _ESP32P4_STRAPPING_PINS:
|
||||
_LOGGER.warning(
|
||||
"GPIO%d is a Strapping PIN and should be avoided.\n"
|
||||
"Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n"
|
||||
"See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins",
|
||||
value,
|
||||
)
|
||||
if value in _ESP32P4_USB_JTAG_PINS:
|
||||
_LOGGER.warning(
|
||||
"GPIO%d is reserved for the USB-Serial-JTAG interface.\n"
|
||||
@@ -40,4 +34,5 @@ def esp32_p4_validate_supports(value):
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
check_strapping_pin(value, _ESP32P4_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
@@ -48,7 +48,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
|
||||
void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; }
|
||||
|
||||
void set_address(uint64_t address) {
|
||||
virtual void set_address(uint64_t address) {
|
||||
this->address_ = address;
|
||||
this->remote_bda_[0] = (address >> 40) & 0xFF;
|
||||
this->remote_bda_[1] = (address >> 32) & 0xFF;
|
||||
|
320
esphome/components/espnow/__init__.py
Normal file
320
esphome/components/espnow/__init__.py
Normal file
@@ -0,0 +1,320 @@
|
||||
from esphome import automation, core
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import wifi
|
||||
from esphome.components.udp import CONF_ON_RECEIVE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_CHANNEL,
|
||||
CONF_DATA,
|
||||
CONF_ENABLE_ON_BOOT,
|
||||
CONF_ID,
|
||||
CONF_ON_ERROR,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_WIFI,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
|
||||
byte_vector = cg.std_vector.template(cg.uint8)
|
||||
peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6)
|
||||
|
||||
espnow_ns = cg.esphome_ns.namespace("espnow")
|
||||
ESPNowComponent = espnow_ns.class_("ESPNowComponent", cg.Component)
|
||||
|
||||
# Handler interfaces that other components can use to register callbacks
|
||||
ESPNowReceivedPacketHandler = espnow_ns.class_("ESPNowReceivedPacketHandler")
|
||||
ESPNowUnknownPeerHandler = espnow_ns.class_("ESPNowUnknownPeerHandler")
|
||||
ESPNowBroadcastedHandler = espnow_ns.class_("ESPNowBroadcastedHandler")
|
||||
|
||||
ESPNowRecvInfo = espnow_ns.class_("ESPNowRecvInfo")
|
||||
ESPNowRecvInfoConstRef = ESPNowRecvInfo.operator("const").operator("ref")
|
||||
|
||||
SendAction = espnow_ns.class_("SendAction", automation.Action)
|
||||
SetChannelAction = espnow_ns.class_("SetChannelAction", automation.Action)
|
||||
AddPeerAction = espnow_ns.class_("AddPeerAction", automation.Action)
|
||||
DeletePeerAction = espnow_ns.class_("DeletePeerAction", automation.Action)
|
||||
|
||||
ESPNowHandlerTrigger = automation.Trigger.template(
|
||||
ESPNowRecvInfoConstRef,
|
||||
cg.uint8.operator("const").operator("ptr"),
|
||||
cg.uint8,
|
||||
)
|
||||
|
||||
OnUnknownPeerTrigger = espnow_ns.class_(
|
||||
"OnUnknownPeerTrigger", ESPNowHandlerTrigger, ESPNowUnknownPeerHandler
|
||||
)
|
||||
OnReceiveTrigger = espnow_ns.class_(
|
||||
"OnReceiveTrigger", ESPNowHandlerTrigger, ESPNowReceivedPacketHandler
|
||||
)
|
||||
OnBroadcastedTrigger = espnow_ns.class_(
|
||||
"OnBroadcastedTrigger", ESPNowHandlerTrigger, ESPNowBroadcastedHandler
|
||||
)
|
||||
|
||||
|
||||
CONF_AUTO_ADD_PEER = "auto_add_peer"
|
||||
CONF_PEERS = "peers"
|
||||
CONF_ON_SENT = "on_sent"
|
||||
CONF_ON_UNKNOWN_PEER = "on_unknown_peer"
|
||||
CONF_ON_BROADCAST = "on_broadcast"
|
||||
CONF_CONTINUE_ON_ERROR = "continue_on_error"
|
||||
CONF_WAIT_FOR_SENT = "wait_for_sent"
|
||||
|
||||
MAX_ESPNOW_PACKET_SIZE = 250 # Maximum size of the payload in bytes
|
||||
|
||||
|
||||
def _validate_unknown_peer(config):
|
||||
if config[CONF_AUTO_ADD_PEER] and config.get(CONF_ON_UNKNOWN_PEER):
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_ON_UNKNOWN_PEER}' cannot be used when '{CONF_AUTO_ADD_PEER}' is enabled.",
|
||||
path=[CONF_ON_UNKNOWN_PEER],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ESPNowComponent),
|
||||
cv.OnlyWithout(CONF_CHANNEL, CONF_WIFI): wifi.validate_channel,
|
||||
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_AUTO_ADD_PEER, default=False): cv.boolean,
|
||||
cv.Optional(CONF_PEERS): cv.ensure_list(cv.mac_address),
|
||||
cv.Optional(CONF_ON_UNKNOWN_PEER): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnUnknownPeerTrigger),
|
||||
},
|
||||
single=True,
|
||||
),
|
||||
cv.Optional(CONF_ON_RECEIVE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnReceiveTrigger),
|
||||
cv.Optional(CONF_ADDRESS): cv.mac_address,
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_BROADCAST): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnBroadcastedTrigger),
|
||||
cv.Optional(CONF_ADDRESS): cv.mac_address,
|
||||
}
|
||||
),
|
||||
},
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_on_esp32,
|
||||
_validate_unknown_peer,
|
||||
)
|
||||
|
||||
|
||||
async def _trigger_to_code(config):
|
||||
if address := config.get(CONF_ADDRESS):
|
||||
address = address.parts
|
||||
trigger = cg.new_Pvariable(config[CONF_TRIGGER_ID], address)
|
||||
await automation.build_automation(
|
||||
trigger,
|
||||
[
|
||||
(ESPNowRecvInfoConstRef, "info"),
|
||||
(cg.uint8.operator("const").operator("ptr"), "data"),
|
||||
(cg.uint8, "size"),
|
||||
],
|
||||
config,
|
||||
)
|
||||
return trigger
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
print(config)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
if CORE.using_arduino:
|
||||
cg.add_library("WiFi", None)
|
||||
|
||||
cg.add_define("USE_ESPNOW")
|
||||
if wifi_channel := config.get(CONF_CHANNEL):
|
||||
cg.add(var.set_wifi_channel(wifi_channel))
|
||||
|
||||
cg.add(var.set_auto_add_peer(config[CONF_AUTO_ADD_PEER]))
|
||||
|
||||
for peer in config.get(CONF_PEERS, []):
|
||||
cg.add(var.add_peer(peer.parts))
|
||||
|
||||
if on_receive := config.get(CONF_ON_UNKNOWN_PEER):
|
||||
trigger = await _trigger_to_code(on_receive)
|
||||
cg.add(var.register_unknown_peer_handler(trigger))
|
||||
|
||||
for on_receive in config.get(CONF_ON_RECEIVE, []):
|
||||
trigger = await _trigger_to_code(on_receive)
|
||||
cg.add(var.register_received_handler(trigger))
|
||||
|
||||
for on_receive in config.get(CONF_ON_BROADCAST, []):
|
||||
trigger = await _trigger_to_code(on_receive)
|
||||
cg.add(var.register_broadcasted_handler(trigger))
|
||||
|
||||
|
||||
# ========================================== A C T I O N S ================================================
|
||||
|
||||
|
||||
def validate_peer(value):
|
||||
if isinstance(value, cv.Lambda):
|
||||
return cv.returning_lambda(value)
|
||||
return cv.mac_address(value)
|
||||
|
||||
|
||||
def _validate_raw_data(value):
|
||||
if isinstance(value, str):
|
||||
if len(value) >= MAX_ESPNOW_PACKET_SIZE:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_DATA}' must be less than {MAX_ESPNOW_PACKET_SIZE} characters long, got {len(value)}"
|
||||
)
|
||||
return value
|
||||
if isinstance(value, list):
|
||||
if len(value) > MAX_ESPNOW_PACKET_SIZE:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_DATA}' must be less than {MAX_ESPNOW_PACKET_SIZE} bytes long, got {len(value)}"
|
||||
)
|
||||
return cv.Schema([cv.hex_uint8_t])(value)
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_DATA}' must either be a string wrapped in quotes or a list of bytes"
|
||||
)
|
||||
|
||||
|
||||
async def register_peer(var, config, args):
|
||||
peer = config[CONF_ADDRESS]
|
||||
if isinstance(peer, core.MACAddress):
|
||||
peer = [HexInt(p) for p in peer.parts]
|
||||
|
||||
template_ = await cg.templatable(peer, args, peer_address_t, peer_address_t)
|
||||
cg.add(var.set_address(template_))
|
||||
|
||||
|
||||
PEER_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(ESPNowComponent),
|
||||
cv.Required(CONF_ADDRESS): cv.templatable(cv.mac_address),
|
||||
}
|
||||
)
|
||||
|
||||
SEND_SCHEMA = PEER_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_DATA): cv.templatable(_validate_raw_data),
|
||||
cv.Optional(CONF_ON_SENT): automation.validate_action_list,
|
||||
cv.Optional(CONF_ON_ERROR): automation.validate_action_list,
|
||||
cv.Optional(CONF_WAIT_FOR_SENT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_CONTINUE_ON_ERROR, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _validate_send_action(config):
|
||||
if not config[CONF_WAIT_FOR_SENT] and not config[CONF_CONTINUE_ON_ERROR]:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_CONTINUE_ON_ERROR}' cannot be false if '{CONF_WAIT_FOR_SENT}' is false as the automation will not wait for the failed result.",
|
||||
path=[CONF_CONTINUE_ON_ERROR],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
SEND_SCHEMA.add_extra(_validate_send_action)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"espnow.send",
|
||||
SendAction,
|
||||
SEND_SCHEMA,
|
||||
)
|
||||
@automation.register_action(
|
||||
"espnow.broadcast",
|
||||
SendAction,
|
||||
cv.maybe_simple_value(
|
||||
SEND_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_ADDRESS, default="FF:FF:FF:FF:FF:FF"): cv.mac_address,
|
||||
}
|
||||
),
|
||||
key=CONF_DATA,
|
||||
),
|
||||
)
|
||||
async def send_action(
|
||||
config: ConfigType,
|
||||
action_id: core.ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: list[tuple],
|
||||
):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
await register_peer(var, config, args)
|
||||
|
||||
data = config.get(CONF_DATA, [])
|
||||
if isinstance(data, str):
|
||||
data = [cg.RawExpression(f"'{c}'") for c in data]
|
||||
templ = await cg.templatable(data, args, byte_vector, byte_vector)
|
||||
cg.add(var.set_data(templ))
|
||||
|
||||
cg.add(var.set_wait_for_sent(config[CONF_WAIT_FOR_SENT]))
|
||||
cg.add(var.set_continue_on_error(config[CONF_CONTINUE_ON_ERROR]))
|
||||
|
||||
if on_sent_config := config.get(CONF_ON_SENT):
|
||||
actions = await automation.build_action_list(on_sent_config, template_arg, args)
|
||||
cg.add(var.add_on_sent(actions))
|
||||
if on_error_config := config.get(CONF_ON_ERROR):
|
||||
actions = await automation.build_action_list(
|
||||
on_error_config, template_arg, args
|
||||
)
|
||||
cg.add(var.add_on_error(actions))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"espnow.peer.add",
|
||||
AddPeerAction,
|
||||
cv.maybe_simple_value(
|
||||
PEER_SCHEMA,
|
||||
key=CONF_ADDRESS,
|
||||
),
|
||||
)
|
||||
@automation.register_action(
|
||||
"espnow.peer.delete",
|
||||
DeletePeerAction,
|
||||
cv.maybe_simple_value(
|
||||
PEER_SCHEMA,
|
||||
key=CONF_ADDRESS,
|
||||
),
|
||||
)
|
||||
async def peer_action(
|
||||
config: ConfigType,
|
||||
action_id: core.ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: list[tuple],
|
||||
):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
await register_peer(var, config, args)
|
||||
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"espnow.set_channel",
|
||||
SetChannelAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(ESPNowComponent),
|
||||
cv.Required(CONF_CHANNEL): cv.templatable(wifi.validate_channel),
|
||||
},
|
||||
key=CONF_CHANNEL,
|
||||
),
|
||||
)
|
||||
async def channel_action(
|
||||
config: ConfigType,
|
||||
action_id: core.ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: list[tuple],
|
||||
):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
template_ = await cg.templatable(config[CONF_CHANNEL], args, cg.uint8)
|
||||
cg.add(var.set_channel(template_))
|
||||
return var
|
175
esphome/components/espnow/automation.h
Normal file
175
esphome/components/espnow/automation.h
Normal file
@@ -0,0 +1,175 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "espnow_component.h"
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/base_automation.h"
|
||||
|
||||
namespace esphome::espnow {
|
||||
|
||||
template<typename... Ts> class SendAction : public Action<Ts...>, public Parented<ESPNowComponent> {
|
||||
TEMPLATABLE_VALUE(peer_address_t, address);
|
||||
TEMPLATABLE_VALUE(std::vector<uint8_t>, data);
|
||||
|
||||
public:
|
||||
void add_on_sent(const std::vector<Action<Ts...> *> &actions) {
|
||||
this->sent_.add_actions(actions);
|
||||
if (this->flags_.wait_for_sent) {
|
||||
this->sent_.add_action(new LambdaAction<Ts...>([this](Ts... x) { this->play_next_(x...); }));
|
||||
}
|
||||
}
|
||||
void add_on_error(const std::vector<Action<Ts...> *> &actions) {
|
||||
this->error_.add_actions(actions);
|
||||
if (this->flags_.wait_for_sent) {
|
||||
this->error_.add_action(new LambdaAction<Ts...>([this](Ts... x) {
|
||||
if (this->flags_.continue_on_error) {
|
||||
this->play_next_(x...);
|
||||
} else {
|
||||
this->stop_complex();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void set_wait_for_sent(bool wait_for_sent) { this->flags_.wait_for_sent = wait_for_sent; }
|
||||
void set_continue_on_error(bool continue_on_error) { this->flags_.continue_on_error = continue_on_error; }
|
||||
|
||||
void play_complex(Ts... x) override {
|
||||
this->num_running_++;
|
||||
send_callback_t send_callback = [this, x...](esp_err_t status) {
|
||||
if (status == ESP_OK) {
|
||||
if (this->sent_.empty() && this->flags_.wait_for_sent) {
|
||||
this->play_next_(x...);
|
||||
} else if (!this->sent_.empty()) {
|
||||
this->sent_.play(x...);
|
||||
}
|
||||
} else {
|
||||
if (this->error_.empty() && this->flags_.wait_for_sent) {
|
||||
if (this->flags_.continue_on_error) {
|
||||
this->play_next_(x...);
|
||||
} else {
|
||||
this->stop_complex();
|
||||
}
|
||||
} else if (!this->error_.empty()) {
|
||||
this->error_.play(x...);
|
||||
}
|
||||
}
|
||||
};
|
||||
peer_address_t address = this->address_.value(x...);
|
||||
std::vector<uint8_t> data = this->data_.value(x...);
|
||||
esp_err_t err = this->parent_->send(address.data(), data, send_callback);
|
||||
if (err != ESP_OK) {
|
||||
send_callback(err);
|
||||
} else if (!this->flags_.wait_for_sent) {
|
||||
this->play_next_(x...);
|
||||
}
|
||||
}
|
||||
|
||||
void play(Ts... x) override { /* ignore - see play_complex */
|
||||
}
|
||||
|
||||
void stop() override {
|
||||
this->sent_.stop();
|
||||
this->error_.stop();
|
||||
}
|
||||
|
||||
protected:
|
||||
ActionList<Ts...> sent_;
|
||||
ActionList<Ts...> error_;
|
||||
|
||||
struct {
|
||||
uint8_t wait_for_sent : 1; // Wait for the send operation to complete before continuing automation
|
||||
uint8_t continue_on_error : 1; // Continue automation even if the send operation fails
|
||||
uint8_t reserved : 6; // Reserved for future use
|
||||
} flags_{0};
|
||||
};
|
||||
|
||||
template<typename... Ts> class AddPeerAction : public Action<Ts...>, public Parented<ESPNowComponent> {
|
||||
TEMPLATABLE_VALUE(peer_address_t, address);
|
||||
|
||||
public:
|
||||
void play(Ts... x) override {
|
||||
peer_address_t address = this->address_.value(x...);
|
||||
this->parent_->add_peer(address.data());
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class DeletePeerAction : public Action<Ts...>, public Parented<ESPNowComponent> {
|
||||
TEMPLATABLE_VALUE(peer_address_t, address);
|
||||
|
||||
public:
|
||||
void play(Ts... x) override {
|
||||
peer_address_t address = this->address_.value(x...);
|
||||
this->parent_->del_peer(address.data());
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetChannelAction : public Action<Ts...>, public Parented<ESPNowComponent> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint8_t, channel)
|
||||
void play(Ts... x) override {
|
||||
if (this->parent_->is_wifi_enabled()) {
|
||||
return;
|
||||
}
|
||||
this->parent_->set_wifi_channel(this->channel_.value(x...));
|
||||
this->parent_->apply_wifi_channel();
|
||||
}
|
||||
};
|
||||
|
||||
class OnReceiveTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
|
||||
public ESPNowReceivedPacketHandler {
|
||||
public:
|
||||
explicit OnReceiveTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) {
|
||||
memcpy(this->address_, address.data(), ESP_NOW_ETH_ALEN);
|
||||
}
|
||||
|
||||
explicit OnReceiveTrigger() : has_address_(false) {}
|
||||
|
||||
bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
|
||||
bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0);
|
||||
if (!match)
|
||||
return false;
|
||||
|
||||
this->trigger(info, data, size);
|
||||
return false; // Return false to continue processing other internal handlers
|
||||
}
|
||||
|
||||
protected:
|
||||
bool has_address_{false};
|
||||
const uint8_t *address_[ESP_NOW_ETH_ALEN];
|
||||
};
|
||||
class OnUnknownPeerTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
|
||||
public ESPNowUnknownPeerHandler {
|
||||
public:
|
||||
bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
|
||||
this->trigger(info, data, size);
|
||||
return false; // Return false to continue processing other internal handlers
|
||||
}
|
||||
};
|
||||
class OnBroadcastedTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
|
||||
public ESPNowBroadcastedHandler {
|
||||
public:
|
||||
explicit OnBroadcastedTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) {
|
||||
memcpy(this->address_, address.data(), ESP_NOW_ETH_ALEN);
|
||||
}
|
||||
explicit OnBroadcastedTrigger() : has_address_(false) {}
|
||||
|
||||
bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
|
||||
bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0);
|
||||
if (!match)
|
||||
return false;
|
||||
|
||||
this->trigger(info, data, size);
|
||||
return false; // Return false to continue processing other internal handlers
|
||||
}
|
||||
|
||||
protected:
|
||||
bool has_address_{false};
|
||||
const uint8_t *address_[ESP_NOW_ETH_ALEN];
|
||||
};
|
||||
|
||||
} // namespace esphome::espnow
|
||||
|
||||
#endif // USE_ESP32
|
468
esphome/components/espnow/espnow_component.cpp
Normal file
468
esphome/components/espnow/espnow_component.cpp
Normal file
@@ -0,0 +1,468 @@
|
||||
#include "espnow_component.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "espnow_err.h"
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <esp_event.h>
|
||||
#include <esp_mac.h>
|
||||
#include <esp_now.h>
|
||||
#include <esp_random.h>
|
||||
#include <esp_wifi.h>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
#ifdef USE_WIFI
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::espnow {
|
||||
|
||||
static constexpr const char *TAG = "espnow";
|
||||
|
||||
static const esp_err_t CONFIG_ESPNOW_WAKE_WINDOW = 50;
|
||||
static const esp_err_t CONFIG_ESPNOW_WAKE_INTERVAL = 100;
|
||||
|
||||
ESPNowComponent *global_esp_now = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
static const LogString *espnow_error_to_str(esp_err_t error) {
|
||||
switch (error) {
|
||||
case ESP_ERR_ESPNOW_FAILED:
|
||||
return LOG_STR("ESPNow is in fail mode");
|
||||
case ESP_ERR_ESPNOW_OWN_ADDRESS:
|
||||
return LOG_STR("Message to your self");
|
||||
case ESP_ERR_ESPNOW_DATA_SIZE:
|
||||
return LOG_STR("Data size to large");
|
||||
case ESP_ERR_ESPNOW_PEER_NOT_SET:
|
||||
return LOG_STR("Peer address not set");
|
||||
case ESP_ERR_ESPNOW_PEER_NOT_PAIRED:
|
||||
return LOG_STR("Peer address not paired");
|
||||
case ESP_ERR_ESPNOW_NOT_INIT:
|
||||
return LOG_STR("Not init");
|
||||
case ESP_ERR_ESPNOW_ARG:
|
||||
return LOG_STR("Invalid argument");
|
||||
case ESP_ERR_ESPNOW_INTERNAL:
|
||||
return LOG_STR("Internal Error");
|
||||
case ESP_ERR_ESPNOW_NO_MEM:
|
||||
return LOG_STR("Our of memory");
|
||||
case ESP_ERR_ESPNOW_NOT_FOUND:
|
||||
return LOG_STR("Peer not found");
|
||||
case ESP_ERR_ESPNOW_IF:
|
||||
return LOG_STR("Interface does not match");
|
||||
case ESP_OK:
|
||||
return LOG_STR("OK");
|
||||
case ESP_NOW_SEND_FAIL:
|
||||
return LOG_STR("Failed");
|
||||
default:
|
||||
return LOG_STR("Unknown Error");
|
||||
}
|
||||
}
|
||||
|
||||
std::string peer_str(uint8_t *peer) {
|
||||
if (peer == nullptr || peer[0] == 0) {
|
||||
return "[Not Set]";
|
||||
} else if (memcmp(peer, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
|
||||
return "[Broadcast]";
|
||||
} else if (memcmp(peer, ESPNOW_MULTICAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
|
||||
return "[Multicast]";
|
||||
} else {
|
||||
return format_mac_address_pretty(peer);
|
||||
}
|
||||
}
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
|
||||
void on_send_report(const esp_now_send_info_t *info, esp_now_send_status_t status)
|
||||
#else
|
||||
void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status)
|
||||
#endif
|
||||
{
|
||||
// Allocate an event from the pool
|
||||
ESPNowPacket *packet = global_esp_now->receive_packet_pool_.allocate();
|
||||
if (packet == nullptr) {
|
||||
// No events available - queue is full or we're out of memory
|
||||
global_esp_now->receive_packet_queue_.increment_dropped_count();
|
||||
return;
|
||||
}
|
||||
|
||||
// Load new packet data (replaces previous packet)
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
|
||||
packet->load_sent_data(info->des_addr, status);
|
||||
#else
|
||||
packet->load_sent_data(mac_addr, status);
|
||||
#endif
|
||||
|
||||
// Push the packet to the queue
|
||||
global_esp_now->receive_packet_queue_.push(packet);
|
||||
// Push always because we're the only producer and the pool ensures we never exceed queue size
|
||||
}
|
||||
|
||||
void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
|
||||
// Allocate an event from the pool
|
||||
ESPNowPacket *packet = global_esp_now->receive_packet_pool_.allocate();
|
||||
if (packet == nullptr) {
|
||||
// No events available - queue is full or we're out of memory
|
||||
global_esp_now->receive_packet_queue_.increment_dropped_count();
|
||||
return;
|
||||
}
|
||||
|
||||
// Load new packet data (replaces previous packet)
|
||||
packet->load_received_data(info, data, size);
|
||||
|
||||
// Push the packet to the queue
|
||||
global_esp_now->receive_packet_queue_.push(packet);
|
||||
// Push always because we're the only producer and the pool ensures we never exceed queue size
|
||||
}
|
||||
|
||||
ESPNowComponent::ESPNowComponent() { global_esp_now = this; }
|
||||
|
||||
void ESPNowComponent::dump_config() {
|
||||
uint32_t version = 0;
|
||||
esp_now_get_version(&version);
|
||||
|
||||
ESP_LOGCONFIG(TAG, "espnow:");
|
||||
if (this->is_disabled()) {
|
||||
ESP_LOGCONFIG(TAG, " Disabled");
|
||||
return;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Own address: %s\n"
|
||||
" Version: v%" PRIu32 "\n"
|
||||
" Wi-Fi channel: %d",
|
||||
format_mac_address_pretty(this->own_address_).c_str(), version, this->wifi_channel_);
|
||||
#ifdef USE_WIFI
|
||||
ESP_LOGCONFIG(TAG, " Wi-Fi enabled: %s", YESNO(this->is_wifi_enabled()));
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ESPNowComponent::is_wifi_enabled() {
|
||||
#ifdef USE_WIFI
|
||||
return wifi::global_wifi_component != nullptr && !wifi::global_wifi_component->is_disabled();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void ESPNowComponent::setup() {
|
||||
if (this->enable_on_boot_) {
|
||||
this->enable_();
|
||||
} else {
|
||||
this->state_ = ESPNOW_STATE_DISABLED;
|
||||
}
|
||||
}
|
||||
|
||||
void ESPNowComponent::enable() {
|
||||
if (this->state_ != ESPNOW_STATE_ENABLED)
|
||||
return;
|
||||
|
||||
ESP_LOGD(TAG, "Enabling");
|
||||
this->state_ = ESPNOW_STATE_OFF;
|
||||
|
||||
this->enable_();
|
||||
}
|
||||
|
||||
void ESPNowComponent::enable_() {
|
||||
if (!this->is_wifi_enabled()) {
|
||||
esp_event_loop_create_default();
|
||||
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
ESP_ERROR_CHECK(esp_wifi_disconnect());
|
||||
|
||||
this->apply_wifi_channel();
|
||||
}
|
||||
#ifdef USE_WIFI
|
||||
else {
|
||||
this->wifi_channel_ = wifi::global_wifi_component->get_wifi_channel();
|
||||
}
|
||||
#endif
|
||||
|
||||
esp_err_t err = esp_now_init();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_now_init failed: %s", esp_err_to_name(err));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_now_register_recv_cb(on_data_received);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_now_register_recv_cb failed: %s", esp_err_to_name(err));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_now_register_send_cb(on_send_report);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_now_register_recv_cb failed: %s", esp_err_to_name(err));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
esp_wifi_get_mac(WIFI_IF_STA, this->own_address_);
|
||||
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
esp_now_set_wake_window(CONFIG_ESPNOW_WAKE_WINDOW);
|
||||
esp_wifi_connectionless_module_set_wake_interval(CONFIG_ESPNOW_WAKE_INTERVAL);
|
||||
#endif
|
||||
|
||||
for (auto peer : this->peers_) {
|
||||
this->add_peer(peer.address);
|
||||
}
|
||||
this->state_ = ESPNOW_STATE_ENABLED;
|
||||
}
|
||||
|
||||
void ESPNowComponent::disable() {
|
||||
if (this->state_ == ESPNOW_STATE_DISABLED)
|
||||
return;
|
||||
|
||||
ESP_LOGD(TAG, "Disabling");
|
||||
this->state_ = ESPNOW_STATE_DISABLED;
|
||||
|
||||
esp_now_unregister_recv_cb();
|
||||
esp_now_unregister_send_cb();
|
||||
|
||||
for (auto peer : this->peers_) {
|
||||
this->del_peer(peer.address);
|
||||
}
|
||||
|
||||
esp_err_t err = esp_now_deinit();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_now_deinit failed! 0x%x", err);
|
||||
}
|
||||
}
|
||||
|
||||
void ESPNowComponent::apply_wifi_channel() {
|
||||
if (this->state_ == ESPNOW_STATE_DISABLED) {
|
||||
ESP_LOGE(TAG, "Cannot set channel when ESPNOW disabled");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->is_wifi_enabled()) {
|
||||
ESP_LOGE(TAG, "Cannot set channel when Wi-Fi enabled");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Channel set to %d.", this->wifi_channel_);
|
||||
esp_wifi_set_promiscuous(true);
|
||||
esp_wifi_set_channel(this->wifi_channel_, WIFI_SECOND_CHAN_NONE);
|
||||
esp_wifi_set_promiscuous(false);
|
||||
}
|
||||
|
||||
void ESPNowComponent::loop() {
|
||||
#ifdef USE_WIFI
|
||||
if (wifi::global_wifi_component != nullptr && wifi::global_wifi_component->is_connected()) {
|
||||
int32_t new_channel = wifi::global_wifi_component->get_wifi_channel();
|
||||
if (new_channel != this->wifi_channel_) {
|
||||
ESP_LOGI(TAG, "Wifi Channel is changed from %d to %d.", this->wifi_channel_, new_channel);
|
||||
this->wifi_channel_ = new_channel;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Process received packets
|
||||
ESPNowPacket *packet = this->receive_packet_queue_.pop();
|
||||
while (packet != nullptr) {
|
||||
switch (packet->type_) {
|
||||
case ESPNowPacket::RECEIVED: {
|
||||
const ESPNowRecvInfo info = packet->get_receive_info();
|
||||
if (!esp_now_is_peer_exist(info.src_addr)) {
|
||||
if (this->auto_add_peer_) {
|
||||
this->add_peer(info.src_addr);
|
||||
} else {
|
||||
for (auto *handler : this->unknown_peer_handlers_) {
|
||||
if (handler->on_unknown_peer(info, packet->packet_.receive.data, packet->packet_.receive.size))
|
||||
break; // If a handler returns true, stop processing further handlers
|
||||
}
|
||||
}
|
||||
}
|
||||
// Intentionally left as if instead of else in case the peer is added above
|
||||
if (esp_now_is_peer_exist(info.src_addr)) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
ESP_LOGV(TAG, "<<< [%s -> %s] %s", format_mac_address_pretty(info.src_addr).c_str(),
|
||||
format_mac_address_pretty(info.des_addr).c_str(),
|
||||
format_hex_pretty(packet->packet_.receive.data, packet->packet_.receive.size).c_str());
|
||||
#endif
|
||||
if (memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
|
||||
for (auto *handler : this->broadcasted_handlers_) {
|
||||
if (handler->on_broadcasted(info, packet->packet_.receive.data, packet->packet_.receive.size))
|
||||
break; // If a handler returns true, stop processing further handlers
|
||||
}
|
||||
} else {
|
||||
for (auto *handler : this->received_handlers_) {
|
||||
if (handler->on_received(info, packet->packet_.receive.data, packet->packet_.receive.size))
|
||||
break; // If a handler returns true, stop processing further handlers
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESPNowPacket::SENT: {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
ESP_LOGV(TAG, ">>> [%s] %s", format_mac_address_pretty(packet->packet_.sent.address).c_str(),
|
||||
LOG_STR_ARG(espnow_error_to_str(packet->packet_.sent.status)));
|
||||
#endif
|
||||
if (this->current_send_packet_ != nullptr) {
|
||||
this->current_send_packet_->callback_(packet->packet_.sent.status);
|
||||
this->send_packet_pool_.release(this->current_send_packet_);
|
||||
this->current_send_packet_ = nullptr; // Reset current packet after sending
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Return the packet to the pool
|
||||
this->receive_packet_pool_.release(packet);
|
||||
packet = this->receive_packet_queue_.pop();
|
||||
}
|
||||
|
||||
// Process sending packet queue
|
||||
if (this->current_send_packet_ == nullptr) {
|
||||
this->send_();
|
||||
}
|
||||
|
||||
// Log dropped received packets periodically
|
||||
uint16_t received_dropped = this->receive_packet_queue_.get_and_reset_dropped_count();
|
||||
if (received_dropped > 0) {
|
||||
ESP_LOGW(TAG, "Dropped %u received packets due to buffer overflow", received_dropped);
|
||||
}
|
||||
|
||||
// Log dropped send packets periodically
|
||||
uint16_t send_dropped = this->send_packet_queue_.get_and_reset_dropped_count();
|
||||
if (send_dropped > 0) {
|
||||
ESP_LOGW(TAG, "Dropped %u send packets due to buffer overflow", send_dropped);
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payload, size_t size,
|
||||
const send_callback_t &callback) {
|
||||
if (this->state_ != ESPNOW_STATE_ENABLED) {
|
||||
return ESP_ERR_ESPNOW_NOT_INIT;
|
||||
} else if (this->is_failed()) {
|
||||
return ESP_ERR_ESPNOW_FAILED;
|
||||
} else if (peer_address == 0ULL) {
|
||||
return ESP_ERR_ESPNOW_PEER_NOT_SET;
|
||||
} else if (memcmp(peer_address, this->own_address_, ESP_NOW_ETH_ALEN) == 0) {
|
||||
return ESP_ERR_ESPNOW_OWN_ADDRESS;
|
||||
} else if (size > ESP_NOW_MAX_DATA_LEN) {
|
||||
return ESP_ERR_ESPNOW_DATA_SIZE;
|
||||
} else if (!esp_now_is_peer_exist(peer_address)) {
|
||||
if (memcmp(peer_address, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0 || this->auto_add_peer_) {
|
||||
esp_err_t err = this->add_peer(peer_address);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
} else {
|
||||
return ESP_ERR_ESPNOW_PEER_NOT_PAIRED;
|
||||
}
|
||||
}
|
||||
// Allocate a packet from the pool
|
||||
ESPNowSendPacket *packet = this->send_packet_pool_.allocate();
|
||||
if (packet == nullptr) {
|
||||
this->send_packet_queue_.increment_dropped_count();
|
||||
ESP_LOGE(TAG, "Failed to allocate send packet from pool");
|
||||
this->status_momentary_warning("send-packet-pool-full");
|
||||
return ESP_ERR_ESPNOW_NO_MEM;
|
||||
}
|
||||
// Load the packet data
|
||||
packet->load_data(peer_address, payload, size, callback);
|
||||
// Push the packet to the send queue
|
||||
this->send_packet_queue_.push(packet);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void ESPNowComponent::send_() {
|
||||
ESPNowSendPacket *packet = this->send_packet_queue_.pop();
|
||||
if (packet == nullptr) {
|
||||
return; // No packets to send
|
||||
}
|
||||
|
||||
this->current_send_packet_ = packet;
|
||||
esp_err_t err = esp_now_send(packet->address_, packet->data_, packet->size_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to send packet to %s - %s", format_mac_address_pretty(packet->address_).c_str(),
|
||||
LOG_STR_ARG(espnow_error_to_str(err)));
|
||||
if (packet->callback_ != nullptr) {
|
||||
packet->callback_(err);
|
||||
}
|
||||
this->status_momentary_warning("send-failed");
|
||||
this->send_packet_pool_.release(packet);
|
||||
this->current_send_packet_ = nullptr; // Reset current packet
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t ESPNowComponent::add_peer(const uint8_t *peer) {
|
||||
if (this->state_ != ESPNOW_STATE_ENABLED || this->is_failed()) {
|
||||
return ESP_ERR_ESPNOW_NOT_INIT;
|
||||
}
|
||||
|
||||
if (memcmp(peer, this->own_address_, ESP_NOW_ETH_ALEN) == 0) {
|
||||
this->mark_failed();
|
||||
return ESP_ERR_INVALID_MAC;
|
||||
}
|
||||
|
||||
if (!esp_now_is_peer_exist(peer)) {
|
||||
esp_now_peer_info_t peer_info = {};
|
||||
memset(&peer_info, 0, sizeof(esp_now_peer_info_t));
|
||||
peer_info.ifidx = WIFI_IF_STA;
|
||||
memcpy(peer_info.peer_addr, peer, ESP_NOW_ETH_ALEN);
|
||||
esp_err_t err = esp_now_add_peer(&peer_info);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to add peer %s - %s", format_mac_address_pretty(peer).c_str(),
|
||||
LOG_STR_ARG(espnow_error_to_str(err)));
|
||||
this->status_momentary_warning("peer-add-failed");
|
||||
return err;
|
||||
}
|
||||
}
|
||||
bool found = false;
|
||||
for (auto &it : this->peers_) {
|
||||
if (it == peer) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
ESPNowPeer new_peer;
|
||||
memcpy(new_peer.address, peer, ESP_NOW_ETH_ALEN);
|
||||
this->peers_.push_back(new_peer);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ESPNowComponent::del_peer(const uint8_t *peer) {
|
||||
if (this->state_ != ESPNOW_STATE_ENABLED || this->is_failed()) {
|
||||
return ESP_ERR_ESPNOW_NOT_INIT;
|
||||
}
|
||||
if (esp_now_is_peer_exist(peer)) {
|
||||
esp_err_t err = esp_now_del_peer(peer);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to delete peer %s - %s", format_mac_address_pretty(peer).c_str(),
|
||||
LOG_STR_ARG(espnow_error_to_str(err)));
|
||||
this->status_momentary_warning("peer-del-failed");
|
||||
return err;
|
||||
}
|
||||
}
|
||||
for (auto it = this->peers_.begin(); it != this->peers_.end(); ++it) {
|
||||
if (*it == peer) {
|
||||
this->peers_.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
} // namespace esphome::espnow
|
||||
|
||||
#endif // USE_ESP32
|
182
esphome/components/espnow/espnow_component.h
Normal file
182
esphome/components/espnow/espnow_component.h
Normal file
@@ -0,0 +1,182 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/event_pool.h"
|
||||
#include "esphome/core/lock_free_queue.h"
|
||||
#include "espnow_packet.h"
|
||||
|
||||
#include <esp_idf_version.h>
|
||||
|
||||
#include <esp_mac.h>
|
||||
#include <esp_now.h>
|
||||
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::espnow {
|
||||
|
||||
// Maximum size of the ESPNow event queue - must be power of 2 for lock-free queue
|
||||
static constexpr size_t MAX_ESP_NOW_SEND_QUEUE_SIZE = 16;
|
||||
static constexpr size_t MAX_ESP_NOW_RECEIVE_QUEUE_SIZE = 16;
|
||||
|
||||
using peer_address_t = std::array<uint8_t, ESP_NOW_ETH_ALEN>;
|
||||
|
||||
enum class ESPNowTriggers : uint8_t {
|
||||
TRIGGER_NONE = 0,
|
||||
ON_NEW_PEER = 1,
|
||||
ON_RECEIVED = 2,
|
||||
ON_BROADCASTED = 3,
|
||||
ON_SUCCEED = 10,
|
||||
ON_FAILED = 11,
|
||||
};
|
||||
|
||||
enum ESPNowState : uint8_t {
|
||||
/** Nothing has been initialized yet. */
|
||||
ESPNOW_STATE_OFF = 0,
|
||||
/** ESPNOW is disabled. */
|
||||
ESPNOW_STATE_DISABLED,
|
||||
/** ESPNOW is enabled. */
|
||||
ESPNOW_STATE_ENABLED,
|
||||
};
|
||||
|
||||
struct ESPNowPeer {
|
||||
uint8_t address[ESP_NOW_ETH_ALEN]; // MAC address of the peer
|
||||
|
||||
bool operator==(const ESPNowPeer &other) const { return memcmp(this->address, other.address, ESP_NOW_ETH_ALEN) == 0; }
|
||||
bool operator==(const uint8_t *other) const { return memcmp(this->address, other, ESP_NOW_ETH_ALEN) == 0; }
|
||||
};
|
||||
|
||||
/// Handler interface for receiving ESPNow packets from unknown peers
|
||||
/// Components should inherit from this class to handle incoming ESPNow data
|
||||
class ESPNowUnknownPeerHandler {
|
||||
public:
|
||||
/// Called when an ESPNow packet is received from an unknown peer
|
||||
/// @param info Information about the received packet (sender MAC, etc.)
|
||||
/// @param data Pointer to the received data payload
|
||||
/// @param size Size of the received data in bytes
|
||||
/// @return true if the packet was handled, false otherwise
|
||||
virtual bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
|
||||
};
|
||||
|
||||
/// Handler interface for receiving ESPNow packets
|
||||
/// Components should inherit from this class to handle incoming ESPNow data
|
||||
class ESPNowReceivedPacketHandler {
|
||||
public:
|
||||
/// Called when an ESPNow packet is received
|
||||
/// @param info Information about the received packet (sender MAC, etc.)
|
||||
/// @param data Pointer to the received data payload
|
||||
/// @param size Size of the received data in bytes
|
||||
/// @return true if the packet was handled, false otherwise
|
||||
virtual bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
|
||||
};
|
||||
/// Handler interface for receiving broadcasted ESPNow packets
|
||||
/// Components should inherit from this class to handle incoming ESPNow data
|
||||
class ESPNowBroadcastedHandler {
|
||||
public:
|
||||
/// Called when a broadcasted ESPNow packet is received
|
||||
/// @param info Information about the received packet (sender MAC, etc.)
|
||||
/// @param data Pointer to the received data payload
|
||||
/// @param size Size of the received data in bytes
|
||||
/// @return true if the packet was handled, false otherwise
|
||||
virtual bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
|
||||
};
|
||||
|
||||
class ESPNowComponent : public Component {
|
||||
public:
|
||||
ESPNowComponent();
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::LATE; }
|
||||
|
||||
// Add a peer to the internal list of peers
|
||||
void add_peer(peer_address_t address) {
|
||||
ESPNowPeer peer;
|
||||
memcpy(peer.address, address.data(), ESP_NOW_ETH_ALEN);
|
||||
this->peers_.push_back(peer);
|
||||
}
|
||||
// Add a peer with the esp_now api and add to the internal list if doesnt exist already
|
||||
esp_err_t add_peer(const uint8_t *peer);
|
||||
// Remove a peer with the esp_now api and remove from the internal list if exists
|
||||
esp_err_t del_peer(const uint8_t *peer);
|
||||
|
||||
void set_wifi_channel(uint8_t channel) { this->wifi_channel_ = channel; }
|
||||
void apply_wifi_channel();
|
||||
|
||||
void set_auto_add_peer(bool value) { this->auto_add_peer_ = value; }
|
||||
|
||||
void enable();
|
||||
void disable();
|
||||
bool is_disabled() const { return this->state_ == ESPNOW_STATE_DISABLED; };
|
||||
void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; }
|
||||
bool is_wifi_enabled();
|
||||
|
||||
/// @brief Queue a packet to be sent to a specific peer address.
|
||||
/// This method will add the packet to the internal queue and
|
||||
/// call the callback when the packet is sent.
|
||||
/// Only one packet will be sent at any given time and the next one will not be sent until
|
||||
/// the previous one has been acknowledged or failed.
|
||||
/// @param peer_address MAC address of the peer to send the packet to
|
||||
/// @param payload Data payload to send
|
||||
/// @param callback Callback to call when the send operation is complete
|
||||
/// @return ESP_OK on success, or an error code on failure
|
||||
esp_err_t send(const uint8_t *peer_address, const std::vector<uint8_t> &payload,
|
||||
const send_callback_t &callback = nullptr) {
|
||||
return this->send(peer_address, payload.data(), payload.size(), callback);
|
||||
}
|
||||
esp_err_t send(const uint8_t *peer_address, const uint8_t *payload, size_t size,
|
||||
const send_callback_t &callback = nullptr);
|
||||
|
||||
void register_received_handler(ESPNowReceivedPacketHandler *handler) { this->received_handlers_.push_back(handler); }
|
||||
void register_unknown_peer_handler(ESPNowUnknownPeerHandler *handler) {
|
||||
this->unknown_peer_handlers_.push_back(handler);
|
||||
}
|
||||
void register_broadcasted_handler(ESPNowBroadcastedHandler *handler) {
|
||||
this->broadcasted_handlers_.push_back(handler);
|
||||
}
|
||||
|
||||
protected:
|
||||
friend void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size);
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
|
||||
friend void on_send_report(const esp_now_send_info_t *info, esp_now_send_status_t status);
|
||||
#else
|
||||
friend void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status);
|
||||
#endif
|
||||
|
||||
void enable_();
|
||||
void send_();
|
||||
|
||||
std::vector<ESPNowUnknownPeerHandler *> unknown_peer_handlers_;
|
||||
std::vector<ESPNowReceivedPacketHandler *> received_handlers_;
|
||||
std::vector<ESPNowBroadcastedHandler *> broadcasted_handlers_;
|
||||
|
||||
std::vector<ESPNowPeer> peers_{};
|
||||
|
||||
uint8_t own_address_[ESP_NOW_ETH_ALEN]{0};
|
||||
LockFreeQueue<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE> receive_packet_queue_{};
|
||||
EventPool<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE> receive_packet_pool_{};
|
||||
|
||||
LockFreeQueue<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE> send_packet_queue_{};
|
||||
EventPool<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE> send_packet_pool_{};
|
||||
ESPNowSendPacket *current_send_packet_{nullptr}; // Currently sending packet, nullptr if none
|
||||
|
||||
uint8_t wifi_channel_{0};
|
||||
ESPNowState state_{ESPNOW_STATE_OFF};
|
||||
|
||||
bool auto_add_peer_{false};
|
||||
bool enable_on_boot_{true};
|
||||
};
|
||||
|
||||
extern ESPNowComponent *global_esp_now; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
} // namespace esphome::espnow
|
||||
|
||||
#endif // USE_ESP32
|
19
esphome/components/espnow/espnow_err.h
Normal file
19
esphome/components/espnow/espnow_err.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <esp_err.h>
|
||||
#include <esp_now.h>
|
||||
|
||||
namespace esphome::espnow {
|
||||
|
||||
static const esp_err_t ESP_ERR_ESPNOW_CMP_BASE = (ESP_ERR_ESPNOW_BASE + 20);
|
||||
static const esp_err_t ESP_ERR_ESPNOW_FAILED = (ESP_ERR_ESPNOW_CMP_BASE + 1);
|
||||
static const esp_err_t ESP_ERR_ESPNOW_OWN_ADDRESS = (ESP_ERR_ESPNOW_CMP_BASE + 2);
|
||||
static const esp_err_t ESP_ERR_ESPNOW_DATA_SIZE = (ESP_ERR_ESPNOW_CMP_BASE + 3);
|
||||
static const esp_err_t ESP_ERR_ESPNOW_PEER_NOT_SET = (ESP_ERR_ESPNOW_CMP_BASE + 4);
|
||||
static const esp_err_t ESP_ERR_ESPNOW_PEER_NOT_PAIRED = (ESP_ERR_ESPNOW_CMP_BASE + 5);
|
||||
|
||||
} // namespace esphome::espnow
|
||||
|
||||
#endif // USE_ESP32
|
166
esphome/components/espnow/espnow_packet.h
Normal file
166
esphome/components/espnow/espnow_packet.h
Normal file
@@ -0,0 +1,166 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "espnow_err.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <esp_err.h>
|
||||
#include <esp_idf_version.h>
|
||||
#include <esp_now.h>
|
||||
|
||||
namespace esphome::espnow {
|
||||
|
||||
static const uint8_t ESPNOW_BROADCAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
static const uint8_t ESPNOW_MULTICAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE};
|
||||
|
||||
struct WifiPacketRxControl {
|
||||
int8_t rssi; // Received Signal Strength Indicator (RSSI) of packet, unit: dBm
|
||||
uint32_t timestamp; // Timestamp in microseconds when the packet was received, precise only if modem sleep or
|
||||
// light sleep is not enabled
|
||||
};
|
||||
|
||||
struct ESPNowRecvInfo {
|
||||
uint8_t src_addr[ESP_NOW_ETH_ALEN]; /**< Source address of ESPNOW packet */
|
||||
uint8_t des_addr[ESP_NOW_ETH_ALEN]; /**< Destination address of ESPNOW packet */
|
||||
wifi_pkt_rx_ctrl_t *rx_ctrl; /**< Rx control info of ESPNOW packet */
|
||||
};
|
||||
|
||||
using send_callback_t = std::function<void(esp_err_t)>;
|
||||
|
||||
class ESPNowPacket {
|
||||
public:
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
enum esp_now_packet_type_t : uint8_t {
|
||||
RECEIVED,
|
||||
SENT,
|
||||
};
|
||||
|
||||
// Constructor for received data
|
||||
ESPNowPacket(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
|
||||
this->init_received_data_(info, data, size);
|
||||
};
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
|
||||
// Constructor for sent data
|
||||
ESPNowPacket(const esp_now_send_info_t *info, esp_now_send_status_t status) {
|
||||
this->init_sent_data(info->src_addr, status);
|
||||
}
|
||||
#else
|
||||
// Constructor for sent data
|
||||
ESPNowPacket(const uint8_t *mac_addr, esp_now_send_status_t status) { this->init_sent_data_(mac_addr, status); }
|
||||
#endif
|
||||
|
||||
// Default constructor for pre-allocation in pool
|
||||
ESPNowPacket() {}
|
||||
|
||||
void release() {}
|
||||
|
||||
void load_received_data(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
|
||||
this->type_ = RECEIVED;
|
||||
this->init_received_data_(info, data, size);
|
||||
}
|
||||
|
||||
void load_sent_data(const uint8_t *mac_addr, esp_now_send_status_t status) {
|
||||
this->type_ = SENT;
|
||||
this->init_sent_data_(mac_addr, status);
|
||||
}
|
||||
|
||||
// Disable copy to prevent double-delete
|
||||
ESPNowPacket(const ESPNowPacket &) = delete;
|
||||
ESPNowPacket &operator=(const ESPNowPacket &) = delete;
|
||||
|
||||
union {
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
struct received_data {
|
||||
ESPNowRecvInfo info; // Information about the received packet
|
||||
uint8_t data[ESP_NOW_MAX_DATA_LEN]; // Data received in the packet
|
||||
uint8_t size; // Size of the received data
|
||||
WifiPacketRxControl rx_ctrl; // Status of the received packet
|
||||
} receive;
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
struct sent_data {
|
||||
uint8_t address[ESP_NOW_ETH_ALEN];
|
||||
esp_now_send_status_t status;
|
||||
} sent;
|
||||
} packet_;
|
||||
|
||||
esp_now_packet_type_t type_;
|
||||
|
||||
esp_now_packet_type_t type() const { return this->type_; }
|
||||
const ESPNowRecvInfo &get_receive_info() const { return this->packet_.receive.info; }
|
||||
|
||||
private:
|
||||
void init_received_data_(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
|
||||
memcpy(this->packet_.receive.info.src_addr, info->src_addr, ESP_NOW_ETH_ALEN);
|
||||
memcpy(this->packet_.receive.info.des_addr, info->des_addr, ESP_NOW_ETH_ALEN);
|
||||
memcpy(this->packet_.receive.data, data, size);
|
||||
this->packet_.receive.size = size;
|
||||
|
||||
this->packet_.receive.rx_ctrl.rssi = info->rx_ctrl->rssi;
|
||||
this->packet_.receive.rx_ctrl.timestamp = info->rx_ctrl->timestamp;
|
||||
|
||||
this->packet_.receive.info.rx_ctrl = reinterpret_cast<wifi_pkt_rx_ctrl_t *>(&this->packet_.receive.rx_ctrl);
|
||||
}
|
||||
|
||||
void init_sent_data_(const uint8_t *mac_addr, esp_now_send_status_t status) {
|
||||
memcpy(this->packet_.sent.address, mac_addr, ESP_NOW_ETH_ALEN);
|
||||
this->packet_.sent.status = status;
|
||||
}
|
||||
};
|
||||
|
||||
class ESPNowSendPacket {
|
||||
public:
|
||||
ESPNowSendPacket(const uint8_t *peer_address, const uint8_t *payload, size_t size, const send_callback_t &&callback)
|
||||
: callback_(callback) {
|
||||
this->init_data_(peer_address, payload, size);
|
||||
}
|
||||
ESPNowSendPacket(const uint8_t *peer_address, const uint8_t *payload, size_t size) {
|
||||
this->init_data_(peer_address, payload, size);
|
||||
}
|
||||
|
||||
// Default constructor for pre-allocation in pool
|
||||
ESPNowSendPacket() {}
|
||||
|
||||
void release() {}
|
||||
|
||||
// Disable copy to prevent double-delete
|
||||
ESPNowSendPacket(const ESPNowSendPacket &) = delete;
|
||||
ESPNowSendPacket &operator=(const ESPNowSendPacket &) = delete;
|
||||
|
||||
void load_data(const uint8_t *peer_address, const uint8_t *payload, size_t size, const send_callback_t &callback) {
|
||||
this->init_data_(peer_address, payload, size);
|
||||
this->callback_ = callback;
|
||||
}
|
||||
|
||||
void load_data(const uint8_t *peer_address, const uint8_t *payload, size_t size) {
|
||||
this->init_data_(peer_address, payload, size);
|
||||
this->callback_ = nullptr; // Reset callback
|
||||
}
|
||||
|
||||
uint8_t address_[ESP_NOW_ETH_ALEN]{0}; // MAC address of the peer to send the packet to
|
||||
uint8_t data_[ESP_NOW_MAX_DATA_LEN]{0}; // Data to send
|
||||
uint8_t size_{0}; // Size of the data to send, must be <= ESP_NOW_MAX_DATA_LEN
|
||||
send_callback_t callback_{nullptr}; // Callback to call when the send operation is complete
|
||||
|
||||
private:
|
||||
void init_data_(const uint8_t *peer_address, const uint8_t *payload, size_t size) {
|
||||
memcpy(this->address_, peer_address, ESP_NOW_ETH_ALEN);
|
||||
if (size > ESP_NOW_MAX_DATA_LEN) {
|
||||
this->size_ = 0;
|
||||
return;
|
||||
}
|
||||
this->size_ = size;
|
||||
memcpy(this->data_, payload, this->size_);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::espnow
|
||||
|
||||
#endif // USE_ESP32
|
@@ -108,6 +108,24 @@ class ImageEncoder:
|
||||
:return:
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def is_endian(cls) -> bool:
|
||||
"""
|
||||
Check if the image encoder supports endianness configuration
|
||||
"""
|
||||
return getattr(cls, "set_big_endian", None) is not None
|
||||
|
||||
@classmethod
|
||||
def get_options(cls) -> list[str]:
|
||||
"""
|
||||
Get the available options for this image encoder
|
||||
"""
|
||||
options = [*OPTIONS]
|
||||
if not cls.is_endian():
|
||||
options.remove(CONF_BYTE_ORDER)
|
||||
options.append(CONF_RAW_DATA_ID)
|
||||
return options
|
||||
|
||||
|
||||
def is_alpha_only(image: Image):
|
||||
"""
|
||||
@@ -446,13 +464,14 @@ def validate_type(image_types):
|
||||
return validate
|
||||
|
||||
|
||||
def validate_settings(value):
|
||||
def validate_settings(value, path=()):
|
||||
"""
|
||||
Validate the settings for a single image configuration.
|
||||
"""
|
||||
conf_type = value[CONF_TYPE]
|
||||
type_class = IMAGE_TYPE[conf_type]
|
||||
transparency = value[CONF_TRANSPARENCY].lower()
|
||||
|
||||
transparency = value.get(CONF_TRANSPARENCY, CONF_OPAQUE).lower()
|
||||
if transparency not in type_class.allow_config:
|
||||
raise cv.Invalid(
|
||||
f"Image format '{conf_type}' cannot have transparency: {transparency}"
|
||||
@@ -464,11 +483,10 @@ def validate_settings(value):
|
||||
and CONF_INVERT_ALPHA not in type_class.allow_config
|
||||
):
|
||||
raise cv.Invalid("No alpha channel to invert")
|
||||
if value.get(CONF_BYTE_ORDER) is not None and not callable(
|
||||
getattr(type_class, "set_big_endian", None)
|
||||
):
|
||||
if value.get(CONF_BYTE_ORDER) is not None and not type_class.is_endian():
|
||||
raise cv.Invalid(
|
||||
f"Image format '{conf_type}' does not support byte order configuration"
|
||||
f"Image format '{conf_type}' does not support byte order configuration",
|
||||
path=path,
|
||||
)
|
||||
if file := value.get(CONF_FILE):
|
||||
file = Path(file)
|
||||
@@ -479,7 +497,7 @@ def validate_settings(value):
|
||||
Image.open(file)
|
||||
except UnidentifiedImageError as exc:
|
||||
raise cv.Invalid(
|
||||
f"File can't be opened as image: {file.absolute()}"
|
||||
f"File can't be opened as image: {file.absolute()}", path=path
|
||||
) from exc
|
||||
return value
|
||||
|
||||
@@ -499,6 +517,10 @@ OPTIONS_SCHEMA = {
|
||||
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
|
||||
cv.Optional(CONF_BYTE_ORDER): cv.one_of("BIG_ENDIAN", "LITTLE_ENDIAN", upper=True),
|
||||
cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
|
||||
}
|
||||
|
||||
DEFAULTS_SCHEMA = {
|
||||
**OPTIONS_SCHEMA,
|
||||
cv.Optional(CONF_TYPE): validate_type(IMAGE_TYPE),
|
||||
}
|
||||
|
||||
@@ -510,47 +532,61 @@ IMAGE_SCHEMA_NO_DEFAULTS = {
|
||||
**{cv.Optional(key): OPTIONS_SCHEMA[key] for key in OPTIONS},
|
||||
}
|
||||
|
||||
BASE_SCHEMA = cv.Schema(
|
||||
IMAGE_SCHEMA = cv.Schema(
|
||||
{
|
||||
**IMAGE_ID_SCHEMA,
|
||||
**OPTIONS_SCHEMA,
|
||||
}
|
||||
).add_extra(validate_settings)
|
||||
|
||||
IMAGE_SCHEMA = BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def apply_defaults(image, defaults, path):
|
||||
"""
|
||||
Apply defaults to an image configuration
|
||||
"""
|
||||
type = image.get(CONF_TYPE, defaults.get(CONF_TYPE))
|
||||
if type is None:
|
||||
raise cv.Invalid(
|
||||
"Type is required either in the image config or in the defaults", path=path
|
||||
)
|
||||
type_class = IMAGE_TYPE[type]
|
||||
config = {
|
||||
**{key: image.get(key, defaults.get(key)) for key in type_class.get_options()},
|
||||
**{key.schema: image[key.schema] for key in IMAGE_ID_SCHEMA},
|
||||
CONF_TYPE: image.get(CONF_TYPE, defaults.get(CONF_TYPE)),
|
||||
}
|
||||
validate_settings(config, path)
|
||||
return config
|
||||
|
||||
|
||||
def validate_defaults(value):
|
||||
"""
|
||||
Validate the options for images with defaults
|
||||
Apply defaults to the images in the configuration and flatten to a single list.
|
||||
"""
|
||||
defaults = value[CONF_DEFAULTS]
|
||||
result = []
|
||||
for index, image in enumerate(value[CONF_IMAGES]):
|
||||
type = image.get(CONF_TYPE, defaults.get(CONF_TYPE))
|
||||
if type is None:
|
||||
raise cv.Invalid(
|
||||
"Type is required either in the image config or in the defaults",
|
||||
path=[CONF_IMAGES, index],
|
||||
)
|
||||
type_class = IMAGE_TYPE[type]
|
||||
# A default byte order should be simply ignored if the type does not support it
|
||||
available_options = [*OPTIONS]
|
||||
if (
|
||||
not callable(getattr(type_class, "set_big_endian", None))
|
||||
and CONF_BYTE_ORDER not in image
|
||||
):
|
||||
available_options.remove(CONF_BYTE_ORDER)
|
||||
config = {
|
||||
**{key: image.get(key, defaults.get(key)) for key in available_options},
|
||||
**{key.schema: image[key.schema] for key in IMAGE_ID_SCHEMA},
|
||||
}
|
||||
validate_settings(config)
|
||||
result.append(config)
|
||||
# Apply defaults to the images: list and add the list entries to the result
|
||||
for index, image in enumerate(value.get(CONF_IMAGES, [])):
|
||||
result.append(apply_defaults(image, defaults, [CONF_IMAGES, index]))
|
||||
|
||||
# Apply defaults to images under the type keys and add them to the result
|
||||
for image_type, type_config in value.items():
|
||||
type_upper = image_type.upper()
|
||||
if type_upper not in IMAGE_TYPE:
|
||||
continue
|
||||
type_class = IMAGE_TYPE[type_upper]
|
||||
if isinstance(type_config, list):
|
||||
# If the type is a list, apply defaults to each entry
|
||||
for index, image in enumerate(type_config):
|
||||
result.append(apply_defaults(image, defaults, [image_type, index]))
|
||||
else:
|
||||
# Handle transparency options for the type
|
||||
for trans_type in set(type_class.allow_config).intersection(type_config):
|
||||
for index, image in enumerate(type_config[trans_type]):
|
||||
result.append(
|
||||
apply_defaults(image, defaults, [image_type, trans_type, index])
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@@ -562,16 +598,20 @@ def typed_image_schema(image_type):
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(t.lower()): cv.ensure_list(
|
||||
BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_TRANSPARENCY, default=t
|
||||
): validate_transparency((t,)),
|
||||
cv.Optional(CONF_TYPE, default=image_type): validate_type(
|
||||
(image_type,)
|
||||
),
|
||||
}
|
||||
)
|
||||
{
|
||||
**IMAGE_ID_SCHEMA,
|
||||
**{
|
||||
cv.Optional(key): OPTIONS_SCHEMA[key]
|
||||
for key in OPTIONS
|
||||
if key != CONF_TRANSPARENCY
|
||||
},
|
||||
cv.Optional(
|
||||
CONF_TRANSPARENCY, default=t
|
||||
): validate_transparency((t,)),
|
||||
cv.Optional(CONF_TYPE, default=image_type): validate_type(
|
||||
(image_type,)
|
||||
),
|
||||
}
|
||||
)
|
||||
for t in IMAGE_TYPE[image_type].allow_config.intersection(
|
||||
TRANSPARENCY_TYPES
|
||||
@@ -580,46 +620,44 @@ def typed_image_schema(image_type):
|
||||
),
|
||||
# Allow a default configuration with no transparency preselected
|
||||
cv.ensure_list(
|
||||
BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(
|
||||
CONF_TRANSPARENCY, default=CONF_OPAQUE
|
||||
): validate_transparency(),
|
||||
cv.Optional(CONF_TYPE, default=image_type): validate_type(
|
||||
(image_type,)
|
||||
),
|
||||
}
|
||||
)
|
||||
{
|
||||
**IMAGE_SCHEMA_NO_DEFAULTS,
|
||||
cv.Optional(CONF_TYPE, default=image_type): validate_type(
|
||||
(image_type,)
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# The config schema can be a (possibly empty) single list of images,
|
||||
# or a dictionary of image types each with a list of images
|
||||
# or a dictionary with keys `defaults:` and `images:`
|
||||
# or a dictionary with optional keys `defaults:`, `images:` and the image types
|
||||
|
||||
|
||||
def _config_schema(config):
|
||||
if isinstance(config, list):
|
||||
return cv.Schema([IMAGE_SCHEMA])(config)
|
||||
if not isinstance(config, dict):
|
||||
def _config_schema(value):
|
||||
if isinstance(value, list) or (
|
||||
isinstance(value, dict) and (CONF_ID in value or CONF_FILE in value)
|
||||
):
|
||||
return cv.ensure_list(cv.All(IMAGE_SCHEMA, validate_settings))(value)
|
||||
if not isinstance(value, dict):
|
||||
raise cv.Invalid(
|
||||
"Badly formed image configuration, expected a list or a dictionary"
|
||||
"Badly formed image configuration, expected a list or a dictionary",
|
||||
)
|
||||
if CONF_DEFAULTS in config or CONF_IMAGES in config:
|
||||
return validate_defaults(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_DEFAULTS): OPTIONS_SCHEMA,
|
||||
cv.Required(CONF_IMAGES): cv.ensure_list(IMAGE_SCHEMA_NO_DEFAULTS),
|
||||
}
|
||||
)(config)
|
||||
)
|
||||
if CONF_ID in config or CONF_FILE in config:
|
||||
return cv.ensure_list(IMAGE_SCHEMA)([config])
|
||||
return cv.Schema(
|
||||
{cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}
|
||||
)(config)
|
||||
return cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_DEFAULTS, default={}): DEFAULTS_SCHEMA,
|
||||
cv.Optional(CONF_IMAGES, default=[]): cv.ensure_list(
|
||||
{
|
||||
**IMAGE_SCHEMA_NO_DEFAULTS,
|
||||
cv.Optional(CONF_TYPE): validate_type(IMAGE_TYPE),
|
||||
}
|
||||
),
|
||||
**{cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE},
|
||||
}
|
||||
),
|
||||
validate_defaults,
|
||||
)(value)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = _config_schema
|
||||
@@ -668,7 +706,7 @@ async def write_image(config, all_frames=False):
|
||||
else Image.Dither.FLOYDSTEINBERG
|
||||
)
|
||||
type = config[CONF_TYPE]
|
||||
transparency = config[CONF_TRANSPARENCY]
|
||||
transparency = config.get(CONF_TRANSPARENCY, CONF_OPAQUE)
|
||||
invert_alpha = config[CONF_INVERT_ALPHA]
|
||||
frame_count = 1
|
||||
if all_frames:
|
||||
@@ -699,14 +737,9 @@ async def write_image(config, all_frames=False):
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
if isinstance(config, list):
|
||||
for entry in config:
|
||||
await to_code(entry)
|
||||
elif CONF_ID not in config:
|
||||
for entry in config.values():
|
||||
await to_code(entry)
|
||||
else:
|
||||
prog_arr, width, height, image_type, trans_value, _ = await write_image(config)
|
||||
# By now the config should be a simple list.
|
||||
for entry in config:
|
||||
prog_arr, width, height, image_type, trans_value, _ = await write_image(entry)
|
||||
cg.new_Pvariable(
|
||||
config[CONF_ID], prog_arr, width, height, image_type, trans_value
|
||||
entry[CONF_ID], prog_arr, width, height, image_type, trans_value
|
||||
)
|
||||
|
@@ -4,6 +4,7 @@ from esphome.automation import build_automation, register_action, validate_autom
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING
|
||||
from esphome.components.display import Display
|
||||
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AUTO_CLEAR_ENABLED,
|
||||
@@ -219,7 +220,7 @@ def final_validation(configs):
|
||||
draw_rounding, config[CONF_DRAW_ROUNDING]
|
||||
)
|
||||
buffer_frac = config[CONF_BUFFER_SIZE]
|
||||
if CORE.is_esp32 and buffer_frac > 0.5 and "psram" not in global_config:
|
||||
if CORE.is_esp32 and buffer_frac > 0.5 and PSRAM_DOMAIN not in global_config:
|
||||
LOGGER.warning("buffer_size: may need to be reduced without PSRAM")
|
||||
for image_id in lv_images_used:
|
||||
path = global_config.get_path_for_id(image_id)[:-1]
|
||||
|
@@ -54,15 +54,15 @@ class IrFollowMeData : public IrData {
|
||||
void set_fahrenheit(bool val) { this->set_mask_(2, val, 32); }
|
||||
|
||||
protected:
|
||||
static const uint8_t MIN_TEMP_C = 0;
|
||||
static const uint8_t MAX_TEMP_C = 37;
|
||||
inline static constexpr uint8_t MIN_TEMP_C = 0;
|
||||
inline static constexpr uint8_t MAX_TEMP_C = 37;
|
||||
|
||||
// see
|
||||
// https://github.com/crankyoldgit/IRremoteESP8266/blob/9bdf8abcb465268c5409db99dc83a26df64c7445/src/ir_Midea.h#L116
|
||||
static const uint8_t MIN_TEMP_F = 32;
|
||||
inline static constexpr uint8_t MIN_TEMP_F = 32;
|
||||
// see
|
||||
// https://github.com/crankyoldgit/IRremoteESP8266/blob/9bdf8abcb465268c5409db99dc83a26df64c7445/src/ir_Midea.h#L117
|
||||
static const uint8_t MAX_TEMP_F = 99;
|
||||
inline static constexpr uint8_t MAX_TEMP_F = 99;
|
||||
};
|
||||
|
||||
class IrSpecialData : public IrData {
|
||||
|
@@ -25,6 +25,7 @@ from esphome.components.mipi import (
|
||||
power_of_two,
|
||||
requires_buffer,
|
||||
)
|
||||
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
||||
from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.config_validation import ALLOW_EXTRA
|
||||
@@ -292,7 +293,7 @@ def _final_validate(config):
|
||||
# If no drawing methods are configured, and LVGL is not enabled, show a test card
|
||||
config[CONF_SHOW_TEST_CARD] = True
|
||||
|
||||
if "psram" not in global_config and CONF_BUFFER_SIZE not in config:
|
||||
if PSRAM_DOMAIN not in global_config and CONF_BUFFER_SIZE not in config:
|
||||
if not requires_buffer(config):
|
||||
return config # No buffer needed, so no need to set a buffer size
|
||||
# If PSRAM is not enabled, choose a small buffer size by default
|
||||
|
@@ -124,7 +124,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
],
|
||||
)
|
||||
|
||||
if config[KEY_BOOTLOADER] == BOOTLOADER_ADAFRUIT:
|
||||
if config[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT:
|
||||
cg.add_define("USE_BOOTLOADER_MCUBOOT")
|
||||
else:
|
||||
# make sure that firmware.zip is created
|
||||
# for Adafruit_nRF52_Bootloader
|
||||
cg.add_platformio_option("board_upload.protocol", "nrfutil")
|
||||
|
@@ -2,3 +2,17 @@ BOOTLOADER_ADAFRUIT = "adafruit"
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD132 = "adafruit_nrf52_sd132"
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6 = "adafruit_nrf52_sd140_v6"
|
||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7 = "adafruit_nrf52_sd140_v7"
|
||||
EXTRA_ADC = [
|
||||
"VDD",
|
||||
"VDDHDIV5",
|
||||
]
|
||||
AIN_TO_GPIO = {
|
||||
"AIN0": 2,
|
||||
"AIN1": 3,
|
||||
"AIN2": 4,
|
||||
"AIN3": 5,
|
||||
"AIN4": 28,
|
||||
"AIN5": 29,
|
||||
"AIN6": 30,
|
||||
"AIN7": 31,
|
||||
}
|
||||
|
@@ -2,12 +2,23 @@ from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.zephyr.const import zephyr_ns
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_INVERTED, CONF_MODE, CONF_NUMBER, PLATFORM_NRF52
|
||||
from esphome.const import (
|
||||
CONF_ANALOG,
|
||||
CONF_ID,
|
||||
CONF_INVERTED,
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
PLATFORM_NRF52,
|
||||
)
|
||||
|
||||
from .const import AIN_TO_GPIO, EXTRA_ADC
|
||||
|
||||
ZephyrGPIOPin = zephyr_ns.class_("ZephyrGPIOPin", cg.InternalGPIOPin)
|
||||
|
||||
|
||||
def _translate_pin(value):
|
||||
if value in AIN_TO_GPIO:
|
||||
return AIN_TO_GPIO[value]
|
||||
if isinstance(value, dict) or value is None:
|
||||
raise cv.Invalid(
|
||||
"This variable only supports pin numbers, not full pin schemas "
|
||||
@@ -28,18 +39,33 @@ def _translate_pin(value):
|
||||
|
||||
|
||||
def validate_gpio_pin(value):
|
||||
if value in EXTRA_ADC:
|
||||
return value
|
||||
value = _translate_pin(value)
|
||||
if value < 0 or value > (32 + 16):
|
||||
raise cv.Invalid(f"NRF52: Invalid pin number: {value}")
|
||||
return value
|
||||
|
||||
|
||||
def validate_supports(value):
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_analog = mode[CONF_ANALOG]
|
||||
if is_analog:
|
||||
if num in EXTRA_ADC:
|
||||
return value
|
||||
if num not in AIN_TO_GPIO.values():
|
||||
raise cv.Invalid(f"Cannot use {num} as analog pin")
|
||||
return value
|
||||
|
||||
|
||||
NRF52_PIN_SCHEMA = cv.All(
|
||||
pins.gpio_base_schema(
|
||||
ZephyrGPIOPin,
|
||||
validate_gpio_pin,
|
||||
modes=pins.GPIO_STANDARD_MODES,
|
||||
modes=pins.GPIO_STANDARD_MODES + (CONF_ANALOG,),
|
||||
),
|
||||
validate_supports,
|
||||
)
|
||||
|
||||
|
||||
|
@@ -28,12 +28,13 @@ from esphome.core import CORE
|
||||
import esphome.final_validate as fv
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DOMAIN = "psram"
|
||||
|
||||
DEPENDENCIES = [PLATFORM_ESP32]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
psram_ns = cg.esphome_ns.namespace("psram")
|
||||
psram_ns = cg.esphome_ns.namespace(DOMAIN)
|
||||
PsramComponent = psram_ns.class_("PsramComponent", cg.Component)
|
||||
|
||||
TYPE_QUAD = "quad"
|
||||
|
@@ -599,7 +599,9 @@ async def throttle_filter_to_code(config, filter_id):
|
||||
TIMEOUT_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value(
|
||||
{
|
||||
cv.Required(CONF_TIMEOUT): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_VALUE, default="nan"): cv.ensure_list(cv.float_),
|
||||
cv.Optional(CONF_VALUE, default="nan"): cv.Any(
|
||||
cv.templatable(cv.float_), [cv.templatable(cv.float_)]
|
||||
),
|
||||
},
|
||||
key=CONF_TIMEOUT,
|
||||
)
|
||||
@@ -611,6 +613,8 @@ TIMEOUT_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value(
|
||||
TIMEOUT_WITH_PRIORITY_SCHEMA,
|
||||
)
|
||||
async def throttle_with_priority_filter_to_code(config, filter_id):
|
||||
if not isinstance(config[CONF_VALUE], list):
|
||||
config[CONF_VALUE] = [config[CONF_VALUE]]
|
||||
template_ = [await cg.templatable(x, [], float) for x in config[CONF_VALUE]]
|
||||
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_)
|
||||
|
||||
|
@@ -225,7 +225,7 @@ optional<float> SlidingWindowMovingAverageFilter::new_value(float value) {
|
||||
|
||||
// ExponentialMovingAverageFilter
|
||||
ExponentialMovingAverageFilter::ExponentialMovingAverageFilter(float alpha, size_t send_every, size_t send_first_at)
|
||||
: send_every_(send_every), send_at_(send_every - send_first_at), alpha_(alpha) {}
|
||||
: alpha_(alpha), send_every_(send_every), send_at_(send_every - send_first_at) {}
|
||||
optional<float> ExponentialMovingAverageFilter::new_value(float value) {
|
||||
if (!std::isnan(value)) {
|
||||
if (this->first_value_) {
|
||||
@@ -325,7 +325,7 @@ optional<float> FilterOutValueFilter::new_value(float value) {
|
||||
// ThrottleFilter
|
||||
ThrottleFilter::ThrottleFilter(uint32_t min_time_between_inputs) : min_time_between_inputs_(min_time_between_inputs) {}
|
||||
optional<float> ThrottleFilter::new_value(float value) {
|
||||
const uint32_t now = millis();
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_) {
|
||||
this->last_input_ = now;
|
||||
return value;
|
||||
@@ -369,19 +369,17 @@ optional<float> ThrottleWithPriorityFilter::new_value(float value) {
|
||||
|
||||
// DeltaFilter
|
||||
DeltaFilter::DeltaFilter(float delta, bool percentage_mode)
|
||||
: delta_(delta), current_delta_(delta), percentage_mode_(percentage_mode), last_value_(NAN) {}
|
||||
: delta_(delta), current_delta_(delta), last_value_(NAN), percentage_mode_(percentage_mode) {}
|
||||
optional<float> DeltaFilter::new_value(float value) {
|
||||
if (std::isnan(value)) {
|
||||
if (std::isnan(this->last_value_)) {
|
||||
return {};
|
||||
} else {
|
||||
if (this->percentage_mode_) {
|
||||
this->current_delta_ = fabsf(value * this->delta_);
|
||||
}
|
||||
return this->last_value_ = value;
|
||||
}
|
||||
}
|
||||
if (std::isnan(this->last_value_) || fabsf(value - this->last_value_) >= this->current_delta_) {
|
||||
float diff = fabsf(value - this->last_value_);
|
||||
if (std::isnan(this->last_value_) || (diff > 0.0f && diff >= this->current_delta_)) {
|
||||
if (this->percentage_mode_) {
|
||||
this->current_delta_ = fabsf(value * this->delta_);
|
||||
}
|
||||
|
@@ -221,11 +221,11 @@ class ExponentialMovingAverageFilter : public Filter {
|
||||
void set_alpha(float alpha);
|
||||
|
||||
protected:
|
||||
bool first_value_{true};
|
||||
float accumulator_{NAN};
|
||||
float alpha_;
|
||||
size_t send_every_;
|
||||
size_t send_at_;
|
||||
float alpha_;
|
||||
bool first_value_{true};
|
||||
};
|
||||
|
||||
/** Simple throttle average filter.
|
||||
@@ -243,9 +243,9 @@ class ThrottleAverageFilter : public Filter, public Component {
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
uint32_t time_period_;
|
||||
float sum_{0.0f};
|
||||
unsigned int n_{0};
|
||||
uint32_t time_period_;
|
||||
bool have_nan_{false};
|
||||
};
|
||||
|
||||
@@ -378,8 +378,8 @@ class DeltaFilter : public Filter {
|
||||
protected:
|
||||
float delta_;
|
||||
float current_delta_;
|
||||
bool percentage_mode_;
|
||||
float last_value_{NAN};
|
||||
bool percentage_mode_;
|
||||
};
|
||||
|
||||
class OrFilter : public Filter {
|
||||
@@ -401,8 +401,8 @@ class OrFilter : public Filter {
|
||||
};
|
||||
|
||||
std::vector<Filter *> filters_;
|
||||
bool has_value_{false};
|
||||
PhiNode phi_;
|
||||
bool has_value_{false};
|
||||
};
|
||||
|
||||
class CalibrateLinearFilter : public Filter {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import os
|
||||
from typing import Final, TypedDict
|
||||
from typing import TypedDict
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.const import CONF_BOARD
|
||||
@@ -8,18 +8,19 @@ from esphome.helpers import copy_file_if_changed, write_file_if_changed
|
||||
|
||||
from .const import (
|
||||
BOOTLOADER_MCUBOOT,
|
||||
KEY_BOARD,
|
||||
KEY_BOOTLOADER,
|
||||
KEY_EXTRA_BUILD_FILES,
|
||||
KEY_OVERLAY,
|
||||
KEY_PM_STATIC,
|
||||
KEY_PRJ_CONF,
|
||||
KEY_USER,
|
||||
KEY_ZEPHYR,
|
||||
zephyr_ns,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@tomaszduda23"]
|
||||
AUTO_LOAD = ["preferences"]
|
||||
KEY_BOARD: Final = "board"
|
||||
|
||||
PrjConfValueType = bool | str | int
|
||||
|
||||
@@ -49,6 +50,7 @@ class ZephyrData(TypedDict):
|
||||
overlay: str
|
||||
extra_build_files: dict[str, str]
|
||||
pm_static: list[Section]
|
||||
user: dict[str, list[str]]
|
||||
|
||||
|
||||
def zephyr_set_core_data(config):
|
||||
@@ -59,6 +61,7 @@ def zephyr_set_core_data(config):
|
||||
overlay="",
|
||||
extra_build_files={},
|
||||
pm_static=[],
|
||||
user={},
|
||||
)
|
||||
return config
|
||||
|
||||
@@ -178,7 +181,25 @@ def zephyr_add_pm_static(section: Section):
|
||||
CORE.data[KEY_ZEPHYR][KEY_PM_STATIC].extend(section)
|
||||
|
||||
|
||||
def zephyr_add_user(key, value):
|
||||
user = zephyr_data()[KEY_USER]
|
||||
if key not in user:
|
||||
user[key] = []
|
||||
user[key] += [value]
|
||||
|
||||
|
||||
def copy_files():
|
||||
user = zephyr_data()[KEY_USER]
|
||||
if user:
|
||||
zephyr_add_overlay(
|
||||
f"""
|
||||
/ {{
|
||||
zephyr,user {{
|
||||
{[f"{key} = {', '.join(value)};" for key, value in user.items()][0]}
|
||||
}};
|
||||
}};"""
|
||||
)
|
||||
|
||||
want_opts = zephyr_data()[KEY_PRJ_CONF]
|
||||
|
||||
prj_conf = (
|
||||
|
@@ -10,5 +10,7 @@ KEY_OVERLAY: Final = "overlay"
|
||||
KEY_PM_STATIC: Final = "pm_static"
|
||||
KEY_PRJ_CONF: Final = "prj_conf"
|
||||
KEY_ZEPHYR = "zephyr"
|
||||
KEY_BOARD: Final = "board"
|
||||
KEY_USER: Final = "user"
|
||||
|
||||
zephyr_ns = cg.esphome_ns.namespace("zephyr")
|
||||
|
@@ -329,6 +329,28 @@ class ConfigValidationStep(abc.ABC):
|
||||
def run(self, result: Config) -> None: ... # noqa: E704
|
||||
|
||||
|
||||
class LoadTargetPlatformValidationStep(ConfigValidationStep):
|
||||
"""Load target platform step."""
|
||||
|
||||
def __init__(self, domain: str, conf: ConfigType):
|
||||
self.domain = domain
|
||||
self.conf = conf
|
||||
|
||||
def run(self, result: Config) -> None:
|
||||
if self.conf is None:
|
||||
result[self.domain] = self.conf = {}
|
||||
result.add_output_path([self.domain], self.domain)
|
||||
component = get_component(self.domain)
|
||||
|
||||
result[self.domain] = self.conf
|
||||
path = [self.domain]
|
||||
CORE.loaded_integrations.add(self.domain)
|
||||
|
||||
result.add_validation_step(
|
||||
SchemaValidationStep(self.domain, path, self.conf, component)
|
||||
)
|
||||
|
||||
|
||||
class LoadValidationStep(ConfigValidationStep):
|
||||
"""Load step, this step is called once for each domain config fragment.
|
||||
|
||||
@@ -582,16 +604,18 @@ class MetadataValidationStep(ConfigValidationStep):
|
||||
)
|
||||
return
|
||||
for i, part_conf in enumerate(self.conf):
|
||||
path = self.path + [i]
|
||||
result.add_validation_step(
|
||||
SchemaValidationStep(
|
||||
self.domain, self.path + [i], part_conf, self.comp
|
||||
)
|
||||
SchemaValidationStep(self.domain, path, part_conf, self.comp)
|
||||
)
|
||||
result.add_validation_step(FinalValidateValidationStep(path, self.comp))
|
||||
|
||||
return
|
||||
|
||||
result.add_validation_step(
|
||||
SchemaValidationStep(self.domain, self.path, self.conf, self.comp)
|
||||
)
|
||||
result.add_validation_step(FinalValidateValidationStep(self.path, self.comp))
|
||||
|
||||
|
||||
class SchemaValidationStep(ConfigValidationStep):
|
||||
@@ -628,7 +652,6 @@ class SchemaValidationStep(ConfigValidationStep):
|
||||
result.set_by_path(self.path, validated)
|
||||
|
||||
path_context.reset(token)
|
||||
result.add_validation_step(FinalValidateValidationStep(self.path, self.comp))
|
||||
|
||||
|
||||
class IDPassValidationStep(ConfigValidationStep):
|
||||
@@ -909,7 +932,7 @@ def validate_config(
|
||||
|
||||
# First run platform validation steps
|
||||
result.add_validation_step(
|
||||
LoadValidationStep(target_platform, config[target_platform])
|
||||
LoadTargetPlatformValidationStep(target_platform, config[target_platform])
|
||||
)
|
||||
result.run_validation_steps()
|
||||
|
||||
|
@@ -147,6 +147,7 @@
|
||||
#define USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
#define USE_BLUETOOTH_PROXY
|
||||
#define BLUETOOTH_PROXY_MAX_CONNECTIONS 3
|
||||
#define USE_CAPTIVE_PORTAL
|
||||
#define USE_ESP32_BLE
|
||||
#define USE_ESP32_BLE_CLIENT
|
||||
|
@@ -125,7 +125,7 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script
|
||||
; This are common settings for the ESP32 (all variants) using Arduino.
|
||||
[common:esp32-arduino]
|
||||
extends = common:arduino
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21-1/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21-2/platform-espressif32.zip
|
||||
platform_packages =
|
||||
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.2.1/esp32-3.2.1.zip
|
||||
|
||||
@@ -161,7 +161,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script
|
||||
; This are common settings for the ESP32 (all variants) using IDF.
|
||||
[common:esp32-idf]
|
||||
extends = common:idf
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21-1/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21-2/platform-espressif32.zip
|
||||
platform_packages =
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.4.2/esp-idf-v5.4.2.zip
|
||||
|
||||
|
@@ -9,10 +9,10 @@ tzlocal==5.3.1 # from time
|
||||
tzdata>=2021.1 # from time
|
||||
pyserial==3.5
|
||||
platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
||||
esptool==4.9.0
|
||||
esptool==5.0.2
|
||||
click==8.1.7
|
||||
esphome-dashboard==20250514.0
|
||||
aioesphomeapi==37.2.1
|
||||
aioesphomeapi==37.2.3
|
||||
zeroconf==0.147.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.18.14 # dashboard_import
|
||||
|
@@ -342,6 +342,11 @@ def create_field_type_info(
|
||||
# Check if this repeated field has fixed_array_size option
|
||||
if (fixed_size := get_field_opt(field, pb.fixed_array_size)) is not None:
|
||||
return FixedArrayRepeatedType(field, fixed_size)
|
||||
# Check if this repeated field has fixed_array_size_define option
|
||||
if (
|
||||
size_define := get_field_opt(field, pb.fixed_array_size_define)
|
||||
) is not None:
|
||||
return FixedArrayRepeatedType(field, size_define)
|
||||
return RepeatedTypeInfo(field)
|
||||
|
||||
# Check for fixed_array_size option on bytes fields
|
||||
@@ -1065,9 +1070,10 @@ class FixedArrayRepeatedType(TypeInfo):
|
||||
control how many items we receive when decoding.
|
||||
"""
|
||||
|
||||
def __init__(self, field: descriptor.FieldDescriptorProto, size: int) -> None:
|
||||
def __init__(self, field: descriptor.FieldDescriptorProto, size: int | str) -> None:
|
||||
super().__init__(field)
|
||||
self.array_size = size
|
||||
self.is_define = isinstance(size, str)
|
||||
# Check if we should skip encoding when all elements are zero
|
||||
# Use getattr to handle older versions of api_options_pb2
|
||||
self.skip_zero = get_field_opt(
|
||||
@@ -1124,6 +1130,14 @@ class FixedArrayRepeatedType(TypeInfo):
|
||||
|
||||
# If skip_zero is enabled, wrap encoding in a zero check
|
||||
if self.skip_zero:
|
||||
if self.is_define:
|
||||
# When using a define, we need to use a loop-based approach
|
||||
o = f"for (const auto &it : this->{self.field_name}) {{\n"
|
||||
o += " if (it != 0) {\n"
|
||||
o += f" {encode_element('it')}\n"
|
||||
o += " }\n"
|
||||
o += "}"
|
||||
return o
|
||||
# Build the condition to check if at least one element is non-zero
|
||||
non_zero_checks = " || ".join(
|
||||
[f"this->{self.field_name}[{i}] != 0" for i in range(self.array_size)]
|
||||
@@ -1134,6 +1148,13 @@ class FixedArrayRepeatedType(TypeInfo):
|
||||
]
|
||||
return f"if ({non_zero_checks}) {{\n" + "\n".join(encode_lines) + "\n}"
|
||||
|
||||
# When using a define, always use loop-based approach
|
||||
if self.is_define:
|
||||
o = f"for (const auto &it : this->{self.field_name}) {{\n"
|
||||
o += f" {encode_element('it')}\n"
|
||||
o += "}"
|
||||
return o
|
||||
|
||||
# Unroll small arrays for efficiency
|
||||
if self.array_size == 1:
|
||||
return encode_element(f"this->{self.field_name}[0]")
|
||||
@@ -1164,6 +1185,14 @@ class FixedArrayRepeatedType(TypeInfo):
|
||||
def get_size_calculation(self, name: str, force: bool = False) -> str:
|
||||
# If skip_zero is enabled, wrap size calculation in a zero check
|
||||
if self.skip_zero:
|
||||
if self.is_define:
|
||||
# When using a define, we need to use a loop-based approach
|
||||
o = f"for (const auto &it : {name}) {{\n"
|
||||
o += " if (it != 0) {\n"
|
||||
o += f" {self._ti.get_size_calculation('it', True)}\n"
|
||||
o += " }\n"
|
||||
o += "}"
|
||||
return o
|
||||
# Build the condition to check if at least one element is non-zero
|
||||
non_zero_checks = " || ".join(
|
||||
[f"{name}[{i}] != 0" for i in range(self.array_size)]
|
||||
@@ -1174,6 +1203,13 @@ class FixedArrayRepeatedType(TypeInfo):
|
||||
]
|
||||
return f"if ({non_zero_checks}) {{\n" + "\n".join(size_lines) + "\n}"
|
||||
|
||||
# When using a define, always use loop-based approach
|
||||
if self.is_define:
|
||||
o = f"for (const auto &it : {name}) {{\n"
|
||||
o += f" {self._ti.get_size_calculation('it', True)}\n"
|
||||
o += "}"
|
||||
return o
|
||||
|
||||
# For fixed arrays, we always encode all elements
|
||||
|
||||
# Special case for single-element arrays - no loop needed
|
||||
@@ -1197,6 +1233,11 @@ class FixedArrayRepeatedType(TypeInfo):
|
||||
def get_estimated_size(self) -> int:
|
||||
# For fixed arrays, estimate underlying type size * array size
|
||||
underlying_size = self._ti.get_estimated_size()
|
||||
if self.is_define:
|
||||
# When using a define, we don't know the actual size so just guess 3
|
||||
# This is only used for documentation and never actually used since
|
||||
# fixed arrays are only for SOURCE_SERVER (encode-only) messages
|
||||
return underlying_size * 3
|
||||
return underlying_size * self.array_size
|
||||
|
||||
|
||||
|
@@ -205,7 +205,12 @@ def main():
|
||||
parser.add_argument(
|
||||
"-c", "--changed", action="store_true", help="only run on changed files"
|
||||
)
|
||||
parser.add_argument("-g", "--grep", help="only run on files containing value")
|
||||
parser.add_argument(
|
||||
"-g",
|
||||
"--grep",
|
||||
action="append",
|
||||
help="only run on files containing value",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--split-num", type=int, help="split the files into X jobs.", default=None
|
||||
)
|
||||
|
@@ -338,12 +338,12 @@ def filter_changed(files: list[str]) -> list[str]:
|
||||
return files
|
||||
|
||||
|
||||
def filter_grep(files: list[str], value: str) -> list[str]:
|
||||
def filter_grep(files: list[str], value: list[str]) -> list[str]:
|
||||
matched = []
|
||||
for file in files:
|
||||
with open(file, encoding="utf-8") as handle:
|
||||
contents = handle.read()
|
||||
if value in contents:
|
||||
if any(v in contents for v in value):
|
||||
matched.append(file)
|
||||
return matched
|
||||
|
||||
|
@@ -25,6 +25,7 @@ int main() { return 0;}
|
||||
Path(zephyr_dir / "prj.conf").write_text(
|
||||
"""
|
||||
CONFIG_NEWLIB_LIBC=y
|
||||
CONFIG_ADC=y
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
@@ -65,6 +65,7 @@ def set_core_config() -> Generator[SetCoreConfigCallable]:
|
||||
*,
|
||||
core_data: ConfigType | None = None,
|
||||
platform_data: ConfigType | None = None,
|
||||
full_config: dict[str, ConfigType] | None = None,
|
||||
) -> None:
|
||||
platform, framework = platform_framework.value
|
||||
|
||||
@@ -83,7 +84,7 @@ def set_core_config() -> Generator[SetCoreConfigCallable]:
|
||||
CORE.data[platform.value] = platform_data
|
||||
|
||||
config.path_context.set([])
|
||||
final_validate.full_config.set(Config())
|
||||
final_validate.full_config.set(full_config or Config())
|
||||
|
||||
yield setter
|
||||
|
||||
|
@@ -8,10 +8,13 @@ import pytest
|
||||
|
||||
from esphome.components.esp32 import VARIANTS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import PlatformFramework
|
||||
from esphome.const import CONF_ESPHOME, PlatformFramework
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
def test_esp32_config(set_core_config) -> None:
|
||||
def test_esp32_config(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
from esphome.components.esp32 import CONFIG_SCHEMA
|
||||
@@ -60,14 +63,49 @@ def test_esp32_config(set_core_config) -> None:
|
||||
r"Option 'variant' does not match selected board. @ data\['variant'\]",
|
||||
id="mismatched_board_variant_config",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"variant": "esp32s2",
|
||||
"framework": {
|
||||
"type": "esp-idf",
|
||||
"advanced": {"execute_from_psram": True},
|
||||
},
|
||||
},
|
||||
r"'execute_from_psram' is only supported on ESP32S3 variant @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]",
|
||||
id="execute_from_psram_invalid_for_variant_config",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"variant": "esp32s3",
|
||||
"framework": {
|
||||
"type": "esp-idf",
|
||||
"advanced": {"execute_from_psram": True},
|
||||
},
|
||||
},
|
||||
r"'execute_from_psram' requires PSRAM to be configured @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]",
|
||||
id="execute_from_psram_requires_psram_config",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"variant": "esp32s3",
|
||||
"framework": {
|
||||
"type": "esp-idf",
|
||||
"advanced": {"ignore_efuse_mac_crc": True},
|
||||
},
|
||||
},
|
||||
r"'ignore_efuse_mac_crc' is not supported on ESP32S3 @ data\['framework'\]\['advanced'\]\['ignore_efuse_mac_crc'\]",
|
||||
id="ignore_efuse_mac_crc_only_on_esp32",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_esp32_configuration_errors(
|
||||
config: Any,
|
||||
error_match: str,
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
set_core_config(PlatformFramework.ESP32_IDF, full_config={CONF_ESPHOME: {}})
|
||||
"""Test detection of invalid configuration."""
|
||||
from esphome.components.esp32 import CONFIG_SCHEMA
|
||||
from esphome.components.esp32 import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
|
||||
|
||||
with pytest.raises(cv.Invalid, match=error_match):
|
||||
CONFIG_SCHEMA(config)
|
||||
FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config))
|
||||
|
@@ -5,10 +5,12 @@ esp32:
|
||||
board: esp32s3box
|
||||
|
||||
image:
|
||||
- file: image.png
|
||||
byte_order: little_endian
|
||||
id: cat_img
|
||||
defaults:
|
||||
type: rgb565
|
||||
byte_order: little_endian
|
||||
images:
|
||||
- file: image.png
|
||||
id: cat_img
|
||||
|
||||
spi:
|
||||
mosi_pin: 6
|
||||
|
@@ -9,7 +9,8 @@ from typing import Any
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.image import CONFIG_SCHEMA
|
||||
from esphome.components.image import CONF_TRANSPARENCY, CONFIG_SCHEMA
|
||||
from esphome.const import CONF_ID, CONF_RAW_DATA_ID, CONF_TYPE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -22,12 +23,12 @@ from esphome.components.image import CONFIG_SCHEMA
|
||||
),
|
||||
pytest.param(
|
||||
{"id": "image_id", "type": "rgb565"},
|
||||
r"required key not provided @ data\[0\]\['file'\]",
|
||||
r"required key not provided @ data\['file'\]",
|
||||
id="missing_file",
|
||||
),
|
||||
pytest.param(
|
||||
{"file": "image.png", "type": "rgb565"},
|
||||
r"required key not provided @ data\[0\]\['id'\]",
|
||||
r"required key not provided @ data\['id'\]",
|
||||
id="missing_id",
|
||||
),
|
||||
pytest.param(
|
||||
@@ -160,13 +161,66 @@ def test_image_configuration_errors(
|
||||
},
|
||||
id="type_based_organization",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"defaults": {
|
||||
"type": "binary",
|
||||
"transparency": "chroma_key",
|
||||
"byte_order": "little_endian",
|
||||
"dither": "FloydSteinberg",
|
||||
"resize": "100x100",
|
||||
"invert_alpha": False,
|
||||
},
|
||||
"rgb565": {
|
||||
"alpha_channel": [
|
||||
{
|
||||
"id": "image_id",
|
||||
"file": "image.png",
|
||||
"transparency": "alpha_channel",
|
||||
"dither": "none",
|
||||
}
|
||||
]
|
||||
},
|
||||
"binary": [
|
||||
{
|
||||
"id": "image_id",
|
||||
"file": "image.png",
|
||||
"transparency": "opaque",
|
||||
}
|
||||
],
|
||||
},
|
||||
id="type_based_with_defaults",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"defaults": {
|
||||
"type": "rgb565",
|
||||
"transparency": "alpha_channel",
|
||||
},
|
||||
"binary": {
|
||||
"opaque": [
|
||||
{
|
||||
"id": "image_id",
|
||||
"file": "image.png",
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
id="binary_with_defaults",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_image_configuration_success(
|
||||
config: dict[str, Any] | list[dict[str, Any]],
|
||||
) -> None:
|
||||
"""Test successful configuration validation."""
|
||||
CONFIG_SCHEMA(config)
|
||||
result = CONFIG_SCHEMA(config)
|
||||
# All valid configurations should return a list of images
|
||||
assert isinstance(result, list)
|
||||
for key in (CONF_TYPE, CONF_ID, CONF_TRANSPARENCY, CONF_RAW_DATA_ID):
|
||||
assert all(key in x for x in result), (
|
||||
f"Missing key {key} in image configuration"
|
||||
)
|
||||
|
||||
|
||||
def test_image_generation(
|
||||
|
43
tests/component_tests/mipi_spi/conftest.py
Normal file
43
tests/component_tests/mipi_spi/conftest.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Tests for mpip_spi configuration validation."""
|
||||
|
||||
from collections.abc import Callable, Generator
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.esp32 import KEY_ESP32, KEY_VARIANT, VARIANTS
|
||||
from esphome.components.esp32.gpio import validate_gpio_pin
|
||||
from esphome.const import CONF_INPUT, CONF_OUTPUT
|
||||
from esphome.core import CORE
|
||||
from esphome.pins import gpio_pin_schema
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def choose_variant_with_pins() -> Generator[Callable[[list], None]]:
|
||||
"""
|
||||
Set the ESP32 variant for the given model based on pins. For ESP32 only since the other platforms
|
||||
do not have variants.
|
||||
"""
|
||||
|
||||
def chooser(pins: list) -> None:
|
||||
for v in VARIANTS:
|
||||
try:
|
||||
CORE.data[KEY_ESP32][KEY_VARIANT] = v
|
||||
for pin in pins:
|
||||
if pin is not None:
|
||||
pin = gpio_pin_schema(
|
||||
{
|
||||
CONF_INPUT: True,
|
||||
CONF_OUTPUT: True,
|
||||
},
|
||||
internal=True,
|
||||
)(pin)
|
||||
validate_gpio_pin(pin)
|
||||
return
|
||||
except cv.Invalid:
|
||||
continue
|
||||
raise cv.Invalid(
|
||||
f"No compatible variant found for pins: {', '.join(map(str, pins))}"
|
||||
)
|
||||
|
||||
yield chooser
|
@@ -9,13 +9,10 @@ import pytest
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.esp32 import (
|
||||
KEY_BOARD,
|
||||
KEY_ESP32,
|
||||
KEY_VARIANT,
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANTS,
|
||||
)
|
||||
from esphome.components.esp32.gpio import validate_gpio_pin
|
||||
from esphome.components.mipi import CONF_NATIVE_HEIGHT
|
||||
from esphome.components.mipi_spi.display import (
|
||||
CONF_BUS_MODE,
|
||||
@@ -32,8 +29,6 @@ from esphome.const import (
|
||||
CONF_WIDTH,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.pins import internal_gpio_pin_number
|
||||
from esphome.types import ConfigType
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
@@ -43,28 +38,6 @@ def run_schema_validation(config: ConfigType) -> None:
|
||||
FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def choose_variant_with_pins() -> Callable[..., None]:
|
||||
"""
|
||||
Set the ESP32 variant for the given model based on pins. For ESP32 only since the other platforms
|
||||
do not have variants.
|
||||
"""
|
||||
|
||||
def chooser(*pins: int | str | None) -> None:
|
||||
for v in VARIANTS:
|
||||
try:
|
||||
CORE.data[KEY_ESP32][KEY_VARIANT] = v
|
||||
for pin in pins:
|
||||
if pin is not None:
|
||||
pin = internal_gpio_pin_number(pin)
|
||||
validate_gpio_pin(pin)
|
||||
return
|
||||
except cv.Invalid:
|
||||
continue
|
||||
|
||||
return chooser
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error_match"),
|
||||
[
|
||||
@@ -315,7 +288,7 @@ def test_custom_model_with_all_options(
|
||||
def test_all_predefined_models(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
set_component_config: Callable[[str, Any], None],
|
||||
choose_variant_with_pins: Callable[..., None],
|
||||
choose_variant_with_pins: Callable[[list], None],
|
||||
) -> None:
|
||||
"""Test all predefined display models validate successfully with appropriate defaults."""
|
||||
set_core_config(
|
||||
|
@@ -18,4 +18,5 @@ class SetCoreConfigCallable(Protocol):
|
||||
*,
|
||||
core_data: ConfigType | None = None,
|
||||
platform_data: ConfigType | None = None,
|
||||
full_config: dict[str, ConfigType] | None = None,
|
||||
) -> None: ...
|
||||
|
23
tests/components/adc/test.nrf52-adafruit.yaml
Normal file
23
tests/components/adc/test.nrf52-adafruit.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
sensor:
|
||||
- platform: adc
|
||||
pin: VDDHDIV5
|
||||
name: "VDDH Voltage"
|
||||
update_interval: 5sec
|
||||
filters:
|
||||
- multiply: 5
|
||||
- platform: adc
|
||||
pin: VDD
|
||||
name: "VDD Voltage"
|
||||
update_interval: 5sec
|
||||
- platform: adc
|
||||
pin: AIN0
|
||||
name: "AIN0 Voltage"
|
||||
update_interval: 5sec
|
||||
- platform: adc
|
||||
pin: P0.03
|
||||
name: "AIN1 Voltage"
|
||||
update_interval: 5sec
|
||||
- platform: adc
|
||||
name: "AIN2 Voltage"
|
||||
update_interval: 5sec
|
||||
pin: 4
|
23
tests/components/adc/test.nrf52-mcumgr.yaml
Normal file
23
tests/components/adc/test.nrf52-mcumgr.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
sensor:
|
||||
- platform: adc
|
||||
pin: VDDHDIV5
|
||||
name: "VDDH Voltage"
|
||||
update_interval: 5sec
|
||||
filters:
|
||||
- multiply: 5
|
||||
- platform: adc
|
||||
pin: VDD
|
||||
name: "VDD Voltage"
|
||||
update_interval: 5sec
|
||||
- platform: adc
|
||||
pin: AIN0
|
||||
name: "AIN0 Voltage"
|
||||
update_interval: 5sec
|
||||
- platform: adc
|
||||
pin: P0.03
|
||||
name: "AIN1 Voltage"
|
||||
update_interval: 5sec
|
||||
- platform: adc
|
||||
name: "AIN2 Voltage"
|
||||
update_interval: 5sec
|
||||
pin: 4
|
1
tests/components/debug/test.nrf52-adafruit.yaml
Normal file
1
tests/components/debug/test.nrf52-adafruit.yaml
Normal file
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
1
tests/components/debug/test.nrf52-mcumgr.yaml
Normal file
1
tests/components/debug/test.nrf52-mcumgr.yaml
Normal file
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
12
tests/components/esp32/test.esp32-s3-idf.yaml
Normal file
12
tests/components/esp32/test.esp32-s3-idf.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
execute_from_psram: true
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
speed: 80MHz
|
||||
|
||||
logger:
|
52
tests/components/espnow/common.yaml
Normal file
52
tests/components/espnow/common.yaml
Normal file
@@ -0,0 +1,52 @@
|
||||
espnow:
|
||||
auto_add_peer: false
|
||||
channel: 1
|
||||
peers:
|
||||
- 11:22:33:44:55:66
|
||||
on_receive:
|
||||
- logger.log:
|
||||
format: "Received from: %s = '%s' RSSI: %d"
|
||||
args:
|
||||
- format_mac_address_pretty(info.src_addr).c_str()
|
||||
- format_hex_pretty(data, size).c_str()
|
||||
- info.rx_ctrl->rssi
|
||||
- espnow.send:
|
||||
address: 11:22:33:44:55:66
|
||||
data: "Hello from ESPHome"
|
||||
on_sent:
|
||||
- logger.log: "ESPNow message sent successfully"
|
||||
on_error:
|
||||
- logger.log: "ESPNow message failed to send"
|
||||
wait_for_sent: true
|
||||
continue_on_error: true
|
||||
|
||||
- espnow.send:
|
||||
address: 11:22:33:44:55:66
|
||||
data: [0x01, 0x02, 0x03, 0x04, 0x05]
|
||||
- espnow.send:
|
||||
address: 11:22:33:44:55:66
|
||||
data: !lambda 'return {0x01, 0x02, 0x03, 0x04, 0x05};'
|
||||
- espnow.broadcast:
|
||||
data: "Hello, World!"
|
||||
- espnow.broadcast:
|
||||
data: [0x01, 0x02, 0x03, 0x04, 0x05]
|
||||
- espnow.broadcast:
|
||||
data: !lambda 'return {0x01, 0x02, 0x03, 0x04, 0x05};'
|
||||
- espnow.peer.add:
|
||||
address: 11:22:33:44:55:66
|
||||
- espnow.peer.delete:
|
||||
address: 11:22:33:44:55:66
|
||||
on_broadcast:
|
||||
- logger.log:
|
||||
format: "Broadcast from: %s = '%s' RSSI: %d"
|
||||
args:
|
||||
- format_mac_address_pretty(info.src_addr).c_str()
|
||||
- format_hex_pretty(data, size).c_str()
|
||||
- info.rx_ctrl->rssi
|
||||
on_unknown_peer:
|
||||
- logger.log:
|
||||
format: "Unknown peer: %s = '%s' RSSI: %d"
|
||||
args:
|
||||
- format_mac_address_pretty(info.src_addr).c_str()
|
||||
- format_hex_pretty(data, size).c_str()
|
||||
- info.rx_ctrl->rssi
|
1
tests/components/espnow/test.esp32-idf.yaml
Normal file
1
tests/components/espnow/test.esp32-idf.yaml
Normal file
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
@@ -5,10 +5,10 @@ binary_sensor:
|
||||
|
||||
output:
|
||||
- platform: gpio
|
||||
pin: 3
|
||||
pin: P0.3
|
||||
id: gpio_output
|
||||
|
||||
switch:
|
||||
- platform: gpio
|
||||
pin: 4
|
||||
pin: P1.2
|
||||
id: gpio_switch
|
||||
|
@@ -5,10 +5,10 @@ binary_sensor:
|
||||
|
||||
output:
|
||||
- platform: gpio
|
||||
pin: 3
|
||||
pin: P0.3
|
||||
id: gpio_output
|
||||
|
||||
switch:
|
||||
- platform: gpio
|
||||
pin: 4
|
||||
pin: P1.2
|
||||
id: gpio_switch
|
||||
|
@@ -135,10 +135,17 @@ sensor:
|
||||
- throttle: 1s
|
||||
- throttle_average: 2s
|
||||
- throttle_with_priority: 5s
|
||||
- throttle_with_priority:
|
||||
timeout: 3s
|
||||
value: 42.0
|
||||
- throttle_with_priority:
|
||||
timeout: 3s
|
||||
value: !lambda return 1.0f / 2.0f;
|
||||
- throttle_with_priority:
|
||||
timeout: 3s
|
||||
value:
|
||||
- 42.0
|
||||
- !lambda return 2.0f / 2.0f;
|
||||
- nan
|
||||
- timeout:
|
||||
timeout: 10s
|
||||
|
Reference in New Issue
Block a user