mirror of
https://github.com/esphome/esphome.git
synced 2025-09-08 14:22:21 +01:00
Merge branch 'dev' into multi_device_args
This commit is contained in:
@@ -168,6 +168,8 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
* `platformio.ini`: Configures the PlatformIO build environments for different microcontrollers.
|
* `platformio.ini`: Configures the PlatformIO build environments for different microcontrollers.
|
||||||
* `.pre-commit-config.yaml`: Configures the pre-commit hooks for linting and formatting.
|
* `.pre-commit-config.yaml`: Configures the pre-commit hooks for linting and formatting.
|
||||||
* **CI/CD Pipeline:** Defined in `.github/workflows`.
|
* **CI/CD Pipeline:** Defined in `.github/workflows`.
|
||||||
|
* **Static Analysis & Development:**
|
||||||
|
* `esphome/core/defines.h`: A comprehensive header file containing all `#define` directives that can be added by components using `cg.add_define()` in Python. This file is used exclusively for development, static analysis tools, and CI testing - it is not used during runtime compilation. When developing components that add new defines, they must be added to this file to ensure proper IDE support and static analysis coverage. The file includes feature flags, build configurations, and platform-specific defines that help static analyzers understand the complete codebase without needing to compile for specific platforms.
|
||||||
|
|
||||||
## 6. Development & Testing Workflow
|
## 6. Development & Testing Workflow
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -298,20 +298,20 @@ def upload_using_esptool(
|
|||||||
|
|
||||||
def run_esptool(baud_rate):
|
def run_esptool(baud_rate):
|
||||||
cmd = [
|
cmd = [
|
||||||
"esptool.py",
|
"esptool",
|
||||||
"--before",
|
"--before",
|
||||||
"default_reset",
|
"default-reset",
|
||||||
"--after",
|
"--after",
|
||||||
"hard_reset",
|
"hard-reset",
|
||||||
"--baud",
|
"--baud",
|
||||||
str(baud_rate),
|
str(baud_rate),
|
||||||
"--port",
|
"--port",
|
||||||
port,
|
port,
|
||||||
"--chip",
|
"--chip",
|
||||||
mcu,
|
mcu,
|
||||||
"write_flash",
|
"write-flash",
|
||||||
"-z",
|
"-z",
|
||||||
"--flash_size",
|
"--flash-size",
|
||||||
"detect",
|
"detect",
|
||||||
]
|
]
|
||||||
for img in flash_images:
|
for img in flash_images:
|
||||||
@@ -826,6 +826,12 @@ POST_CONFIG_ACTIONS = {
|
|||||||
"discover": command_discover,
|
"discover": command_discover,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SIMPLE_CONFIG_ACTIONS = [
|
||||||
|
"clean",
|
||||||
|
"clean-mqtt",
|
||||||
|
"config",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def parse_args(argv):
|
def parse_args(argv):
|
||||||
options_parser = argparse.ArgumentParser(add_help=False)
|
options_parser = argparse.ArgumentParser(add_help=False)
|
||||||
@@ -1094,6 +1100,13 @@ def parse_args(argv):
|
|||||||
arguments = argv[1:]
|
arguments = argv[1:]
|
||||||
|
|
||||||
argcomplete.autocomplete(parser)
|
argcomplete.autocomplete(parser)
|
||||||
|
|
||||||
|
if len(arguments) > 0 and arguments[0] in SIMPLE_CONFIG_ACTIONS:
|
||||||
|
args, unknown_args = parser.parse_known_args(arguments)
|
||||||
|
if unknown_args:
|
||||||
|
_LOGGER.warning("Ignored unrecognized arguments: %s", unknown_args)
|
||||||
|
return args
|
||||||
|
|
||||||
return parser.parse_args(arguments)
|
return parser.parse_args(arguments)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -391,8 +391,7 @@ async def build_action(full_config, template_arg, args):
|
|||||||
)
|
)
|
||||||
action_id = full_config[CONF_TYPE_ID]
|
action_id = full_config[CONF_TYPE_ID]
|
||||||
builder = registry_entry.coroutine_fun
|
builder = registry_entry.coroutine_fun
|
||||||
ret = await builder(config, action_id, template_arg, args)
|
return await builder(config, action_id, template_arg, args)
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
async def build_action_list(config, templ, arg_type):
|
async def build_action_list(config, templ, arg_type):
|
||||||
@@ -409,8 +408,7 @@ async def build_condition(full_config, template_arg, args):
|
|||||||
)
|
)
|
||||||
action_id = full_config[CONF_TYPE_ID]
|
action_id = full_config[CONF_TYPE_ID]
|
||||||
builder = registry_entry.coroutine_fun
|
builder = registry_entry.coroutine_fun
|
||||||
ret = await builder(config, action_id, template_arg, args)
|
return await builder(config, action_id, template_arg, args)
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
async def build_condition_list(config, templ, args):
|
async def build_condition_list(config, templ, args):
|
||||||
|
@@ -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>;
|
||||||
|
}};
|
||||||
|
}};
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
@@ -301,8 +301,7 @@ async def alarm_action_disarm_to_code(config, action_id, template_arg, args):
|
|||||||
)
|
)
|
||||||
async def alarm_action_pending_to_code(config, action_id, template_arg, args):
|
async def alarm_action_pending_to_code(config, action_id, template_arg, args):
|
||||||
paren = await cg.get_variable(config[CONF_ID])
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
@automation.register_action(
|
@automation.register_action(
|
||||||
@@ -310,8 +309,7 @@ async def alarm_action_pending_to_code(config, action_id, template_arg, args):
|
|||||||
)
|
)
|
||||||
async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
|
async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
|
||||||
paren = await cg.get_variable(config[CONF_ID])
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
@automation.register_action(
|
@automation.register_action(
|
||||||
@@ -319,8 +317,7 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
|
|||||||
)
|
)
|
||||||
async def alarm_action_chime_to_code(config, action_id, template_arg, args):
|
async def alarm_action_chime_to_code(config, action_id, template_arg, args):
|
||||||
paren = await cg.get_variable(config[CONF_ID])
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
@automation.register_action(
|
@automation.register_action(
|
||||||
@@ -333,8 +330,7 @@ async def alarm_action_chime_to_code(config, action_id, template_arg, args):
|
|||||||
)
|
)
|
||||||
async def alarm_action_ready_to_code(config, action_id, template_arg, args):
|
async def alarm_action_ready_to_code(config, action_id, template_arg, args):
|
||||||
paren = await cg.get_variable(config[CONF_ID])
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
@automation.register_condition(
|
@automation.register_condition(
|
||||||
@@ -352,4 +348,3 @@ async def alarm_control_panel_is_armed_to_code(
|
|||||||
@coroutine_with_priority(100.0)
|
@coroutine_with_priority(100.0)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_global(alarm_control_panel_ns.using)
|
cg.add_global(alarm_control_panel_ns.using)
|
||||||
cg.add_define("USE_ALARM_CONTROL_PANEL")
|
|
||||||
|
@@ -34,17 +34,20 @@ 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.Optional(CONF_LOOP): cv.All(
|
cv.Required(CONF_ID): cv.declare_id(Animation_),
|
||||||
{
|
cv.Optional(CONF_LOOP): cv.All(
|
||||||
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
|
{
|
||||||
cv.Optional(CONF_END_FRAME): cv.positive_int,
|
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
|
||||||
cv.Optional(CONF_REPEAT): cv.positive_int,
|
cv.Optional(CONF_END_FRAME): cv.positive_int,
|
||||||
}
|
cv.Optional(CONF_REPEAT): cv.positive_int,
|
||||||
),
|
}
|
||||||
},
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
espImage.validate_settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -250,8 +250,8 @@ message DeviceInfoResponse {
|
|||||||
// Supports receiving and saving api encryption key
|
// Supports receiving and saving api encryption key
|
||||||
bool api_encryption_supported = 19 [(field_ifdef) = "USE_API_NOISE"];
|
bool api_encryption_supported = 19 [(field_ifdef) = "USE_API_NOISE"];
|
||||||
|
|
||||||
repeated DeviceInfo devices = 20 [(field_ifdef) = "USE_DEVICES"];
|
repeated DeviceInfo devices = 20 [(field_ifdef) = "USE_DEVICES", (fixed_array_size_define) = "ESPHOME_DEVICE_COUNT"];
|
||||||
repeated AreaInfo areas = 21 [(field_ifdef) = "USE_AREAS"];
|
repeated AreaInfo areas = 21 [(field_ifdef) = "USE_AREAS", (fixed_array_size_define) = "ESPHOME_AREA_COUNT"];
|
||||||
|
|
||||||
// Top-level area info to phase out suggested_area
|
// Top-level area info to phase out suggested_area
|
||||||
AreaInfo area = 22 [(field_ifdef) = "USE_AREAS"];
|
AreaInfo area = 22 [(field_ifdef) = "USE_AREAS"];
|
||||||
@@ -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(this);
|
||||||
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) {
|
||||||
@@ -1464,18 +1462,22 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
|||||||
resp.api_encryption_supported = true;
|
resp.api_encryption_supported = true;
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
|
size_t device_index = 0;
|
||||||
for (auto const &device : App.get_devices()) {
|
for (auto const &device : App.get_devices()) {
|
||||||
resp.devices.emplace_back();
|
if (device_index >= ESPHOME_DEVICE_COUNT)
|
||||||
auto &device_info = resp.devices.back();
|
break;
|
||||||
|
auto &device_info = resp.devices[device_index++];
|
||||||
device_info.device_id = device->get_device_id();
|
device_info.device_id = device->get_device_id();
|
||||||
device_info.set_name(StringRef(device->get_name()));
|
device_info.set_name(StringRef(device->get_name()));
|
||||||
device_info.area_id = device->get_area_id();
|
device_info.area_id = device->get_area_id();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_AREAS
|
#ifdef USE_AREAS
|
||||||
|
size_t area_index = 0;
|
||||||
for (auto const &area : App.get_areas()) {
|
for (auto const &area : App.get_areas()) {
|
||||||
resp.areas.emplace_back();
|
if (area_index >= ESPHOME_AREA_COUNT)
|
||||||
auto &area_info = resp.areas.back();
|
break;
|
||||||
|
auto &area_info = resp.areas[area_index++];
|
||||||
area_info.area_id = area->get_area_id();
|
area_info.area_id = area->get_area_id();
|
||||||
area_info.set_name(StringRef(area->get_name()));
|
area_info.set_name(StringRef(area->get_name()));
|
||||||
}
|
}
|
||||||
|
@@ -703,10 +703,16 @@ class APIConnection : public APIServerConnection {
|
|||||||
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type,
|
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type,
|
||||||
uint8_t estimated_size) {
|
uint8_t estimated_size) {
|
||||||
// Try to send immediately if:
|
// Try to send immediately if:
|
||||||
// 1. We should try to send immediately (should_try_send_immediately = true)
|
// 1. It's an UpdateStateResponse (always send immediately to handle cases where
|
||||||
// 2. Batch delay is 0 (user has opted in to immediate sending)
|
// the main loop is blocked, e.g., during OTA updates)
|
||||||
// 3. Buffer has space available
|
// 2. OR: We should try to send immediately (should_try_send_immediately = true)
|
||||||
if (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0 &&
|
// AND Batch delay is 0 (user has opted in to immediate sending)
|
||||||
|
// 3. AND: Buffer has space available
|
||||||
|
if ((
|
||||||
|
#ifdef USE_UPDATE
|
||||||
|
message_type == UpdateStateResponse::MESSAGE_TYPE ||
|
||||||
|
#endif
|
||||||
|
(this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) &&
|
||||||
this->helper_->can_write_without_blocking()) {
|
this->helper_->can_write_without_blocking()) {
|
||||||
// Now actually encode and send
|
// Now actually encode and send
|
||||||
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&
|
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&
|
||||||
|
@@ -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.
|
||||||
//
|
//
|
||||||
|
@@ -115,12 +115,12 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_bool(19, this->api_encryption_supported);
|
buffer.encode_bool(19, this->api_encryption_supported);
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
for (auto &it : this->devices) {
|
for (const auto &it : this->devices) {
|
||||||
buffer.encode_message(20, it, true);
|
buffer.encode_message(20, it, true);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_AREAS
|
#ifdef USE_AREAS
|
||||||
for (auto &it : this->areas) {
|
for (const auto &it : this->areas) {
|
||||||
buffer.encode_message(21, it, true);
|
buffer.encode_message(21, it, true);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -167,10 +167,14 @@ void DeviceInfoResponse::calculate_size(ProtoSize &size) const {
|
|||||||
size.add_bool(2, this->api_encryption_supported);
|
size.add_bool(2, this->api_encryption_supported);
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
size.add_repeated_message(2, this->devices);
|
for (const auto &it : this->devices) {
|
||||||
|
size.add_message_object_force(2, it);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_AREAS
|
#ifdef USE_AREAS
|
||||||
size.add_repeated_message(2, this->areas);
|
for (const auto &it : this->areas) {
|
||||||
|
size.add_message_object_force(2, it);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_AREAS
|
#ifdef USE_AREAS
|
||||||
size.add_message_object(2, this->area);
|
size.add_message_object(2, this->area);
|
||||||
@@ -2073,15 +2077,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) {
|
||||||
buffer.encode_uint64(3, it, true);
|
if (it != 0) {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -490,7 +490,7 @@ class DeviceInfo : public ProtoMessage {
|
|||||||
class DeviceInfoResponse : public ProtoMessage {
|
class DeviceInfoResponse : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 10;
|
static constexpr uint8_t MESSAGE_TYPE = 10;
|
||||||
static constexpr uint8_t ESTIMATED_SIZE = 211;
|
static constexpr uint8_t ESTIMATED_SIZE = 247;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "device_info_response"; }
|
const char *message_name() const override { return "device_info_response"; }
|
||||||
#endif
|
#endif
|
||||||
@@ -543,10 +543,10 @@ class DeviceInfoResponse : public ProtoMessage {
|
|||||||
bool api_encryption_supported{false};
|
bool api_encryption_supported{false};
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
std::vector<DeviceInfo> devices{};
|
std::array<DeviceInfo, ESPHOME_DEVICE_COUNT> devices{};
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_AREAS
|
#ifdef USE_AREAS
|
||||||
std::vector<AreaInfo> areas{};
|
std::array<AreaInfo, ESPHOME_AREA_COUNT> areas{};
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_AREAS
|
#ifdef USE_AREAS
|
||||||
AreaInfo area{};
|
AreaInfo area{};
|
||||||
@@ -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
|
||||||
|
@@ -56,6 +56,14 @@ class CustomAPIDevice {
|
|||||||
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
|
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
|
||||||
global_api_server->register_user_service(service);
|
global_api_server->register_user_service(service);
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
template<typename T, typename... Ts>
|
||||||
|
void register_service(void (T::*callback)(Ts...), const std::string &name,
|
||||||
|
const std::array<std::string, sizeof...(Ts)> &arg_names) {
|
||||||
|
static_assert(
|
||||||
|
sizeof(T) == 0,
|
||||||
|
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/** Register a custom native API service that will show up in Home Assistant.
|
/** Register a custom native API service that will show up in Home Assistant.
|
||||||
@@ -81,6 +89,12 @@ class CustomAPIDevice {
|
|||||||
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
|
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
|
||||||
global_api_server->register_user_service(service);
|
global_api_server->register_user_service(service);
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
|
||||||
|
static_assert(
|
||||||
|
sizeof(T) == 0,
|
||||||
|
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
@@ -135,6 +149,22 @@ class CustomAPIDevice {
|
|||||||
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
|
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
|
||||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
|
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
template<typename T>
|
||||||
|
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
|
||||||
|
const std::string &attribute = "") {
|
||||||
|
static_assert(sizeof(T) == 0,
|
||||||
|
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
|
||||||
|
"of your YAML configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
|
||||||
|
const std::string &attribute = "") {
|
||||||
|
static_assert(sizeof(T) == 0,
|
||||||
|
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
|
||||||
|
"of your YAML configuration");
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
@@ -222,6 +252,28 @@ class CustomAPIDevice {
|
|||||||
}
|
}
|
||||||
global_api_server->send_homeassistant_service_call(resp);
|
global_api_server->send_homeassistant_service_call(resp);
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
template<typename T = void> void call_homeassistant_service(const std::string &service_name) {
|
||||||
|
static_assert(sizeof(T) == 0, "call_homeassistant_service() requires 'homeassistant_services: true' in the 'api:' "
|
||||||
|
"section of your YAML configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T = void>
|
||||||
|
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||||
|
static_assert(sizeof(T) == 0, "call_homeassistant_service() requires 'homeassistant_services: true' in the 'api:' "
|
||||||
|
"section of your YAML configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T = void> void fire_homeassistant_event(const std::string &event_name) {
|
||||||
|
static_assert(sizeof(T) == 0, "fire_homeassistant_event() requires 'homeassistant_services: true' in the 'api:' "
|
||||||
|
"section of your YAML configuration");
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T = void>
|
||||||
|
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||||
|
static_assert(sizeof(T) == 0, "fire_homeassistant_event() requires 'homeassistant_services: true' in the 'api:' "
|
||||||
|
"section of your YAML configuration");
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -654,7 +654,6 @@ async def binary_sensor_is_off_to_code(config, condition_id, template_arg, args)
|
|||||||
|
|
||||||
@coroutine_with_priority(100.0)
|
@coroutine_with_priority(100.0)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_define("USE_BINARY_SENSOR")
|
|
||||||
cg.add_global(binary_sensor_ns.using)
|
cg.add_global(binary_sensor_ns.using)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -175,8 +175,7 @@ BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema(
|
|||||||
)
|
)
|
||||||
async def ble_disconnect_to_code(config, action_id, template_arg, args):
|
async def ble_disconnect_to_code(config, action_id, template_arg, args):
|
||||||
parent = await cg.get_variable(config[CONF_ID])
|
parent = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, parent)
|
return cg.new_Pvariable(action_id, template_arg, parent)
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
@automation.register_action(
|
@automation.register_action(
|
||||||
@@ -184,8 +183,7 @@ async def ble_disconnect_to_code(config, action_id, template_arg, args):
|
|||||||
)
|
)
|
||||||
async def ble_connect_to_code(config, action_id, template_arg, args):
|
async def ble_connect_to_code(config, action_id, template_arg, args):
|
||||||
parent = await cg.get_variable(config[CONF_ID])
|
parent = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, parent)
|
return cg.new_Pvariable(action_id, template_arg, parent)
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
@automation.register_action(
|
@automation.register_action(
|
||||||
@@ -282,9 +280,7 @@ async def passkey_reply_to_code(config, action_id, template_arg, args):
|
|||||||
)
|
)
|
||||||
async def remove_bond_to_code(config, action_id, template_arg, args):
|
async def remove_bond_to_code(config, action_id, template_arg, args):
|
||||||
parent = await cg.get_variable(config[CONF_ID])
|
parent = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, parent)
|
return cg.new_Pvariable(action_id, template_arg, parent)
|
||||||
|
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
|
@@ -1,14 +1,20 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker
|
from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker
|
||||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||||
from esphome.components.esp32_ble import BTLoggers
|
from esphome.components.esp32_ble import BTLoggers
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ACTIVE, CONF_ID
|
from esphome.const import CONF_ACTIVE, CONF_ID
|
||||||
|
from esphome.core import CORE
|
||||||
|
from esphome.log import AnsiFore, color
|
||||||
|
|
||||||
AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"]
|
AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"]
|
||||||
DEPENDENCIES = ["api", "esp32"]
|
DEPENDENCIES = ["api", "esp32"]
|
||||||
CODEOWNERS = ["@jesserockz"]
|
CODEOWNERS = ["@jesserockz"]
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_CONNECTION_SLOTS = "connection_slots"
|
CONF_CONNECTION_SLOTS = "connection_slots"
|
||||||
CONF_CACHE_SERVICES = "cache_services"
|
CONF_CACHE_SERVICES = "cache_services"
|
||||||
CONF_CONNECTIONS = "connections"
|
CONF_CONNECTIONS = "connections"
|
||||||
@@ -41,6 +47,27 @@ def validate_connections(config):
|
|||||||
esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")(
|
esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")(
|
||||||
config
|
config
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Warn about connection slot waste when using Arduino framework
|
||||||
|
if CORE.using_arduino and connection_slots:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"Bluetooth Proxy with active connections on Arduino framework has suboptimal performance.\n"
|
||||||
|
"If BLE connections fail, they can waste connection slots for 10 seconds because\n"
|
||||||
|
"Arduino doesn't allow configuring the BLE connection timeout (fixed at 30s).\n"
|
||||||
|
"ESP-IDF framework allows setting it to 20s to match client timeouts.\n"
|
||||||
|
"\n"
|
||||||
|
"To switch to ESP-IDF, add this to your YAML:\n"
|
||||||
|
" esp32:\n"
|
||||||
|
" framework:\n"
|
||||||
|
" type: esp-idf\n"
|
||||||
|
"\n"
|
||||||
|
"For detailed migration instructions, see:\n"
|
||||||
|
"%s",
|
||||||
|
color(
|
||||||
|
AnsiFore.BLUE, "https://esphome.io/guides/esp32_arduino_to_idf.html"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
**config,
|
**config,
|
||||||
CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)],
|
CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)],
|
||||||
@@ -87,6 +114,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)
|
||||||
|
@@ -37,6 +37,37 @@ static void fill_gatt_uuid(std::array<uint64_t, 2> &uuid_128, uint32_t &short_uu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Constants for size estimation
|
||||||
|
static constexpr uint8_t SERVICE_OVERHEAD_LEGACY = 25; // UUID(20) + handle(4) + overhead(1)
|
||||||
|
static constexpr uint8_t SERVICE_OVERHEAD_EFFICIENT = 10; // UUID(6) + handle(4)
|
||||||
|
static constexpr uint8_t CHAR_SIZE_128BIT = 35; // UUID(20) + handle(4) + props(4) + overhead(7)
|
||||||
|
static constexpr uint8_t DESC_SIZE_128BIT = 25; // UUID(20) + handle(4) + overhead(1)
|
||||||
|
static constexpr uint8_t DESC_SIZE_16BIT = 10; // UUID(6) + handle(4)
|
||||||
|
static constexpr uint8_t DESC_PER_CHAR = 1; // Assume 1 descriptor per characteristic
|
||||||
|
|
||||||
|
// Helper to estimate service size before fetching all data
|
||||||
|
/**
|
||||||
|
* Estimate the size of a Bluetooth service based on the number of characteristics and UUID format.
|
||||||
|
*
|
||||||
|
* @param char_count The number of characteristics in the service.
|
||||||
|
* @param use_efficient_uuids Whether to use efficient UUIDs (16-bit or 32-bit) for newer APIVersions.
|
||||||
|
* @return The estimated size of the service in bytes.
|
||||||
|
*
|
||||||
|
* This function calculates the size of a Bluetooth service by considering:
|
||||||
|
* - A service overhead, which depends on whether efficient UUIDs are used.
|
||||||
|
* - The size of each characteristic, assuming 128-bit UUIDs for safety.
|
||||||
|
* - The size of descriptors, assuming one 128-bit descriptor per characteristic.
|
||||||
|
*/
|
||||||
|
static size_t estimate_service_size(uint16_t char_count, bool use_efficient_uuids) {
|
||||||
|
size_t service_overhead = use_efficient_uuids ? SERVICE_OVERHEAD_EFFICIENT : SERVICE_OVERHEAD_LEGACY;
|
||||||
|
// Always assume 128-bit UUIDs for characteristics to be safe
|
||||||
|
size_t char_size = CHAR_SIZE_128BIT;
|
||||||
|
// Assume one 128-bit descriptor per characteristic
|
||||||
|
size_t desc_size = DESC_SIZE_128BIT * DESC_PER_CHAR;
|
||||||
|
|
||||||
|
return service_overhead + (char_size + desc_size) * char_count;
|
||||||
|
}
|
||||||
|
|
||||||
bool BluetoothConnection::supports_efficient_uuids_() const {
|
bool BluetoothConnection::supports_efficient_uuids_() const {
|
||||||
auto *api_conn = this->proxy_->get_api_connection();
|
auto *api_conn = this->proxy_->get_api_connection();
|
||||||
return api_conn && api_conn->client_supports_api_version(1, 12);
|
return api_conn && api_conn->client_supports_api_version(1, 12);
|
||||||
@@ -47,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();
|
||||||
|
|
||||||
@@ -95,16 +150,21 @@ void BluetoothConnection::send_service_for_discovery_() {
|
|||||||
// Check if client supports efficient UUIDs
|
// Check if client supports efficient UUIDs
|
||||||
bool use_efficient_uuids = this->supports_efficient_uuids_();
|
bool use_efficient_uuids = this->supports_efficient_uuids_();
|
||||||
|
|
||||||
// Prepare response for up to 3 services
|
// Prepare response
|
||||||
api::BluetoothGATTGetServicesResponse resp;
|
api::BluetoothGATTGetServicesResponse resp;
|
||||||
resp.address = this->address_;
|
resp.address = this->address_;
|
||||||
|
|
||||||
// Process up to 3 services in this iteration
|
// Dynamic batching based on actual size
|
||||||
uint8_t services_to_process =
|
// Conservative MTU limit for API messages (accounts for WPA3 overhead)
|
||||||
std::min(MAX_SERVICES_PER_BATCH, static_cast<uint8_t>(this->service_count_ - this->send_service_));
|
static constexpr size_t MAX_PACKET_SIZE = 1360;
|
||||||
resp.services.reserve(services_to_process);
|
|
||||||
|
|
||||||
for (int service_idx = 0; service_idx < services_to_process; service_idx++) {
|
// Keep running total of actual message size
|
||||||
|
size_t current_size = 0;
|
||||||
|
api::ProtoSize size;
|
||||||
|
resp.calculate_size(size);
|
||||||
|
current_size = size.get_size();
|
||||||
|
|
||||||
|
while (this->send_service_ < this->service_count_) {
|
||||||
esp_gattc_service_elem_t service_result;
|
esp_gattc_service_elem_t service_result;
|
||||||
uint16_t service_count = 1;
|
uint16_t service_count = 1;
|
||||||
esp_gatt_status_t service_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr,
|
esp_gatt_status_t service_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr,
|
||||||
@@ -118,15 +178,7 @@ void BluetoothConnection::send_service_for_discovery_() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this->send_service_++;
|
// Get the number of characteristics BEFORE adding to response
|
||||||
resp.services.emplace_back();
|
|
||||||
auto &service_resp = resp.services.back();
|
|
||||||
|
|
||||||
fill_gatt_uuid(service_resp.uuid, service_resp.short_uuid, service_result.uuid, use_efficient_uuids);
|
|
||||||
|
|
||||||
service_resp.handle = service_result.start_handle;
|
|
||||||
|
|
||||||
// Get the number of characteristics directly with one call
|
|
||||||
uint16_t total_char_count = 0;
|
uint16_t total_char_count = 0;
|
||||||
esp_gatt_status_t char_count_status =
|
esp_gatt_status_t char_count_status =
|
||||||
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC,
|
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC,
|
||||||
@@ -139,91 +191,133 @@ void BluetoothConnection::send_service_for_discovery_() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (total_char_count == 0) {
|
// If this service likely won't fit, send current batch (unless it's the first)
|
||||||
// No characteristics, continue to next service
|
size_t estimated_size = estimate_service_size(total_char_count, use_efficient_uuids);
|
||||||
continue;
|
if (!resp.services.empty() && (current_size + estimated_size > MAX_PACKET_SIZE)) {
|
||||||
|
// This service likely won't fit, send current batch
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reserve space and process characteristics
|
// Now add the service since we know it will likely fit
|
||||||
service_resp.characteristics.reserve(total_char_count);
|
resp.services.emplace_back();
|
||||||
uint16_t char_offset = 0;
|
auto &service_resp = resp.services.back();
|
||||||
esp_gattc_char_elem_t char_result;
|
|
||||||
while (true) { // characteristics
|
|
||||||
uint16_t char_count = 1;
|
|
||||||
esp_gatt_status_t char_status =
|
|
||||||
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle,
|
|
||||||
service_result.end_handle, &char_result, &char_count, char_offset);
|
|
||||||
if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (char_status != ESP_GATT_OK) {
|
|
||||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_,
|
|
||||||
this->address_str().c_str(), char_status);
|
|
||||||
this->send_service_ = DONE_SENDING_SERVICES;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (char_count == 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
service_resp.characteristics.emplace_back();
|
fill_gatt_uuid(service_resp.uuid, service_resp.short_uuid, service_result.uuid, use_efficient_uuids);
|
||||||
auto &characteristic_resp = service_resp.characteristics.back();
|
|
||||||
|
|
||||||
fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, char_result.uuid, use_efficient_uuids);
|
service_resp.handle = service_result.start_handle;
|
||||||
|
|
||||||
characteristic_resp.handle = char_result.char_handle;
|
if (total_char_count > 0) {
|
||||||
characteristic_resp.properties = char_result.properties;
|
// Reserve space and process characteristics
|
||||||
char_offset++;
|
service_resp.characteristics.reserve(total_char_count);
|
||||||
|
uint16_t char_offset = 0;
|
||||||
// Get the number of descriptors directly with one call
|
esp_gattc_char_elem_t char_result;
|
||||||
uint16_t total_desc_count = 0;
|
while (true) { // characteristics
|
||||||
esp_gatt_status_t desc_count_status = esp_ble_gattc_get_attr_count(
|
uint16_t char_count = 1;
|
||||||
this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, 0, 0, char_result.char_handle, &total_desc_count);
|
esp_gatt_status_t char_status =
|
||||||
|
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle,
|
||||||
if (desc_count_status != ESP_GATT_OK) {
|
service_result.end_handle, &char_result, &char_count, char_offset);
|
||||||
ESP_LOGE(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", this->connection_index_,
|
if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) {
|
||||||
this->address_str().c_str(), char_result.char_handle, desc_count_status);
|
|
||||||
this->send_service_ = DONE_SENDING_SERVICES;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (total_desc_count == 0) {
|
|
||||||
// No descriptors, continue to next characteristic
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reserve space and process descriptors
|
|
||||||
characteristic_resp.descriptors.reserve(total_desc_count);
|
|
||||||
uint16_t desc_offset = 0;
|
|
||||||
esp_gattc_descr_elem_t desc_result;
|
|
||||||
while (true) { // descriptors
|
|
||||||
uint16_t desc_count = 1;
|
|
||||||
esp_gatt_status_t desc_status = esp_ble_gattc_get_all_descr(
|
|
||||||
this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result, &desc_count, desc_offset);
|
|
||||||
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (desc_status != ESP_GATT_OK) {
|
if (char_status != ESP_GATT_OK) {
|
||||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", this->connection_index_,
|
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_,
|
||||||
this->address_str().c_str(), desc_status);
|
this->address_str().c_str(), char_status);
|
||||||
this->send_service_ = DONE_SENDING_SERVICES;
|
this->send_service_ = DONE_SENDING_SERVICES;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (desc_count == 0) {
|
if (char_count == 0) {
|
||||||
break; // No more descriptors
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
characteristic_resp.descriptors.emplace_back();
|
service_resp.characteristics.emplace_back();
|
||||||
auto &descriptor_resp = characteristic_resp.descriptors.back();
|
auto &characteristic_resp = service_resp.characteristics.back();
|
||||||
|
|
||||||
fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid, desc_result.uuid, use_efficient_uuids);
|
fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, char_result.uuid, use_efficient_uuids);
|
||||||
|
|
||||||
descriptor_resp.handle = desc_result.handle;
|
characteristic_resp.handle = char_result.char_handle;
|
||||||
desc_offset++;
|
characteristic_resp.properties = char_result.properties;
|
||||||
|
char_offset++;
|
||||||
|
|
||||||
|
// Get the number of descriptors directly with one call
|
||||||
|
uint16_t total_desc_count = 0;
|
||||||
|
esp_gatt_status_t desc_count_status = esp_ble_gattc_get_attr_count(
|
||||||
|
this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, 0, 0, char_result.char_handle, &total_desc_count);
|
||||||
|
|
||||||
|
if (desc_count_status != ESP_GATT_OK) {
|
||||||
|
ESP_LOGE(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d",
|
||||||
|
this->connection_index_, this->address_str().c_str(), char_result.char_handle, desc_count_status);
|
||||||
|
this->send_service_ = DONE_SENDING_SERVICES;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (total_desc_count == 0) {
|
||||||
|
// No descriptors, continue to next characteristic
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reserve space and process descriptors
|
||||||
|
characteristic_resp.descriptors.reserve(total_desc_count);
|
||||||
|
uint16_t desc_offset = 0;
|
||||||
|
esp_gattc_descr_elem_t desc_result;
|
||||||
|
while (true) { // descriptors
|
||||||
|
uint16_t desc_count = 1;
|
||||||
|
esp_gatt_status_t desc_status = esp_ble_gattc_get_all_descr(
|
||||||
|
this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result, &desc_count, desc_offset);
|
||||||
|
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (desc_status != ESP_GATT_OK) {
|
||||||
|
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", this->connection_index_,
|
||||||
|
this->address_str().c_str(), desc_status);
|
||||||
|
this->send_service_ = DONE_SENDING_SERVICES;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (desc_count == 0) {
|
||||||
|
break; // No more descriptors
|
||||||
|
}
|
||||||
|
|
||||||
|
characteristic_resp.descriptors.emplace_back();
|
||||||
|
auto &descriptor_resp = characteristic_resp.descriptors.back();
|
||||||
|
|
||||||
|
fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid, desc_result.uuid, use_efficient_uuids);
|
||||||
|
|
||||||
|
descriptor_resp.handle = desc_result.handle;
|
||||||
|
desc_offset++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} // end if (total_char_count > 0)
|
||||||
|
|
||||||
|
// Calculate the actual size of just this service
|
||||||
|
api::ProtoSize service_sizer;
|
||||||
|
service_resp.calculate_size(service_sizer);
|
||||||
|
size_t service_size = service_sizer.get_size() + 1; // +1 for field tag
|
||||||
|
|
||||||
|
// Check if adding this service would exceed the limit
|
||||||
|
if (current_size + service_size > MAX_PACKET_SIZE) {
|
||||||
|
// We would go over - pop the last service if we have more than one
|
||||||
|
if (resp.services.size() > 1) {
|
||||||
|
resp.services.pop_back();
|
||||||
|
ESP_LOGD(TAG, "[%d] [%s] Service %d would exceed limit (current: %d + service: %d > %d), sending current batch",
|
||||||
|
this->connection_index_, this->address_str().c_str(), this->send_service_, current_size, service_size,
|
||||||
|
MAX_PACKET_SIZE);
|
||||||
|
// Don't increment send_service_ - we'll retry this service in next batch
|
||||||
|
} else {
|
||||||
|
// This single service is too large, but we have to send it anyway
|
||||||
|
ESP_LOGV(TAG, "[%d] [%s] Service %d is too large (%d bytes) but sending anyway", this->connection_index_,
|
||||||
|
this->address_str().c_str(), this->send_service_, service_size);
|
||||||
|
// Increment so we don't get stuck
|
||||||
|
this->send_service_++;
|
||||||
|
}
|
||||||
|
// Send what we have
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now we know we're keeping this service, add its size
|
||||||
|
current_size += service_size;
|
||||||
|
// Successfully added this service, increment counter
|
||||||
|
this->send_service_++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the message with 1-3 services
|
// Send the message with dynamically batched services
|
||||||
api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
|
api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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 = BLUETOOTH_PROXY_MAX_CONNECTIONS;
|
||||||
|
this->connections_free_response_.free = BLUETOOTH_PROXY_MAX_CONNECTIONS;
|
||||||
|
|
||||||
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);
|
||||||
@@ -131,26 +134,13 @@ void BluetoothProxy::dump_config() {
|
|||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
" Active: %s\n"
|
" Active: %s\n"
|
||||||
" Connections: %d",
|
" Connections: %d",
|
||||||
YESNO(this->active_), this->connections_.size());
|
YESNO(this->active_), this->connection_count_);
|
||||||
}
|
|
||||||
|
|
||||||
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 (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||||
|
auto *connection = this->connections_[i];
|
||||||
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
|
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
|
||||||
connection->disconnect();
|
connection->disconnect();
|
||||||
}
|
}
|
||||||
@@ -173,7 +163,8 @@ esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_par
|
|||||||
}
|
}
|
||||||
|
|
||||||
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
|
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
|
||||||
for (auto *connection : this->connections_) {
|
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||||
|
auto *connection = this->connections_[i];
|
||||||
if (connection->get_address() == address)
|
if (connection->get_address() == address)
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
@@ -181,7 +172,8 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
|
|||||||
if (!reserve)
|
if (!reserve)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
for (auto *connection : this->connections_) {
|
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||||
|
auto *connection = this->connections_[i];
|
||||||
if (connection->get_address() == 0) {
|
if (connection->get_address() == 0) {
|
||||||
connection->send_service_ = DONE_SENDING_SERVICES;
|
connection->send_service_ = DONE_SENDING_SERVICES;
|
||||||
connection->set_address(address);
|
connection->set_address(address);
|
||||||
@@ -439,17 +431,13 @@ void BluetoothProxy::send_device_connection(uint64_t address, bool connected, ui
|
|||||||
this->api_connection_->send_message(call, api::BluetoothDeviceConnectionResponse::MESSAGE_TYPE);
|
this->api_connection_->send_message(call, api::BluetoothDeviceConnectionResponse::MESSAGE_TYPE);
|
||||||
}
|
}
|
||||||
void BluetoothProxy::send_connections_free() {
|
void BluetoothProxy::send_connections_free() {
|
||||||
if (this->api_connection_ == nullptr)
|
if (this->api_connection_ != nullptr) {
|
||||||
return;
|
this->send_connections_free(this->api_connection_);
|
||||||
api::BluetoothConnectionsFreeResponse call;
|
|
||||||
call.free = this->get_bluetooth_connections_free();
|
|
||||||
call.limit = this->get_bluetooth_connections_limit();
|
|
||||||
for (auto *connection : this->connections_) {
|
|
||||||
if (connection->address_ != 0) {
|
|
||||||
call.allocated.push_back(connection->address_);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this->api_connection_->send_message(call, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE);
|
}
|
||||||
|
|
||||||
|
void BluetoothProxy::send_connections_free(api::APIConnection *api_connection) {
|
||||||
|
api_connection->send_message(this->connections_free_response_, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BluetoothProxy::send_gatt_services_done(uint64_t address) {
|
void BluetoothProxy::send_gatt_services_done(uint64_t address) {
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -22,7 +23,6 @@ namespace esphome::bluetooth_proxy {
|
|||||||
|
|
||||||
static const esp_err_t ESP_GATT_NOT_CONNECTED = -1;
|
static const esp_err_t ESP_GATT_NOT_CONNECTED = -1;
|
||||||
static const int DONE_SENDING_SERVICES = -2;
|
static const int DONE_SENDING_SERVICES = -2;
|
||||||
static const uint8_t MAX_SERVICES_PER_BATCH = 3;
|
|
||||||
|
|
||||||
using namespace esp32_ble_client;
|
using namespace esp32_ble_client;
|
||||||
|
|
||||||
@@ -50,6 +50,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 update connections_free_response_
|
||||||
public:
|
public:
|
||||||
BluetoothProxy();
|
BluetoothProxy();
|
||||||
#ifdef USE_ESP32_BLE_DEVICE
|
#ifdef USE_ESP32_BLE_DEVICE
|
||||||
@@ -63,8 +64,10 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
|
|||||||
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
|
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
|
||||||
|
|
||||||
void register_connection(BluetoothConnection *connection) {
|
void register_connection(BluetoothConnection *connection) {
|
||||||
this->connections_.push_back(connection);
|
if (this->connection_count_ < BLUETOOTH_PROXY_MAX_CONNECTIONS) {
|
||||||
connection->proxy_ = this;
|
this->connections_[this->connection_count_++] = connection;
|
||||||
|
connection->proxy_ = this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void bluetooth_device_request(const api::BluetoothDeviceRequest &msg);
|
void bluetooth_device_request(const api::BluetoothDeviceRequest &msg);
|
||||||
@@ -75,15 +78,13 @@ 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_; }
|
||||||
|
|
||||||
void send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK);
|
void send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK);
|
||||||
void send_connections_free();
|
void send_connections_free();
|
||||||
|
void send_connections_free(api::APIConnection *api_connection);
|
||||||
void send_gatt_services_done(uint64_t address);
|
void send_gatt_services_done(uint64_t address);
|
||||||
void send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error);
|
void send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error);
|
||||||
void send_device_pairing(uint64_t address, bool paired, esp_err_t error = ESP_OK);
|
void send_device_pairing(uint64_t address, bool paired, esp_err_t error = ESP_OK);
|
||||||
@@ -140,8 +141,8 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
|
|||||||
// Group 1: Pointers (4 bytes each, naturally aligned)
|
// Group 1: Pointers (4 bytes each, naturally aligned)
|
||||||
api::APIConnection *api_connection_{nullptr};
|
api::APIConnection *api_connection_{nullptr};
|
||||||
|
|
||||||
// Group 2: Container types (typically 12 bytes on 32-bit)
|
// Group 2: Fixed-size array of connection pointers
|
||||||
std::vector<BluetoothConnection *> connections_{};
|
std::array<BluetoothConnection *, BLUETOOTH_PROXY_MAX_CONNECTIONS> connections_{};
|
||||||
|
|
||||||
// BLE advertisement batching
|
// BLE advertisement batching
|
||||||
std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_;
|
std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_;
|
||||||
@@ -150,10 +151,14 @@ 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};
|
||||||
// 2 bytes used, 2 bytes padding
|
uint8_t connection_count_{0};
|
||||||
|
// 3 bytes used, 1 byte padding
|
||||||
};
|
};
|
||||||
|
|
||||||
extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
@@ -137,4 +137,3 @@ async def button_press_to_code(config, action_id, template_arg, args):
|
|||||||
@coroutine_with_priority(100.0)
|
@coroutine_with_priority(100.0)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_global(button_ns.using)
|
cg.add_global(button_ns.using)
|
||||||
cg.add_define("USE_BUTTON")
|
|
||||||
|
@@ -519,5 +519,4 @@ async def climate_control_to_code(config, action_id, template_arg, args):
|
|||||||
|
|
||||||
@coroutine_with_priority(100.0)
|
@coroutine_with_priority(100.0)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_define("USE_CLIMATE")
|
|
||||||
cg.add_global(climate_ns.using)
|
cg.add_global(climate_ns.using)
|
||||||
|
@@ -265,5 +265,4 @@ async def cover_control_to_code(config, action_id, template_arg, args):
|
|||||||
|
|
||||||
@coroutine_with_priority(100.0)
|
@coroutine_with_priority(100.0)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_define("USE_COVER")
|
|
||||||
cg.add_global(cover_ns.using)
|
cg.add_global(cover_ns.using)
|
||||||
|
@@ -164,7 +164,6 @@ async def register_datetime(var, config):
|
|||||||
cg.add(getattr(cg.App, f"register_{entity_type}")(var))
|
cg.add(getattr(cg.App, f"register_{entity_type}")(var))
|
||||||
CORE.register_platform_component(entity_type, var)
|
CORE.register_platform_component(entity_type, var)
|
||||||
await setup_datetime_core_(var, config)
|
await setup_datetime_core_(var, config)
|
||||||
cg.add_define(f"USE_DATETIME_{config[CONF_TYPE]}")
|
|
||||||
|
|
||||||
|
|
||||||
async def new_datetime(config, *args):
|
async def new_datetime(config, *args):
|
||||||
@@ -175,7 +174,6 @@ async def new_datetime(config, *args):
|
|||||||
|
|
||||||
@coroutine_with_priority(100.0)
|
@coroutine_with_priority(100.0)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_define("USE_DATETIME")
|
|
||||||
cg.add_global(datetime_ns.using)
|
cg.add_global(datetime_ns.using)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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
|
|
||||||
|
|
||||||
pio_flash_size_key = "board_upload.flash_size"
|
|
||||||
pio_partitions_key = "board_build.partitions"
|
|
||||||
if CONF_PARTITIONS in config and pio_partitions_key in pio_options:
|
|
||||||
raise cv.Invalid(
|
|
||||||
f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32"
|
|
||||||
)
|
|
||||||
|
|
||||||
if pio_flash_size_key in pio_options:
|
|
||||||
raise cv.Invalid(
|
|
||||||
f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
errs = []
|
||||||
|
full_config = fv.full_config.get()
|
||||||
|
if pio_options := full_config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS):
|
||||||
|
pio_flash_size_key = "board_upload.flash_size"
|
||||||
|
pio_partitions_key = "board_build.partitions"
|
||||||
|
if CONF_PARTITIONS in config and pio_partitions_key in pio_options:
|
||||||
|
errs.append(
|
||||||
|
cv.Invalid(
|
||||||
|
f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if pio_flash_size_key in pio_options:
|
||||||
|
errs.append(
|
||||||
|
cv.Invalid(
|
||||||
|
f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only"
|
||||||
|
)
|
||||||
|
)
|
||||||
if (
|
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(
|
||||||
@@ -651,6 +680,64 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _FrameworkMigrationWarning:
|
||||||
|
shown = False
|
||||||
|
|
||||||
|
|
||||||
|
def _show_framework_migration_message(name: str, variant: str) -> None:
|
||||||
|
"""Show a friendly message about framework migration when defaulting to Arduino."""
|
||||||
|
if _FrameworkMigrationWarning.shown:
|
||||||
|
return
|
||||||
|
_FrameworkMigrationWarning.shown = True
|
||||||
|
|
||||||
|
from esphome.log import AnsiFore, color
|
||||||
|
|
||||||
|
message = (
|
||||||
|
color(
|
||||||
|
AnsiFore.BOLD_CYAN,
|
||||||
|
f"💡 IMPORTANT: {name} doesn't have a framework specified!",
|
||||||
|
)
|
||||||
|
+ "\n\n"
|
||||||
|
+ f"Currently, {variant} defaults to the Arduino framework.\n"
|
||||||
|
+ color(AnsiFore.YELLOW, "This will change to ESP-IDF in ESPHome 2026.1.0.\n")
|
||||||
|
+ "\n"
|
||||||
|
+ "Note: Newer ESP32 variants (C6, H2, P4, etc.) already use ESP-IDF by default.\n"
|
||||||
|
+ "\n"
|
||||||
|
+ "Why change? ESP-IDF offers:\n"
|
||||||
|
+ color(AnsiFore.GREEN, " ✨ Up to 40% smaller binaries\n")
|
||||||
|
+ color(AnsiFore.GREEN, " 🚀 Better performance and optimization\n")
|
||||||
|
+ color(AnsiFore.GREEN, " 📦 Custom-built firmware for your exact needs\n")
|
||||||
|
+ color(
|
||||||
|
AnsiFore.GREEN,
|
||||||
|
" 🔧 Active development and testing by ESPHome developers\n",
|
||||||
|
)
|
||||||
|
+ "\n"
|
||||||
|
+ "Trade-offs:\n"
|
||||||
|
+ color(AnsiFore.YELLOW, " ⏱️ Compile times are ~25% longer\n")
|
||||||
|
+ color(AnsiFore.YELLOW, " 🔄 Some components need migration\n")
|
||||||
|
+ "\n"
|
||||||
|
+ "What should I do?\n"
|
||||||
|
+ color(AnsiFore.CYAN, " Option 1")
|
||||||
|
+ ": Migrate to ESP-IDF (recommended)\n"
|
||||||
|
+ " Add this to your YAML under 'esp32:':\n"
|
||||||
|
+ color(AnsiFore.WHITE, " framework:\n")
|
||||||
|
+ color(AnsiFore.WHITE, " type: esp-idf\n")
|
||||||
|
+ "\n"
|
||||||
|
+ color(AnsiFore.CYAN, " Option 2")
|
||||||
|
+ ": Keep using Arduino (still supported)\n"
|
||||||
|
+ " Add this to your YAML under 'esp32:':\n"
|
||||||
|
+ color(AnsiFore.WHITE, " framework:\n")
|
||||||
|
+ color(AnsiFore.WHITE, " type: arduino\n")
|
||||||
|
+ "\n"
|
||||||
|
+ "Need help? Check out the migration guide:\n"
|
||||||
|
+ color(
|
||||||
|
AnsiFore.BLUE,
|
||||||
|
"https://esphome.io/guides/esp32_arduino_to_idf.html",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_LOGGER.warning(message)
|
||||||
|
|
||||||
|
|
||||||
def _set_default_framework(config):
|
def _set_default_framework(config):
|
||||||
if CONF_FRAMEWORK not in config:
|
if CONF_FRAMEWORK not in config:
|
||||||
config = config.copy()
|
config = config.copy()
|
||||||
@@ -659,6 +746,10 @@ def _set_default_framework(config):
|
|||||||
if variant in ARDUINO_ALLOWED_VARIANTS:
|
if variant in ARDUINO_ALLOWED_VARIANTS:
|
||||||
config[CONF_FRAMEWORK] = ARDUINO_FRAMEWORK_SCHEMA({})
|
config[CONF_FRAMEWORK] = ARDUINO_FRAMEWORK_SCHEMA({})
|
||||||
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO
|
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO
|
||||||
|
# Show the migration message
|
||||||
|
_show_framework_migration_message(
|
||||||
|
config.get(CONF_NAME, "This device"), variant
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
config[CONF_FRAMEWORK] = ESP_IDF_FRAMEWORK_SCHEMA({})
|
config[CONF_FRAMEWORK] = ESP_IDF_FRAMEWORK_SCHEMA({})
|
||||||
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF
|
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF
|
||||||
@@ -792,6 +883,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
|
||||||
@@ -892,7 +986,7 @@ def get_arduino_partition_csv(flash_size):
|
|||||||
eeprom_partition_start = app1_partition_start + app_partition_size
|
eeprom_partition_start = app1_partition_start + app_partition_size
|
||||||
spiffs_partition_start = eeprom_partition_start + eeprom_partition_size
|
spiffs_partition_start = eeprom_partition_start + eeprom_partition_size
|
||||||
|
|
||||||
partition_csv = f"""\
|
return f"""\
|
||||||
nvs, data, nvs, 0x9000, 0x5000,
|
nvs, data, nvs, 0x9000, 0x5000,
|
||||||
otadata, data, ota, 0xE000, 0x2000,
|
otadata, data, ota, 0xE000, 0x2000,
|
||||||
app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X},
|
app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X},
|
||||||
@@ -900,20 +994,18 @@ app1, app, ota_1, 0x{app1_partition_start:X}, 0x{app_partition_size:X},
|
|||||||
eeprom, data, 0x99, 0x{eeprom_partition_start:X}, 0x{eeprom_partition_size:X},
|
eeprom, data, 0x99, 0x{eeprom_partition_start:X}, 0x{eeprom_partition_size:X},
|
||||||
spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X}
|
spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X}
|
||||||
"""
|
"""
|
||||||
return partition_csv
|
|
||||||
|
|
||||||
|
|
||||||
def get_idf_partition_csv(flash_size):
|
def get_idf_partition_csv(flash_size):
|
||||||
app_partition_size = APP_PARTITION_SIZES[flash_size]
|
app_partition_size = APP_PARTITION_SIZES[flash_size]
|
||||||
|
|
||||||
partition_csv = f"""\
|
return f"""\
|
||||||
otadata, data, ota, , 0x2000,
|
otadata, data, ota, , 0x2000,
|
||||||
phy_init, data, phy, , 0x1000,
|
phy_init, data, phy, , 0x1000,
|
||||||
app0, app, ota_0, , 0x{app_partition_size:X},
|
app0, app, ota_0, , 0x{app_partition_size:X},
|
||||||
app1, app, ota_1, , 0x{app_partition_size:X},
|
app1, app, ota_1, , 0x{app_partition_size:X},
|
||||||
nvs, data, nvs, , 0x6D000,
|
nvs, data, nvs, , 0x6D000,
|
||||||
"""
|
"""
|
||||||
return partition_csv
|
|
||||||
|
|
||||||
|
|
||||||
def _format_sdkconfig_val(value: SdkconfigValueType) -> str:
|
def _format_sdkconfig_val(value: SdkconfigValueType) -> str:
|
||||||
|
@@ -187,8 +187,7 @@ def validate_supports(value):
|
|||||||
"Open-drain only works with output mode", [CONF_MODE, CONF_OPEN_DRAIN]
|
"Open-drain only works with output mode", [CONF_MODE, CONF_OPEN_DRAIN]
|
||||||
)
|
)
|
||||||
|
|
||||||
value = _esp32_validations[variant].usage_validation(value)
|
return _esp32_validations[variant].usage_validation(value)
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
# https://docs.espressif.com/projects/esp-idf/en/v3.3.5/api-reference/peripherals/gpio.html#_CPPv416gpio_drive_cap_t
|
# https://docs.espressif.com/projects/esp-idf/en/v3.3.5/api-reference/peripherals/gpio.html#_CPPv416gpio_drive_cap_t
|
||||||
|
@@ -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
|
||||||
|
@@ -93,8 +93,8 @@ def merge_factory_bin(source, target, env):
|
|||||||
"esptool",
|
"esptool",
|
||||||
"--chip",
|
"--chip",
|
||||||
chip,
|
chip,
|
||||||
"merge_bin",
|
"merge-bin",
|
||||||
"--flash_size",
|
"--flash-size",
|
||||||
flash_size,
|
flash_size,
|
||||||
"--output",
|
"--output",
|
||||||
str(output_path),
|
str(output_path),
|
||||||
@@ -110,7 +110,7 @@ def merge_factory_bin(source, target, env):
|
|||||||
if result == 0:
|
if result == 0:
|
||||||
print(f"Successfully created {output_path}")
|
print(f"Successfully created {output_path}")
|
||||||
else:
|
else:
|
||||||
print(f"Error: esptool merge_bin failed with code {result}")
|
print(f"Error: esptool merge-bin failed with code {result}")
|
||||||
|
|
||||||
|
|
||||||
def esp32_copy_ota_bin(source, target, env):
|
def esp32_copy_ota_bin(source, target, env):
|
||||||
|
@@ -6,7 +6,7 @@ import esphome.codegen as cg
|
|||||||
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
|
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ESPHOME, CONF_ID, CONF_NAME
|
from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ESPHOME, CONF_ID, CONF_NAME
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE, TimePeriod
|
||||||
from esphome.core.config import CONF_NAME_ADD_MAC_SUFFIX
|
from esphome.core.config import CONF_NAME_ADD_MAC_SUFFIX
|
||||||
import esphome.final_validate as fv
|
import esphome.final_validate as fv
|
||||||
|
|
||||||
@@ -117,6 +117,7 @@ CONF_BLE_ID = "ble_id"
|
|||||||
CONF_IO_CAPABILITY = "io_capability"
|
CONF_IO_CAPABILITY = "io_capability"
|
||||||
CONF_ADVERTISING_CYCLE_TIME = "advertising_cycle_time"
|
CONF_ADVERTISING_CYCLE_TIME = "advertising_cycle_time"
|
||||||
CONF_DISABLE_BT_LOGS = "disable_bt_logs"
|
CONF_DISABLE_BT_LOGS = "disable_bt_logs"
|
||||||
|
CONF_CONNECTION_TIMEOUT = "connection_timeout"
|
||||||
|
|
||||||
NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2]
|
NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2]
|
||||||
|
|
||||||
@@ -167,6 +168,11 @@ CONFIG_SCHEMA = cv.Schema(
|
|||||||
cv.SplitDefault(CONF_DISABLE_BT_LOGS, esp32_idf=True): cv.All(
|
cv.SplitDefault(CONF_DISABLE_BT_LOGS, esp32_idf=True): cv.All(
|
||||||
cv.only_with_esp_idf, cv.boolean
|
cv.only_with_esp_idf, cv.boolean
|
||||||
),
|
),
|
||||||
|
cv.SplitDefault(CONF_CONNECTION_TIMEOUT, esp32_idf="20s"): cv.All(
|
||||||
|
cv.only_with_esp_idf,
|
||||||
|
cv.positive_time_period_seconds,
|
||||||
|
cv.Range(min=TimePeriod(seconds=10), max=TimePeriod(seconds=180)),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
).extend(cv.COMPONENT_SCHEMA)
|
).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
|
||||||
@@ -255,6 +261,17 @@ async def to_code(config):
|
|||||||
if logger not in _required_loggers:
|
if logger not in _required_loggers:
|
||||||
add_idf_sdkconfig_option(f"{logger.value}_NONE", True)
|
add_idf_sdkconfig_option(f"{logger.value}_NONE", True)
|
||||||
|
|
||||||
|
# Set BLE connection establishment timeout to match aioesphomeapi/bleak-retry-connector
|
||||||
|
# Default is 20 seconds instead of ESP-IDF's 30 seconds. Because there is no way to
|
||||||
|
# cancel a BLE connection in progress, when aioesphomeapi times out at 20 seconds,
|
||||||
|
# the connection slot remains occupied for the remaining time, preventing new connection
|
||||||
|
# attempts and wasting valuable connection slots.
|
||||||
|
if CONF_CONNECTION_TIMEOUT in config:
|
||||||
|
timeout_seconds = int(config[CONF_CONNECTION_TIMEOUT].total_seconds)
|
||||||
|
add_idf_sdkconfig_option(
|
||||||
|
"CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT", timeout_seconds
|
||||||
|
)
|
||||||
|
|
||||||
cg.add_define("USE_ESP32_BLE")
|
cg.add_define("USE_ESP32_BLE")
|
||||||
|
|
||||||
|
|
||||||
|
@@ -23,21 +23,14 @@
|
|||||||
|
|
||||||
namespace esphome::esp32_ble {
|
namespace esphome::esp32_ble {
|
||||||
|
|
||||||
// Maximum number of BLE scan results to buffer
|
// Maximum size of the BLE event queue
|
||||||
// Sized to handle bursts of advertisements while allowing for processing delays
|
// Increased to absorb the ring buffer capacity from esp32_ble_tracker
|
||||||
// With 16 advertisements per batch and some safety margin:
|
|
||||||
// - Without PSRAM: 24 entries (1.5× batch size)
|
|
||||||
// - With PSRAM: 36 entries (2.25× batch size)
|
|
||||||
// The reduced structure size (~80 bytes vs ~400 bytes) allows for larger buffers
|
|
||||||
#ifdef USE_PSRAM
|
#ifdef USE_PSRAM
|
||||||
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 36;
|
static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 100; // 64 + 36 (ring buffer size with PSRAM)
|
||||||
#else
|
#else
|
||||||
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 24;
|
static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 88; // 64 + 24 (ring buffer size without PSRAM)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Maximum size of the BLE event queue - must be power of 2 for lock-free queue
|
|
||||||
static constexpr size_t MAX_BLE_QUEUE_SIZE = 64;
|
|
||||||
|
|
||||||
uint64_t ble_addr_to_uint64(const esp_bd_addr_t address);
|
uint64_t ble_addr_to_uint64(const esp_bd_addr_t address);
|
||||||
|
|
||||||
// NOLINTNEXTLINE(modernize-use-using)
|
// NOLINTNEXTLINE(modernize-use-using)
|
||||||
|
@@ -16,8 +16,8 @@ static const char *const TAG = "esp32_ble_client";
|
|||||||
// Intermediate connection parameters for standard operation
|
// Intermediate connection parameters for standard operation
|
||||||
// ESP-IDF defaults (12.5-15ms) are too slow for stable connections through WiFi-based BLE proxies,
|
// ESP-IDF defaults (12.5-15ms) are too slow for stable connections through WiFi-based BLE proxies,
|
||||||
// causing disconnections. These medium parameters balance responsiveness with bandwidth usage.
|
// causing disconnections. These medium parameters balance responsiveness with bandwidth usage.
|
||||||
static const uint16_t MEDIUM_MIN_CONN_INTERVAL = 0x08; // 8 * 1.25ms = 10ms
|
static const uint16_t MEDIUM_MIN_CONN_INTERVAL = 0x07; // 7 * 1.25ms = 8.75ms
|
||||||
static const uint16_t MEDIUM_MAX_CONN_INTERVAL = 0x0A; // 10 * 1.25ms = 12.5ms
|
static const uint16_t MEDIUM_MAX_CONN_INTERVAL = 0x09; // 9 * 1.25ms = 11.25ms
|
||||||
// The timeout value was increased from 6s to 8s to address stability issues observed
|
// The timeout value was increased from 6s to 8s to address stability issues observed
|
||||||
// in certain BLE devices when operating through WiFi-based BLE proxies. The longer
|
// in certain BLE devices when operating through WiFi-based BLE proxies. The longer
|
||||||
// timeout reduces the likelihood of disconnections during periods of high latency.
|
// timeout reduces the likelihood of disconnections during periods of high latency.
|
||||||
@@ -45,8 +45,10 @@ void BLEClientBase::set_state(espbt::ClientState st) {
|
|||||||
ESPBTClient::set_state(st);
|
ESPBTClient::set_state(st);
|
||||||
|
|
||||||
if (st == espbt::ClientState::READY_TO_CONNECT) {
|
if (st == espbt::ClientState::READY_TO_CONNECT) {
|
||||||
// Enable loop when we need to connect
|
// Enable loop for state processing
|
||||||
this->enable_loop();
|
this->enable_loop();
|
||||||
|
// Connect immediately instead of waiting for next loop
|
||||||
|
this->connect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,11 +65,6 @@ void BLEClientBase::loop() {
|
|||||||
}
|
}
|
||||||
this->set_state(espbt::ClientState::IDLE);
|
this->set_state(espbt::ClientState::IDLE);
|
||||||
}
|
}
|
||||||
// READY_TO_CONNECT means we have discovered the device
|
|
||||||
// and the scanner has been stopped by the tracker.
|
|
||||||
else if (this->state_ == espbt::ClientState::READY_TO_CONNECT) {
|
|
||||||
this->connect();
|
|
||||||
}
|
|
||||||
// If its idle, we can disable the loop as set_state
|
// If its idle, we can disable the loop as set_state
|
||||||
// will enable it again when we need to connect.
|
// will enable it again when we need to connect.
|
||||||
else if (this->state_ == espbt::ClientState::IDLE) {
|
else if (this->state_ == espbt::ClientState::IDLE) {
|
||||||
@@ -148,6 +145,36 @@ void BLEClientBase::connect() {
|
|||||||
this->remote_addr_type_);
|
this->remote_addr_type_);
|
||||||
this->paired_ = false;
|
this->paired_ = false;
|
||||||
|
|
||||||
|
// Set preferred connection parameters before connecting
|
||||||
|
// Use FAST for all V3 connections (better latency and reliability)
|
||||||
|
// Use MEDIUM for V1/legacy connections (balanced performance)
|
||||||
|
uint16_t min_interval, max_interval, timeout;
|
||||||
|
const char *param_type;
|
||||||
|
|
||||||
|
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
|
||||||
|
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||||
|
min_interval = FAST_MIN_CONN_INTERVAL;
|
||||||
|
max_interval = FAST_MAX_CONN_INTERVAL;
|
||||||
|
timeout = FAST_CONN_TIMEOUT;
|
||||||
|
param_type = "fast";
|
||||||
|
} else {
|
||||||
|
min_interval = MEDIUM_MIN_CONN_INTERVAL;
|
||||||
|
max_interval = MEDIUM_MAX_CONN_INTERVAL;
|
||||||
|
timeout = MEDIUM_CONN_TIMEOUT;
|
||||||
|
param_type = "medium";
|
||||||
|
}
|
||||||
|
|
||||||
|
auto param_ret = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval,
|
||||||
|
0, // latency: 0
|
||||||
|
timeout);
|
||||||
|
if (param_ret != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gap_set_prefer_conn_params failed: %d", this->connection_index_,
|
||||||
|
this->address_str_.c_str(), param_ret);
|
||||||
|
} else {
|
||||||
|
ESP_LOGD(TAG, "[%d] [%s] Set %s conn params", this->connection_index_, this->address_str_.c_str(), param_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now open the connection
|
||||||
auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true);
|
auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true);
|
||||||
if (ret) {
|
if (ret) {
|
||||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_open error, status=%d", this->connection_index_, this->address_str_.c_str(),
|
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_open error, status=%d", this->connection_index_, this->address_str_.c_str(),
|
||||||
@@ -155,34 +182,6 @@ void BLEClientBase::connect() {
|
|||||||
this->set_state(espbt::ClientState::IDLE);
|
this->set_state(espbt::ClientState::IDLE);
|
||||||
} else {
|
} else {
|
||||||
this->set_state(espbt::ClientState::CONNECTING);
|
this->set_state(espbt::ClientState::CONNECTING);
|
||||||
|
|
||||||
// Always set connection parameters to ensure stable operation
|
|
||||||
// Use FAST for V3_WITHOUT_CACHE (devices that need lowest latency)
|
|
||||||
// Use MEDIUM for all other connections (balanced performance)
|
|
||||||
uint16_t min_interval, max_interval, timeout;
|
|
||||||
const char *param_type;
|
|
||||||
|
|
||||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
|
|
||||||
min_interval = FAST_MIN_CONN_INTERVAL;
|
|
||||||
max_interval = FAST_MAX_CONN_INTERVAL;
|
|
||||||
timeout = FAST_CONN_TIMEOUT;
|
|
||||||
param_type = "fast";
|
|
||||||
} else {
|
|
||||||
min_interval = MEDIUM_MIN_CONN_INTERVAL;
|
|
||||||
max_interval = MEDIUM_MAX_CONN_INTERVAL;
|
|
||||||
timeout = MEDIUM_CONN_TIMEOUT;
|
|
||||||
param_type = "medium";
|
|
||||||
}
|
|
||||||
|
|
||||||
auto param_ret = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval,
|
|
||||||
0, // latency: 0
|
|
||||||
timeout);
|
|
||||||
if (param_ret != ESP_OK) {
|
|
||||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gap_set_prefer_conn_params failed: %d", this->connection_index_,
|
|
||||||
this->address_str_.c_str(), param_ret);
|
|
||||||
} else {
|
|
||||||
ESP_LOGD(TAG, "[%d] [%s] Set %s conn params", this->connection_index_, this->address_str_.c_str(), param_type);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,6 +256,19 @@ void BLEClientBase::log_event_(const char *name) {
|
|||||||
ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name);
|
ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BLEClientBase::restore_medium_conn_params_() {
|
||||||
|
// Restore to medium connection parameters after initial connection phase
|
||||||
|
// This balances performance with bandwidth usage for normal operation
|
||||||
|
esp_ble_conn_update_params_t conn_params = {{0}};
|
||||||
|
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
|
||||||
|
conn_params.min_int = MEDIUM_MIN_CONN_INTERVAL;
|
||||||
|
conn_params.max_int = MEDIUM_MAX_CONN_INTERVAL;
|
||||||
|
conn_params.latency = 0;
|
||||||
|
conn_params.timeout = MEDIUM_CONN_TIMEOUT;
|
||||||
|
ESP_LOGD(TAG, "[%d] [%s] Restoring medium conn params", this->connection_index_, this->address_str_.c_str());
|
||||||
|
esp_ble_gap_update_conn_params(&conn_params);
|
||||||
|
}
|
||||||
|
|
||||||
bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
|
bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
|
||||||
esp_ble_gattc_cb_param_t *param) {
|
esp_ble_gattc_cb_param_t *param) {
|
||||||
if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id)
|
if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id)
|
||||||
@@ -285,7 +297,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
|||||||
if (!this->check_addr(param->open.remote_bda))
|
if (!this->check_addr(param->open.remote_bda))
|
||||||
return false;
|
return false;
|
||||||
this->log_event_("ESP_GATTC_OPEN_EVT");
|
this->log_event_("ESP_GATTC_OPEN_EVT");
|
||||||
this->conn_id_ = param->open.conn_id;
|
// conn_id was already set in ESP_GATTC_CONNECT_EVT
|
||||||
this->service_count_ = 0;
|
this->service_count_ = 0;
|
||||||
if (this->state_ != espbt::ClientState::CONNECTING) {
|
if (this->state_ != espbt::ClientState::CONNECTING) {
|
||||||
// This should not happen but lets log it in case it does
|
// This should not happen but lets log it in case it does
|
||||||
@@ -319,15 +331,15 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
|||||||
this->conn_id_ = UNSET_CONN_ID;
|
this->conn_id_ = UNSET_CONN_ID;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->open.conn_id);
|
// MTU negotiation already started in ESP_GATTC_CONNECT_EVT
|
||||||
if (ret) {
|
|
||||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_,
|
|
||||||
this->address_str_.c_str(), ret);
|
|
||||||
}
|
|
||||||
this->set_state(espbt::ClientState::CONNECTED);
|
this->set_state(espbt::ClientState::CONNECTED);
|
||||||
ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str());
|
ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str());
|
||||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||||
ESP_LOGI(TAG, "[%d] [%s] Using cached services", this->connection_index_, this->address_str_.c_str());
|
ESP_LOGI(TAG, "[%d] [%s] Using cached services", this->connection_index_, this->address_str_.c_str());
|
||||||
|
|
||||||
|
// Restore to medium connection parameters for cached connections too
|
||||||
|
this->restore_medium_conn_params_();
|
||||||
|
|
||||||
// only set our state, subclients might have more stuff to do yet.
|
// only set our state, subclients might have more stuff to do yet.
|
||||||
this->state_ = espbt::ClientState::ESTABLISHED;
|
this->state_ = espbt::ClientState::ESTABLISHED;
|
||||||
break;
|
break;
|
||||||
@@ -340,6 +352,16 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
|||||||
if (!this->check_addr(param->connect.remote_bda))
|
if (!this->check_addr(param->connect.remote_bda))
|
||||||
return false;
|
return false;
|
||||||
this->log_event_("ESP_GATTC_CONNECT_EVT");
|
this->log_event_("ESP_GATTC_CONNECT_EVT");
|
||||||
|
this->conn_id_ = param->connect.conn_id;
|
||||||
|
// Start MTU negotiation immediately as recommended by ESP-IDF examples
|
||||||
|
// (gatt_client, ble_throughput) which call esp_ble_gattc_send_mtu_req in
|
||||||
|
// ESP_GATTC_CONNECT_EVT instead of waiting for ESP_GATTC_OPEN_EVT.
|
||||||
|
// This saves ~3ms in the connection process.
|
||||||
|
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->connect.conn_id);
|
||||||
|
if (ret) {
|
||||||
|
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_,
|
||||||
|
this->address_str_.c_str(), ret);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ESP_GATTC_DISCONNECT_EVT: {
|
case ESP_GATTC_DISCONNECT_EVT: {
|
||||||
@@ -411,18 +433,11 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
|||||||
}
|
}
|
||||||
ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_.c_str());
|
ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_.c_str());
|
||||||
|
|
||||||
// For non-cached connections, restore to medium connection parameters after service discovery
|
// For V3 connections, restore to medium connection parameters after service discovery
|
||||||
// This balances performance with bandwidth usage after the critical discovery phase
|
// This balances performance with bandwidth usage after the critical discovery phase
|
||||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
|
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
|
||||||
esp_ble_conn_update_params_t conn_params = {{0}};
|
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||||
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
|
this->restore_medium_conn_params_();
|
||||||
conn_params.min_int = MEDIUM_MIN_CONN_INTERVAL;
|
|
||||||
conn_params.max_int = MEDIUM_MAX_CONN_INTERVAL;
|
|
||||||
conn_params.latency = 0;
|
|
||||||
conn_params.timeout = MEDIUM_CONN_TIMEOUT;
|
|
||||||
ESP_LOGD(TAG, "[%d] [%s] Restored medium conn params after service discovery", this->connection_index_,
|
|
||||||
this->address_str_.c_str());
|
|
||||||
esp_ble_gap_update_conn_params(&conn_params);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this->state_ = espbt::ClientState::ESTABLISHED;
|
this->state_ = espbt::ClientState::ESTABLISHED;
|
||||||
|
@@ -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;
|
||||||
@@ -66,7 +66,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
|||||||
(uint8_t) (this->address_ >> 0) & 0xff);
|
(uint8_t) (this->address_ >> 0) & 0xff);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std::string address_str() const { return this->address_str_; }
|
const std::string &address_str() const { return this->address_str_; }
|
||||||
|
|
||||||
BLEService *get_service(espbt::ESPBTUUID uuid);
|
BLEService *get_service(espbt::ESPBTUUID uuid);
|
||||||
BLEService *get_service(uint16_t uuid);
|
BLEService *get_service(uint16_t uuid);
|
||||||
@@ -127,6 +127,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
|||||||
// 6 bytes used, 2 bytes padding
|
// 6 bytes used, 2 bytes padding
|
||||||
|
|
||||||
void log_event_(const char *name);
|
void log_event_(const char *name);
|
||||||
|
void restore_medium_conn_params_();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esp32_ble_client
|
} // namespace esp32_ble_client
|
||||||
|
@@ -628,5 +628,4 @@ async def ble_server_descriptor_set_value(config, action_id, template_arg, args)
|
|||||||
)
|
)
|
||||||
async def ble_server_characteristic_notify(config, action_id, template_arg, args):
|
async def ble_server_characteristic_notify(config, action_id, template_arg, args):
|
||||||
paren = await cg.get_variable(config[CONF_ID])
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
return var
|
|
||||||
|
@@ -49,13 +49,6 @@ void ESP32BLETracker::setup() {
|
|||||||
ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE");
|
ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
RAMAllocator<BLEScanResult> allocator;
|
|
||||||
this->scan_ring_buffer_ = allocator.allocate(SCAN_RESULT_BUFFER_SIZE);
|
|
||||||
|
|
||||||
if (this->scan_ring_buffer_ == nullptr) {
|
|
||||||
ESP_LOGE(TAG, "Could not allocate ring buffer for BLE Tracker!");
|
|
||||||
this->mark_failed();
|
|
||||||
}
|
|
||||||
|
|
||||||
global_esp32_ble_tracker = this;
|
global_esp32_ble_tracker = this;
|
||||||
|
|
||||||
@@ -117,77 +110,8 @@ void ESP32BLETracker::loop() {
|
|||||||
}
|
}
|
||||||
bool promote_to_connecting = discovered && !searching && !connecting;
|
bool promote_to_connecting = discovered && !searching && !connecting;
|
||||||
|
|
||||||
// Process scan results from lock-free SPSC ring buffer
|
// All scan result processing is now done immediately in gap_scan_event_handler
|
||||||
// Consumer side: This runs in the main loop thread
|
// No ring buffer processing needed here
|
||||||
if (this->scanner_state_ == ScannerState::RUNNING) {
|
|
||||||
// Load our own index with relaxed ordering (we're the only writer)
|
|
||||||
uint8_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed);
|
|
||||||
|
|
||||||
// Load producer's index with acquire to see their latest writes
|
|
||||||
uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire);
|
|
||||||
|
|
||||||
while (read_idx != write_idx) {
|
|
||||||
// Calculate how many contiguous results we can process in one batch
|
|
||||||
// If write > read: process all results from read to write
|
|
||||||
// If write <= read (wraparound): process from read to end of buffer first
|
|
||||||
size_t batch_size = (write_idx > read_idx) ? (write_idx - read_idx) : (SCAN_RESULT_BUFFER_SIZE - read_idx);
|
|
||||||
|
|
||||||
// Process the batch for raw advertisements
|
|
||||||
if (this->raw_advertisements_) {
|
|
||||||
for (auto *listener : this->listeners_) {
|
|
||||||
listener->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size);
|
|
||||||
}
|
|
||||||
for (auto *client : this->clients_) {
|
|
||||||
client->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process individual results for parsed advertisements
|
|
||||||
if (this->parse_advertisements_) {
|
|
||||||
#ifdef USE_ESP32_BLE_DEVICE
|
|
||||||
for (size_t i = 0; i < batch_size; i++) {
|
|
||||||
BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx + i];
|
|
||||||
ESPBTDevice device;
|
|
||||||
device.parse_scan_rst(scan_result);
|
|
||||||
|
|
||||||
bool found = false;
|
|
||||||
for (auto *listener : this->listeners_) {
|
|
||||||
if (listener->parse_device(device))
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto *client : this->clients_) {
|
|
||||||
if (client->parse_device(device)) {
|
|
||||||
found = true;
|
|
||||||
if (!connecting && client->state() == ClientState::DISCOVERED) {
|
|
||||||
promote_to_connecting = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found && !this->scan_continuous_) {
|
|
||||||
this->print_bt_device_info(device);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif // USE_ESP32_BLE_DEVICE
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update read index for entire batch
|
|
||||||
read_idx = (read_idx + batch_size) % SCAN_RESULT_BUFFER_SIZE;
|
|
||||||
|
|
||||||
// Store with release to ensure reads complete before index update
|
|
||||||
this->ring_read_index_.store(read_idx, std::memory_order_release);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log dropped results periodically
|
|
||||||
size_t dropped = this->scan_results_dropped_.exchange(0, std::memory_order_relaxed);
|
|
||||||
if (dropped > 0) {
|
|
||||||
ESP_LOGW(TAG, "Dropped %zu BLE scan results due to buffer overflow", dropped);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this->scanner_state_ == ScannerState::STOPPED) {
|
|
||||||
this->end_of_scan_(); // Change state to IDLE
|
|
||||||
}
|
|
||||||
if (this->scanner_state_ == ScannerState::FAILED ||
|
if (this->scanner_state_ == ScannerState::FAILED ||
|
||||||
(this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
|
(this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
|
||||||
this->stop_scan_();
|
this->stop_scan_();
|
||||||
@@ -232,8 +156,10 @@ void ESP32BLETracker::loop() {
|
|||||||
}
|
}
|
||||||
// If there is a discovered client and no connecting
|
// If there is a discovered client and no connecting
|
||||||
// clients and no clients using the scanner to search for
|
// clients and no clients using the scanner to search for
|
||||||
// devices, then stop scanning and promote the discovered
|
// devices, then promote the discovered client to ready to connect.
|
||||||
// client to ready to connect.
|
// Note: Scanning is already stopped by gap_scan_event_handler when
|
||||||
|
// a discovered client is found, so we only need to handle promotion
|
||||||
|
// when the scanner is IDLE.
|
||||||
if (promote_to_connecting &&
|
if (promote_to_connecting &&
|
||||||
(this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::IDLE)) {
|
(this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::IDLE)) {
|
||||||
for (auto *client : this->clients_) {
|
for (auto *client : this->clients_) {
|
||||||
@@ -241,19 +167,21 @@ void ESP32BLETracker::loop() {
|
|||||||
if (this->scanner_state_ == ScannerState::RUNNING) {
|
if (this->scanner_state_ == ScannerState::RUNNING) {
|
||||||
ESP_LOGD(TAG, "Stopping scan to make connection");
|
ESP_LOGD(TAG, "Stopping scan to make connection");
|
||||||
this->stop_scan_();
|
this->stop_scan_();
|
||||||
} else if (this->scanner_state_ == ScannerState::IDLE) {
|
// Don't wait for scan stop complete - promote immediately.
|
||||||
ESP_LOGD(TAG, "Promoting client to connect");
|
// This is safe because ESP-IDF processes BLE commands sequentially through its internal mailbox queue.
|
||||||
// We only want to promote one client at a time.
|
// This guarantees that the stop scan command will be fully processed before any subsequent connect command,
|
||||||
// once the scanner is fully stopped.
|
// preventing race conditions or overlapping operations.
|
||||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
|
||||||
ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
|
|
||||||
if (!this->coex_prefer_ble_) {
|
|
||||||
this->coex_prefer_ble_ = true;
|
|
||||||
esp_coex_preference_set(ESP_COEX_PREFER_BT); // Prioritize Bluetooth
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
client->set_state(ClientState::READY_TO_CONNECT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Promoting client to connect");
|
||||||
|
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||||
|
ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
|
||||||
|
if (!this->coex_prefer_ble_) {
|
||||||
|
this->coex_prefer_ble_ = true;
|
||||||
|
esp_coex_preference_set(ESP_COEX_PREFER_BT); // Prioritize Bluetooth
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
client->set_state(ClientState::READY_TO_CONNECT);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,8 +206,6 @@ void ESP32BLETracker::stop_scan_() {
|
|||||||
ESP_LOGE(TAG, "Scan is starting while trying to stop.");
|
ESP_LOGE(TAG, "Scan is starting while trying to stop.");
|
||||||
} else if (this->scanner_state_ == ScannerState::STOPPING) {
|
} else if (this->scanner_state_ == ScannerState::STOPPING) {
|
||||||
ESP_LOGE(TAG, "Scan is already stopping while trying to stop.");
|
ESP_LOGE(TAG, "Scan is already stopping while trying to stop.");
|
||||||
} else if (this->scanner_state_ == ScannerState::STOPPED) {
|
|
||||||
ESP_LOGE(TAG, "Scan is already stopped while trying to stop.");
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -306,8 +232,6 @@ void ESP32BLETracker::start_scan_(bool first) {
|
|||||||
ESP_LOGE(TAG, "Cannot start scan while already stopping.");
|
ESP_LOGE(TAG, "Cannot start scan while already stopping.");
|
||||||
} else if (this->scanner_state_ == ScannerState::FAILED) {
|
} else if (this->scanner_state_ == ScannerState::FAILED) {
|
||||||
ESP_LOGE(TAG, "Cannot start scan while already failed.");
|
ESP_LOGE(TAG, "Cannot start scan while already failed.");
|
||||||
} else if (this->scanner_state_ == ScannerState::STOPPED) {
|
|
||||||
ESP_LOGE(TAG, "Cannot start scan while already stopped.");
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -342,21 +266,6 @@ void ESP32BLETracker::start_scan_(bool first) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ESP32BLETracker::end_of_scan_() {
|
|
||||||
// The lock must be held when calling this function.
|
|
||||||
if (this->scanner_state_ != ScannerState::STOPPED) {
|
|
||||||
ESP_LOGE(TAG, "end_of_scan_ called while scanner is not stopped.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ESP_LOGD(TAG, "End of scan, set scanner state to IDLE.");
|
|
||||||
this->already_discovered_.clear();
|
|
||||||
this->cancel_timeout("scan");
|
|
||||||
|
|
||||||
for (auto *listener : this->listeners_)
|
|
||||||
listener->on_scan_end();
|
|
||||||
this->set_scanner_state_(ScannerState::IDLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ESP32BLETracker::register_client(ESPBTClient *client) {
|
void ESP32BLETracker::register_client(ESPBTClient *client) {
|
||||||
client->app_id = ++this->app_id_;
|
client->app_id = ++this->app_id_;
|
||||||
this->clients_.push_back(client);
|
this->clients_.push_back(client);
|
||||||
@@ -389,6 +298,8 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
|
void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
|
||||||
|
// Note: This handler is called from the main loop context, not directly from the BT task.
|
||||||
|
// The esp32_ble component queues events via enqueue_ble_event() and processes them in loop().
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
|
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
|
||||||
this->gap_scan_set_param_complete_(param->scan_param_cmpl);
|
this->gap_scan_set_param_complete_(param->scan_param_cmpl);
|
||||||
@@ -409,30 +320,19 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
|
void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
|
||||||
|
// Note: This handler is called from the main loop context via esp32_ble's event queue.
|
||||||
|
// We process advertisements immediately instead of buffering them.
|
||||||
ESP_LOGV(TAG, "gap_scan_result - event %d", scan_result.search_evt);
|
ESP_LOGV(TAG, "gap_scan_result - event %d", scan_result.search_evt);
|
||||||
|
|
||||||
if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
|
if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
|
||||||
// Lock-free SPSC ring buffer write (Producer side)
|
// Process the scan result immediately
|
||||||
// This runs in the ESP-IDF Bluetooth stack callback thread
|
bool found_discovered_client = this->process_scan_result_(scan_result);
|
||||||
// IMPORTANT: Only this thread writes to ring_write_index_
|
|
||||||
|
|
||||||
// Load our own index with relaxed ordering (we're the only writer)
|
// If we found a discovered client that needs promotion, stop scanning
|
||||||
uint8_t write_idx = this->ring_write_index_.load(std::memory_order_relaxed);
|
// This replaces the promote_to_connecting logic from loop()
|
||||||
uint8_t next_write_idx = (write_idx + 1) % SCAN_RESULT_BUFFER_SIZE;
|
if (found_discovered_client && this->scanner_state_ == ScannerState::RUNNING) {
|
||||||
|
ESP_LOGD(TAG, "Found discovered client, stopping scan for connection");
|
||||||
// Load consumer's index with acquire to see their latest updates
|
this->stop_scan_();
|
||||||
uint8_t read_idx = this->ring_read_index_.load(std::memory_order_acquire);
|
|
||||||
|
|
||||||
// Check if buffer is full
|
|
||||||
if (next_write_idx != read_idx) {
|
|
||||||
// Write to ring buffer
|
|
||||||
this->scan_ring_buffer_[write_idx] = scan_result;
|
|
||||||
|
|
||||||
// Store with release to ensure the write is visible before index update
|
|
||||||
this->ring_write_index_.store(next_write_idx, std::memory_order_release);
|
|
||||||
} else {
|
|
||||||
// Buffer full, track dropped results
|
|
||||||
this->scan_results_dropped_.fetch_add(1, std::memory_order_relaxed);
|
|
||||||
}
|
}
|
||||||
} else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) {
|
} else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) {
|
||||||
// Scan finished on its own
|
// Scan finished on its own
|
||||||
@@ -445,15 +345,15 @@ void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
|
|||||||
ESP_LOGE(TAG, "Scan was in failed state when scan completed.");
|
ESP_LOGE(TAG, "Scan was in failed state when scan completed.");
|
||||||
} else if (this->scanner_state_ == ScannerState::IDLE) {
|
} else if (this->scanner_state_ == ScannerState::IDLE) {
|
||||||
ESP_LOGE(TAG, "Scan was idle when scan completed.");
|
ESP_LOGE(TAG, "Scan was idle when scan completed.");
|
||||||
} else if (this->scanner_state_ == ScannerState::STOPPED) {
|
|
||||||
ESP_LOGE(TAG, "Scan was stopped when scan completed.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this->set_scanner_state_(ScannerState::STOPPED);
|
// Scan completed naturally, perform cleanup and transition to IDLE
|
||||||
|
this->cleanup_scan_state_(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m) {
|
void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m) {
|
||||||
|
// Called from main loop context via gap_event_handler after being queued from BT task
|
||||||
ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status);
|
ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status);
|
||||||
if (param.status == ESP_BT_STATUS_DONE) {
|
if (param.status == ESP_BT_STATUS_DONE) {
|
||||||
this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
|
this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
|
||||||
@@ -463,6 +363,7 @@ void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m) {
|
void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m) {
|
||||||
|
// Called from main loop context via gap_event_handler after being queued from BT task
|
||||||
ESP_LOGV(TAG, "gap_scan_start_complete - status %d", param.status);
|
ESP_LOGV(TAG, "gap_scan_start_complete - status %d", param.status);
|
||||||
this->scan_start_failed_ = param.status;
|
this->scan_start_failed_ = param.status;
|
||||||
if (this->scanner_state_ != ScannerState::STARTING) {
|
if (this->scanner_state_ != ScannerState::STARTING) {
|
||||||
@@ -474,8 +375,6 @@ void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble
|
|||||||
ESP_LOGE(TAG, "Scan was in failed state when start complete.");
|
ESP_LOGE(TAG, "Scan was in failed state when start complete.");
|
||||||
} else if (this->scanner_state_ == ScannerState::IDLE) {
|
} else if (this->scanner_state_ == ScannerState::IDLE) {
|
||||||
ESP_LOGE(TAG, "Scan was idle when start complete.");
|
ESP_LOGE(TAG, "Scan was idle when start complete.");
|
||||||
} else if (this->scanner_state_ == ScannerState::STOPPED) {
|
|
||||||
ESP_LOGE(TAG, "Scan was stopped when start complete.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (param.status == ESP_BT_STATUS_SUCCESS) {
|
if (param.status == ESP_BT_STATUS_SUCCESS) {
|
||||||
@@ -490,6 +389,8 @@ void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m) {
|
void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m) {
|
||||||
|
// Called from main loop context via gap_event_handler after being queued from BT task
|
||||||
|
// This allows us to safely transition to IDLE state and perform cleanup without race conditions
|
||||||
ESP_LOGV(TAG, "gap_scan_stop_complete - status %d", param.status);
|
ESP_LOGV(TAG, "gap_scan_stop_complete - status %d", param.status);
|
||||||
if (this->scanner_state_ != ScannerState::STOPPING) {
|
if (this->scanner_state_ != ScannerState::STOPPING) {
|
||||||
if (this->scanner_state_ == ScannerState::RUNNING) {
|
if (this->scanner_state_ == ScannerState::RUNNING) {
|
||||||
@@ -500,11 +401,11 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_
|
|||||||
ESP_LOGE(TAG, "Scan was in failed state when stop complete.");
|
ESP_LOGE(TAG, "Scan was in failed state when stop complete.");
|
||||||
} else if (this->scanner_state_ == ScannerState::IDLE) {
|
} else if (this->scanner_state_ == ScannerState::IDLE) {
|
||||||
ESP_LOGE(TAG, "Scan was idle when stop complete.");
|
ESP_LOGE(TAG, "Scan was idle when stop complete.");
|
||||||
} else if (this->scanner_state_ == ScannerState::STOPPED) {
|
|
||||||
ESP_LOGE(TAG, "Scan was stopped when stop complete.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this->set_scanner_state_(ScannerState::STOPPED);
|
|
||||||
|
// Perform cleanup and transition to IDLE
|
||||||
|
this->cleanup_scan_state_(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||||
@@ -794,9 +695,6 @@ void ESP32BLETracker::dump_config() {
|
|||||||
case ScannerState::STOPPING:
|
case ScannerState::STOPPING:
|
||||||
ESP_LOGCONFIG(TAG, " Scanner State: STOPPING");
|
ESP_LOGCONFIG(TAG, " Scanner State: STOPPING");
|
||||||
break;
|
break;
|
||||||
case ScannerState::STOPPED:
|
|
||||||
ESP_LOGCONFIG(TAG, " Scanner State: STOPPED");
|
|
||||||
break;
|
|
||||||
case ScannerState::FAILED:
|
case ScannerState::FAILED:
|
||||||
ESP_LOGCONFIG(TAG, " Scanner State: FAILED");
|
ESP_LOGCONFIG(TAG, " Scanner State: FAILED");
|
||||||
break;
|
break;
|
||||||
@@ -879,8 +777,77 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
|
|||||||
return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) &&
|
return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) &&
|
||||||
ecb_ciphertext[13] == ((addr64 >> 16) & 0xff);
|
ecb_ciphertext[13] == ((addr64 >> 16) & 0xff);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ESP32BLETracker::has_connecting_clients_() const {
|
||||||
|
for (auto *client : this->clients_) {
|
||||||
|
auto state = client->state();
|
||||||
|
if (state == ClientState::CONNECTING || state == ClientState::READY_TO_CONNECT) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
#endif // USE_ESP32_BLE_DEVICE
|
#endif // USE_ESP32_BLE_DEVICE
|
||||||
|
|
||||||
|
bool ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
|
||||||
|
bool found_discovered_client = false;
|
||||||
|
|
||||||
|
// Process raw advertisements
|
||||||
|
if (this->raw_advertisements_) {
|
||||||
|
for (auto *listener : this->listeners_) {
|
||||||
|
listener->parse_devices(&scan_result, 1);
|
||||||
|
}
|
||||||
|
for (auto *client : this->clients_) {
|
||||||
|
client->parse_devices(&scan_result, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process parsed advertisements
|
||||||
|
if (this->parse_advertisements_) {
|
||||||
|
#ifdef USE_ESP32_BLE_DEVICE
|
||||||
|
ESPBTDevice device;
|
||||||
|
device.parse_scan_rst(scan_result);
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
for (auto *listener : this->listeners_) {
|
||||||
|
if (listener->parse_device(device))
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto *client : this->clients_) {
|
||||||
|
if (client->parse_device(device)) {
|
||||||
|
found = true;
|
||||||
|
// Check if this client is discovered and needs promotion
|
||||||
|
if (client->state() == ClientState::DISCOVERED) {
|
||||||
|
// Only check for connecting clients if we found a discovered client
|
||||||
|
// This matches the original logic: !connecting && client->state() == DISCOVERED
|
||||||
|
if (!this->has_connecting_clients_()) {
|
||||||
|
found_discovered_client = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found && !this->scan_continuous_) {
|
||||||
|
this->print_bt_device_info(device);
|
||||||
|
}
|
||||||
|
#endif // USE_ESP32_BLE_DEVICE
|
||||||
|
}
|
||||||
|
|
||||||
|
return found_discovered_client;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
|
||||||
|
ESP_LOGD(TAG, "Scan %scomplete, set scanner state to IDLE.", is_stop_complete ? "stop " : "");
|
||||||
|
this->already_discovered_.clear();
|
||||||
|
this->cancel_timeout("scan");
|
||||||
|
|
||||||
|
for (auto *listener : this->listeners_)
|
||||||
|
listener->on_scan_end();
|
||||||
|
|
||||||
|
this->set_scanner_state_(ScannerState::IDLE);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace esphome::esp32_ble_tracker
|
} // namespace esphome::esp32_ble_tracker
|
||||||
|
|
||||||
#endif // USE_ESP32
|
#endif // USE_ESP32
|
||||||
|
@@ -6,7 +6,6 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <atomic>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -21,6 +20,7 @@
|
|||||||
|
|
||||||
#include "esphome/components/esp32_ble/ble.h"
|
#include "esphome/components/esp32_ble/ble.h"
|
||||||
#include "esphome/components/esp32_ble/ble_uuid.h"
|
#include "esphome/components/esp32_ble/ble_uuid.h"
|
||||||
|
#include "esphome/components/esp32_ble/ble_scan_result.h"
|
||||||
|
|
||||||
namespace esphome::esp32_ble_tracker {
|
namespace esphome::esp32_ble_tracker {
|
||||||
|
|
||||||
@@ -158,18 +158,16 @@ enum class ClientState : uint8_t {
|
|||||||
};
|
};
|
||||||
|
|
||||||
enum class ScannerState {
|
enum class ScannerState {
|
||||||
// Scanner is idle, init state, set from the main loop when processing STOPPED
|
// Scanner is idle, init state
|
||||||
IDLE,
|
IDLE,
|
||||||
// Scanner is starting, set from the main loop only
|
// Scanner is starting
|
||||||
STARTING,
|
STARTING,
|
||||||
// Scanner is running, set from the ESP callback only
|
// Scanner is running
|
||||||
RUNNING,
|
RUNNING,
|
||||||
// Scanner failed to start, set from the ESP callback only
|
// Scanner failed to start
|
||||||
FAILED,
|
FAILED,
|
||||||
// Scanner is stopping, set from the main loop only
|
// Scanner is stopping
|
||||||
STOPPING,
|
STOPPING,
|
||||||
// Scanner is stopped, set from the ESP callback only
|
|
||||||
STOPPED,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class ConnectionType : uint8_t {
|
enum class ConnectionType : uint8_t {
|
||||||
@@ -262,8 +260,6 @@ class ESP32BLETracker : public Component,
|
|||||||
void stop_scan_();
|
void stop_scan_();
|
||||||
/// Start a single scan by setting up the parameters and doing some esp-idf calls.
|
/// Start a single scan by setting up the parameters and doing some esp-idf calls.
|
||||||
void start_scan_(bool first);
|
void start_scan_(bool first);
|
||||||
/// Called when a scan ends
|
|
||||||
void end_of_scan_();
|
|
||||||
/// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received.
|
/// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received.
|
||||||
void gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m);
|
void gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m);
|
||||||
/// Called when a `ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT` event is received.
|
/// Called when a `ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT` event is received.
|
||||||
@@ -274,6 +270,15 @@ class ESP32BLETracker : public Component,
|
|||||||
void gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m);
|
void gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m);
|
||||||
/// Called to set the scanner state. Will also call callbacks to let listeners know when state is changed.
|
/// Called to set the scanner state. Will also call callbacks to let listeners know when state is changed.
|
||||||
void set_scanner_state_(ScannerState state);
|
void set_scanner_state_(ScannerState state);
|
||||||
|
/// Common cleanup logic when transitioning scanner to IDLE state
|
||||||
|
void cleanup_scan_state_(bool is_stop_complete);
|
||||||
|
/// Process a single scan result immediately
|
||||||
|
/// Returns true if a discovered client needs promotion to READY_TO_CONNECT
|
||||||
|
bool process_scan_result_(const BLEScanResult &scan_result);
|
||||||
|
#ifdef USE_ESP32_BLE_DEVICE
|
||||||
|
/// Check if any clients are in connecting or ready to connect state
|
||||||
|
bool has_connecting_clients_() const;
|
||||||
|
#endif
|
||||||
|
|
||||||
uint8_t app_id_{0};
|
uint8_t app_id_{0};
|
||||||
|
|
||||||
@@ -297,15 +302,6 @@ class ESP32BLETracker : public Component,
|
|||||||
bool raw_advertisements_{false};
|
bool raw_advertisements_{false};
|
||||||
bool parse_advertisements_{false};
|
bool parse_advertisements_{false};
|
||||||
|
|
||||||
// Lock-free Single-Producer Single-Consumer (SPSC) ring buffer for scan results
|
|
||||||
// Producer: ESP-IDF Bluetooth stack callback (gap_scan_event_handler)
|
|
||||||
// Consumer: ESPHome main loop (loop() method)
|
|
||||||
// This design ensures zero blocking in the BT callback and prevents scan result loss
|
|
||||||
BLEScanResult *scan_ring_buffer_;
|
|
||||||
std::atomic<uint8_t> ring_write_index_{0}; // Written only by BT callback (producer)
|
|
||||||
std::atomic<uint8_t> ring_read_index_{0}; // Written only by main loop (consumer)
|
|
||||||
std::atomic<uint16_t> scan_results_dropped_{0}; // Tracks buffer overflow events
|
|
||||||
|
|
||||||
esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
|
esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
|
||||||
esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
|
esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
|
||||||
int connecting_{0};
|
int connecting_{0};
|
||||||
|
@@ -345,7 +345,7 @@ async def to_code(config):
|
|||||||
cg.add_define("USE_CAMERA")
|
cg.add_define("USE_CAMERA")
|
||||||
|
|
||||||
if CORE.using_esp_idf:
|
if CORE.using_esp_idf:
|
||||||
add_idf_component(name="espressif/esp32-camera", ref="2.0.15")
|
add_idf_component(name="espressif/esp32-camera", ref="2.1.0")
|
||||||
|
|
||||||
for conf in config.get(CONF_ON_STREAM_START, []):
|
for conf in config.get(CONF_ON_STREAM_START, []):
|
||||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||||
|
@@ -2,11 +2,7 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#if defined(USE_ESP32_VARIANT_ESP32) || defined(USE_ESP32_VARIANT_ESP32S2)
|
||||||
|
|
||||||
#ifdef USE_ARDUINO
|
|
||||||
#include <esp32-hal-dac.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace esp32_dac {
|
namespace esp32_dac {
|
||||||
@@ -23,18 +19,12 @@ void ESP32DAC::setup() {
|
|||||||
this->pin_->setup();
|
this->pin_->setup();
|
||||||
this->turn_off();
|
this->turn_off();
|
||||||
|
|
||||||
#ifdef USE_ESP_IDF
|
|
||||||
const dac_channel_t channel = this->pin_->get_pin() == DAC0_PIN ? DAC_CHAN_0 : DAC_CHAN_1;
|
const dac_channel_t channel = this->pin_->get_pin() == DAC0_PIN ? DAC_CHAN_0 : DAC_CHAN_1;
|
||||||
const dac_oneshot_config_t oneshot_cfg{channel};
|
const dac_oneshot_config_t oneshot_cfg{channel};
|
||||||
dac_oneshot_new_channel(&oneshot_cfg, &this->dac_handle_);
|
dac_oneshot_new_channel(&oneshot_cfg, &this->dac_handle_);
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ESP32DAC::on_safe_shutdown() {
|
void ESP32DAC::on_safe_shutdown() { dac_oneshot_del_channel(this->dac_handle_); }
|
||||||
#ifdef USE_ESP_IDF
|
|
||||||
dac_oneshot_del_channel(this->dac_handle_);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void ESP32DAC::dump_config() {
|
void ESP32DAC::dump_config() {
|
||||||
ESP_LOGCONFIG(TAG, "ESP32 DAC:");
|
ESP_LOGCONFIG(TAG, "ESP32 DAC:");
|
||||||
@@ -48,15 +38,10 @@ void ESP32DAC::write_state(float state) {
|
|||||||
|
|
||||||
state = state * 255;
|
state = state * 255;
|
||||||
|
|
||||||
#ifdef USE_ESP_IDF
|
|
||||||
dac_oneshot_output_voltage(this->dac_handle_, state);
|
dac_oneshot_output_voltage(this->dac_handle_, state);
|
||||||
#endif
|
|
||||||
#ifdef USE_ARDUINO
|
|
||||||
dacWrite(this->pin_->get_pin(), state);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace esp32_dac
|
} // namespace esp32_dac
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
||||||
#endif
|
#endif // USE_ESP32_VARIANT_ESP32 || USE_ESP32_VARIANT_ESP32S2
|
||||||
|
@@ -1,15 +1,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/output/float_output.h"
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
#include "esphome/core/automation.h"
|
|
||||||
#include "esphome/components/output/float_output.h"
|
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#if defined(USE_ESP32_VARIANT_ESP32) || defined(USE_ESP32_VARIANT_ESP32S2)
|
||||||
|
|
||||||
#ifdef USE_ESP_IDF
|
|
||||||
#include <driver/dac_oneshot.h>
|
#include <driver/dac_oneshot.h>
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace esp32_dac {
|
namespace esp32_dac {
|
||||||
@@ -29,12 +27,10 @@ class ESP32DAC : public output::FloatOutput, public Component {
|
|||||||
void write_state(float state) override;
|
void write_state(float state) override;
|
||||||
|
|
||||||
InternalGPIOPin *pin_;
|
InternalGPIOPin *pin_;
|
||||||
#ifdef USE_ESP_IDF
|
|
||||||
dac_oneshot_handle_t dac_handle_;
|
dac_oneshot_handle_t dac_handle_;
|
||||||
#endif
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esp32_dac
|
} // namespace esp32_dac
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
||||||
#endif
|
#endif // USE_ESP32_VARIANT_ESP32 || USE_ESP32_VARIANT_ESP32S2
|
||||||
|
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
|
@@ -145,5 +145,4 @@ async def event_fire_to_code(config, action_id, template_arg, args):
|
|||||||
|
|
||||||
@coroutine_with_priority(100.0)
|
@coroutine_with_priority(100.0)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_define("USE_EVENT")
|
|
||||||
cg.add_global(event_ns.using)
|
cg.add_global(event_ns.using)
|
||||||
|
@@ -400,5 +400,4 @@ async def fan_is_on_off_to_code(config, condition_id, template_arg, args):
|
|||||||
|
|
||||||
@coroutine_with_priority(100.0)
|
@coroutine_with_priority(100.0)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_define("USE_FAN")
|
|
||||||
cg.add_global(fan_ns.using)
|
cg.add_global(fan_ns.using)
|
||||||
|
@@ -15,6 +15,7 @@ from freetype import (
|
|||||||
FT_LOAD_RENDER,
|
FT_LOAD_RENDER,
|
||||||
FT_LOAD_TARGET_MONO,
|
FT_LOAD_TARGET_MONO,
|
||||||
Face,
|
Face,
|
||||||
|
FT_Exception,
|
||||||
ft_pixel_mode_mono,
|
ft_pixel_mode_mono,
|
||||||
)
|
)
|
||||||
import requests
|
import requests
|
||||||
@@ -94,7 +95,14 @@ class FontCache(MutableMapping):
|
|||||||
return self.store[self._keytransform(item)]
|
return self.store[self._keytransform(item)]
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
self.store[self._keytransform(key)] = Face(str(value))
|
transformed = self._keytransform(key)
|
||||||
|
try:
|
||||||
|
self.store[transformed] = Face(str(value))
|
||||||
|
except FT_Exception as exc:
|
||||||
|
file = transformed.split(":", 1)
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"{file[0].capitalize()} {file[1]} is not a valid font file"
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
|
||||||
FONT_CACHE = FontCache()
|
FONT_CACHE = FontCache()
|
||||||
|
@@ -20,12 +20,11 @@ static const size_t MAX_BUTTONS = 4; // max number of buttons scanned
|
|||||||
|
|
||||||
#define ERROR_CHECK(err) \
|
#define ERROR_CHECK(err) \
|
||||||
if ((err) != i2c::ERROR_OK) { \
|
if ((err) != i2c::ERROR_OK) { \
|
||||||
this->status_set_warning("Communication failure"); \
|
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); \
|
||||||
return; \
|
return; \
|
||||||
}
|
}
|
||||||
|
|
||||||
void GT911Touchscreen::setup() {
|
void GT911Touchscreen::setup() {
|
||||||
i2c::ErrorCode err;
|
|
||||||
if (this->reset_pin_ != nullptr) {
|
if (this->reset_pin_ != nullptr) {
|
||||||
this->reset_pin_->setup();
|
this->reset_pin_->setup();
|
||||||
this->reset_pin_->digital_write(false);
|
this->reset_pin_->digital_write(false);
|
||||||
@@ -35,9 +34,14 @@ void GT911Touchscreen::setup() {
|
|||||||
this->interrupt_pin_->digital_write(false);
|
this->interrupt_pin_->digital_write(false);
|
||||||
}
|
}
|
||||||
delay(2);
|
delay(2);
|
||||||
this->reset_pin_->digital_write(true);
|
this->reset_pin_->digital_write(true); // wait 50ms after reset
|
||||||
delay(50); // NOLINT
|
this->set_timeout(50, [this] { this->setup_internal_(); });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
this->setup_internal_();
|
||||||
|
}
|
||||||
|
|
||||||
|
void GT911Touchscreen::setup_internal_() {
|
||||||
if (this->interrupt_pin_ != nullptr) {
|
if (this->interrupt_pin_ != nullptr) {
|
||||||
// set pre-configured input mode
|
// set pre-configured input mode
|
||||||
this->interrupt_pin_->setup();
|
this->interrupt_pin_->setup();
|
||||||
@@ -45,7 +49,7 @@ void GT911Touchscreen::setup() {
|
|||||||
|
|
||||||
// check the configuration of the int line.
|
// check the configuration of the int line.
|
||||||
uint8_t data[4];
|
uint8_t data[4];
|
||||||
err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
|
i2c::ErrorCode err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
|
||||||
if (err != i2c::ERROR_OK && this->address_ == PRIMARY_ADDRESS) {
|
if (err != i2c::ERROR_OK && this->address_ == PRIMARY_ADDRESS) {
|
||||||
this->address_ = SECONDARY_ADDRESS;
|
this->address_ = SECONDARY_ADDRESS;
|
||||||
err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
|
err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
|
||||||
@@ -53,7 +57,7 @@ void GT911Touchscreen::setup() {
|
|||||||
if (err == i2c::ERROR_OK) {
|
if (err == i2c::ERROR_OK) {
|
||||||
err = this->read(data, 1);
|
err = this->read(data, 1);
|
||||||
if (err == i2c::ERROR_OK) {
|
if (err == i2c::ERROR_OK) {
|
||||||
ESP_LOGD(TAG, "Read from switches at address 0x%02X: 0x%02X", this->address_, data[0]);
|
ESP_LOGD(TAG, "Switches ADDR: 0x%02X DATA: 0x%02X", this->address_, data[0]);
|
||||||
if (this->interrupt_pin_ != nullptr) {
|
if (this->interrupt_pin_ != nullptr) {
|
||||||
this->attach_interrupt_(this->interrupt_pin_,
|
this->attach_interrupt_(this->interrupt_pin_,
|
||||||
(data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE);
|
(data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE);
|
||||||
@@ -75,16 +79,24 @@ void GT911Touchscreen::setup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (err != i2c::ERROR_OK) {
|
if (err != i2c::ERROR_OK) {
|
||||||
this->mark_failed("Failed to read calibration");
|
this->mark_failed("Calibration error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err != i2c::ERROR_OK) {
|
if (err != i2c::ERROR_OK) {
|
||||||
this->mark_failed("Failed to communicate");
|
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
this->setup_done_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void GT911Touchscreen::update_touches() {
|
void GT911Touchscreen::update_touches() {
|
||||||
|
this->skip_update_ = true; // skip send touch events by default, set to false after successful error checks
|
||||||
|
if (!this->setup_done_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
i2c::ErrorCode err;
|
i2c::ErrorCode err;
|
||||||
uint8_t touch_state = 0;
|
uint8_t touch_state = 0;
|
||||||
uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte
|
uint8_t data[MAX_TOUCHES + 1][8]; // 8 bytes each for each point, plus extra space for the key byte
|
||||||
@@ -97,7 +109,6 @@ void GT911Touchscreen::update_touches() {
|
|||||||
uint8_t num_of_touches = touch_state & 0x07;
|
uint8_t num_of_touches = touch_state & 0x07;
|
||||||
|
|
||||||
if ((touch_state & 0x80) == 0 || num_of_touches > MAX_TOUCHES) {
|
if ((touch_state & 0x80) == 0 || num_of_touches > MAX_TOUCHES) {
|
||||||
this->skip_update_ = true; // skip send touch events, touchscreen is not ready yet.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +118,7 @@ void GT911Touchscreen::update_touches() {
|
|||||||
err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1);
|
err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1);
|
||||||
ERROR_CHECK(err);
|
ERROR_CHECK(err);
|
||||||
|
|
||||||
|
this->skip_update_ = false; // All error checks passed, send touch events
|
||||||
for (uint8_t i = 0; i != num_of_touches; i++) {
|
for (uint8_t i = 0; i != num_of_touches; i++) {
|
||||||
uint16_t id = data[i][0];
|
uint16_t id = data[i][0];
|
||||||
uint16_t x = encode_uint16(data[i][2], data[i][1]);
|
uint16_t x = encode_uint16(data[i][2], data[i][1]);
|
||||||
|
@@ -15,8 +15,20 @@ class GT911ButtonListener {
|
|||||||
|
|
||||||
class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
|
class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
|
||||||
public:
|
public:
|
||||||
|
/// @brief Initialize the GT911 touchscreen.
|
||||||
|
///
|
||||||
|
/// If @ref reset_pin_ is set, the touchscreen will be hardware reset,
|
||||||
|
/// and the rest of the setup will be scheduled to run 50ms later using @ref set_timeout()
|
||||||
|
/// to allow the device to stabilize after reset.
|
||||||
|
///
|
||||||
|
/// If @ref interrupt_pin_ is set, it will be temporarily configured during reset
|
||||||
|
/// to control I2C address selection.
|
||||||
|
///
|
||||||
|
/// After the timeout, or immediately if no reset is performed, @ref setup_internal_()
|
||||||
|
/// is called to complete the initialization.
|
||||||
void setup() override;
|
void setup() override;
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
bool can_proceed() override { return this->setup_done_; }
|
||||||
|
|
||||||
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
|
||||||
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
|
void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
|
||||||
@@ -25,8 +37,20 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
|
|||||||
protected:
|
protected:
|
||||||
void update_touches() override;
|
void update_touches() override;
|
||||||
|
|
||||||
InternalGPIOPin *interrupt_pin_{};
|
/// @brief Perform the internal setup routine for the GT911 touchscreen.
|
||||||
GPIOPin *reset_pin_{};
|
///
|
||||||
|
/// This function checks the I2C address, configures the interrupt pin (if available),
|
||||||
|
/// reads the touchscreen mode from the controller, and attempts to read calibration
|
||||||
|
/// data (maximum X and Y values) if not already set.
|
||||||
|
///
|
||||||
|
/// On success, sets @ref setup_done_ to true.
|
||||||
|
/// On failure, calls @ref mark_failed() with an appropriate error message.
|
||||||
|
void setup_internal_();
|
||||||
|
/// @brief True if the touchscreen setup has completed successfully.
|
||||||
|
bool setup_done_{false};
|
||||||
|
|
||||||
|
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||||
|
GPIOPin *reset_pin_{nullptr};
|
||||||
std::vector<GT911ButtonListener *> button_listeners_;
|
std::vector<GT911ButtonListener *> button_listeners_;
|
||||||
uint8_t button_state_{0xFF}; // last button state. Initial FF guarantees first update.
|
uint8_t button_state_{0xFF}; // last button state. Initial FF guarantees first update.
|
||||||
};
|
};
|
||||||
|
@@ -330,8 +330,7 @@ HAIER_HON_BASE_ACTION_SCHEMA = automation.maybe_simple_id(
|
|||||||
)
|
)
|
||||||
async def display_action_to_code(config, action_id, template_arg, args):
|
async def display_action_to_code(config, action_id, template_arg, args):
|
||||||
paren = await cg.get_variable(config[CONF_ID])
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
@automation.register_action(
|
@automation.register_action(
|
||||||
@@ -342,8 +341,7 @@ async def display_action_to_code(config, action_id, template_arg, args):
|
|||||||
)
|
)
|
||||||
async def beeper_action_to_code(config, action_id, template_arg, args):
|
async def beeper_action_to_code(config, action_id, template_arg, args):
|
||||||
paren = await cg.get_variable(config[CONF_ID])
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
# Start self cleaning or steri-cleaning action action
|
# Start self cleaning or steri-cleaning action action
|
||||||
@@ -359,8 +357,7 @@ async def beeper_action_to_code(config, action_id, template_arg, args):
|
|||||||
)
|
)
|
||||||
async def start_cleaning_to_code(config, action_id, template_arg, args):
|
async def start_cleaning_to_code(config, action_id, template_arg, args):
|
||||||
paren = await cg.get_variable(config[CONF_ID])
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
# Set vertical airflow direction action
|
# Set vertical airflow direction action
|
||||||
@@ -417,8 +414,7 @@ async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg,
|
|||||||
)
|
)
|
||||||
async def health_action_to_code(config, action_id, template_arg, args):
|
async def health_action_to_code(config, action_id, template_arg, args):
|
||||||
paren = await cg.get_variable(config[CONF_ID])
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
@automation.register_action(
|
@automation.register_action(
|
||||||
@@ -432,8 +428,7 @@ async def health_action_to_code(config, action_id, template_arg, args):
|
|||||||
)
|
)
|
||||||
async def power_action_to_code(config, action_id, template_arg, args):
|
async def power_action_to_code(config, action_id, template_arg, args):
|
||||||
paren = await cg.get_variable(config[CONF_ID])
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
def _final_validate(config):
|
def _final_validate(config):
|
||||||
|
@@ -24,9 +24,6 @@ static const uint32_t READ_DURATION_MS = 16;
|
|||||||
static const size_t TASK_STACK_SIZE = 4096;
|
static const size_t TASK_STACK_SIZE = 4096;
|
||||||
static const ssize_t TASK_PRIORITY = 23;
|
static const ssize_t TASK_PRIORITY = 23;
|
||||||
|
|
||||||
// Use an exponential moving average to correct a DC offset with weight factor 1/1000
|
|
||||||
static const int32_t DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR = 1000;
|
|
||||||
|
|
||||||
static const char *const TAG = "i2s_audio.microphone";
|
static const char *const TAG = "i2s_audio.microphone";
|
||||||
|
|
||||||
enum MicrophoneEventGroupBits : uint32_t {
|
enum MicrophoneEventGroupBits : uint32_t {
|
||||||
@@ -381,26 +378,57 @@ void I2SAudioMicrophone::mic_task(void *params) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void I2SAudioMicrophone::fix_dc_offset_(std::vector<uint8_t> &data) {
|
void I2SAudioMicrophone::fix_dc_offset_(std::vector<uint8_t> &data) {
|
||||||
|
/**
|
||||||
|
* From https://www.musicdsp.org/en/latest/Filters/135-dc-filter.html:
|
||||||
|
*
|
||||||
|
* y(n) = x(n) - x(n-1) + R * y(n-1)
|
||||||
|
* R = 1 - (pi * 2 * frequency / samplerate)
|
||||||
|
*
|
||||||
|
* From https://en.wikipedia.org/wiki/Hearing_range:
|
||||||
|
* The human range is commonly given as 20Hz up.
|
||||||
|
*
|
||||||
|
* From https://en.wikipedia.org/wiki/High-resolution_audio:
|
||||||
|
* A reasonable upper bound for sample rate seems to be 96kHz.
|
||||||
|
*
|
||||||
|
* Calculate R value for 20Hz on a 96kHz sample rate:
|
||||||
|
* R = 1 - (pi * 2 * 20 / 96000)
|
||||||
|
* R = 0.9986910031
|
||||||
|
*
|
||||||
|
* Transform floating point to bit-shifting approximation:
|
||||||
|
* output = input - prev_input + R * prev_output
|
||||||
|
* output = input - prev_input + (prev_output - (prev_output >> S))
|
||||||
|
*
|
||||||
|
* Approximate bit-shift value S from R:
|
||||||
|
* R = 1 - (1 >> S)
|
||||||
|
* R = 1 - (1 / 2^S)
|
||||||
|
* R = 1 - 2^-S
|
||||||
|
* 0.9986910031 = 1 - 2^-S
|
||||||
|
* S = 9.57732 ~= 10
|
||||||
|
*
|
||||||
|
* Actual R from S:
|
||||||
|
* R = 1 - 2^-10 = 0.9990234375
|
||||||
|
*
|
||||||
|
* Confirm this has effect outside human hearing on 96000kHz sample:
|
||||||
|
* 0.9990234375 = 1 - (pi * 2 * f / 96000)
|
||||||
|
* f = 14.9208Hz
|
||||||
|
*
|
||||||
|
* Confirm this has effect outside human hearing on PDM 16kHz sample:
|
||||||
|
* 0.9990234375 = 1 - (pi * 2 * f / 16000)
|
||||||
|
* f = 2.4868Hz
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const uint8_t dc_filter_shift = 10;
|
||||||
const size_t bytes_per_sample = this->audio_stream_info_.samples_to_bytes(1);
|
const size_t bytes_per_sample = this->audio_stream_info_.samples_to_bytes(1);
|
||||||
const uint32_t total_samples = this->audio_stream_info_.bytes_to_samples(data.size());
|
const uint32_t total_samples = this->audio_stream_info_.bytes_to_samples(data.size());
|
||||||
|
|
||||||
if (total_samples == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int64_t offset_accumulator = 0;
|
|
||||||
for (uint32_t sample_index = 0; sample_index < total_samples; ++sample_index) {
|
for (uint32_t sample_index = 0; sample_index < total_samples; ++sample_index) {
|
||||||
const uint32_t byte_index = sample_index * bytes_per_sample;
|
const uint32_t byte_index = sample_index * bytes_per_sample;
|
||||||
int32_t sample = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample);
|
int32_t input = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample);
|
||||||
offset_accumulator += sample;
|
int32_t output = input - this->dc_offset_prev_input_ +
|
||||||
sample -= this->dc_offset_;
|
(this->dc_offset_prev_output_ - (this->dc_offset_prev_output_ >> dc_filter_shift));
|
||||||
audio::pack_q31_as_audio_sample(sample, &data[byte_index], bytes_per_sample);
|
this->dc_offset_prev_input_ = input;
|
||||||
|
this->dc_offset_prev_output_ = output;
|
||||||
|
audio::pack_q31_as_audio_sample(output, &data[byte_index], bytes_per_sample);
|
||||||
}
|
}
|
||||||
|
|
||||||
const int32_t new_offset = offset_accumulator / total_samples;
|
|
||||||
this->dc_offset_ = new_offset / DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR +
|
|
||||||
(DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR - 1) * this->dc_offset_ /
|
|
||||||
DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait) {
|
size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait) {
|
||||||
|
@@ -82,7 +82,8 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
|
|||||||
|
|
||||||
bool correct_dc_offset_;
|
bool correct_dc_offset_;
|
||||||
bool locked_driver_{false};
|
bool locked_driver_{false};
|
||||||
int32_t dc_offset_{0};
|
int32_t dc_offset_prev_input_{0};
|
||||||
|
int32_t dc_offset_prev_output_{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace i2s_audio
|
} // namespace i2s_audio
|
||||||
|
@@ -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 apply_defaults(image, defaults, path):
|
||||||
|
"""
|
||||||
|
Apply defaults to an image configuration
|
||||||
|
"""
|
||||||
|
type = image.get(CONF_TYPE, defaults.get(CONF_TYPE))
|
||||||
|
if type is None:
|
||||||
|
raise cv.Invalid(
|
||||||
|
"Type is required either in the image config or in the defaults", path=path
|
||||||
|
)
|
||||||
|
type_class = IMAGE_TYPE[type]
|
||||||
|
config = {
|
||||||
|
**{key: image.get(key, defaults.get(key)) for key in type_class.get_options()},
|
||||||
|
**{key.schema: image[key.schema] for key in IMAGE_ID_SCHEMA},
|
||||||
|
CONF_TYPE: image.get(CONF_TYPE, defaults.get(CONF_TYPE)),
|
||||||
|
}
|
||||||
|
validate_settings(config, path)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
def validate_defaults(value):
|
def validate_defaults(value):
|
||||||
"""
|
"""
|
||||||
Validate the options for images with defaults
|
Apply defaults to the images in the configuration and flatten to a single list.
|
||||||
"""
|
"""
|
||||||
defaults = value[CONF_DEFAULTS]
|
defaults = value[CONF_DEFAULTS]
|
||||||
result = []
|
result = []
|
||||||
for index, image in enumerate(value[CONF_IMAGES]):
|
# Apply defaults to the images: list and add the list entries to the result
|
||||||
type = image.get(CONF_TYPE, defaults.get(CONF_TYPE))
|
for index, image in enumerate(value.get(CONF_IMAGES, [])):
|
||||||
if type is None:
|
result.append(apply_defaults(image, defaults, [CONF_IMAGES, index]))
|
||||||
raise cv.Invalid(
|
|
||||||
"Type is required either in the image config or in the defaults",
|
# Apply defaults to images under the type keys and add them to the result
|
||||||
path=[CONF_IMAGES, index],
|
for image_type, type_config in value.items():
|
||||||
)
|
type_upper = image_type.upper()
|
||||||
type_class = IMAGE_TYPE[type]
|
if type_upper not in IMAGE_TYPE:
|
||||||
# A default byte order should be simply ignored if the type does not support it
|
continue
|
||||||
available_options = [*OPTIONS]
|
type_class = IMAGE_TYPE[type_upper]
|
||||||
if (
|
if isinstance(type_config, list):
|
||||||
not callable(getattr(type_class, "set_big_endian", None))
|
# If the type is a list, apply defaults to each entry
|
||||||
and CONF_BYTE_ORDER not in image
|
for index, image in enumerate(type_config):
|
||||||
):
|
result.append(apply_defaults(image, defaults, [image_type, index]))
|
||||||
available_options.remove(CONF_BYTE_ORDER)
|
else:
|
||||||
config = {
|
# Handle transparency options for the type
|
||||||
**{key: image.get(key, defaults.get(key)) for key in available_options},
|
for trans_type in set(type_class.allow_config).intersection(type_config):
|
||||||
**{key.schema: image[key.schema] for key in IMAGE_ID_SCHEMA},
|
for index, image in enumerate(type_config[trans_type]):
|
||||||
}
|
result.append(
|
||||||
validate_settings(config)
|
apply_defaults(image, defaults, [image_type, trans_type, index])
|
||||||
result.append(config)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@@ -562,16 +598,20 @@ 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(
|
**{
|
||||||
CONF_TRANSPARENCY, default=t
|
cv.Optional(key): OPTIONS_SCHEMA[key]
|
||||||
): validate_transparency((t,)),
|
for key in OPTIONS
|
||||||
cv.Optional(CONF_TYPE, default=image_type): validate_type(
|
if key != CONF_TRANSPARENCY
|
||||||
(image_type,)
|
},
|
||||||
),
|
cv.Optional(
|
||||||
}
|
CONF_TRANSPARENCY, default=t
|
||||||
)
|
): validate_transparency((t,)),
|
||||||
|
cv.Optional(CONF_TYPE, default=image_type): validate_type(
|
||||||
|
(image_type,)
|
||||||
|
),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
for t in IMAGE_TYPE[image_type].allow_config.intersection(
|
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(
|
{
|
||||||
{
|
**IMAGE_SCHEMA_NO_DEFAULTS,
|
||||||
cv.Optional(
|
cv.Optional(CONF_TYPE, default=image_type): validate_type(
|
||||||
CONF_TRANSPARENCY, default=CONF_OPAQUE
|
(image_type,)
|
||||||
): validate_transparency(),
|
),
|
||||||
cv.Optional(CONF_TYPE, default=image_type): validate_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.Optional(CONF_DEFAULTS, default={}): DEFAULTS_SCHEMA,
|
||||||
cv.Required(CONF_DEFAULTS): OPTIONS_SCHEMA,
|
cv.Optional(CONF_IMAGES, default=[]): cv.ensure_list(
|
||||||
cv.Required(CONF_IMAGES): cv.ensure_list(IMAGE_SCHEMA_NO_DEFAULTS),
|
{
|
||||||
}
|
**IMAGE_SCHEMA_NO_DEFAULTS,
|
||||||
)(config)
|
cv.Optional(CONF_TYPE): validate_type(IMAGE_TYPE),
|
||||||
)
|
}
|
||||||
if CONF_ID in config or CONF_FILE in config:
|
),
|
||||||
return cv.ensure_list(IMAGE_SCHEMA)([config])
|
**{cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE},
|
||||||
return cv.Schema(
|
}
|
||||||
{cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}
|
),
|
||||||
)(config)
|
validate_defaults,
|
||||||
|
)(value)
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
)
|
)
|
||||||
|
@@ -285,5 +285,4 @@ async def new_light(config, *args):
|
|||||||
|
|
||||||
@coroutine_with_priority(100.0)
|
@coroutine_with_priority(100.0)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_define("USE_LIGHT")
|
|
||||||
cg.add_global(light_ns.using)
|
cg.add_global(light_ns.using)
|
||||||
|
@@ -353,10 +353,9 @@ async def addressable_lambda_effect_to_code(config, effect_id):
|
|||||||
(bool, "initial_run"),
|
(bool, "initial_run"),
|
||||||
]
|
]
|
||||||
lambda_ = await cg.process_lambda(config[CONF_LAMBDA], args, return_type=cg.void)
|
lambda_ = await cg.process_lambda(config[CONF_LAMBDA], args, return_type=cg.void)
|
||||||
var = cg.new_Pvariable(
|
return cg.new_Pvariable(
|
||||||
effect_id, config[CONF_NAME], lambda_, config[CONF_UPDATE_INTERVAL]
|
effect_id, config[CONF_NAME], lambda_, config[CONF_UPDATE_INTERVAL]
|
||||||
)
|
)
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
@register_addressable_effect(
|
@register_addressable_effect(
|
||||||
|
@@ -158,4 +158,3 @@ async def lock_is_off_to_code(config, condition_id, template_arg, args):
|
|||||||
@coroutine_with_priority(100.0)
|
@coroutine_with_priority(100.0)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_global(lock_ns.using)
|
cg.add_global(lock_ns.using)
|
||||||
cg.add_define("USE_LOCK")
|
|
||||||
|
@@ -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]
|
||||||
|
@@ -85,8 +85,7 @@ async def action_to_code(
|
|||||||
async with LambdaContext(parameters=args, where=action_id) as context:
|
async with LambdaContext(parameters=args, where=action_id) as context:
|
||||||
for widget in widgets:
|
for widget in widgets:
|
||||||
await action(widget)
|
await action(widget)
|
||||||
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
|
return cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
async def update_to_code(config, action_id, template_arg, args):
|
async def update_to_code(config, action_id, template_arg, args):
|
||||||
@@ -354,8 +353,7 @@ async def widget_focus(config, action_id, template_arg, args):
|
|||||||
|
|
||||||
if config[CONF_FREEZE]:
|
if config[CONF_FREEZE]:
|
||||||
lv.group_focus_freeze(group, True)
|
lv.group_focus_freeze(group, True)
|
||||||
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
|
return cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
@automation.register_action(
|
@automation.register_action(
|
||||||
|
@@ -271,8 +271,7 @@ padding = LValidator(padding_validator, int32, retmapper=literal)
|
|||||||
|
|
||||||
|
|
||||||
def zoom_validator(value):
|
def zoom_validator(value):
|
||||||
value = cv.float_range(0.1, 10.0)(value)
|
return cv.float_range(0.1, 10.0)(value)
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def zoom_retmapper(value):
|
def zoom_retmapper(value):
|
||||||
|
@@ -66,8 +66,7 @@ async def style_update_to_code(config, action_id, template_arg, args):
|
|||||||
async with LambdaContext(parameters=args, where=action_id) as context:
|
async with LambdaContext(parameters=args, where=action_id) as context:
|
||||||
await style_set(style, config)
|
await style_set(style, config)
|
||||||
|
|
||||||
var = cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
|
return cg.new_Pvariable(action_id, template_arg, await context.get_lambda())
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
async def theme_to_code(config):
|
async def theme_to_code(config):
|
||||||
|
@@ -189,7 +189,7 @@ class Widget:
|
|||||||
for matrix buttons
|
for matrix buttons
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return None
|
return
|
||||||
|
|
||||||
def get_max(self):
|
def get_max(self):
|
||||||
return self.type.get_max(self.config)
|
return self.type.get_max(self.config)
|
||||||
|
@@ -193,7 +193,7 @@ class ButtonMatrixType(WidgetType):
|
|||||||
async def to_code(self, w: Widget, config):
|
async def to_code(self, w: Widget, config):
|
||||||
lvgl_components_required.add("BUTTONMATRIX")
|
lvgl_components_required.add("BUTTONMATRIX")
|
||||||
if CONF_ROWS not in config:
|
if CONF_ROWS not in config:
|
||||||
return []
|
return
|
||||||
text_list, ctrl_list, width_list, key_list = await get_button_data(
|
text_list, ctrl_list, width_list, key_list = await get_button_data(
|
||||||
config[CONF_ROWS], w
|
config[CONF_ROWS], w
|
||||||
)
|
)
|
||||||
|
@@ -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 {
|
||||||
|
@@ -77,6 +77,7 @@ BRIGHTNESS = 0x51
|
|||||||
WRDISBV = 0x51
|
WRDISBV = 0x51
|
||||||
RDDISBV = 0x52
|
RDDISBV = 0x52
|
||||||
WRCTRLD = 0x53
|
WRCTRLD = 0x53
|
||||||
|
WCE = 0x58
|
||||||
SWIRE1 = 0x5A
|
SWIRE1 = 0x5A
|
||||||
SWIRE2 = 0x5B
|
SWIRE2 = 0x5B
|
||||||
IFMODE = 0xB0
|
IFMODE = 0xB0
|
||||||
@@ -91,6 +92,7 @@ PWCTR2 = 0xC1
|
|||||||
PWCTR3 = 0xC2
|
PWCTR3 = 0xC2
|
||||||
PWCTR4 = 0xC3
|
PWCTR4 = 0xC3
|
||||||
PWCTR5 = 0xC4
|
PWCTR5 = 0xC4
|
||||||
|
SPIMODESEL = 0xC4
|
||||||
VMCTR1 = 0xC5
|
VMCTR1 = 0xC5
|
||||||
IFCTR = 0xC6
|
IFCTR = 0xC6
|
||||||
VMCTR2 = 0xC7
|
VMCTR2 = 0xC7
|
||||||
|
@@ -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
|
||||||
|
@@ -5,10 +5,13 @@ from esphome.components.mipi import (
|
|||||||
PAGESEL,
|
PAGESEL,
|
||||||
PIXFMT,
|
PIXFMT,
|
||||||
SLPOUT,
|
SLPOUT,
|
||||||
|
SPIMODESEL,
|
||||||
SWIRE1,
|
SWIRE1,
|
||||||
SWIRE2,
|
SWIRE2,
|
||||||
TEON,
|
TEON,
|
||||||
|
WCE,
|
||||||
WRAM,
|
WRAM,
|
||||||
|
WRCTRLD,
|
||||||
DriverChip,
|
DriverChip,
|
||||||
delay,
|
delay,
|
||||||
)
|
)
|
||||||
@@ -87,4 +90,19 @@ T4_S3_AMOLED = RM690B0.extend(
|
|||||||
bus_mode=TYPE_QUAD,
|
bus_mode=TYPE_QUAD,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CO5300 = DriverChip(
|
||||||
|
"CO5300",
|
||||||
|
brightness=0xD0,
|
||||||
|
color_order=MODE_RGB,
|
||||||
|
bus_mode=TYPE_QUAD,
|
||||||
|
initsequence=(
|
||||||
|
(SLPOUT,), # Requires early SLPOUT
|
||||||
|
(PAGESEL, 0x00),
|
||||||
|
(SPIMODESEL, 0x80),
|
||||||
|
(WRCTRLD, 0x20),
|
||||||
|
(WCE, 0x00),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
models = {}
|
models = {}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
from esphome.components.mipi import DriverChip
|
from esphome.components.mipi import DriverChip
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
|
|
||||||
|
from .amoled import CO5300
|
||||||
from .ili import ILI9488_A
|
from .ili import ILI9488_A
|
||||||
|
|
||||||
DriverChip(
|
DriverChip(
|
||||||
@@ -140,3 +141,14 @@ ILI9488_A.extend(
|
|||||||
data_rate="20MHz",
|
data_rate="20MHz",
|
||||||
invert_colors=True,
|
invert_colors=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CO5300.extend(
|
||||||
|
"WAVESHARE-ESP32-S3-TOUCH-AMOLED-1.75",
|
||||||
|
width=466,
|
||||||
|
height=466,
|
||||||
|
pixel_mode="16bit",
|
||||||
|
offset_height=0,
|
||||||
|
offset_width=6,
|
||||||
|
cs_pin=12,
|
||||||
|
reset_pin=39,
|
||||||
|
)
|
||||||
|
@@ -312,14 +312,13 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
def exp_mqtt_message(config):
|
def exp_mqtt_message(config):
|
||||||
if config is None:
|
if config is None:
|
||||||
return cg.optional(cg.TemplateArguments(MQTTMessage))
|
return cg.optional(cg.TemplateArguments(MQTTMessage))
|
||||||
exp = cg.StructInitializer(
|
return cg.StructInitializer(
|
||||||
MQTTMessage,
|
MQTTMessage,
|
||||||
("topic", config[CONF_TOPIC]),
|
("topic", config[CONF_TOPIC]),
|
||||||
("payload", config.get(CONF_PAYLOAD, "")),
|
("payload", config.get(CONF_PAYLOAD, "")),
|
||||||
("qos", config[CONF_QOS]),
|
("qos", config[CONF_QOS]),
|
||||||
("retain", config[CONF_RETAIN]),
|
("retain", config[CONF_RETAIN]),
|
||||||
)
|
)
|
||||||
return exp
|
|
||||||
|
|
||||||
|
|
||||||
@coroutine_with_priority(40.0)
|
@coroutine_with_priority(40.0)
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
#include "binary_sensor.h"
|
#include "nfc_binary_sensor.h"
|
||||||
#include "../nfc_helpers.h"
|
#include "../nfc_helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -323,7 +323,6 @@ async def number_in_range_to_code(config, condition_id, template_arg, args):
|
|||||||
|
|
||||||
@coroutine_with_priority(100.0)
|
@coroutine_with_priority(100.0)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_define("USE_NUMBER")
|
|
||||||
cg.add_global(number_ns.using)
|
cg.add_global(number_ns.using)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -18,13 +18,12 @@ def one_wire_device_schema():
|
|||||||
|
|
||||||
:return: The 1-wire device schema, `extend` this in your config schema.
|
:return: The 1-wire device schema, `extend` this in your config schema.
|
||||||
"""
|
"""
|
||||||
schema = cv.Schema(
|
return cv.Schema(
|
||||||
{
|
{
|
||||||
cv.GenerateID(CONF_ONE_WIRE_ID): cv.use_id(OneWireBus),
|
cv.GenerateID(CONF_ONE_WIRE_ID): cv.use_id(OneWireBus),
|
||||||
cv.Optional(CONF_ADDRESS): cv.hex_uint64_t,
|
cv.Optional(CONF_ADDRESS): cv.hex_uint64_t,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return schema
|
|
||||||
|
|
||||||
|
|
||||||
async def register_one_wire_device(var, config):
|
async def register_one_wire_device(var, config):
|
||||||
|
@@ -186,8 +186,7 @@ def _process_package(package_config, config):
|
|||||||
package_config = _process_base_package(package_config)
|
package_config = _process_base_package(package_config)
|
||||||
if isinstance(package_config, dict):
|
if isinstance(package_config, dict):
|
||||||
recursive_package = do_packages_pass(package_config)
|
recursive_package = do_packages_pass(package_config)
|
||||||
config = merge_config(recursive_package, config)
|
return merge_config(recursive_package, config)
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
def do_packages_pass(config: dict):
|
def do_packages_pass(config: dict):
|
||||||
|
@@ -114,8 +114,7 @@ PMWCS3_CALIBRATION_SCHEMA = cv.Schema(
|
|||||||
)
|
)
|
||||||
async def pmwcs3_calibration_to_code(config, action_id, template_arg, args):
|
async def pmwcs3_calibration_to_code(config, action_id, template_arg, args):
|
||||||
parent = await cg.get_variable(config[CONF_ID])
|
parent = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, parent)
|
return cg.new_Pvariable(action_id, template_arg, parent)
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
PMWCS3_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value(
|
PMWCS3_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value(
|
||||||
|
@@ -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"
|
||||||
|
@@ -136,8 +136,7 @@ RFBRIDGE_ID_SCHEMA = cv.Schema({cv.GenerateID(): cv.use_id(RFBridgeComponent)})
|
|||||||
@automation.register_action("rf_bridge.learn", RFBridgeLearnAction, RFBRIDGE_ID_SCHEMA)
|
@automation.register_action("rf_bridge.learn", RFBridgeLearnAction, RFBRIDGE_ID_SCHEMA)
|
||||||
async def rf_bridge_learnx_to_code(config, action_id, template_args, args):
|
async def rf_bridge_learnx_to_code(config, action_id, template_args, args):
|
||||||
paren = await cg.get_variable(config[CONF_ID])
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_args, paren)
|
return cg.new_Pvariable(action_id, template_args, paren)
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
@automation.register_action(
|
@automation.register_action(
|
||||||
@@ -149,8 +148,7 @@ async def rf_bridge_start_advanced_sniffing_to_code(
|
|||||||
config, action_id, template_args, args
|
config, action_id, template_args, args
|
||||||
):
|
):
|
||||||
paren = await cg.get_variable(config[CONF_ID])
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_args, paren)
|
return cg.new_Pvariable(action_id, template_args, paren)
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
@automation.register_action(
|
@automation.register_action(
|
||||||
@@ -162,8 +160,7 @@ async def rf_bridge_stop_advanced_sniffing_to_code(
|
|||||||
config, action_id, template_args, args
|
config, action_id, template_args, args
|
||||||
):
|
):
|
||||||
paren = await cg.get_variable(config[CONF_ID])
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_args, paren)
|
return cg.new_Pvariable(action_id, template_args, paren)
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
@automation.register_action(
|
@automation.register_action(
|
||||||
@@ -175,8 +172,7 @@ async def rf_bridge_start_bucket_sniffing_to_code(
|
|||||||
config, action_id, template_args, args
|
config, action_id, template_args, args
|
||||||
):
|
):
|
||||||
paren = await cg.get_variable(config[CONF_ID])
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_args, paren)
|
return cg.new_Pvariable(action_id, template_args, paren)
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
RFBRIDGE_SEND_ADVANCED_CODE_SCHEMA = cv.Schema(
|
RFBRIDGE_SEND_ADVANCED_CODE_SCHEMA = cv.Schema(
|
||||||
|
@@ -125,8 +125,7 @@ writezero:
|
|||||||
|
|
||||||
def time_to_cycles(time_us):
|
def time_to_cycles(time_us):
|
||||||
cycles_per_us = 57.5
|
cycles_per_us = 57.5
|
||||||
cycles = round(float(time_us) * cycles_per_us)
|
return round(float(time_us) * cycles_per_us)
|
||||||
return cycles
|
|
||||||
|
|
||||||
|
|
||||||
CONF_PIO = "pio"
|
CONF_PIO = "pio"
|
||||||
|
@@ -126,7 +126,6 @@ async def new_select(config, *, options: list[str]):
|
|||||||
|
|
||||||
@coroutine_with_priority(100.0)
|
@coroutine_with_priority(100.0)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_define("USE_SELECT")
|
|
||||||
cg.add_global(select_ns.using)
|
cg.add_global(select_ns.using)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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_)
|
||||||
|
|
||||||
@@ -1135,5 +1139,4 @@ def _lstsq(a, b):
|
|||||||
|
|
||||||
@coroutine_with_priority(100.0)
|
@coroutine_with_priority(100.0)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_define("USE_SENSOR")
|
|
||||||
cg.add_global(sensor_ns.using)
|
cg.add_global(sensor_ns.using)
|
||||||
|
@@ -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 {
|
||||||
|
@@ -171,8 +171,7 @@ async def sim800l_dial_to_code(config, action_id, template_arg, args):
|
|||||||
)
|
)
|
||||||
async def sim800l_connect_to_code(config, action_id, template_arg, args):
|
async def sim800l_connect_to_code(config, action_id, template_arg, args):
|
||||||
paren = await cg.get_variable(config[CONF_ID])
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
return var
|
|
||||||
|
|
||||||
|
|
||||||
SIM800L_SEND_USSD_SCHEMA = cv.Schema(
|
SIM800L_SEND_USSD_SCHEMA = cv.Schema(
|
||||||
@@ -201,5 +200,4 @@ async def sim800l_send_ussd_to_code(config, action_id, template_arg, args):
|
|||||||
)
|
)
|
||||||
async def sim800l_disconnect_to_code(config, action_id, template_arg, args):
|
async def sim800l_disconnect_to_code(config, action_id, template_arg, args):
|
||||||
paren = await cg.get_variable(config[CONF_ID])
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
return var
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user