mirror of
https://github.com/esphome/esphome.git
synced 2025-09-06 13:22:19 +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
|
pio_cache_key: tidyesp32-idf
|
||||||
- id: clang-tidy
|
- id: clang-tidy
|
||||||
name: Run script/clang-tidy for ZEPHYR
|
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
|
pio_cache_key: tidy-zephyr
|
||||||
ignore_errors: false
|
ignore_errors: false
|
||||||
|
|
||||||
|
@@ -155,6 +155,7 @@ esphome/components/esp32_rmt/* @jesserockz
|
|||||||
esphome/components/esp32_rmt_led_strip/* @jesserockz
|
esphome/components/esp32_rmt_led_strip/* @jesserockz
|
||||||
esphome/components/esp8266/* @esphome/core
|
esphome/components/esp8266/* @esphome/core
|
||||||
esphome/components/esp_ldo/* @clydebarrow
|
esphome/components/esp_ldo/* @clydebarrow
|
||||||
|
esphome/components/espnow/* @jesserockz
|
||||||
esphome/components/ethernet_info/* @gtjadsonsantos
|
esphome/components/ethernet_info/* @gtjadsonsantos
|
||||||
esphome/components/event/* @nohat
|
esphome/components/event/* @nohat
|
||||||
esphome/components/event_emitter/* @Rapsssito
|
esphome/components/event_emitter/* @Rapsssito
|
||||||
|
@@ -267,6 +267,11 @@ def validate_adc_pin(value):
|
|||||||
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True
|
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True
|
||||||
)(value)
|
)(value)
|
||||||
|
|
||||||
|
if CORE.is_nrf52:
|
||||||
|
return pins.gpio_pin_schema(
|
||||||
|
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True
|
||||||
|
)(value)
|
||||||
|
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
@@ -283,5 +288,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
|||||||
PlatformFramework.RTL87XX_ARDUINO,
|
PlatformFramework.RTL87XX_ARDUINO,
|
||||||
PlatformFramework.LN882X_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
|
#include "hal/adc_types.h" // This defines ADC_CHANNEL_MAX
|
||||||
#endif // USE_ESP32
|
#endif // USE_ESP32
|
||||||
|
|
||||||
|
#ifdef USE_ZEPHYR
|
||||||
|
#include <zephyr/drivers/adc.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace adc {
|
namespace adc {
|
||||||
|
|
||||||
@@ -38,15 +42,15 @@ enum class SamplingMode : uint8_t {
|
|||||||
|
|
||||||
const LogString *sampling_mode_to_str(SamplingMode mode);
|
const LogString *sampling_mode_to_str(SamplingMode mode);
|
||||||
|
|
||||||
class Aggregator {
|
template<typename T> class Aggregator {
|
||||||
public:
|
public:
|
||||||
Aggregator(SamplingMode mode);
|
Aggregator(SamplingMode mode);
|
||||||
void add_sample(uint32_t value);
|
void add_sample(T value);
|
||||||
uint32_t aggregate();
|
T aggregate();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
uint32_t aggr_{0};
|
T aggr_{0};
|
||||||
uint32_t samples_{0};
|
uint8_t samples_{0};
|
||||||
SamplingMode mode_{SamplingMode::AVG};
|
SamplingMode mode_{SamplingMode::AVG};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,6 +73,11 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
|
|||||||
/// @return A float representing the setup priority.
|
/// @return A float representing the setup priority.
|
||||||
float get_setup_priority() const override;
|
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.
|
/// Set the GPIO pin to be used by the ADC sensor.
|
||||||
/// @param pin Pointer to an InternalGPIOPin representing the ADC input pin.
|
/// @param pin Pointer to an InternalGPIOPin representing the ADC input pin.
|
||||||
void set_pin(InternalGPIOPin *pin) { this->pin_ = 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
|
#ifdef USE_RP2040
|
||||||
bool is_temperature_{false};
|
bool is_temperature_{false};
|
||||||
#endif // USE_RP2040
|
#endif // USE_RP2040
|
||||||
|
|
||||||
|
#ifdef USE_ZEPHYR
|
||||||
|
const struct adc_dt_spec *channel_ = nullptr;
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace adc
|
} // namespace adc
|
||||||
|
@@ -18,15 +18,15 @@ const LogString *sampling_mode_to_str(SamplingMode mode) {
|
|||||||
return LOG_STR("unknown");
|
return LOG_STR("unknown");
|
||||||
}
|
}
|
||||||
|
|
||||||
Aggregator::Aggregator(SamplingMode mode) {
|
template<typename T> Aggregator<T>::Aggregator(SamplingMode mode) {
|
||||||
this->mode_ = mode;
|
this->mode_ = mode;
|
||||||
// set to max uint if mode is "min"
|
// set to max uint if mode is "min"
|
||||||
if (mode == SamplingMode::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;
|
this->samples_ += 1;
|
||||||
|
|
||||||
switch (this->mode_) {
|
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->mode_ == SamplingMode::AVG) {
|
||||||
if (this->samples_ == 0) {
|
if (this->samples_ == 0) {
|
||||||
return this->aggr_;
|
return this->aggr_;
|
||||||
@@ -59,6 +59,12 @@ uint32_t Aggregator::aggregate() {
|
|||||||
return this->aggr_;
|
return this->aggr_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_ZEPHYR
|
||||||
|
template class Aggregator<int32_t>;
|
||||||
|
#else
|
||||||
|
template class Aggregator<uint32_t>;
|
||||||
|
#endif
|
||||||
|
|
||||||
void ADCSensor::update() {
|
void ADCSensor::update() {
|
||||||
float value_v = this->sample();
|
float value_v = this->sample();
|
||||||
ESP_LOGV(TAG, "'%s': Voltage=%.4fV", this->get_name().c_str(), value_v);
|
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_() {
|
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++) {
|
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||||
int raw;
|
int raw;
|
||||||
|
@@ -37,7 +37,7 @@ void ADCSensor::dump_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
float ADCSensor::sample() {
|
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++) {
|
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||||
uint32_t raw = 0;
|
uint32_t raw = 0;
|
||||||
|
@@ -30,7 +30,7 @@ void ADCSensor::dump_config() {
|
|||||||
|
|
||||||
float ADCSensor::sample() {
|
float ADCSensor::sample() {
|
||||||
uint32_t raw = 0;
|
uint32_t raw = 0;
|
||||||
auto aggr = Aggregator(this->sampling_mode_);
|
auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
|
||||||
|
|
||||||
if (this->output_raw_) {
|
if (this->output_raw_) {
|
||||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||||
|
@@ -41,7 +41,7 @@ void ADCSensor::dump_config() {
|
|||||||
|
|
||||||
float ADCSensor::sample() {
|
float ADCSensor::sample() {
|
||||||
uint32_t raw = 0;
|
uint32_t raw = 0;
|
||||||
auto aggr = Aggregator(this->sampling_mode_);
|
auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
|
||||||
|
|
||||||
if (this->is_temperature_) {
|
if (this->is_temperature_) {
|
||||||
adc_set_temp_sensor_enabled(true);
|
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
|
import esphome.codegen as cg
|
||||||
from esphome.components import sensor, voltage_sampler
|
from esphome.components import sensor, voltage_sampler
|
||||||
from esphome.components.esp32 import get_esp32_variant
|
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
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_ATTENUATION,
|
CONF_ATTENUATION,
|
||||||
@@ -11,6 +17,7 @@ from esphome.const import (
|
|||||||
CONF_PIN,
|
CONF_PIN,
|
||||||
CONF_RAW,
|
CONF_RAW,
|
||||||
DEVICE_CLASS_VOLTAGE,
|
DEVICE_CLASS_VOLTAGE,
|
||||||
|
PLATFORM_NRF52,
|
||||||
STATE_CLASS_MEASUREMENT,
|
STATE_CLASS_MEASUREMENT,
|
||||||
UNIT_VOLT,
|
UNIT_VOLT,
|
||||||
)
|
)
|
||||||
@@ -60,6 +67,10 @@ ADCSensor = adc_ns.class_(
|
|||||||
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
|
"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(
|
CONFIG_SCHEMA = cv.All(
|
||||||
sensor.sensor_schema(
|
sensor.sensor_schema(
|
||||||
ADCSensor,
|
ADCSensor,
|
||||||
@@ -75,6 +86,7 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All(
|
cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All(
|
||||||
cv.only_on_esp32, _attenuation
|
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_SAMPLES, default=1): cv.int_range(min=1, max=255),
|
||||||
cv.Optional(CONF_SAMPLING_MODE, default="avg"): _sampling_mode,
|
cv.Optional(CONF_SAMPLING_MODE, default="avg"): _sampling_mode,
|
||||||
}
|
}
|
||||||
@@ -83,6 +95,8 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
validate_config,
|
validate_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CONF_ADC_CHANNEL_ID = "adc_channel_id"
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
@@ -93,7 +107,7 @@ async def to_code(config):
|
|||||||
cg.add_define("USE_ADC_SENSOR_VCC")
|
cg.add_define("USE_ADC_SENSOR_VCC")
|
||||||
elif config[CONF_PIN] == "TEMPERATURE":
|
elif config[CONF_PIN] == "TEMPERATURE":
|
||||||
cg.add(var.set_is_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])
|
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||||
cg.add(var.set_pin(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]
|
chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num]
|
||||||
cg.add(var.set_channel(adc_unit_t.ADC_UNIT_2, chan))
|
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,7 +34,8 @@ SetFrameAction = animation_ns.class_(
|
|||||||
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
|
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||||
)
|
)
|
||||||
|
|
||||||
CONFIG_SCHEMA = espImage.IMAGE_SCHEMA.extend(
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
espImage.IMAGE_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_ID): cv.declare_id(Animation_),
|
cv.Required(CONF_ID): cv.declare_id(Animation_),
|
||||||
cv.Optional(CONF_LOOP): cv.All(
|
cv.Optional(CONF_LOOP): cv.All(
|
||||||
@@ -45,6 +46,8 @@ CONFIG_SCHEMA = espImage.IMAGE_SCHEMA.extend(
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
espImage.validate_settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1621,7 +1621,10 @@ message BluetoothConnectionsFreeResponse {
|
|||||||
|
|
||||||
uint32 free = 1;
|
uint32 free = 1;
|
||||||
uint32 limit = 2;
|
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 {
|
message BluetoothGATTErrorResponse {
|
||||||
|
@@ -1105,10 +1105,8 @@ void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg)
|
|||||||
|
|
||||||
bool APIConnection::send_subscribe_bluetooth_connections_free_response(
|
bool APIConnection::send_subscribe_bluetooth_connections_free_response(
|
||||||
const SubscribeBluetoothConnectionsFreeRequest &msg) {
|
const SubscribeBluetoothConnectionsFreeRequest &msg) {
|
||||||
BluetoothConnectionsFreeResponse resp;
|
bluetooth_proxy::global_bluetooth_proxy->send_connections_free();
|
||||||
resp.free = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_free();
|
return true;
|
||||||
resp.limit = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_limit();
|
|
||||||
return this->send_message(resp, BluetoothConnectionsFreeResponse::MESSAGE_TYPE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) {
|
void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) {
|
||||||
|
@@ -29,6 +29,7 @@ extend google.protobuf.FieldOptions {
|
|||||||
optional uint32 fixed_array_size = 50007;
|
optional uint32 fixed_array_size = 50007;
|
||||||
optional bool no_zero_copy = 50008 [default=false];
|
optional bool no_zero_copy = 50008 [default=false];
|
||||||
optional bool fixed_array_skip_zero = 50009 [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.
|
// 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 {
|
void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const {
|
||||||
buffer.encode_uint32(1, this->free);
|
buffer.encode_uint32(1, this->free);
|
||||||
buffer.encode_uint32(2, this->limit);
|
buffer.encode_uint32(2, this->limit);
|
||||||
for (auto &it : this->allocated) {
|
for (const auto &it : this->allocated) {
|
||||||
|
if (it != 0) {
|
||||||
buffer.encode_uint64(3, it, true);
|
buffer.encode_uint64(3, it, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
void BluetoothConnectionsFreeResponse::calculate_size(ProtoSize &size) const {
|
void BluetoothConnectionsFreeResponse::calculate_size(ProtoSize &size) const {
|
||||||
size.add_uint32(1, this->free);
|
size.add_uint32(1, this->free);
|
||||||
size.add_uint32(1, this->limit);
|
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);
|
size.add_uint64_force(1, it);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2076,13 +2076,13 @@ class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage {
|
|||||||
class BluetoothConnectionsFreeResponse : public ProtoMessage {
|
class BluetoothConnectionsFreeResponse : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 81;
|
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
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "bluetooth_connections_free_response"; }
|
const char *message_name() const override { return "bluetooth_connections_free_response"; }
|
||||||
#endif
|
#endif
|
||||||
uint32_t free{0};
|
uint32_t free{0};
|
||||||
uint32_t limit{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 encode(ProtoWriteBuffer buffer) const override;
|
||||||
void calculate_size(ProtoSize &size) const override;
|
void calculate_size(ProtoSize &size) const override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
@@ -87,6 +87,10 @@ async def to_code(config):
|
|||||||
cg.add(var.set_active(config[CONF_ACTIVE]))
|
cg.add(var.set_active(config[CONF_ACTIVE]))
|
||||||
await esp32_ble_tracker.register_raw_ble_device(var, config)
|
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, []):
|
for connection_conf in config.get(CONF_CONNECTIONS, []):
|
||||||
connection_var = cg.new_Pvariable(connection_conf[CONF_ID])
|
connection_var = cg.new_Pvariable(connection_conf[CONF_ID])
|
||||||
await cg.register_component(connection_var, connection_conf)
|
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
|
static constexpr uint8_t DESC_PER_CHAR = 1; // Assume 1 descriptor per characteristic
|
||||||
|
|
||||||
// Helper to estimate service size before fetching all data
|
// 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) {
|
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;
|
size_t service_overhead = use_efficient_uuids ? SERVICE_OVERHEAD_EFFICIENT : SERVICE_OVERHEAD_LEGACY;
|
||||||
// Always assume 128-bit UUIDs for characteristics to be safe
|
// Always assume 128-bit UUIDs for characteristics to be safe
|
||||||
@@ -66,6 +78,30 @@ void BluetoothConnection::dump_config() {
|
|||||||
BLEClientBase::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() {
|
void BluetoothConnection::loop() {
|
||||||
BLEClientBase::loop();
|
BLEClientBase::loop();
|
||||||
|
|
||||||
|
@@ -24,12 +24,15 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
|
|||||||
|
|
||||||
esp_err_t notify_characteristic(uint16_t handle, bool enable);
|
esp_err_t notify_characteristic(uint16_t handle, bool enable);
|
||||||
|
|
||||||
|
void set_address(uint64_t address) override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
friend class BluetoothProxy;
|
friend class BluetoothProxy;
|
||||||
|
|
||||||
bool supports_efficient_uuids_() const;
|
bool supports_efficient_uuids_() const;
|
||||||
void send_service_for_discovery_();
|
void send_service_for_discovery_();
|
||||||
void reset_connection_(esp_err_t reason);
|
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
|
// Memory optimized layout for 32-bit systems
|
||||||
// Group 1: Pointers (4 bytes each, naturally aligned)
|
// 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
|
// 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
|
// 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) {
|
this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) {
|
||||||
if (this->api_connection_ != nullptr) {
|
if (this->api_connection_ != nullptr) {
|
||||||
this->send_bluetooth_scanner_state_(state);
|
this->send_bluetooth_scanner_state_(state);
|
||||||
@@ -134,20 +137,6 @@ void BluetoothProxy::dump_config() {
|
|||||||
YESNO(this->active_), this->connections_.size());
|
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() {
|
void BluetoothProxy::loop() {
|
||||||
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) {
|
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) {
|
||||||
for (auto *connection : this->connections_) {
|
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() {
|
void BluetoothProxy::send_connections_free() {
|
||||||
if (this->api_connection_ == nullptr)
|
if (this->api_connection_ == nullptr)
|
||||||
return;
|
return;
|
||||||
api::BluetoothConnectionsFreeResponse call;
|
|
||||||
call.free = this->get_bluetooth_connections_free();
|
this->api_connection_->send_message(this->connections_free_response_,
|
||||||
call.limit = this->get_bluetooth_connections_limit();
|
api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void BluetoothProxy::send_gatt_services_done(uint64_t address) {
|
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 {
|
class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
|
||||||
|
friend class BluetoothConnection; // Allow connection to call free_connection_
|
||||||
public:
|
public:
|
||||||
BluetoothProxy();
|
BluetoothProxy();
|
||||||
#ifdef USE_ESP32_BLE_DEVICE
|
#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_send_services(const api::BluetoothGATTGetServicesRequest &msg);
|
||||||
void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &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 subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags);
|
||||||
void unsubscribe_api_connection(api::APIConnection *api_connection);
|
void unsubscribe_api_connection(api::APIConnection *api_connection);
|
||||||
api::APIConnection *get_api_connection() { return this->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
|
// Group 3: 4-byte types
|
||||||
uint32_t last_advertisement_flush_time_{0};
|
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
|
// Group 4: 1-byte types grouped together
|
||||||
bool active_;
|
bool active_;
|
||||||
uint8_t advertisement_count_{0};
|
uint8_t advertisement_count_{0};
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.zephyr import zephyr_add_prj_conf
|
||||||
from esphome.config_helpers import filter_source_files_from_platform
|
from esphome.config_helpers import filter_source_files_from_platform
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
@@ -10,6 +11,7 @@ from esphome.const import (
|
|||||||
CONF_LOOP_TIME,
|
CONF_LOOP_TIME,
|
||||||
PlatformFramework,
|
PlatformFramework,
|
||||||
)
|
)
|
||||||
|
from esphome.core import CORE
|
||||||
|
|
||||||
CODEOWNERS = ["@OttoWinter"]
|
CODEOWNERS = ["@OttoWinter"]
|
||||||
DEPENDENCIES = ["logger"]
|
DEPENDENCIES = ["logger"]
|
||||||
@@ -44,6 +46,8 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
|
if CORE.using_zephyr:
|
||||||
|
zephyr_add_prj_conf("HWINFO", True)
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
|
|
||||||
@@ -62,5 +66,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
|||||||
PlatformFramework.RTL87XX_ARDUINO,
|
PlatformFramework.RTL87XX_ARDUINO,
|
||||||
PlatformFramework.LN882X_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
|
import esphome.codegen as cg
|
||||||
from esphome.components import sensor
|
from esphome.components import sensor
|
||||||
from esphome.components.esp32 import CONF_CPU_FREQUENCY
|
from esphome.components.esp32 import CONF_CPU_FREQUENCY
|
||||||
|
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_BLOCK,
|
CONF_BLOCK,
|
||||||
@@ -54,7 +55,7 @@ CONFIG_SCHEMA = {
|
|||||||
),
|
),
|
||||||
cv.Optional(CONF_PSRAM): cv.All(
|
cv.Optional(CONF_PSRAM): cv.All(
|
||||||
cv.only_on_esp32,
|
cv.only_on_esp32,
|
||||||
cv.requires_component("psram"),
|
cv.requires_component(PSRAM_DOMAIN),
|
||||||
sensor.sensor_schema(
|
sensor.sensor_schema(
|
||||||
unit_of_measurement=UNIT_BYTES,
|
unit_of_measurement=UNIT_BYTES,
|
||||||
icon=ICON_COUNTER,
|
icon=ICON_COUNTER,
|
||||||
|
@@ -76,6 +76,7 @@ CONF_ASSERTION_LEVEL = "assertion_level"
|
|||||||
CONF_COMPILER_OPTIMIZATION = "compiler_optimization"
|
CONF_COMPILER_OPTIMIZATION = "compiler_optimization"
|
||||||
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features"
|
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features"
|
||||||
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
|
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
|
||||||
|
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
|
||||||
CONF_RELEASE = "release"
|
CONF_RELEASE = "release"
|
||||||
|
|
||||||
ASSERTION_LEVELS = {
|
ASSERTION_LEVELS = {
|
||||||
@@ -313,7 +314,7 @@ def _format_framework_espidf_version(
|
|||||||
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1)
|
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1)
|
||||||
# The platform-espressif32 version to use for arduino frameworks
|
# The platform-espressif32 version to use for arduino frameworks
|
||||||
# - https://github.com/pioarduino/platform-espressif32/releases
|
# - 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
|
# The default/recommended esp-idf framework version
|
||||||
# - https://github.com/espressif/esp-idf/releases
|
# - 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
|
# The platformio/espressif32 version to use for esp-idf frameworks
|
||||||
# - https://github.com/platformio/platform-espressif32/releases
|
# - https://github.com/platformio/platform-espressif32/releases
|
||||||
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
|
# - 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
|
# List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions
|
||||||
SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
|
SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
|
||||||
@@ -519,32 +520,59 @@ def _detect_variant(value):
|
|||||||
|
|
||||||
|
|
||||||
def final_validate(config):
|
def final_validate(config):
|
||||||
if not (
|
# Imported locally to avoid circular import issues
|
||||||
pio_options := fv.full_config.get()[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS)
|
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
||||||
):
|
|
||||||
# Not specified or empty
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
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_flash_size_key = "board_upload.flash_size"
|
||||||
pio_partitions_key = "board_build.partitions"
|
pio_partitions_key = "board_build.partitions"
|
||||||
if CONF_PARTITIONS in config and pio_partitions_key in pio_options:
|
if CONF_PARTITIONS in config and pio_partitions_key in pio_options:
|
||||||
raise cv.Invalid(
|
errs.append(
|
||||||
|
cv.Invalid(
|
||||||
f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32"
|
f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32"
|
||||||
)
|
)
|
||||||
|
)
|
||||||
if pio_flash_size_key in pio_options:
|
if pio_flash_size_key in pio_options:
|
||||||
raise cv.Invalid(
|
errs.append(
|
||||||
|
cv.Invalid(
|
||||||
f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only"
|
f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only"
|
||||||
)
|
)
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
config[CONF_VARIANT] != VARIANT_ESP32
|
config[CONF_VARIANT] != VARIANT_ESP32
|
||||||
and CONF_ADVANCED in (conf_fw := config[CONF_FRAMEWORK])
|
and CONF_ADVANCED in (conf_fw := config[CONF_FRAMEWORK])
|
||||||
and CONF_IGNORE_EFUSE_MAC_CRC in conf_fw[CONF_ADVANCED]
|
and CONF_IGNORE_EFUSE_MAC_CRC in conf_fw[CONF_ADVANCED]
|
||||||
):
|
):
|
||||||
raise cv.Invalid(
|
errs.append(
|
||||||
f"{CONF_IGNORE_EFUSE_MAC_CRC} is not supported on {config[CONF_VARIANT]}"
|
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
|
return config
|
||||||
|
|
||||||
@@ -627,6 +655,7 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
|
|||||||
cv.Optional(
|
cv.Optional(
|
||||||
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True
|
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True
|
||||||
): cv.boolean,
|
): cv.boolean,
|
||||||
|
cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
|
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)
|
add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
|
||||||
if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
|
if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
|
||||||
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
|
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
|
# Apply LWIP core locking for better socket performance
|
||||||
# This is already enabled by default in Arduino framework, where it provides
|
# This is already enabled by default in Arduino framework, where it provides
|
||||||
|
@@ -2,6 +2,7 @@ import logging
|
|||||||
|
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
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}
|
_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):
|
def esp32_h2_validate_gpio_pin(value):
|
||||||
if value < 0 or value > 27:
|
if value < 0 or value > 27:
|
||||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-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:
|
if value in _ESP32H2_SPI_FLASH_PINS:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"GPIO%d is reserved for SPI Flash communication on some ESP32-H2 chip variants.\n"
|
"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:
|
if is_input:
|
||||||
# All ESP32 pins support input mode
|
# All ESP32 pins support input mode
|
||||||
pass
|
pass
|
||||||
|
check_strapping_pin(value, _ESP32H2_STRAPPING_PINS, _LOGGER)
|
||||||
return value
|
return value
|
||||||
|
@@ -2,6 +2,7 @@ import logging
|
|||||||
|
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||||
|
from esphome.pins import check_strapping_pin
|
||||||
|
|
||||||
_ESP32P4_USB_JTAG_PINS = {24, 25}
|
_ESP32P4_USB_JTAG_PINS = {24, 25}
|
||||||
|
|
||||||
@@ -13,13 +14,6 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
def esp32_p4_validate_gpio_pin(value):
|
def esp32_p4_validate_gpio_pin(value):
|
||||||
if value < 0 or value > 54:
|
if value < 0 or value > 54:
|
||||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-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:
|
if value in _ESP32P4_USB_JTAG_PINS:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"GPIO%d is reserved for the USB-Serial-JTAG interface.\n"
|
"GPIO%d is reserved for the USB-Serial-JTAG interface.\n"
|
||||||
@@ -40,4 +34,5 @@ def esp32_p4_validate_supports(value):
|
|||||||
if is_input:
|
if is_input:
|
||||||
# All ESP32 pins support input mode
|
# All ESP32 pins support input mode
|
||||||
pass
|
pass
|
||||||
|
check_strapping_pin(value, _ESP32P4_STRAPPING_PINS, _LOGGER)
|
||||||
return value
|
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_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->address_ = address;
|
||||||
this->remote_bda_[0] = (address >> 40) & 0xFF;
|
this->remote_bda_[0] = (address >> 40) & 0xFF;
|
||||||
this->remote_bda_[1] = (address >> 32) & 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:
|
: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):
|
def is_alpha_only(image: Image):
|
||||||
"""
|
"""
|
||||||
@@ -446,13 +464,14 @@ def validate_type(image_types):
|
|||||||
return validate
|
return validate
|
||||||
|
|
||||||
|
|
||||||
def validate_settings(value):
|
def validate_settings(value, path=()):
|
||||||
"""
|
"""
|
||||||
Validate the settings for a single image configuration.
|
Validate the settings for a single image configuration.
|
||||||
"""
|
"""
|
||||||
conf_type = value[CONF_TYPE]
|
conf_type = value[CONF_TYPE]
|
||||||
type_class = IMAGE_TYPE[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:
|
if transparency not in type_class.allow_config:
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
f"Image format '{conf_type}' cannot have transparency: {transparency}"
|
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
|
and CONF_INVERT_ALPHA not in type_class.allow_config
|
||||||
):
|
):
|
||||||
raise cv.Invalid("No alpha channel to invert")
|
raise cv.Invalid("No alpha channel to invert")
|
||||||
if value.get(CONF_BYTE_ORDER) is not None and not callable(
|
if value.get(CONF_BYTE_ORDER) is not None and not type_class.is_endian():
|
||||||
getattr(type_class, "set_big_endian", None)
|
|
||||||
):
|
|
||||||
raise cv.Invalid(
|
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):
|
if file := value.get(CONF_FILE):
|
||||||
file = Path(file)
|
file = Path(file)
|
||||||
@@ -479,7 +497,7 @@ def validate_settings(value):
|
|||||||
Image.open(file)
|
Image.open(file)
|
||||||
except UnidentifiedImageError as exc:
|
except UnidentifiedImageError as exc:
|
||||||
raise cv.Invalid(
|
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
|
) from exc
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@@ -499,6 +517,10 @@ OPTIONS_SCHEMA = {
|
|||||||
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
|
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_BYTE_ORDER): cv.one_of("BIG_ENDIAN", "LITTLE_ENDIAN", upper=True),
|
||||||
cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
|
cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULTS_SCHEMA = {
|
||||||
|
**OPTIONS_SCHEMA,
|
||||||
cv.Optional(CONF_TYPE): validate_type(IMAGE_TYPE),
|
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},
|
**{cv.Optional(key): OPTIONS_SCHEMA[key] for key in OPTIONS},
|
||||||
}
|
}
|
||||||
|
|
||||||
BASE_SCHEMA = cv.Schema(
|
IMAGE_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
**IMAGE_ID_SCHEMA,
|
**IMAGE_ID_SCHEMA,
|
||||||
**OPTIONS_SCHEMA,
|
**OPTIONS_SCHEMA,
|
||||||
}
|
|
||||||
).add_extra(validate_settings)
|
|
||||||
|
|
||||||
IMAGE_SCHEMA = BASE_SCHEMA.extend(
|
|
||||||
{
|
|
||||||
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
|
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate_defaults(value):
|
def apply_defaults(image, defaults, path):
|
||||||
"""
|
"""
|
||||||
Validate the options for images with defaults
|
Apply defaults to an image configuration
|
||||||
"""
|
"""
|
||||||
defaults = value[CONF_DEFAULTS]
|
|
||||||
result = []
|
|
||||||
for index, image in enumerate(value[CONF_IMAGES]):
|
|
||||||
type = image.get(CONF_TYPE, defaults.get(CONF_TYPE))
|
type = image.get(CONF_TYPE, defaults.get(CONF_TYPE))
|
||||||
if type is None:
|
if type is None:
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
"Type is required either in the image config or in the defaults",
|
"Type is required either in the image config or in the defaults", path=path
|
||||||
path=[CONF_IMAGES, index],
|
|
||||||
)
|
)
|
||||||
type_class = IMAGE_TYPE[type]
|
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 = {
|
config = {
|
||||||
**{key: image.get(key, defaults.get(key)) for key in available_options},
|
**{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},
|
**{key.schema: image[key.schema] for key in IMAGE_ID_SCHEMA},
|
||||||
|
CONF_TYPE: image.get(CONF_TYPE, defaults.get(CONF_TYPE)),
|
||||||
}
|
}
|
||||||
validate_settings(config)
|
validate_settings(config, path)
|
||||||
result.append(config)
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def validate_defaults(value):
|
||||||
|
"""
|
||||||
|
Apply defaults to the images in the configuration and flatten to a single list.
|
||||||
|
"""
|
||||||
|
defaults = value[CONF_DEFAULTS]
|
||||||
|
result = []
|
||||||
|
# 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
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -562,8 +598,13 @@ def typed_image_schema(image_type):
|
|||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Optional(t.lower()): cv.ensure_list(
|
cv.Optional(t.lower()): cv.ensure_list(
|
||||||
BASE_SCHEMA.extend(
|
|
||||||
{
|
{
|
||||||
|
**IMAGE_ID_SCHEMA,
|
||||||
|
**{
|
||||||
|
cv.Optional(key): OPTIONS_SCHEMA[key]
|
||||||
|
for key in OPTIONS
|
||||||
|
if key != CONF_TRANSPARENCY
|
||||||
|
},
|
||||||
cv.Optional(
|
cv.Optional(
|
||||||
CONF_TRANSPARENCY, default=t
|
CONF_TRANSPARENCY, default=t
|
||||||
): validate_transparency((t,)),
|
): validate_transparency((t,)),
|
||||||
@@ -572,7 +613,6 @@ def typed_image_schema(image_type):
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
|
||||||
for t in IMAGE_TYPE[image_type].allow_config.intersection(
|
for t in IMAGE_TYPE[image_type].allow_config.intersection(
|
||||||
TRANSPARENCY_TYPES
|
TRANSPARENCY_TYPES
|
||||||
)
|
)
|
||||||
@@ -580,46 +620,44 @@ def typed_image_schema(image_type):
|
|||||||
),
|
),
|
||||||
# Allow a default configuration with no transparency preselected
|
# Allow a default configuration with no transparency preselected
|
||||||
cv.ensure_list(
|
cv.ensure_list(
|
||||||
BASE_SCHEMA.extend(
|
|
||||||
{
|
{
|
||||||
cv.Optional(
|
**IMAGE_SCHEMA_NO_DEFAULTS,
|
||||||
CONF_TRANSPARENCY, default=CONF_OPAQUE
|
|
||||||
): validate_transparency(),
|
|
||||||
cv.Optional(CONF_TYPE, default=image_type): validate_type(
|
cv.Optional(CONF_TYPE, default=image_type): validate_type(
|
||||||
(image_type,)
|
(image_type,)
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# The config schema can be a (possibly empty) single list of images,
|
# 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 optional keys `defaults:`, `images:` and the image types
|
||||||
# or a dictionary with keys `defaults:` and `images:`
|
|
||||||
|
|
||||||
|
|
||||||
def _config_schema(config):
|
def _config_schema(value):
|
||||||
if isinstance(config, list):
|
if isinstance(value, list) or (
|
||||||
return cv.Schema([IMAGE_SCHEMA])(config)
|
isinstance(value, dict) and (CONF_ID in value or CONF_FILE in value)
|
||||||
if not isinstance(config, dict):
|
):
|
||||||
|
return cv.ensure_list(cv.All(IMAGE_SCHEMA, validate_settings))(value)
|
||||||
|
if not isinstance(value, dict):
|
||||||
raise cv.Invalid(
|
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 cv.All(
|
||||||
return validate_defaults(
|
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_DEFAULTS): OPTIONS_SCHEMA,
|
cv.Optional(CONF_DEFAULTS, default={}): DEFAULTS_SCHEMA,
|
||||||
cv.Required(CONF_IMAGES): cv.ensure_list(IMAGE_SCHEMA_NO_DEFAULTS),
|
cv.Optional(CONF_IMAGES, default=[]): cv.ensure_list(
|
||||||
|
{
|
||||||
|
**IMAGE_SCHEMA_NO_DEFAULTS,
|
||||||
|
cv.Optional(CONF_TYPE): validate_type(IMAGE_TYPE),
|
||||||
}
|
}
|
||||||
)(config)
|
),
|
||||||
)
|
**{cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE},
|
||||||
if CONF_ID in config or CONF_FILE in config:
|
}
|
||||||
return cv.ensure_list(IMAGE_SCHEMA)([config])
|
),
|
||||||
return cv.Schema(
|
validate_defaults,
|
||||||
{cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}
|
)(value)
|
||||||
)(config)
|
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = _config_schema
|
CONFIG_SCHEMA = _config_schema
|
||||||
@@ -668,7 +706,7 @@ async def write_image(config, all_frames=False):
|
|||||||
else Image.Dither.FLOYDSTEINBERG
|
else Image.Dither.FLOYDSTEINBERG
|
||||||
)
|
)
|
||||||
type = config[CONF_TYPE]
|
type = config[CONF_TYPE]
|
||||||
transparency = config[CONF_TRANSPARENCY]
|
transparency = config.get(CONF_TRANSPARENCY, CONF_OPAQUE)
|
||||||
invert_alpha = config[CONF_INVERT_ALPHA]
|
invert_alpha = config[CONF_INVERT_ALPHA]
|
||||||
frame_count = 1
|
frame_count = 1
|
||||||
if all_frames:
|
if all_frames:
|
||||||
@@ -699,14 +737,9 @@ async def write_image(config, all_frames=False):
|
|||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
if isinstance(config, list):
|
# By now the config should be a simple list.
|
||||||
for entry in config:
|
for entry in config:
|
||||||
await to_code(entry)
|
prog_arr, width, height, image_type, trans_value, _ = await write_image(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)
|
|
||||||
cg.new_Pvariable(
|
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
|
import esphome.codegen as cg
|
||||||
from esphome.components.const import CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING
|
from esphome.components.const import CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING
|
||||||
from esphome.components.display import Display
|
from esphome.components.display import Display
|
||||||
|
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_AUTO_CLEAR_ENABLED,
|
CONF_AUTO_CLEAR_ENABLED,
|
||||||
@@ -219,7 +220,7 @@ def final_validation(configs):
|
|||||||
draw_rounding, config[CONF_DRAW_ROUNDING]
|
draw_rounding, config[CONF_DRAW_ROUNDING]
|
||||||
)
|
)
|
||||||
buffer_frac = config[CONF_BUFFER_SIZE]
|
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")
|
LOGGER.warning("buffer_size: may need to be reduced without PSRAM")
|
||||||
for image_id in lv_images_used:
|
for image_id in lv_images_used:
|
||||||
path = global_config.get_path_for_id(image_id)[:-1]
|
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); }
|
void set_fahrenheit(bool val) { this->set_mask_(2, val, 32); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
static const uint8_t MIN_TEMP_C = 0;
|
inline static constexpr uint8_t MIN_TEMP_C = 0;
|
||||||
static const uint8_t MAX_TEMP_C = 37;
|
inline static constexpr uint8_t MAX_TEMP_C = 37;
|
||||||
|
|
||||||
// see
|
// see
|
||||||
// https://github.com/crankyoldgit/IRremoteESP8266/blob/9bdf8abcb465268c5409db99dc83a26df64c7445/src/ir_Midea.h#L116
|
// 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
|
// see
|
||||||
// https://github.com/crankyoldgit/IRremoteESP8266/blob/9bdf8abcb465268c5409db99dc83a26df64c7445/src/ir_Midea.h#L117
|
// 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 {
|
class IrSpecialData : public IrData {
|
||||||
|
@@ -25,6 +25,7 @@ from esphome.components.mipi import (
|
|||||||
power_of_two,
|
power_of_two,
|
||||||
requires_buffer,
|
requires_buffer,
|
||||||
)
|
)
|
||||||
|
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
||||||
from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
|
from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.config_validation import ALLOW_EXTRA
|
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
|
# If no drawing methods are configured, and LVGL is not enabled, show a test card
|
||||||
config[CONF_SHOW_TEST_CARD] = True
|
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):
|
if not requires_buffer(config):
|
||||||
return config # No buffer needed, so no need to set a buffer size
|
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
|
# 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
|
# make sure that firmware.zip is created
|
||||||
# for Adafruit_nRF52_Bootloader
|
# for Adafruit_nRF52_Bootloader
|
||||||
cg.add_platformio_option("board_upload.protocol", "nrfutil")
|
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_SD132 = "adafruit_nrf52_sd132"
|
||||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6 = "adafruit_nrf52_sd140_v6"
|
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6 = "adafruit_nrf52_sd140_v6"
|
||||||
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7 = "adafruit_nrf52_sd140_v7"
|
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
|
import esphome.codegen as cg
|
||||||
from esphome.components.zephyr.const import zephyr_ns
|
from esphome.components.zephyr.const import zephyr_ns
|
||||||
import esphome.config_validation as cv
|
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)
|
ZephyrGPIOPin = zephyr_ns.class_("ZephyrGPIOPin", cg.InternalGPIOPin)
|
||||||
|
|
||||||
|
|
||||||
def _translate_pin(value):
|
def _translate_pin(value):
|
||||||
|
if value in AIN_TO_GPIO:
|
||||||
|
return AIN_TO_GPIO[value]
|
||||||
if isinstance(value, dict) or value is None:
|
if isinstance(value, dict) or value is None:
|
||||||
raise cv.Invalid(
|
raise cv.Invalid(
|
||||||
"This variable only supports pin numbers, not full pin schemas "
|
"This variable only supports pin numbers, not full pin schemas "
|
||||||
@@ -28,18 +39,33 @@ def _translate_pin(value):
|
|||||||
|
|
||||||
|
|
||||||
def validate_gpio_pin(value):
|
def validate_gpio_pin(value):
|
||||||
|
if value in EXTRA_ADC:
|
||||||
|
return value
|
||||||
value = _translate_pin(value)
|
value = _translate_pin(value)
|
||||||
if value < 0 or value > (32 + 16):
|
if value < 0 or value > (32 + 16):
|
||||||
raise cv.Invalid(f"NRF52: Invalid pin number: {value}")
|
raise cv.Invalid(f"NRF52: Invalid pin number: {value}")
|
||||||
return 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(
|
NRF52_PIN_SCHEMA = cv.All(
|
||||||
pins.gpio_base_schema(
|
pins.gpio_base_schema(
|
||||||
ZephyrGPIOPin,
|
ZephyrGPIOPin,
|
||||||
validate_gpio_pin,
|
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
|
import esphome.final_validate as fv
|
||||||
|
|
||||||
CODEOWNERS = ["@esphome/core"]
|
CODEOWNERS = ["@esphome/core"]
|
||||||
|
DOMAIN = "psram"
|
||||||
|
|
||||||
DEPENDENCIES = [PLATFORM_ESP32]
|
DEPENDENCIES = [PLATFORM_ESP32]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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)
|
PsramComponent = psram_ns.class_("PsramComponent", cg.Component)
|
||||||
|
|
||||||
TYPE_QUAD = "quad"
|
TYPE_QUAD = "quad"
|
||||||
|
@@ -599,7 +599,9 @@ async def throttle_filter_to_code(config, filter_id):
|
|||||||
TIMEOUT_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value(
|
TIMEOUT_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value(
|
||||||
{
|
{
|
||||||
cv.Required(CONF_TIMEOUT): cv.positive_time_period_milliseconds,
|
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,
|
key=CONF_TIMEOUT,
|
||||||
)
|
)
|
||||||
@@ -611,6 +613,8 @@ TIMEOUT_WITH_PRIORITY_SCHEMA = cv.maybe_simple_value(
|
|||||||
TIMEOUT_WITH_PRIORITY_SCHEMA,
|
TIMEOUT_WITH_PRIORITY_SCHEMA,
|
||||||
)
|
)
|
||||||
async def throttle_with_priority_filter_to_code(config, filter_id):
|
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]]
|
template_ = [await cg.templatable(x, [], float) for x in config[CONF_VALUE]]
|
||||||
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_)
|
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_)
|
||||||
|
|
||||||
|
@@ -225,7 +225,7 @@ optional<float> SlidingWindowMovingAverageFilter::new_value(float value) {
|
|||||||
|
|
||||||
// ExponentialMovingAverageFilter
|
// ExponentialMovingAverageFilter
|
||||||
ExponentialMovingAverageFilter::ExponentialMovingAverageFilter(float alpha, size_t send_every, size_t send_first_at)
|
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) {
|
optional<float> ExponentialMovingAverageFilter::new_value(float value) {
|
||||||
if (!std::isnan(value)) {
|
if (!std::isnan(value)) {
|
||||||
if (this->first_value_) {
|
if (this->first_value_) {
|
||||||
@@ -325,7 +325,7 @@ optional<float> FilterOutValueFilter::new_value(float value) {
|
|||||||
// ThrottleFilter
|
// ThrottleFilter
|
||||||
ThrottleFilter::ThrottleFilter(uint32_t min_time_between_inputs) : min_time_between_inputs_(min_time_between_inputs) {}
|
ThrottleFilter::ThrottleFilter(uint32_t min_time_between_inputs) : min_time_between_inputs_(min_time_between_inputs) {}
|
||||||
optional<float> ThrottleFilter::new_value(float value) {
|
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_) {
|
if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_) {
|
||||||
this->last_input_ = now;
|
this->last_input_ = now;
|
||||||
return value;
|
return value;
|
||||||
@@ -369,19 +369,17 @@ optional<float> ThrottleWithPriorityFilter::new_value(float value) {
|
|||||||
|
|
||||||
// DeltaFilter
|
// DeltaFilter
|
||||||
DeltaFilter::DeltaFilter(float delta, bool percentage_mode)
|
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) {
|
optional<float> DeltaFilter::new_value(float value) {
|
||||||
if (std::isnan(value)) {
|
if (std::isnan(value)) {
|
||||||
if (std::isnan(this->last_value_)) {
|
if (std::isnan(this->last_value_)) {
|
||||||
return {};
|
return {};
|
||||||
} else {
|
} else {
|
||||||
if (this->percentage_mode_) {
|
|
||||||
this->current_delta_ = fabsf(value * this->delta_);
|
|
||||||
}
|
|
||||||
return this->last_value_ = value;
|
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_) {
|
if (this->percentage_mode_) {
|
||||||
this->current_delta_ = fabsf(value * this->delta_);
|
this->current_delta_ = fabsf(value * this->delta_);
|
||||||
}
|
}
|
||||||
|
@@ -221,11 +221,11 @@ class ExponentialMovingAverageFilter : public Filter {
|
|||||||
void set_alpha(float alpha);
|
void set_alpha(float alpha);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool first_value_{true};
|
|
||||||
float accumulator_{NAN};
|
float accumulator_{NAN};
|
||||||
|
float alpha_;
|
||||||
size_t send_every_;
|
size_t send_every_;
|
||||||
size_t send_at_;
|
size_t send_at_;
|
||||||
float alpha_;
|
bool first_value_{true};
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Simple throttle average filter.
|
/** Simple throttle average filter.
|
||||||
@@ -243,9 +243,9 @@ class ThrottleAverageFilter : public Filter, public Component {
|
|||||||
float get_setup_priority() const override;
|
float get_setup_priority() const override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
uint32_t time_period_;
|
|
||||||
float sum_{0.0f};
|
float sum_{0.0f};
|
||||||
unsigned int n_{0};
|
unsigned int n_{0};
|
||||||
|
uint32_t time_period_;
|
||||||
bool have_nan_{false};
|
bool have_nan_{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -378,8 +378,8 @@ class DeltaFilter : public Filter {
|
|||||||
protected:
|
protected:
|
||||||
float delta_;
|
float delta_;
|
||||||
float current_delta_;
|
float current_delta_;
|
||||||
bool percentage_mode_;
|
|
||||||
float last_value_{NAN};
|
float last_value_{NAN};
|
||||||
|
bool percentage_mode_;
|
||||||
};
|
};
|
||||||
|
|
||||||
class OrFilter : public Filter {
|
class OrFilter : public Filter {
|
||||||
@@ -401,8 +401,8 @@ class OrFilter : public Filter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
std::vector<Filter *> filters_;
|
std::vector<Filter *> filters_;
|
||||||
bool has_value_{false};
|
|
||||||
PhiNode phi_;
|
PhiNode phi_;
|
||||||
|
bool has_value_{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
class CalibrateLinearFilter : public Filter {
|
class CalibrateLinearFilter : public Filter {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
from typing import Final, TypedDict
|
from typing import TypedDict
|
||||||
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.const import CONF_BOARD
|
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 (
|
from .const import (
|
||||||
BOOTLOADER_MCUBOOT,
|
BOOTLOADER_MCUBOOT,
|
||||||
|
KEY_BOARD,
|
||||||
KEY_BOOTLOADER,
|
KEY_BOOTLOADER,
|
||||||
KEY_EXTRA_BUILD_FILES,
|
KEY_EXTRA_BUILD_FILES,
|
||||||
KEY_OVERLAY,
|
KEY_OVERLAY,
|
||||||
KEY_PM_STATIC,
|
KEY_PM_STATIC,
|
||||||
KEY_PRJ_CONF,
|
KEY_PRJ_CONF,
|
||||||
|
KEY_USER,
|
||||||
KEY_ZEPHYR,
|
KEY_ZEPHYR,
|
||||||
zephyr_ns,
|
zephyr_ns,
|
||||||
)
|
)
|
||||||
|
|
||||||
CODEOWNERS = ["@tomaszduda23"]
|
CODEOWNERS = ["@tomaszduda23"]
|
||||||
AUTO_LOAD = ["preferences"]
|
AUTO_LOAD = ["preferences"]
|
||||||
KEY_BOARD: Final = "board"
|
|
||||||
|
|
||||||
PrjConfValueType = bool | str | int
|
PrjConfValueType = bool | str | int
|
||||||
|
|
||||||
@@ -49,6 +50,7 @@ class ZephyrData(TypedDict):
|
|||||||
overlay: str
|
overlay: str
|
||||||
extra_build_files: dict[str, str]
|
extra_build_files: dict[str, str]
|
||||||
pm_static: list[Section]
|
pm_static: list[Section]
|
||||||
|
user: dict[str, list[str]]
|
||||||
|
|
||||||
|
|
||||||
def zephyr_set_core_data(config):
|
def zephyr_set_core_data(config):
|
||||||
@@ -59,6 +61,7 @@ def zephyr_set_core_data(config):
|
|||||||
overlay="",
|
overlay="",
|
||||||
extra_build_files={},
|
extra_build_files={},
|
||||||
pm_static=[],
|
pm_static=[],
|
||||||
|
user={},
|
||||||
)
|
)
|
||||||
return config
|
return config
|
||||||
|
|
||||||
@@ -178,7 +181,25 @@ def zephyr_add_pm_static(section: Section):
|
|||||||
CORE.data[KEY_ZEPHYR][KEY_PM_STATIC].extend(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():
|
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]
|
want_opts = zephyr_data()[KEY_PRJ_CONF]
|
||||||
|
|
||||||
prj_conf = (
|
prj_conf = (
|
||||||
|
@@ -10,5 +10,7 @@ KEY_OVERLAY: Final = "overlay"
|
|||||||
KEY_PM_STATIC: Final = "pm_static"
|
KEY_PM_STATIC: Final = "pm_static"
|
||||||
KEY_PRJ_CONF: Final = "prj_conf"
|
KEY_PRJ_CONF: Final = "prj_conf"
|
||||||
KEY_ZEPHYR = "zephyr"
|
KEY_ZEPHYR = "zephyr"
|
||||||
|
KEY_BOARD: Final = "board"
|
||||||
|
KEY_USER: Final = "user"
|
||||||
|
|
||||||
zephyr_ns = cg.esphome_ns.namespace("zephyr")
|
zephyr_ns = cg.esphome_ns.namespace("zephyr")
|
||||||
|
@@ -329,6 +329,28 @@ class ConfigValidationStep(abc.ABC):
|
|||||||
def run(self, result: Config) -> None: ... # noqa: E704
|
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):
|
class LoadValidationStep(ConfigValidationStep):
|
||||||
"""Load step, this step is called once for each domain config fragment.
|
"""Load step, this step is called once for each domain config fragment.
|
||||||
|
|
||||||
@@ -582,16 +604,18 @@ class MetadataValidationStep(ConfigValidationStep):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
for i, part_conf in enumerate(self.conf):
|
for i, part_conf in enumerate(self.conf):
|
||||||
|
path = self.path + [i]
|
||||||
result.add_validation_step(
|
result.add_validation_step(
|
||||||
SchemaValidationStep(
|
SchemaValidationStep(self.domain, path, part_conf, self.comp)
|
||||||
self.domain, self.path + [i], part_conf, self.comp
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
result.add_validation_step(FinalValidateValidationStep(path, self.comp))
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
result.add_validation_step(
|
result.add_validation_step(
|
||||||
SchemaValidationStep(self.domain, self.path, self.conf, self.comp)
|
SchemaValidationStep(self.domain, self.path, self.conf, self.comp)
|
||||||
)
|
)
|
||||||
|
result.add_validation_step(FinalValidateValidationStep(self.path, self.comp))
|
||||||
|
|
||||||
|
|
||||||
class SchemaValidationStep(ConfigValidationStep):
|
class SchemaValidationStep(ConfigValidationStep):
|
||||||
@@ -628,7 +652,6 @@ class SchemaValidationStep(ConfigValidationStep):
|
|||||||
result.set_by_path(self.path, validated)
|
result.set_by_path(self.path, validated)
|
||||||
|
|
||||||
path_context.reset(token)
|
path_context.reset(token)
|
||||||
result.add_validation_step(FinalValidateValidationStep(self.path, self.comp))
|
|
||||||
|
|
||||||
|
|
||||||
class IDPassValidationStep(ConfigValidationStep):
|
class IDPassValidationStep(ConfigValidationStep):
|
||||||
@@ -909,7 +932,7 @@ def validate_config(
|
|||||||
|
|
||||||
# First run platform validation steps
|
# First run platform validation steps
|
||||||
result.add_validation_step(
|
result.add_validation_step(
|
||||||
LoadValidationStep(target_platform, config[target_platform])
|
LoadTargetPlatformValidationStep(target_platform, config[target_platform])
|
||||||
)
|
)
|
||||||
result.run_validation_steps()
|
result.run_validation_steps()
|
||||||
|
|
||||||
|
@@ -147,6 +147,7 @@
|
|||||||
#define USE_ESPHOME_TASK_LOG_BUFFER
|
#define USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
|
|
||||||
#define USE_BLUETOOTH_PROXY
|
#define USE_BLUETOOTH_PROXY
|
||||||
|
#define BLUETOOTH_PROXY_MAX_CONNECTIONS 3
|
||||||
#define USE_CAPTIVE_PORTAL
|
#define USE_CAPTIVE_PORTAL
|
||||||
#define USE_ESP32_BLE
|
#define USE_ESP32_BLE
|
||||||
#define USE_ESP32_BLE_CLIENT
|
#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.
|
; This are common settings for the ESP32 (all variants) using Arduino.
|
||||||
[common:esp32-arduino]
|
[common:esp32-arduino]
|
||||||
extends = common: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 =
|
platform_packages =
|
||||||
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.2.1/esp32-3.2.1.zip
|
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.
|
; This are common settings for the ESP32 (all variants) using IDF.
|
||||||
[common:esp32-idf]
|
[common:esp32-idf]
|
||||||
extends = common: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 =
|
platform_packages =
|
||||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.4.2/esp-idf-v5.4.2.zip
|
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
|
tzdata>=2021.1 # from time
|
||||||
pyserial==3.5
|
pyserial==3.5
|
||||||
platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
||||||
esptool==4.9.0
|
esptool==5.0.2
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
esphome-dashboard==20250514.0
|
esphome-dashboard==20250514.0
|
||||||
aioesphomeapi==37.2.1
|
aioesphomeapi==37.2.3
|
||||||
zeroconf==0.147.0
|
zeroconf==0.147.0
|
||||||
puremagic==1.30
|
puremagic==1.30
|
||||||
ruamel.yaml==0.18.14 # dashboard_import
|
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
|
# Check if this repeated field has fixed_array_size option
|
||||||
if (fixed_size := get_field_opt(field, pb.fixed_array_size)) is not None:
|
if (fixed_size := get_field_opt(field, pb.fixed_array_size)) is not None:
|
||||||
return FixedArrayRepeatedType(field, fixed_size)
|
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)
|
return RepeatedTypeInfo(field)
|
||||||
|
|
||||||
# Check for fixed_array_size option on bytes fields
|
# Check for fixed_array_size option on bytes fields
|
||||||
@@ -1065,9 +1070,10 @@ class FixedArrayRepeatedType(TypeInfo):
|
|||||||
control how many items we receive when decoding.
|
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)
|
super().__init__(field)
|
||||||
self.array_size = size
|
self.array_size = size
|
||||||
|
self.is_define = isinstance(size, str)
|
||||||
# Check if we should skip encoding when all elements are zero
|
# Check if we should skip encoding when all elements are zero
|
||||||
# Use getattr to handle older versions of api_options_pb2
|
# Use getattr to handle older versions of api_options_pb2
|
||||||
self.skip_zero = get_field_opt(
|
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 skip_zero is enabled, wrap encoding in a zero check
|
||||||
if self.skip_zero:
|
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
|
# Build the condition to check if at least one element is non-zero
|
||||||
non_zero_checks = " || ".join(
|
non_zero_checks = " || ".join(
|
||||||
[f"this->{self.field_name}[{i}] != 0" for i in range(self.array_size)]
|
[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}"
|
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
|
# Unroll small arrays for efficiency
|
||||||
if self.array_size == 1:
|
if self.array_size == 1:
|
||||||
return encode_element(f"this->{self.field_name}[0]")
|
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:
|
def get_size_calculation(self, name: str, force: bool = False) -> str:
|
||||||
# If skip_zero is enabled, wrap size calculation in a zero check
|
# If skip_zero is enabled, wrap size calculation in a zero check
|
||||||
if self.skip_zero:
|
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
|
# Build the condition to check if at least one element is non-zero
|
||||||
non_zero_checks = " || ".join(
|
non_zero_checks = " || ".join(
|
||||||
[f"{name}[{i}] != 0" for i in range(self.array_size)]
|
[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}"
|
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
|
# For fixed arrays, we always encode all elements
|
||||||
|
|
||||||
# Special case for single-element arrays - no loop needed
|
# Special case for single-element arrays - no loop needed
|
||||||
@@ -1197,6 +1233,11 @@ class FixedArrayRepeatedType(TypeInfo):
|
|||||||
def get_estimated_size(self) -> int:
|
def get_estimated_size(self) -> int:
|
||||||
# For fixed arrays, estimate underlying type size * array size
|
# For fixed arrays, estimate underlying type size * array size
|
||||||
underlying_size = self._ti.get_estimated_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
|
return underlying_size * self.array_size
|
||||||
|
|
||||||
|
|
||||||
|
@@ -205,7 +205,12 @@ def main():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-c", "--changed", action="store_true", help="only run on changed files"
|
"-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(
|
parser.add_argument(
|
||||||
"--split-num", type=int, help="split the files into X jobs.", default=None
|
"--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
|
return files
|
||||||
|
|
||||||
|
|
||||||
def filter_grep(files: list[str], value: str) -> list[str]:
|
def filter_grep(files: list[str], value: list[str]) -> list[str]:
|
||||||
matched = []
|
matched = []
|
||||||
for file in files:
|
for file in files:
|
||||||
with open(file, encoding="utf-8") as handle:
|
with open(file, encoding="utf-8") as handle:
|
||||||
contents = handle.read()
|
contents = handle.read()
|
||||||
if value in contents:
|
if any(v in contents for v in value):
|
||||||
matched.append(file)
|
matched.append(file)
|
||||||
return matched
|
return matched
|
||||||
|
|
||||||
|
@@ -25,6 +25,7 @@ int main() { return 0;}
|
|||||||
Path(zephyr_dir / "prj.conf").write_text(
|
Path(zephyr_dir / "prj.conf").write_text(
|
||||||
"""
|
"""
|
||||||
CONFIG_NEWLIB_LIBC=y
|
CONFIG_NEWLIB_LIBC=y
|
||||||
|
CONFIG_ADC=y
|
||||||
""",
|
""",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
@@ -65,6 +65,7 @@ def set_core_config() -> Generator[SetCoreConfigCallable]:
|
|||||||
*,
|
*,
|
||||||
core_data: ConfigType | None = None,
|
core_data: ConfigType | None = None,
|
||||||
platform_data: ConfigType | None = None,
|
platform_data: ConfigType | None = None,
|
||||||
|
full_config: dict[str, ConfigType] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
platform, framework = platform_framework.value
|
platform, framework = platform_framework.value
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ def set_core_config() -> Generator[SetCoreConfigCallable]:
|
|||||||
CORE.data[platform.value] = platform_data
|
CORE.data[platform.value] = platform_data
|
||||||
|
|
||||||
config.path_context.set([])
|
config.path_context.set([])
|
||||||
final_validate.full_config.set(Config())
|
final_validate.full_config.set(full_config or Config())
|
||||||
|
|
||||||
yield setter
|
yield setter
|
||||||
|
|
||||||
|
@@ -8,10 +8,13 @@ import pytest
|
|||||||
|
|
||||||
from esphome.components.esp32 import VARIANTS
|
from esphome.components.esp32 import VARIANTS
|
||||||
import esphome.config_validation as cv
|
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)
|
set_core_config(PlatformFramework.ESP32_IDF)
|
||||||
|
|
||||||
from esphome.components.esp32 import CONFIG_SCHEMA
|
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'\]",
|
r"Option 'variant' does not match selected board. @ data\['variant'\]",
|
||||||
id="mismatched_board_variant_config",
|
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(
|
def test_esp32_configuration_errors(
|
||||||
config: Any,
|
config: Any,
|
||||||
error_match: str,
|
error_match: str,
|
||||||
|
set_core_config: SetCoreConfigCallable,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
set_core_config(PlatformFramework.ESP32_IDF, full_config={CONF_ESPHOME: {}})
|
||||||
"""Test detection of invalid configuration."""
|
"""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):
|
with pytest.raises(cv.Invalid, match=error_match):
|
||||||
CONFIG_SCHEMA(config)
|
FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config))
|
||||||
|
@@ -5,10 +5,12 @@ esp32:
|
|||||||
board: esp32s3box
|
board: esp32s3box
|
||||||
|
|
||||||
image:
|
image:
|
||||||
- file: image.png
|
defaults:
|
||||||
byte_order: little_endian
|
|
||||||
id: cat_img
|
|
||||||
type: rgb565
|
type: rgb565
|
||||||
|
byte_order: little_endian
|
||||||
|
images:
|
||||||
|
- file: image.png
|
||||||
|
id: cat_img
|
||||||
|
|
||||||
spi:
|
spi:
|
||||||
mosi_pin: 6
|
mosi_pin: 6
|
||||||
|
@@ -9,7 +9,8 @@ from typing import Any
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from esphome import config_validation as cv
|
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(
|
@pytest.mark.parametrize(
|
||||||
@@ -22,12 +23,12 @@ from esphome.components.image import CONFIG_SCHEMA
|
|||||||
),
|
),
|
||||||
pytest.param(
|
pytest.param(
|
||||||
{"id": "image_id", "type": "rgb565"},
|
{"id": "image_id", "type": "rgb565"},
|
||||||
r"required key not provided @ data\[0\]\['file'\]",
|
r"required key not provided @ data\['file'\]",
|
||||||
id="missing_file",
|
id="missing_file",
|
||||||
),
|
),
|
||||||
pytest.param(
|
pytest.param(
|
||||||
{"file": "image.png", "type": "rgb565"},
|
{"file": "image.png", "type": "rgb565"},
|
||||||
r"required key not provided @ data\[0\]\['id'\]",
|
r"required key not provided @ data\['id'\]",
|
||||||
id="missing_id",
|
id="missing_id",
|
||||||
),
|
),
|
||||||
pytest.param(
|
pytest.param(
|
||||||
@@ -160,13 +161,66 @@ def test_image_configuration_errors(
|
|||||||
},
|
},
|
||||||
id="type_based_organization",
|
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(
|
def test_image_configuration_success(
|
||||||
config: dict[str, Any] | list[dict[str, Any]],
|
config: dict[str, Any] | list[dict[str, Any]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test successful configuration validation."""
|
"""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(
|
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 import config_validation as cv
|
||||||
from esphome.components.esp32 import (
|
from esphome.components.esp32 import (
|
||||||
KEY_BOARD,
|
KEY_BOARD,
|
||||||
KEY_ESP32,
|
|
||||||
KEY_VARIANT,
|
KEY_VARIANT,
|
||||||
VARIANT_ESP32,
|
VARIANT_ESP32,
|
||||||
VARIANT_ESP32S3,
|
VARIANT_ESP32S3,
|
||||||
VARIANTS,
|
|
||||||
)
|
)
|
||||||
from esphome.components.esp32.gpio import validate_gpio_pin
|
|
||||||
from esphome.components.mipi import CONF_NATIVE_HEIGHT
|
from esphome.components.mipi import CONF_NATIVE_HEIGHT
|
||||||
from esphome.components.mipi_spi.display import (
|
from esphome.components.mipi_spi.display import (
|
||||||
CONF_BUS_MODE,
|
CONF_BUS_MODE,
|
||||||
@@ -32,8 +29,6 @@ from esphome.const import (
|
|||||||
CONF_WIDTH,
|
CONF_WIDTH,
|
||||||
PlatformFramework,
|
PlatformFramework,
|
||||||
)
|
)
|
||||||
from esphome.core import CORE
|
|
||||||
from esphome.pins import internal_gpio_pin_number
|
|
||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
from tests.component_tests.types import SetCoreConfigCallable
|
from tests.component_tests.types import SetCoreConfigCallable
|
||||||
|
|
||||||
@@ -43,28 +38,6 @@ def run_schema_validation(config: ConfigType) -> None:
|
|||||||
FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config))
|
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(
|
@pytest.mark.parametrize(
|
||||||
("config", "error_match"),
|
("config", "error_match"),
|
||||||
[
|
[
|
||||||
@@ -315,7 +288,7 @@ def test_custom_model_with_all_options(
|
|||||||
def test_all_predefined_models(
|
def test_all_predefined_models(
|
||||||
set_core_config: SetCoreConfigCallable,
|
set_core_config: SetCoreConfigCallable,
|
||||||
set_component_config: Callable[[str, Any], None],
|
set_component_config: Callable[[str, Any], None],
|
||||||
choose_variant_with_pins: Callable[..., None],
|
choose_variant_with_pins: Callable[[list], None],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test all predefined display models validate successfully with appropriate defaults."""
|
"""Test all predefined display models validate successfully with appropriate defaults."""
|
||||||
set_core_config(
|
set_core_config(
|
||||||
|
@@ -18,4 +18,5 @@ class SetCoreConfigCallable(Protocol):
|
|||||||
*,
|
*,
|
||||||
core_data: ConfigType | None = None,
|
core_data: ConfigType | None = None,
|
||||||
platform_data: ConfigType | None = None,
|
platform_data: ConfigType | None = None,
|
||||||
|
full_config: dict[str, ConfigType] | None = 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:
|
output:
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
pin: 3
|
pin: P0.3
|
||||||
id: gpio_output
|
id: gpio_output
|
||||||
|
|
||||||
switch:
|
switch:
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
pin: 4
|
pin: P1.2
|
||||||
id: gpio_switch
|
id: gpio_switch
|
||||||
|
@@ -5,10 +5,10 @@ binary_sensor:
|
|||||||
|
|
||||||
output:
|
output:
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
pin: 3
|
pin: P0.3
|
||||||
id: gpio_output
|
id: gpio_output
|
||||||
|
|
||||||
switch:
|
switch:
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
pin: 4
|
pin: P1.2
|
||||||
id: gpio_switch
|
id: gpio_switch
|
||||||
|
@@ -135,10 +135,17 @@ sensor:
|
|||||||
- throttle: 1s
|
- throttle: 1s
|
||||||
- throttle_average: 2s
|
- throttle_average: 2s
|
||||||
- throttle_with_priority: 5s
|
- 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:
|
- throttle_with_priority:
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
value:
|
value:
|
||||||
- 42.0
|
- 42.0
|
||||||
|
- !lambda return 2.0f / 2.0f;
|
||||||
- nan
|
- nan
|
||||||
- timeout:
|
- timeout:
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
|
Reference in New Issue
Block a user