Merge remote-tracking branch 'upstream/dev' into esp32-sram1-as-iram

# Conflicts:
#	esphome/components/esp32/__init__.py
This commit is contained in:
J. Nick Koston
2026-03-19 10:00:37 -10:00
337 changed files with 9059 additions and 4508 deletions
+1 -1
View File
@@ -1 +1 @@
8e48e836c6fc196d3da000d46eb09db243b87fe33518a74e49c8e009d756074a
44c877ff43765562ac8298902bf2208799643b77facf09c1c0c3c8c4e17187eb
+1 -1
View File
@@ -22,7 +22,7 @@ runs:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
# yamllint disable-line rule:line-length
+52 -16
View File
@@ -47,7 +47,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
# yamllint disable-line rule:line-length
@@ -154,12 +154,12 @@ jobs:
. venv/bin/activate
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -185,6 +185,7 @@ jobs:
cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
benchmarks: ${{ steps.determine.outputs.benchmarks }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -197,7 +198,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Restore components graph cache
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -227,9 +228,10 @@ jobs:
echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT
- name: Save components graph cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -251,7 +253,7 @@ jobs:
python-version: "3.13"
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -308,6 +310,40 @@ jobs:
script/cpp_unit_test.py $ARGS
fi
benchmarks:
name: Run CodSpeed benchmarks
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: >-
(github.event_name == 'push' && github.ref_name == 'dev') ||
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true')
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Build benchmarks
id: build
run: |
. venv/bin/activate
export BENCHMARK_LIB_CONFIG=$(python script/setup_codspeed_lib.py)
# --build-only prints BUILD_BINARY=<path> to stdout
BINARY=$(script/cpp_benchmark.py --all --build-only | grep '^BUILD_BINARY=' | tail -1 | cut -d= -f2-)
echo "binary=$BINARY" >> $GITHUB_OUTPUT
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@281164b0f014a4e7badd2c02cecad9b595b70537 # v4
with:
run: ${{ steps.build.outputs.binary }}
mode: simulation
clang-tidy-single:
name: ${{ matrix.name }}
runs-on: ubuntu-24.04
@@ -351,14 +387,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
@@ -430,14 +466,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -519,14 +555,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -781,7 +817,7 @@ jobs:
- name: Restore cached memory analysis
id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -805,7 +841,7 @@ jobs:
- name: Cache platformio
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -846,7 +882,7 @@ jobs:
- name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -893,7 +929,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: 3.13
python-version: "3.14"
- name: Install Home Assistant
run: |
+1 -1
View File
@@ -244,7 +244,6 @@ esphome/components/hyt271/* @Philippe12
esphome/components/i2c/* @esphome/core
esphome/components/i2c_device/* @gabest11
esphome/components/i2s_audio/* @jesserockz
esphome/components/i2s_audio/media_player/* @jesserockz
esphome/components/i2s_audio/microphone/* @jesserockz
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
esphome/components/iaqcore/* @yozik04
@@ -458,6 +457,7 @@ esphome/components/sn74hc165/* @jesserockz
esphome/components/socket/* @esphome/core
esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/sound_level/* @kahrendt
esphome/components/spa06_base/* @danielkent-net
esphome/components/speaker/* @jesserockz @kahrendt
esphome/components/speaker/media_player/* @kahrendt @synesthesiam
esphome/components/speaker_source/* @kahrendt
+1 -1
View File
@@ -1,4 +1,4 @@
# ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/)
# ESPHome [![Discord Chat](https://img.shields.io/discord/429907082951524364.svg)](https://discord.gg/KhAMKrd) [![GitHub release](https://img.shields.io/github/release/esphome/esphome.svg)](https://GitHub.com/esphome/esphome/releases/) [![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/esphome/esphome)
<a href="https://esphome.io/">
<picture>
@@ -1,22 +1,29 @@
#include "esphome/core/log.h"
#include "absolute_humidity.h"
namespace esphome {
namespace absolute_humidity {
namespace esphome::absolute_humidity {
static const char *const TAG = "absolute_humidity.sensor";
static const char *const TAG{"absolute_humidity.sensor"};
void AbsoluteHumidityComponent::setup() {
this->temperature_sensor_->add_on_state_callback([this](float state) {
this->temperature_ = state;
this->enable_loop();
});
ESP_LOGD(TAG, " Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str());
this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); });
// Get initial value
if (this->temperature_sensor_->has_state()) {
this->temperature_callback_(this->temperature_sensor_->get_state());
this->temperature_ = this->temperature_sensor_->get_state();
}
this->humidity_sensor_->add_on_state_callback([this](float state) {
this->humidity_ = state;
this->enable_loop();
});
ESP_LOGD(TAG, " Added callback for relative humidity '%s'", this->humidity_sensor_->get_name().c_str());
this->humidity_sensor_->add_on_state_callback([this](float state) { this->humidity_callback_(state); });
// Get initial value
if (this->humidity_sensor_->has_state()) {
this->humidity_callback_(this->humidity_sensor_->get_state());
this->humidity_ = this->humidity_sensor_->get_state();
}
}
@@ -46,14 +53,12 @@ void AbsoluteHumidityComponent::dump_config() {
}
void AbsoluteHumidityComponent::loop() {
if (!this->next_update_) {
return;
}
this->next_update_ = false;
// Only run once
this->disable_loop();
// Ensure we have source data
const bool no_temperature = std::isnan(this->temperature_);
const bool no_humidity = std::isnan(this->humidity_);
const bool no_temperature{std::isnan(this->temperature_)};
const bool no_humidity{std::isnan(this->humidity_)};
if (no_temperature || no_humidity) {
if (no_temperature) {
ESP_LOGW(TAG, "No valid state from temperature sensor!");
@@ -67,9 +72,9 @@ void AbsoluteHumidityComponent::loop() {
}
// Convert to desired units
const float temperature_c = this->temperature_;
const float temperature_k = temperature_c + 273.15;
const float hr = this->humidity_ / 100;
const float temperature_c{this->temperature_};
const float temperature_k{temperature_c + 273.15f};
const float hr{this->humidity_ / 100.0f};
// Calculate saturation vapor pressure
float es;
@@ -90,7 +95,7 @@ void AbsoluteHumidityComponent::loop() {
}
// Calculate absolute humidity
const float absolute_humidity = vapor_density(es, hr, temperature_k);
const float absolute_humidity{vapor_density(es, hr, temperature_k)};
ESP_LOGD(TAG, "Saturation vapor pressure %f kPa, absolute humidity %f g/m³", es, absolute_humidity);
@@ -103,16 +108,16 @@ void AbsoluteHumidityComponent::loop() {
// More accurate than Tetens in normal meteorologic conditions
float AbsoluteHumidityComponent::es_buck(float temperature_c) {
float a, b, c, d;
if (temperature_c >= 0) {
a = 0.61121;
b = 18.678;
c = 234.5;
d = 257.14;
if (temperature_c >= 0.0f) {
a = 0.61121f;
b = 18.678f;
c = 234.5f;
d = 257.14f;
} else {
a = 0.61115;
b = 18.678;
c = 233.7;
d = 279.82;
a = 0.61115f;
b = 18.678f;
c = 233.7f;
d = 279.82f;
}
return a * expf((b - (temperature_c / c)) * (temperature_c / (d + temperature_c)));
}
@@ -120,14 +125,14 @@ float AbsoluteHumidityComponent::es_buck(float temperature_c) {
// Tetens equation (https://en.wikipedia.org/wiki/Tetens_equation)
float AbsoluteHumidityComponent::es_tetens(float temperature_c) {
float a, b;
if (temperature_c >= 0) {
a = 17.27;
b = 237.3;
if (temperature_c >= 0.0f) {
a = 17.27f;
b = 237.3f;
} else {
a = 21.875;
b = 265.5;
a = 21.875f;
b = 265.5f;
}
return 0.61078 * expf((a * temperature_c) / (temperature_c + b));
return 0.61078f * expf((a * temperature_c) / (temperature_c + b));
}
// Wobus equation
@@ -146,18 +151,18 @@ float AbsoluteHumidityComponent::es_wobus(float t) {
//
// Baker, Schlatter 17-MAY-1982 Original version.
const float c0 = +0.99999683e00;
const float c1 = -0.90826951e-02;
const float c2 = +0.78736169e-04;
const float c3 = -0.61117958e-06;
const float c4 = +0.43884187e-08;
const float c5 = -0.29883885e-10;
const float c6 = +0.21874425e-12;
const float c7 = -0.17892321e-14;
const float c8 = +0.11112018e-16;
const float c9 = -0.30994571e-19;
const float p = c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9)))))))));
return 0.61078 / pow(p, 8);
constexpr float c0{+0.99999683e+00f};
constexpr float c1{-0.90826951e-02f};
constexpr float c2{+0.78736169e-04f};
constexpr float c3{-0.61117958e-06f};
constexpr float c4{+0.43884187e-08f};
constexpr float c5{-0.29883885e-10f};
constexpr float c6{+0.21874425e-12f};
constexpr float c7{-0.17892321e-14f};
constexpr float c8{+0.11112018e-16f};
constexpr float c9{-0.30994571e-19f};
const float p{c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9)))))))))};
return 0.61078f / powf(p, 8.0f);
}
// From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/
@@ -168,11 +173,10 @@ float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) {
// hr = relative humidity [0-1]
// ta = absolute temperature (K)
const float ea = hr * es * 1000; // vapor pressure of the air (Pa)
const float mw = 18.01528; // molar mass of water (g⋅mol⁻¹)
const float r = 8.31446261815324; // molar gas constant (J⋅K⁻¹)
const float ea{hr * es * 1000.0f}; // vapor pressure of the air (Pa)
const float mw{18.01528f}; // molar mass of water (g⋅mol⁻¹)
const float r{8.31446261815324f}; // molar gas constant (J⋅K⁻¹)
return (ea * mw) / (r * ta);
}
} // namespace absolute_humidity
} // namespace esphome
} // namespace esphome::absolute_humidity
@@ -3,8 +3,7 @@
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
namespace absolute_humidity {
namespace esphome::absolute_humidity {
/// Enum listing all implemented saturation vapor pressure equations.
enum SaturationVaporPressureEquation {
@@ -16,8 +15,6 @@ enum SaturationVaporPressureEquation {
/// This class implements calculation of absolute humidity from temperature and relative humidity.
class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
public:
AbsoluteHumidityComponent() = default;
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
void set_equation(SaturationVaporPressureEquation equation) { this->equation_ = equation; }
@@ -27,15 +24,6 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
void loop() override;
protected:
void temperature_callback_(float state) {
this->next_update_ = true;
this->temperature_ = state;
}
void humidity_callback_(float state) {
this->next_update_ = true;
this->humidity_ = state;
}
/** Buck equation for saturation vapor pressure in kPa.
*
* @param temperature_c Air temperature in °C.
@@ -57,19 +45,15 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
* @param es Saturation vapor pressure in kPa.
* @param hr Relative humidity 0 to 1.
* @param ta Absolute temperature in K.
* @param heater_duration The duration in ms that the heater should turn on for when measuring.
*/
static float vapor_density(float es, float hr, float ta);
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
bool next_update_{false};
float temperature_{NAN};
float humidity_{NAN};
SaturationVaporPressureEquation equation_;
};
} // namespace absolute_humidity
} // namespace esphome
} // namespace esphome::absolute_humidity
@@ -51,22 +51,6 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
}
}
void AlarmControlPanel::add_on_state_callback(std::function<void()> &&callback) {
this->state_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) {
this->cleared_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_chime_callback(std::function<void()> &&callback) {
this->chime_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback) {
this->ready_callback_.add(std::move(callback));
}
void AlarmControlPanel::arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(),
const char *code) {
auto call = this->make_call();
@@ -37,25 +37,24 @@ class AlarmControlPanel : public EntityBase {
*
* @param callback The callback function
*/
void add_on_state_callback(std::function<void()> &&callback);
template<typename F> void add_on_state_callback(F &&callback) {
this->state_callback_.add(std::forward<F>(callback));
}
/** Add a callback for when the state of the alarm_control_panel clears from triggered
*
* @param callback The callback function
*/
void add_on_cleared_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel clears from triggered. */
template<typename F> void add_on_cleared_callback(F &&callback) {
this->cleared_callback_.add(std::forward<F>(callback));
}
/** Add a callback for when a chime zone goes from closed to open
*
* @param callback The callback function
*/
void add_on_chime_callback(std::function<void()> &&callback);
/** Add a callback for when a chime zone goes from closed to open. */
template<typename F> void add_on_chime_callback(F &&callback) {
this->chime_callback_.add(std::forward<F>(callback));
}
/** Add a callback for when a ready state changes
*
* @param callback The callback function
*/
void add_on_ready_callback(std::function<void()> &&callback);
/** Add a callback for when a ready state changes. */
template<typename F> void add_on_ready_callback(F &&callback) {
this->ready_callback_.add(std::forward<F>(callback));
}
/** A numeric representation of the supported features as per HomeAssistant
*
+4 -4
View File
@@ -251,11 +251,11 @@ void APDS9960::read_gesture_data_() {
uint8_t buf[128];
for (uint8_t pos = 0; pos < fifo_level * 4; pos += 32) {
// The ESP's i2c driver has a limited buffer size.
// This way of retrieving the data should be wrong according to the datasheet
// but it seems to work.
// Read in 32-byte chunks due to ESP8266 I2C buffer limit.
// Always read from 0xFC — the FIFO auto-increments through 0xFC-0xFF
// and advances its internal pointer after every 4th byte.
uint8_t read = std::min(32, fifo_level * 4 - pos);
APDS9960_WARNING_CHECK(this->read_bytes(0xFC + pos, buf + pos, read), "Reading FIFO buffer failed.");
APDS9960_WARNING_CHECK(this->read_bytes(0xFC, buf + pos, read), "Reading FIFO buffer failed.");
}
if (millis() - this->gesture_start_ > 500) {
+3 -2
View File
@@ -301,11 +301,12 @@ CONFIG_SCHEMA = cv.All(
# Maximum queued send buffers per connection before dropping connection
# Each buffer uses ~8-12 bytes overhead plus actual message size
# Platform defaults based on available RAM and typical message rates:
# CONF_MAX_SEND_QUEUE defaults are power of 2 for efficient modulo
cv.SplitDefault(
CONF_MAX_SEND_QUEUE,
esp8266=5, # Limited RAM, need to fail fast
esp8266=4, # Limited RAM, need to fail fast
esp32=8, # More RAM, can buffer more
rp2040=5, # Limited RAM
rp2040=8, # Moderate RAM
bk72xx=8, # Moderate RAM
nrf52=8, # Moderate RAM
rtl87xx=8, # Moderate RAM
+36 -125
View File
@@ -100,150 +100,61 @@ const LogString *api_error_to_logstr(APIError err) {
return LOG_STR("UNKNOWN");
}
// Default implementation for loop - handles sending buffered data
APIError APIFrameHelper::loop() {
if (this->tx_buf_count_ > 0) {
APIError err = try_send_tx_buf_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
APIError APIFrameHelper::drain_overflow_and_handle_errors_() {
if (this->overflow_buf_.try_drain(this->socket_.get()) == -1) {
int err = errno;
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
HELPER_LOG("Socket write failed with errno %d", err);
return APIError::SOCKET_WRITE_FAILED;
}
}
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
return APIError::OK;
}
// Common socket write error handling
APIError APIFrameHelper::handle_socket_write_error_() {
const int err = errno;
if (err == EWOULDBLOCK || err == EAGAIN) {
return APIError::WOULD_BLOCK;
}
HELPER_LOG("Socket write failed with errno %d", err);
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
}
// Helper method to buffer data from IOVs
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
uint16_t offset) {
// Check if queue is full
if (this->tx_buf_count_ >= API_MAX_SEND_QUEUE) {
HELPER_LOG("Send queue full (%u buffers), dropping connection", this->tx_buf_count_);
this->state_ = State::FAILED;
return;
}
uint16_t buffer_size = total_write_len - offset;
auto &buffer = this->tx_buf_[this->tx_buf_tail_];
buffer = std::make_unique<SendBuffer>(SendBuffer{
.data = std::make_unique<uint8_t[]>(buffer_size),
.size = buffer_size,
.offset = 0,
});
uint16_t to_skip = offset;
uint16_t write_pos = 0;
for (int i = 0; i < iovcnt; i++) {
if (to_skip >= iov[i].iov_len) {
// Skip this entire segment
to_skip -= static_cast<uint16_t>(iov[i].iov_len);
} else {
// Include this segment (partially or fully)
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
std::memcpy(buffer->data.get() + write_pos, src, len);
write_pos += len;
to_skip = 0;
}
}
// Update circular buffer tracking
this->tx_buf_tail_ = (this->tx_buf_tail_ + 1) % API_MAX_SEND_QUEUE;
this->tx_buf_count_++;
}
// This method writes data to socket or buffers it
// Write data to socket, overflow to backlog buffer if LWIP TCP send buffer is full.
// Returns OK if all data was sent or successfully queued.
// Returns SOCKET_WRITE_FAILED on hard error (sets state to FAILED).
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
// Returns APIError::OK if successful (or would block, but data has been buffered)
// Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to FAILED
if (iovcnt == 0)
return APIError::OK; // Nothing to do, success
#ifdef HELPER_LOG_PACKETS
for (int i = 0; i < iovcnt; i++) {
LOG_PACKET_SENDING(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
}
#endif
// Try to send any existing buffered data first if there is any
if (this->tx_buf_count_ > 0) {
APIError send_result = try_send_tx_buf_();
// If real error occurred (not just WOULD_BLOCK), return it
if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) {
return send_result;
}
uint16_t skip = 0;
// If there is still data in the buffer, we can't send, buffer
// the new data and return
if (this->tx_buf_count_ > 0) {
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
return APIError::OK; // Success, data buffered
}
// Drain any existing backlog first
if (!this->overflow_buf_.empty()) [[unlikely]] {
APIError err = this->drain_overflow_and_handle_errors_();
if (err != APIError::OK)
return err;
}
// Try to send directly if no buffered data
// Optimize for single iovec case (common for plaintext API)
ssize_t sent =
(iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
// If backlog is clear, try direct send
if (this->overflow_buf_.empty()) [[likely]] {
ssize_t sent =
(iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
if (sent == -1) {
APIError err = this->handle_socket_write_error_();
if (err == APIError::WOULD_BLOCK) {
// Socket would block, buffer the data
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
return APIError::OK; // Success, data buffered
}
return err; // Socket write failed
} else if (static_cast<uint16_t>(sent) < total_write_len) {
// Partially sent, buffer the remaining data
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent));
}
return APIError::OK; // Success, all data sent or buffered
}
// Common implementation for trying to send buffered data
// IMPORTANT: Caller MUST ensure tx_buf_count_ > 0 before calling this method
APIError APIFrameHelper::try_send_tx_buf_() {
// Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check
while (this->tx_buf_count_ > 0) {
// Get the first buffer in the queue
SendBuffer *front_buffer = this->tx_buf_[this->tx_buf_head_].get();
// Try to send the remaining data in this buffer
ssize_t sent = this->socket_->write(front_buffer->current_data(), front_buffer->remaining());
if (sent == -1) {
return this->handle_socket_write_error_();
} else if (sent == 0) {
// Nothing sent but not an error
return APIError::WOULD_BLOCK;
} else if (static_cast<uint16_t>(sent) < front_buffer->remaining()) {
// Partially sent, update offset
// Cast to ensure no overflow issues with uint16_t
front_buffer->offset += static_cast<uint16_t>(sent);
return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer
if (sent == -1) [[unlikely]] {
int err = errno;
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
HELPER_LOG("Socket write failed with errno %d", err);
return APIError::SOCKET_WRITE_FAILED;
}
} else if (static_cast<uint16_t>(sent) >= total_write_len) [[likely]] {
return APIError::OK;
} else {
// Buffer completely sent, remove it from the queue
this->tx_buf_[this->tx_buf_head_].reset();
this->tx_buf_head_ = (this->tx_buf_head_ + 1) % API_MAX_SEND_QUEUE;
this->tx_buf_count_--;
// Continue loop to try sending the next buffer
skip = static_cast<uint16_t>(sent);
}
}
return APIError::OK; // All buffers sent successfully
// Queue unsent data into overflow buffer
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, skip)) {
HELPER_LOG("Overflow buffer full, dropping connection");
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
}
return APIError::OK;
}
const char *APIFrameHelper::get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
+18 -25
View File
@@ -9,6 +9,7 @@
#include "esphome/core/defines.h"
#ifdef USE_API
#include "esphome/components/api/api_buffer.h"
#include "esphome/components/api/api_overflow_buffer.h"
#include "esphome/components/socket/socket.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
@@ -104,9 +105,9 @@ class APIFrameHelper {
}
virtual ~APIFrameHelper() = default;
virtual APIError init() = 0;
virtual APIError loop();
virtual APIError loop() = 0;
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
bool can_write_without_blocking() { return this->state_ == State::DATA && this->overflow_buf_.empty(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() {
if (state_ == State::CLOSED)
@@ -189,28 +190,23 @@ class APIFrameHelper {
}
protected:
// Buffer containing data to be sent
struct SendBuffer {
std::unique_ptr<uint8_t[]> data;
uint16_t size{0}; // Total size of the buffer
uint16_t offset{0}; // Current offset within the buffer
// Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes
uint16_t remaining() const { return size - offset; }
const uint8_t *current_data() const { return data.get() + offset; }
};
// Drain backlogged overflow data to the socket and handle errors.
// Called when overflow_buf_.empty() is false. Out-of-line to keep the
// fast path (empty check) inline at call sites.
// Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors.
APIError drain_overflow_and_handle_errors_();
// Common implementation for writing raw data to socket
APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
// Try to send data from the tx buffer
APIError try_send_tx_buf_();
// Helper method to buffer data from IOVs
void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset);
// Common socket write error handling
APIError handle_socket_write_error_();
// Check if a socket write errno is a hard error (not WOULD_BLOCK/EAGAIN).
// Returns WOULD_BLOCK for transient errors, SOCKET_WRITE_FAILED for hard errors.
APIError check_socket_write_err_(int err) {
if (err == EWOULDBLOCK || err == EAGAIN)
return APIError::WOULD_BLOCK;
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
}
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
std::unique_ptr<socket::Socket> socket_;
@@ -245,8 +241,8 @@ class APIFrameHelper {
return APIError::WOULD_BLOCK;
}
// Containers (size varies, but typically 12+ bytes on 32-bit)
std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
// Backlog for unsent data when TCP send buffer is full (rarely used in production)
APIOverflowBuffer overflow_buf_;
APIBuffer rx_buf_;
// Client name buffer - stores name from Hello message or initial peername
@@ -257,9 +253,6 @@ class APIFrameHelper {
State state_{State::INITIALIZE};
uint8_t frame_header_padding_{0};
uint8_t frame_footer_size_{0};
uint8_t tx_buf_head_{0};
uint8_t tx_buf_tail_{0};
uint8_t tx_buf_count_{0};
// Nagle batching counter for log messages. 0 means NODELAY is enabled (immediate send).
// Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch.
// After LOG_NAGLE_COUNT logs, we flush by re-enabling NODELAY and resetting to 0.
@@ -153,8 +153,10 @@ APIError APINoiseFrameHelper::loop() {
}
}
// Use base class implementation for buffer sending
return APIFrameHelper::loop();
if (!this->overflow_buf_.empty()) [[unlikely]] {
return this->drain_overflow_and_handle_errors_();
}
return APIError::OK;
}
/** Read a packet into the rx_buf_.
@@ -64,8 +64,10 @@ APIError APIPlaintextFrameHelper::loop() {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
// Use base class implementation for buffer sending
return APIFrameHelper::loop();
if (!this->overflow_buf_.empty()) [[unlikely]] {
return this->drain_overflow_and_handle_errors_();
}
return APIError::OK;
}
/** Read a packet into the rx_buf_.
@@ -0,0 +1,73 @@
#include "api_overflow_buffer.h"
#ifdef USE_API
#include <cstring>
namespace esphome::api {
APIOverflowBuffer::~APIOverflowBuffer() {
for (auto *entry : this->queue_) {
if (entry != nullptr)
Entry::destroy(entry);
}
}
ssize_t APIOverflowBuffer::try_drain(socket::Socket *socket) {
while (this->count_ > 0) {
Entry *front = this->queue_[this->head_];
ssize_t sent = socket->write(front->current_data(), front->remaining());
if (sent <= 0) {
// -1 = error (caller checks errno for EWOULDBLOCK vs hard error)
// 0 = nothing sent (treat as no progress)
return sent;
}
if (static_cast<uint16_t>(sent) < front->remaining()) {
// Partially sent, update offset and stop
front->offset += static_cast<uint16_t>(sent);
return sent;
}
// Entry fully sent — free it and advance
Entry::destroy(front);
this->queue_[this->head_] = nullptr;
this->head_ = (this->head_ + 1) % API_MAX_SEND_QUEUE;
this->count_--;
}
return 0; // All drained
}
bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip) {
if (this->count_ >= API_MAX_SEND_QUEUE)
return false;
uint16_t buffer_size = total_len - skip;
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
auto *entry = new Entry{new uint8_t[buffer_size], buffer_size, 0};
this->queue_[this->tail_] = entry;
uint16_t to_skip = skip;
uint16_t write_pos = 0;
for (int i = 0; i < iovcnt; i++) {
if (to_skip >= iov[i].iov_len) {
to_skip -= static_cast<uint16_t>(iov[i].iov_len);
} else {
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
std::memcpy(entry->data + write_pos, src, len);
write_pos += len;
to_skip = 0;
}
}
this->tail_ = (this->tail_ + 1) % API_MAX_SEND_QUEUE;
this->count_++;
return true;
}
} // namespace esphome::api
#endif // USE_API
@@ -0,0 +1,76 @@
#pragma once
#include <array>
#include <cstdint>
#include <sys/types.h>
#include "esphome/core/defines.h"
#ifdef USE_API
#include "esphome/components/socket/headers.h"
#include "esphome/components/socket/socket.h"
#include "esphome/core/helpers.h"
namespace esphome::api {
/// Circular queue of heap-allocated byte buffers used as a TCP send backlog.
///
/// Under normal operation this buffer is **never used** — data goes straight
/// from the frame helper to the socket. It only fills when the LWIP TCP
/// send buffer is full (slow client, congested network, heavy logging).
/// The queue drains automatically on subsequent write/loop calls once the
/// socket becomes writable again.
///
/// Capacity is compile-time-fixed via API_MAX_SEND_QUEUE (set from Python
/// config). If the queue fills completely the connection is marked failed.
class APIOverflowBuffer {
public:
/// A single heap-allocated send-backlog entry.
/// Lifetime is manually managed — see destroy().
struct Entry {
uint8_t *data;
uint16_t size; // Total size of the buffer
uint16_t offset; // Current send offset within the buffer
uint16_t remaining() const { return this->size - this->offset; }
const uint8_t *current_data() const { return this->data + this->offset; }
/// Free this entry and its data buffer.
static ESPHOME_ALWAYS_INLINE void destroy(Entry *entry) {
delete[] entry->data;
delete entry; // NOLINT(cppcoreguidelines-owning-memory)
}
};
~APIOverflowBuffer();
/// True when no backlogged data is waiting.
bool empty() const { return this->count_ == 0; }
/// True when the queue has no room for another entry.
bool full() const { return this->count_ >= API_MAX_SEND_QUEUE; }
/// Number of entries currently queued.
uint8_t count() const { return this->count_; }
/// Try to drain queued data to the socket.
/// Returns bytes-written > 0 on success/partial, 0 if all drained or no progress,
/// -1 on error (caller must check errno to distinguish EWOULDBLOCK from hard errors).
/// Callers only need to act on -1; 0 and positive values both mean "no error".
/// Frees entries as they are fully sent.
ssize_t try_drain(socket::Socket *socket);
/// Enqueue unsent IOV data into the backlog.
/// Copies iov data starting at byte offset `skip` into a new entry.
/// Returns false if the queue is full (caller should fail the connection).
bool enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip);
protected:
std::array<Entry *, API_MAX_SEND_QUEUE> queue_{};
uint8_t head_{0};
uint8_t tail_{0};
uint8_t count_{0};
};
} // namespace esphome::api
#endif // USE_API
+17 -10
View File
@@ -136,8 +136,9 @@ class CustomAPIDevice {
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(StringRef), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), std::move(f));
auto *obj = static_cast<T *>(this);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
[obj, callback](StringRef state) { (obj->*callback)(state); });
}
/** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version).
@@ -148,10 +149,12 @@ class CustomAPIDevice {
ESPDEPRECATED("Use void callback(StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0")
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, std::placeholders::_1);
auto *obj = static_cast<T *>(this);
// Explicit type to disambiguate overload resolution
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
std::function<void(const std::string &)>(f));
global_api_server->subscribe_home_assistant_state(
entity_id, optional<std::string>(attribute),
std::function<void(const std::string &)>(
[obj, callback](const std::string &state) { (obj->*callback)(state); }));
}
/** Subscribe to the state (or attribute state) of an entity from Home Assistant.
@@ -176,8 +179,10 @@ class CustomAPIDevice {
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(const std::string &, StringRef), const std::string &entity_id,
const std::string &attribute = "") {
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), std::move(f));
auto *obj = static_cast<T *>(this);
global_api_server->subscribe_home_assistant_state(
entity_id, optional<std::string>(attribute),
[obj, callback, entity_id](StringRef state) { (obj->*callback)(entity_id, state); });
}
/** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version).
@@ -188,10 +193,12 @@ class CustomAPIDevice {
ESPDEPRECATED("Use void callback(const std::string &, StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0")
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
auto *obj = static_cast<T *>(this);
// Explicit type to disambiguate overload resolution
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
std::function<void(const std::string &)>(f));
global_api_server->subscribe_home_assistant_state(
entity_id, optional<std::string>(attribute),
std::function<void(const std::string &)>(
[obj, callback, entity_id](const std::string &state) { (obj->*callback)(entity_id, state); }));
}
#else
template<typename T>
+4
View File
@@ -442,8 +442,12 @@ class ProtoMessage {
virtual const char *message_name() const { return "unknown"; }
#endif
#ifndef USE_HOST
protected:
#endif
// Non-virtual destructor is protected to prevent polymorphic deletion.
// On host platform, made public to allow value-initialization of std::array
// members (e.g. DeviceInfoResponse::devices) without clang errors.
~ProtoMessage() = default;
};
+1 -1
View File
@@ -183,7 +183,7 @@ class BedjetCodec {
BedjetPacket packet_;
BedjetStatusPacket *status_packet_;
BedjetStatusPacket *status_packet_{nullptr};
BedjetStatusPacket buf_;
};
@@ -96,8 +96,7 @@ class MultiClickTrigger : public Trigger<>, public Component {
void setup() override {
this->last_state_ = this->parent_->get_state_default(false);
auto f = std::bind(&MultiClickTrigger::on_state_, this, std::placeholders::_1);
this->parent_->add_on_state_callback(f);
this->parent_->add_on_state_callback([this](bool state) { this->on_state_(state); });
}
float get_setup_priority() const override { return setup_priority::HARDWARE; }
@@ -47,6 +47,8 @@ void BLEClientRSSISensor::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl
switch (event) {
// server response on RSSI request:
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
if (!this->parent()->check_addr(param->read_rssi_cmpl.remote_addr))
return;
if (param->read_rssi_cmpl.status == ESP_BT_STATUS_SUCCESS) {
int8_t rssi = param->read_rssi_cmpl.rssi;
ESP_LOGI(TAG, "ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT RSSI: %d", rssi);
@@ -102,6 +102,10 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
break;
}
case ESP_GATTC_NOTIFY_EVT: {
if (param->notify.value_len == 0) {
ESP_LOGW(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: empty value", this->get_name().c_str());
break;
}
ESP_LOGD(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
param->notify.handle, param->notify.value[0]);
if (param->notify.handle != this->handle)
@@ -131,8 +135,10 @@ float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) {
if (this->has_data_to_value_) {
std::vector<uint8_t> data(value, value + value_len);
return this->data_to_value_func_(data);
} else {
} else if (value_len > 0) {
return value[0];
} else {
return NAN;
}
}
@@ -104,6 +104,10 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_NOTIFY_EVT: {
if (param->notify.handle != this->handle)
break;
if (param->notify.value_len == 0) {
ESP_LOGW(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: empty value", this->get_name().c_str());
break;
}
ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
param->notify.handle, param->notify.value[0]);
this->publish_state(reinterpret_cast<const char *>(param->notify.value), param->notify.value_len);
@@ -521,7 +521,7 @@ int BME680BSECComponent::reinit_bsec_lib_() {
}
void BME680BSECComponent::load_state_() {
uint32_t hash = fnv1_hash("bme680_bsec_state_" + this->device_id_);
uint32_t hash = fnv1_hash_extend(fnv1_hash("bme680_bsec_state_"), this->device_id_);
this->bsec_state_ = global_preferences->make_preference<uint8_t[BSEC_MAX_STATE_BLOB_SIZE]>(hash, true);
if (!this->bsec_state_.load(&this->bsec_state_data_)) {
+2 -2
View File
@@ -186,8 +186,8 @@ async def to_code_base(config):
cg.add_library("SPI", None)
cg.add_library(
"BME68x Sensor library",
"1.3.40408",
"https://github.com/boschsensortec/Bosch-BME68x-Library",
None,
"https://github.com/boschsensortec/Bosch-BME68x-Library#v1.3.40408",
)
cg.add_library(
"BSEC2 Software Library",
@@ -279,7 +279,8 @@ void BME68xBSEC2Component::run_() {
uint32_t meas_dur = 0;
meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_);
ESP_LOGV(TAG, "Queueing read in %uus", meas_dur);
this->set_timeout("read", meas_dur / 1000, [this, curr_time_ns]() { this->read_(curr_time_ns); });
this->trigger_time_ns_ = curr_time_ns;
this->set_timeout("read", meas_dur / 1000, [this]() { this->read_(this->trigger_time_ns_); });
} else {
ESP_LOGV(TAG, "Measurement not required");
this->read_(curr_time_ns);
@@ -116,6 +116,8 @@ class BME68xBSEC2Component : public Component {
int8_t bme68x_status_{BME68X_OK};
int64_t last_time_ms_{0};
int64_t trigger_time_ns_{0}; // Stored for set_timeout lambda to help avoid heap allocation on supported 32-bit
// toolchains with small std::function SBO
uint32_t millis_overflow_counter_{0};
std::queue<std::function<void()>> queue_;
-1
View File
@@ -20,6 +20,5 @@ void Button::press() {
this->press_action();
this->press_callback_.call();
}
void Button::add_on_press_callback(std::function<void()> &&callback) { this->press_callback_.add(std::move(callback)); }
} // namespace esphome::button
+3 -1
View File
@@ -34,7 +34,9 @@ class Button : public EntityBase {
*
* @param callback The void() callback.
*/
void add_on_press_callback(std::function<void()> &&callback);
template<typename F> void add_on_press_callback(F &&callback) {
this->press_callback_.add(std::forward<F>(callback));
}
protected:
/** You should implement this virtual method if you want to create your own button.
+1 -4
View File
@@ -91,10 +91,7 @@ class Canbus : public Component {
* - rtr If this is a remote transmission request
* - data The message data
*/
void add_callback(
std::function<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)> callback) {
this->callback_manager_.add(std::move(callback));
}
template<typename F> void add_callback(F &&callback) { this->callback_manager_.add(std::forward<F>(callback)); }
protected:
template<typename... Ts> friend class CanbusSendAction;
-8
View File
@@ -356,14 +356,6 @@ ClimateCall &ClimateCall::set_swing_mode(optional<ClimateSwingMode> swing_mode)
return *this;
}
void Climate::add_on_state_callback(std::function<void(Climate &)> &&callback) {
this->state_callback_.add(std::move(callback));
}
void Climate::add_on_control_callback(std::function<void(ClimateCall &)> &&callback) {
this->control_callback_.add(std::move(callback));
}
// Random 32bit value; If this changes existing restore preferences are invalidated
static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL;
+6 -2
View File
@@ -192,7 +192,9 @@ class Climate : public EntityBase {
*
* @param callback The callback to call.
*/
void add_on_state_callback(std::function<void(Climate &)> &&callback);
template<typename F> void add_on_state_callback(F &&callback) {
this->state_callback_.add(std::forward<F>(callback));
}
/**
* Add a callback for the climate device configuration; each time the configuration parameters of a climate device
@@ -200,7 +202,9 @@ class Climate : public EntityBase {
*
* @param callback The callback to call.
*/
void add_on_control_callback(std::function<void(ClimateCall &)> &&callback);
template<typename F> void add_on_control_callback(F &&callback) {
this->control_callback_.add(std::forward<F>(callback));
}
/** Make a climate device control call, this is used to control the climate device, see the ClimateCall description
* for more info.
+4
View File
@@ -21,6 +21,10 @@ CONF_REQUEST_HEADERS = "request_headers"
CONF_ROWS = "rows"
CONF_STOP_BITS = "stop_bits"
CONF_USE_PSRAM = "use_psram"
CONF_VOLUME_INCREMENT = "volume_increment"
CONF_VOLUME_INITIAL = "volume_initial"
CONF_VOLUME_MAX = "volume_max"
CONF_VOLUME_MIN = "volume_min"
ICON_CURRENT_DC = "mdi:current-dc"
ICON_SOLAR_PANEL = "mdi:solar-panel"
-1
View File
@@ -139,7 +139,6 @@ bool CoverCall::get_stop() const { return this->stop_; }
CoverCall Cover::make_call() { return {this}; }
void Cover::add_on_state_callback(std::function<void()> &&f) { this->state_callback_.add(std::move(f)); }
void Cover::publish_state(bool save) {
this->position = clamp(this->position, 0.0f, 1.0f);
this->tilt = clamp(this->tilt, 0.0f, 1.0f);
+1 -1
View File
@@ -125,7 +125,7 @@ class Cover : public EntityBase {
/// Construct a new cover call used to control the cover.
CoverCall make_call();
void add_on_state_callback(std::function<void()> &&f);
template<typename F> void add_on_state_callback(F &&f) { this->state_callback_.add(std::forward<F>(f)); }
/** Publish the current state of the cover.
*
+3 -1
View File
@@ -14,7 +14,9 @@ class DateTimeBase : public EntityBase {
public:
virtual ESPTime state_as_esptime() const = 0;
void add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
template<typename F> void add_on_state_callback(F &&callback) {
this->state_callback_.add(std::forward<F>(callback));
}
#ifdef USE_TIME
void set_rtc(time::RealTimeClock *rtc) { this->rtc_ = rtc; }
+4 -2
View File
@@ -49,7 +49,8 @@ static const size_t REBOOT_MAX_LEN = 24;
void DebugComponent::on_shutdown() {
auto *component = App.get_current_component();
char buffer[REBOOT_MAX_LEN]{};
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN,
fnv1_hash_extend(fnv1_hash(REBOOT_KEY), App.get_name().c_str()));
if (component != nullptr) {
strncpy(buffer, LOG_STR_ARG(component->get_component_log_str()), REBOOT_MAX_LEN - 1);
buffer[REBOOT_MAX_LEN - 1] = '\0';
@@ -66,7 +67,8 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
unsigned reason = esp_reset_reason();
if (reason < sizeof(RESET_REASONS) / sizeof(RESET_REASONS[0])) {
if (reason == ESP_RST_SW) {
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN,
fnv1_hash_extend(fnv1_hash(REBOOT_KEY), App.get_name().c_str()));
char reboot_source[REBOOT_MAX_LEN]{};
if (pref.load(&reboot_source)) {
reboot_source[REBOOT_MAX_LEN - 1] = '\0';
+2 -2
View File
@@ -51,8 +51,8 @@ class DFPlayer : public uart::UARTDevice, public Component {
bool is_playing() { return is_playing_; }
void dump_config() override;
void add_on_finished_playback_callback(std::function<void()> callback) {
this->on_finished_playback_callback_.add(std::move(callback));
template<typename F> void add_on_finished_playback_callback(F &&callback) {
this->on_finished_playback_callback_.add(std::forward<F>(callback));
}
protected:
@@ -44,9 +44,9 @@ class MenuItem {
MenuItemMenu *get_parent() { return this->parent_; }
MenuItemType get_type() const { return this->item_type_; }
template<typename V> void set_text(V val) { this->text_ = val; }
void add_on_enter_callback(std::function<void()> &&cb) { this->on_enter_callbacks_.add(std::move(cb)); }
void add_on_leave_callback(std::function<void()> &&cb) { this->on_leave_callbacks_.add(std::move(cb)); }
void add_on_value_callback(std::function<void()> &&cb) { this->on_value_callbacks_.add(std::move(cb)); }
template<typename F> void add_on_enter_callback(F &&cb) { this->on_enter_callbacks_.add(std::forward<F>(cb)); }
template<typename F> void add_on_leave_callback(F &&cb) { this->on_leave_callbacks_.add(std::forward<F>(cb)); }
template<typename F> void add_on_value_callback(F &&cb) { this->on_value_callbacks_.add(std::forward<F>(cb)); }
std::string get_text() const { return const_cast<MenuItem *>(this)->text_.value(this); }
virtual bool get_immediate_edit() const { return false; }
@@ -170,8 +170,8 @@ class MenuItemCommand : public MenuItem {
class MenuItemCustom : public MenuItemEditable {
public:
explicit MenuItemCustom() : MenuItemEditable(MENU_ITEM_CUSTOM) {}
void add_on_next_callback(std::function<void()> &&cb) { this->on_next_callbacks_.add(std::move(cb)); }
void add_on_prev_callback(std::function<void()> &&cb) { this->on_prev_callbacks_.add(std::move(cb)); }
template<typename F> void add_on_next_callback(F &&cb) { this->on_next_callbacks_.add(std::forward<F>(cb)); }
template<typename F> void add_on_prev_callback(F &&cb) { this->on_prev_callbacks_.add(std::forward<F>(cb)); }
bool has_value() const override { return this->value_getter_.has_value(); }
std::string get_value_text() const override;
+1 -2
View File
@@ -127,8 +127,7 @@ void DPS310Component::read_() {
this->update_in_progress_ = false;
this->status_clear_warning();
} else {
auto f = std::bind(&DPS310Component::read_, this);
this->set_timeout("dps310", 10, f);
this->set_timeout("dps310", 10, [this]() { this->read_(); });
}
}
+6 -9
View File
@@ -24,7 +24,7 @@ void EE895Component::setup() {
this->read(serial_number, 20);
crc16_check = (serial_number[19] << 8) + serial_number[18];
if (crc16_check != calc_crc16_(serial_number, 19)) {
if (crc16_check != calc_crc16_(serial_number, 18)) {
this->error_code_ = CRC_CHECK_FAILED;
this->mark_failed();
return;
@@ -84,7 +84,7 @@ void EE895Component::write_command_(uint16_t addr, uint16_t reg_cnt) {
address[2] = addr & 0xFF;
address[3] = (reg_cnt >> 8) & 0xFF;
address[4] = reg_cnt & 0xFF;
crc16 = calc_crc16_(address, 6);
crc16 = calc_crc16_(address, 5);
address[5] = crc16 & 0xFF;
address[6] = (crc16 >> 8) & 0xFF;
this->write(address, 7);
@@ -95,7 +95,7 @@ float EE895Component::read_float_() {
uint8_t i2c_response[8];
this->read(i2c_response, 8);
crc16_check = (i2c_response[7] << 8) + i2c_response[6];
if (crc16_check != calc_crc16_(i2c_response, 7)) {
if (crc16_check != calc_crc16_(i2c_response, 6)) {
this->error_code_ = CRC_CHECK_FAILED;
this->status_set_warning();
return 0;
@@ -107,12 +107,9 @@ float EE895Component::read_float_() {
}
uint16_t EE895Component::calc_crc16_(const uint8_t buf[], uint8_t len) {
uint8_t crc_check_buf[22];
for (int i = 0; i < len; i++) {
crc_check_buf[i + 1] = buf[i];
}
crc_check_buf[0] = this->address_;
return crc16(crc_check_buf, len);
uint8_t addr = this->address_;
uint16_t crc = crc16(&addr, 1);
return crc16(buf, len, crc);
}
} // namespace ee895
} // namespace esphome
+240 -47
View File
@@ -28,6 +28,7 @@ from esphome.const import (
CONF_PLATFORMIO_OPTIONS,
CONF_REF,
CONF_SAFE_MODE,
CONF_SIZE,
CONF_SOURCE,
CONF_TYPE,
CONF_VARIANT,
@@ -97,6 +98,7 @@ CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
CONF_RELEASE = "release"
CONF_SRAM1_AS_IRAM = "sram1_as_iram"
CONF_SUBTYPE = "subtype"
ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32"
ARDUINO_FRAMEWORK_PKG = f"pioarduino/{ARDUINO_FRAMEWORK_NAME}"
@@ -1267,6 +1269,43 @@ def _set_default_framework(config):
return config
RESERVED_PARTITION_NAMES = {
"nvs",
"app0",
"app1",
"otadata",
"eeprom",
"spiffs",
"phy_init",
}
VALID_APP_SUBTYPES = {"factory", "test"}
VALID_DATA_SUBTYPES = {
"nvs",
"nvs_keys",
"spiffs",
"coredump",
"efuse",
"fat",
"undefined",
"littlefs",
}
def _validate_custom_partition(config: ConfigType) -> ConfigType:
"""Voluptuous validator for custom partition schema."""
try:
_validate_partition(
config[CONF_NAME],
config[CONF_TYPE],
config[CONF_SUBTYPE],
config[CONF_SIZE],
)
except ValueError as e:
raise cv.Invalid(str(e)) from e
return config
FLASH_SIZES = [
"2MB",
"4MB",
@@ -1289,7 +1328,28 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of(
*FLASH_SIZES, upper=True
),
cv.Optional(CONF_PARTITIONS): cv.file_,
cv.Optional(CONF_PARTITIONS): cv.Any(
cv.file_,
cv.ensure_list(
cv.All(
cv.Schema(
{
cv.Required(CONF_NAME): cv.string_strict,
cv.Required(CONF_TYPE): cv.All(
cv.Any(cv.string_strict, cv.int_range(0x40, 0xFE)),
cv.int_to_hex_string,
),
cv.Required(CONF_SUBTYPE): cv.All(
cv.Any(cv.string_strict, cv.int_range(0, 0xFE)),
cv.int_to_hex_string,
),
cv.Required(CONF_SIZE): cv.int_range(min=0x1000),
}
),
_validate_custom_partition,
),
),
),
cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True),
cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA,
}
@@ -1768,9 +1828,18 @@ async def to_code(config):
if use_platformio:
cg.add_platformio_option("board_build.partitions", "partitions.csv")
if CONF_PARTITIONS in config:
add_extra_build_file(
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
)
if isinstance(config[CONF_PARTITIONS], list):
for partition in config[CONF_PARTITIONS]:
add_partition(
partition[CONF_NAME],
partition[CONF_TYPE],
partition[CONF_SUBTYPE],
partition[CONF_SIZE],
)
else:
add_extra_build_file(
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
)
if assertion_level := advanced.get(CONF_ASSERTION_LEVEL):
for key, flag in ASSERTION_LEVELS.items():
@@ -1904,45 +1973,175 @@ async def to_code(config):
CORE.add_job(_write_arduino_libraries_sdkconfig)
APP_PARTITION_SIZES = {
"2MB": 0x0C0000, # 768 KB
"4MB": 0x1C0000, # 1792 KB
"8MB": 0x3C0000, # 3840 KB
"16MB": 0x7C0000, # 7936 KB
"32MB": 0xFC0000, # 16128 KB
KEY_CUSTOM_PARTITIONS = "custom_partitions"
@dataclass
class PartitionEntry:
name: str
type: str
subtype: str
size: int
# Partition sizes (offsets auto-placed by gen_esp32part.py).
# These constants are the single source of truth — used in both
# the CSV generation and the overhead calculation.
BOOTLOADER_SIZE = 0x8000
PARTITION_TABLE_SIZE = 0x1000
FIRST_PARTITION_OFFSET = BOOTLOADER_SIZE + PARTITION_TABLE_SIZE
OTADATA_SIZE = 0x2000
PHY_INIT_SIZE = 0x1000
EEPROM_SIZE = 0x1000 # Arduino only
SPIFFS_SIZE = 0xF000 # Arduino only
ARDUINO_NVS_SIZE = 0x60000
IDF_NVS_SIZE = 0x70000
def _get_partition_overhead() -> int:
"""Total non-app partition budget (system partitions + nvs + padding).
Custom partitions are appended at the end and steal from app.
"""
# otadata + phy_init are followed by app0 which requires 64KB alignment,
# so pad up to the next 64KB boundary.
overhead = (
FIRST_PARTITION_OFFSET + OTADATA_SIZE + PHY_INIT_SIZE + 0xFFFF
) & ~0xFFFF
if CORE.using_arduino:
overhead += EEPROM_SIZE + SPIFFS_SIZE + ARDUINO_NVS_SIZE
else:
overhead += IDF_NVS_SIZE
return overhead
VALID_SUBTYPES: dict[str, set[str]] = {
"app": VALID_APP_SUBTYPES,
"data": VALID_DATA_SUBTYPES,
}
def get_arduino_partition_csv(flash_size: str):
app_partition_size = APP_PARTITION_SIZES[flash_size]
eeprom_partition_size = 0x1000 # 4 KB
spiffs_partition_size = 0xF000 # 60 KB
app0_partition_start = 0x010000 # 64 KB
app1_partition_start = app0_partition_start + app_partition_size
eeprom_partition_start = app1_partition_start + app_partition_size
spiffs_partition_start = eeprom_partition_start + eeprom_partition_size
return f"""\
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xE000, 0x2000,
app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X},
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},
spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X}
"""
def _validate_partition(
name: str, p_type: str | int, subtype: str | int, size: int
) -> None:
"""Validate partition parameters. Raises ValueError on invalid input."""
if name in RESERVED_PARTITION_NAMES:
raise ValueError(f"Partition name '{name}' is reserved.")
if size % 0x1000 != 0:
raise ValueError("Partition size must be 4KB (0x1000) aligned.")
# Numeric or already-normalized hex types/subtypes skip string validation
if not isinstance(p_type, str) or p_type.startswith("0x"):
return
if p_type not in VALID_SUBTYPES:
raise ValueError(
f"Type '{p_type}' is invalid. Only 'app' and 'data' are allowed."
" Use numbers for custom types."
)
if not isinstance(subtype, str) or subtype.startswith("0x"):
return
valid = VALID_SUBTYPES[p_type]
if subtype not in valid:
raise ValueError(
f"Subtype '{subtype}' is invalid for {p_type} type."
f" Only {', '.join(sorted(valid))} are allowed."
" Use numbers for custom subtypes."
)
def get_idf_partition_csv(flash_size: str):
app_partition_size = APP_PARTITION_SIZES[flash_size]
def add_partition(name: str, p_type: str | int, subtype: str | int, size: int) -> None:
"""Register a custom partition to be appended to the partition table.
return f"""\
otadata, data, ota, , 0x2000,
phy_init, data, phy, , 0x1000,
app0, app, ota_0, , 0x{app_partition_size:X},
app1, app, ota_1, , 0x{app_partition_size:X},
nvs, data, nvs, , 0x6D000,
"""
Called from component to_code() to request additional flash partitions.
Size must be 4KB aligned. Integer types/subtypes are converted to hex strings.
"""
if name in CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}):
raise ValueError(f"Partition name '{name}' is already defined.")
_validate_partition(name, p_type, subtype, size)
p_type_str = f"0x{p_type:X}" if isinstance(p_type, int) else p_type
subtype_str = f"0x{subtype:X}" if isinstance(subtype, int) else subtype
custom_partitions = CORE.data[KEY_ESP32].setdefault(KEY_CUSTOM_PARTITIONS, {})
custom_partitions[name] = PartitionEntry(
name=name, type=p_type_str, subtype=subtype_str, size=size
)
def _flash_size_to_bytes(flash_size_mb: str) -> int:
"""Convert flash size string (e.g. '4MB') to bytes."""
return int(flash_size_mb.removesuffix("MB")) * 1024 * 1024
def _get_custom_partitions_total_size() -> int:
"""Total size of custom partitions including alignment padding."""
size = 0
for partition in CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}).values():
if partition.type == "app":
size = (size + 0xFFFF) & ~0xFFFF # align to 64KB
size += partition.size
return size
def _get_app_partition_size(flash_size_mb: str) -> int:
flash_bytes = _flash_size_to_bytes(flash_size_mb)
custom_total = _get_custom_partitions_total_size()
# Align down to 64KB — app partitions require 64KB-aligned offsets,
# so the size must also be aligned to avoid unbudgeted padding.
raw_size = (flash_bytes - _get_partition_overhead() - custom_total) // 2
app_size = raw_size & ~0xFFFF
wasted = (raw_size - app_size) * 2
if wasted:
_LOGGER.info(
"Custom partitions cause %dKB of wasted flash due to 64KB app partition alignment.",
wasted // 1024,
)
if app_size <= 0x10000: # 64 KB
raise ValueError(
"Custom partitions are too large to fit in the available flash size. "
"Reduce custom partition sizes."
)
if app_size <= 0x80000: # 512 KB
_LOGGER.warning(
"App partition size is only %dKB. This may be too small for firmware with "
"many components. Consider reducing custom partition sizes or using a "
"larger flash chip.",
app_size // 1024,
)
return app_size
def get_partition_csv(flash_size_mb: str) -> str:
app_size = _get_app_partition_size(flash_size_mb)
partitions: list[PartitionEntry] = [
PartitionEntry(name="otadata", type="data", subtype="ota", size=OTADATA_SIZE),
PartitionEntry(name="phy_init", type="data", subtype="phy", size=PHY_INIT_SIZE),
PartitionEntry(name="app0", type="app", subtype="ota_0", size=app_size),
PartitionEntry(name="app1", type="app", subtype="ota_1", size=app_size),
]
if CORE.using_arduino:
partitions.append(
PartitionEntry(name="eeprom", type="data", subtype="0x99", size=EEPROM_SIZE)
)
partitions.append(
PartitionEntry(
name="spiffs", type="data", subtype="spiffs", size=SPIFFS_SIZE
)
)
partitions.append(
PartitionEntry(
name="nvs", type="data", subtype="nvs", size=ARDUINO_NVS_SIZE
)
)
else:
partitions.append(
PartitionEntry(name="nvs", type="data", subtype="nvs", size=IDF_NVS_SIZE)
)
partitions.extend(CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}).values())
csv = "".join(
f"{p.name}, {p.type}, {p.subtype}, , 0x{p.size:X},\n" for p in partitions
)
_LOGGER.debug("Partition table:\n%s", csv)
return csv
def _format_sdkconfig_val(value: SdkconfigValueType) -> str:
@@ -2049,16 +2248,10 @@ def copy_files():
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE]
if CORE.using_arduino:
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_arduino_partition_csv(flash_size),
)
else:
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_idf_partition_csv(flash_size),
)
write_file_if_changed(
CORE.relative_build_path("partitions.csv"),
get_partition_csv(flash_size),
)
# IDF build scripts look for version string to put in the build.
# However, if the build path does not have an initialized git repo,
# and no version.txt file exists, the CMake script fails for some setups.
-3
View File
@@ -53,9 +53,6 @@ void arch_init() {
}
void HOT arch_feed_wdt() { esp_task_wdt_reset(); }
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
const char *progmem_read_ptr(const char *const *addr) { return *addr; }
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() {
uint32_t freq = 0;
@@ -0,0 +1,27 @@
#pragma once
#ifdef USE_ESP32
#include <cstddef>
#include <cstdint>
namespace esphome::esp32 {
class ESP32PreferenceBackend final {
public:
bool save(const uint8_t *data, size_t len);
bool load(uint8_t *data, size_t len);
uint32_t key;
uint32_t nvs_handle;
};
class ESP32Preferences;
ESP32Preferences *get_preferences();
} // namespace esphome::esp32
namespace esphome {
using PreferenceBackend = esp32::ESP32PreferenceBackend;
} // namespace esphome
#endif // USE_ESP32
+147 -159
View File
@@ -1,16 +1,14 @@
#ifdef USE_ESP32
#include "preferences.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
#include <nvs_flash.h>
#include <cinttypes>
#include <cstring>
#include <memory>
#include <vector>
namespace esphome {
namespace esp32 {
namespace esphome::esp32 {
static const char *const TAG = "esp32.preferences";
@@ -24,185 +22,175 @@ struct NVSData {
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
class ESP32PreferenceBackend : public ESPPreferenceBackend {
public:
uint32_t key;
uint32_t nvs_handle;
bool save(const uint8_t *data, size_t len) override {
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
obj.data.set(data, len);
return true;
}
bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) {
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
obj.data.set(data, len);
return true;
}
NVSData save{};
save.key = this->key;
save.data.set(data, len);
s_pending_save.push_back(std::move(save));
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
return true;
}
bool load(uint8_t *data, size_t len) override {
// try find in pending saves and load from that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
if (obj.data.size() != len) {
// size mismatch
return false;
}
memcpy(data, obj.data.data(), len);
return true;
}
}
NVSData save{};
save.key = this->key;
save.data.set(data, len);
s_pending_save.push_back(std::move(save));
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
return true;
}
bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
// try find in pending saves and load from that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
if (obj.data.size() != len) {
// size mismatch
return false;
}
memcpy(data, obj.data.data(), len);
return true;
}
}
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key);
size_t actual_len;
esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
return false;
}
if (actual_len != len) {
ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len);
return false;
}
err = nvs_get_blob(this->nvs_handle, key_str, data, &len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
return false;
} else {
ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key_str, len);
}
return true;
}
void ESP32Preferences::open() {
nvs_flash_init();
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
if (err == 0)
return;
ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err));
nvs_flash_deinit();
nvs_flash_erase();
nvs_flash_init();
err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
if (err != 0) {
this->nvs_handle = 0;
}
}
ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type) {
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->nvs_handle = this->nvs_handle;
pref->key = type;
return ESPPreferenceObject(pref);
}
bool ESP32Preferences::sync() {
if (s_pending_save.empty())
return true;
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
int cached = 0, written = 0, failed = 0;
esp_err_t last_err = ESP_OK;
uint32_t last_key = 0;
for (const auto &save : s_pending_save) {
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key);
size_t actual_len;
esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
return false;
}
if (actual_len != len) {
ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len);
return false;
}
err = nvs_get_blob(this->nvs_handle, key_str, data, &len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
return false;
} else {
ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key_str, len);
}
return true;
}
};
class ESP32Preferences : public ESPPreferences {
public:
uint32_t nvs_handle;
void open() {
nvs_flash_init();
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &nvs_handle);
if (err == 0)
return;
ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err));
nvs_flash_deinit();
nvs_flash_erase();
nvs_flash_init();
err = nvs_open("esphome", NVS_READWRITE, &nvs_handle);
if (err != 0) {
nvs_handle = 0;
}
}
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override {
return this->make_preference(length, type);
}
ESPPreferenceObject make_preference(size_t length, uint32_t type) override {
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->nvs_handle = this->nvs_handle;
pref->key = type;
return ESPPreferenceObject(pref);
}
bool sync() override {
if (s_pending_save.empty())
return true;
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
int cached = 0, written = 0, failed = 0;
esp_err_t last_err = ESP_OK;
uint32_t last_key = 0;
for (const auto &save : s_pending_save) {
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
if (this->is_changed_(this->nvs_handle, save, key_str)) {
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
if (err != 0) {
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
failed++;
last_err = err;
last_key = save.key;
continue;
}
written++;
} else {
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
cached++;
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
if (this->is_changed_(this->nvs_handle, save, key_str)) {
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
if (err != 0) {
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
failed++;
last_err = err;
last_key = save.key;
continue;
}
written++;
} else {
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
cached++;
}
s_pending_save.clear();
}
s_pending_save.clear();
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
failed);
if (failed > 0) {
ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%" PRIu32, failed, esp_err_to_name(last_err),
last_key);
}
// note: commit on esp-idf currently is a no-op, nvs_set_blob always writes
esp_err_t err = nvs_commit(this->nvs_handle);
if (err != 0) {
ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err));
return false;
}
return failed == 0;
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
failed);
if (failed > 0) {
ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%" PRIu32, failed, esp_err_to_name(last_err),
last_key);
}
protected:
bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str) {
size_t actual_len;
esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
return true;
}
// Check size first before allocating memory
if (actual_len != to_save.data.size()) {
return true;
}
// Most preferences are small, use stack buffer with heap fallback for large ones
SmallBufferWithHeapFallback<256> stored_data(actual_len);
err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
return true;
}
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
// note: commit on esp-idf currently is a no-op, nvs_set_blob always writes
esp_err_t err = nvs_commit(this->nvs_handle);
if (err != 0) {
ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err));
return false;
}
bool reset() override {
ESP_LOGD(TAG, "Erasing storage");
s_pending_save.clear();
return failed == 0;
}
nvs_flash_deinit();
nvs_flash_erase();
// Make the handle invalid to prevent any saves until restart
nvs_handle = 0;
bool ESP32Preferences::is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str) {
size_t actual_len;
esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
return true;
}
};
// Check size first before allocating memory
if (actual_len != to_save.data.size()) {
return true;
}
// Most preferences are small, use stack buffer with heap fallback for large ones
SmallBufferWithHeapFallback<256> stored_data(actual_len);
err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len);
if (err != 0) {
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
return true;
}
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
}
bool ESP32Preferences::reset() {
ESP_LOGD(TAG, "Erasing storage");
s_pending_save.clear();
nvs_flash_deinit();
nvs_flash_erase();
// Make the handle invalid to prevent any saves until restart
this->nvs_handle = 0;
return true;
}
static ESP32Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
ESP32Preferences *get_preferences() { return &s_preferences; }
void setup_preferences() {
s_preferences.open();
global_preferences = &s_preferences;
}
} // namespace esp32
} // namespace esphome::esp32
namespace esphome {
ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome
#endif // USE_ESP32
+25 -4
View File
@@ -1,12 +1,33 @@
#pragma once
#ifdef USE_ESP32
namespace esphome {
namespace esp32 {
#include "esphome/core/preference_backend.h"
namespace esphome::esp32 {
struct NVSData;
class ESP32Preferences final : public PreferencesMixin<ESP32Preferences> {
public:
using PreferencesMixin<ESP32Preferences>::make_preference;
void open();
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) {
return this->make_preference(length, type);
}
ESPPreferenceObject make_preference(size_t length, uint32_t type);
bool sync();
bool reset();
uint32_t nvs_handle;
protected:
bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str);
};
void setup_preferences();
} // namespace esp32
} // namespace esphome
} // namespace esphome::esp32
DECLARE_PREFERENCE_ALIASES(esphome::esp32::ESP32Preferences)
#endif // USE_ESP32
+2 -3
View File
@@ -81,8 +81,6 @@ void ESP32BLE::disable() {
this->state_ = BLE_COMPONENT_STATE_DISABLE;
}
bool ESP32BLE::is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; }
#ifdef USE_ESP32_BLE_ADVERTISING
void ESP32BLE::advertising_start() {
this->advertising_init_();
@@ -575,8 +573,9 @@ template<typename... Args> void enqueue_ble_event(Args... args) {
load_ble_event(event, args...);
// Push the event to the queue
// Push always succeeds: pool is sized to queue capacity (N-1), so if
// allocate() returned non-null, the queue is guaranteed to have room.
global_ble->ble_events_.push(event);
// Push always succeeds because we're the only producer and the pool ensures we never exceed queue size
}
// Explicit template instantiations for the friend function
+8 -2
View File
@@ -135,7 +135,7 @@ class ESP32BLE : public Component {
void enable();
void disable();
bool is_active();
ESPHOME_ALWAYS_INLINE bool is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; }
void setup() override;
void loop() override;
void dump_config() override;
@@ -221,7 +221,13 @@ class ESP32BLE : public Component {
// Large objects (size depends on template parameters, but typically aligned to 4 bytes)
esphome::LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_;
esphome::EventPool<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_event_pool_;
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
// buffer that holds N-1 elements (one slot distinguishes full from empty).
// This guarantees allocate() returns nullptr before push() can fail, which:
// 1. Prevents leaking a pool slot (the Nth allocate succeeds but push fails)
// 2. Avoids needing release() on the producer path after a failed push(),
// preserving the SPSC contract on the pool's internal free list
esphome::EventPool<BLEEvent, MAX_BLE_QUEUE_SIZE - 1> ble_event_pool_;
// 4-byte aligned members
#ifdef USE_ESP32_BLE_ADVERTISING
@@ -16,13 +16,9 @@ BLECharacteristic::~BLECharacteristic() {
for (auto *descriptor : this->descriptors_) {
delete descriptor; // NOLINT(cppcoreguidelines-owning-memory)
}
vSemaphoreDelete(this->set_value_lock_);
}
BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) : uuid_(uuid) {
this->set_value_lock_ = xSemaphoreCreateBinary();
xSemaphoreGive(this->set_value_lock_);
this->properties_ = (esp_gatt_char_prop_t) 0;
this->set_broadcast_property((properties & PROPERTY_BROADCAST) != 0);
@@ -35,11 +31,7 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties)
void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) {
xSemaphoreTake(this->set_value_lock_, 0L);
this->value_ = std::move(buffer);
xSemaphoreGive(this->set_value_lock_);
}
void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) { this->value_ = std::move(buffer); }
void BLECharacteristic::set_value(std::initializer_list<uint8_t> data) {
this->set_value(std::vector<uint8_t>(data)); // Delegate to move overload
@@ -16,8 +16,6 @@
#include <esp_gattc_api.h>
#include <esp_gatts_api.h>
#include <esp_bt_defs.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
namespace esphome {
namespace esp32_ble_server {
@@ -84,8 +82,6 @@ class BLECharacteristic {
uint16_t value_read_offset_{0};
std::vector<uint8_t> value_;
SemaphoreHandle_t set_value_lock_;
std::vector<BLEDescriptor *> descriptors_;
struct ClientNotificationEntry {
@@ -7,7 +7,6 @@
#ifdef USE_ESP32
#include <algorithm>
#include <nvs_flash.h>
#include <freertos/FreeRTOSConfig.h>
#include <esp_bt_main.h>
@@ -39,16 +38,17 @@ void BLEServer::loop() {
case RUNNING: {
// Start all services that are pending to start
if (!this->services_to_start_.empty()) {
for (auto &service : this->services_to_start_) {
size_t write_idx = 0;
for (auto *service : this->services_to_start_) {
if (service->is_created()) {
service->start(); // Needs to be called once per characteristic in the service
}
// Remove services that have started or are starting
if (!service->is_starting() && !service->is_running()) {
this->services_to_start_[write_idx++] = service;
}
}
// Remove services that have been started
this->services_to_start_.erase(
std::remove_if(this->services_to_start_.begin(), this->services_to_start_.end(),
[](BLEService *service) { return service->is_starting() || service->is_running(); }),
this->services_to_start_.end());
this->services_to_start_.erase(this->services_to_start_.begin() + write_idx, this->services_to_start_.end());
}
break;
}
@@ -91,8 +91,6 @@ void BLEServer::loop() {
}
}
bool BLEServer::is_running() { return this->parent_->is_active() && this->state_ == RUNNING; }
bool BLEServer::can_proceed() { return this->is_running() || !this->parent_->is_active(); }
void BLEServer::restart_advertising_() {
@@ -32,7 +32,7 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
float get_setup_priority() const override;
bool can_proceed() override;
bool is_running();
ESPHOME_ALWAYS_INLINE bool is_running() { return this->parent_->is_active() && this->state_ == RUNNING; }
void set_manufacturer_data(const std::vector<uint8_t> &data) {
this->manufacturer_data_ = data;
@@ -350,8 +350,7 @@ void ESP32ImprovComponent::process_incoming_data_() {
ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
command.password.c_str());
auto f = std::bind(&ESP32ImprovComponent::on_wifi_connect_timeout_, this);
this->set_timeout("wifi-connect-timeout", 30000, f);
this->set_timeout("wifi-connect-timeout", 30000, [this]() { this->on_wifi_connect_timeout_(); });
this->incoming_data_.clear();
break;
}
@@ -48,8 +48,8 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase {
bool should_start() const { return this->should_start_; }
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
void add_on_state_callback(std::function<void(improv::State, improv::Error)> &&callback) {
this->state_callback_.add(std::move(callback));
template<typename F> void add_on_state_callback(F &&callback) {
this->state_callback_.add(std::forward<F>(callback));
}
#endif
#ifdef USE_BINARY_SENSOR
@@ -0,0 +1,29 @@
#pragma once
#ifdef USE_ESP8266
#include <cstddef>
#include <cstdint>
namespace esphome::esp8266 {
class ESP8266PreferenceBackend final {
public:
bool save(const uint8_t *data, size_t len);
bool load(uint8_t *data, size_t len);
uint32_t type = 0;
uint16_t offset = 0;
uint8_t length_words = 0; // Max 255 words (1020 bytes of data)
bool in_flash = false;
};
class ESP8266Preferences;
ESP8266Preferences *get_preferences();
} // namespace esphome::esp8266
namespace esphome {
using PreferenceBackend = esp8266::ESP8266PreferenceBackend;
} // namespace esphome
#endif // USE_ESP8266
+113 -135
View File
@@ -5,11 +5,9 @@ extern "C" {
#include "spi_flash.h"
}
#include "esphome/core/defines.h"
#include "preferences.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
#include "preferences.h"
#include <cstring>
@@ -137,155 +135,135 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) {
static constexpr size_t PREF_MAX_BUFFER_WORDS =
ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS;
class ESP8266PreferenceBackend : public ESPPreferenceBackend {
public:
uint32_t type = 0;
uint16_t offset = 0;
uint8_t length_words = 0; // Max 255 words (1020 bytes of data)
bool in_flash = false;
bool ESP8266PreferenceBackend::save(const uint8_t *data, size_t len) {
if (bytes_to_words(len) != this->length_words)
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
if (buffer_size > PREF_MAX_BUFFER_WORDS)
return false;
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
memset(buffer, 0, buffer_size * sizeof(uint32_t));
memcpy(buffer, data, len);
buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type);
return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size)
: save_to_rtc(this->offset, buffer, buffer_size);
}
bool save(const uint8_t *data, size_t len) override {
if (bytes_to_words(len) != this->length_words)
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
if (buffer_size > PREF_MAX_BUFFER_WORDS)
return false;
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
memset(buffer, 0, buffer_size * sizeof(uint32_t));
memcpy(buffer, data, len);
buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type);
return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size)
: save_to_rtc(this->offset, buffer, buffer_size);
bool ESP8266PreferenceBackend::load(uint8_t *data, size_t len) {
if (bytes_to_words(len) != this->length_words)
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
if (buffer_size > PREF_MAX_BUFFER_WORDS)
return false;
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
: load_from_rtc(this->offset, buffer, buffer_size);
if (!ret)
return false;
if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type))
return false;
memcpy(data, buffer, len);
return true;
}
void ESP8266Preferences::setup() {
ESP_LOGVV(TAG, "Loading preferences from flash");
{
InterruptLock lock;
spi_flash_read(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4);
}
}
ESPPreferenceObject ESP8266Preferences::make_preference(size_t length, uint32_t type, bool in_flash) {
const uint32_t length_words = bytes_to_words(length);
if (length_words > MAX_PREFERENCE_WORDS) {
ESP_LOGE(TAG, "Preference too large: %u words", static_cast<unsigned int>(length_words));
return {};
}
bool load(uint8_t *data, size_t len) override {
if (bytes_to_words(len) != this->length_words)
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
if (buffer_size > PREF_MAX_BUFFER_WORDS)
return false;
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
: load_from_rtc(this->offset, buffer, buffer_size);
if (!ret)
return false;
if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type))
return false;
memcpy(data, buffer, len);
return true;
}
};
const uint32_t total_words = length_words + 1; // +1 for CRC
uint16_t offset;
class ESP8266Preferences : public ESPPreferences {
public:
uint32_t current_offset = 0;
uint32_t current_flash_offset = 0; // in words
void setup() {
ESP_LOGVV(TAG, "Loading preferences from flash");
{
InterruptLock lock;
spi_flash_read(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4);
}
}
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override {
const uint32_t length_words = bytes_to_words(length);
if (length_words > MAX_PREFERENCE_WORDS) {
ESP_LOGE(TAG, "Preference too large: %u words", static_cast<unsigned int>(length_words));
if (in_flash) {
if (this->current_flash_offset + total_words > ESP8266_FLASH_STORAGE_SIZE)
return {};
offset = static_cast<uint16_t>(this->current_flash_offset);
this->current_flash_offset += total_words;
} else {
uint32_t start = this->current_offset;
bool in_normal = start < RTC_NORMAL_REGION_WORDS;
// Normal: offset 0-95 maps to RTC offset 32-127
// Eboot: offset 96-127 maps to RTC offset 0-31
if (in_normal && start + total_words > RTC_NORMAL_REGION_WORDS) {
// start is in normal but end is not -> switch to Eboot
this->current_offset = start = RTC_NORMAL_REGION_WORDS;
in_normal = false;
}
const uint32_t total_words = length_words + 1; // +1 for CRC
uint16_t offset;
if (in_flash) {
if (this->current_flash_offset + total_words > ESP8266_FLASH_STORAGE_SIZE)
return {};
offset = static_cast<uint16_t>(this->current_flash_offset);
this->current_flash_offset += total_words;
} else {
uint32_t start = this->current_offset;
bool in_normal = start < RTC_NORMAL_REGION_WORDS;
// Normal: offset 0-95 maps to RTC offset 32-127
// Eboot: offset 96-127 maps to RTC offset 0-31
if (in_normal && start + total_words > RTC_NORMAL_REGION_WORDS) {
// start is in normal but end is not -> switch to Eboot
this->current_offset = start = RTC_NORMAL_REGION_WORDS;
in_normal = false;
}
if (start + total_words > PREF_TOTAL_WORDS)
return {}; // Doesn't fit in RTC memory
// Convert preference offset to RTC memory offset
offset = static_cast<uint16_t>(in_normal ? start + RTC_EBOOT_REGION_WORDS : start - RTC_NORMAL_REGION_WORDS);
this->current_offset = start + total_words;
}
auto *pref = new ESP8266PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->offset = offset;
pref->type = type;
pref->length_words = static_cast<uint8_t>(length_words);
pref->in_flash = in_flash;
return pref;
if (start + total_words > PREF_TOTAL_WORDS)
return {}; // Doesn't fit in RTC memory
// Convert preference offset to RTC memory offset
offset = static_cast<uint16_t>(in_normal ? start + RTC_EBOOT_REGION_WORDS : start - RTC_NORMAL_REGION_WORDS);
this->current_offset = start + total_words;
}
ESPPreferenceObject make_preference(size_t length, uint32_t type) override {
#ifdef USE_ESP8266_PREFERENCES_FLASH
return make_preference(length, type, true);
#else
return make_preference(length, type, false);
#endif
}
auto *pref = new ESP8266PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->offset = offset;
pref->type = type;
pref->length_words = static_cast<uint8_t>(length_words);
pref->in_flash = in_flash;
return ESPPreferenceObject(pref);
}
bool sync() override {
if (!s_flash_dirty)
return true;
if (s_prevent_write)
return false;
ESP_LOGD(TAG, "Saving");
SpiFlashOpResult erase_res, write_res = SPI_FLASH_RESULT_OK;
{
InterruptLock lock;
erase_res = spi_flash_erase_sector(get_esp8266_flash_sector());
if (erase_res == SPI_FLASH_RESULT_OK) {
write_res = spi_flash_write(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4);
}
}
if (erase_res != SPI_FLASH_RESULT_OK) {
ESP_LOGE(TAG, "Erasing failed");
return false;
}
if (write_res != SPI_FLASH_RESULT_OK) {
ESP_LOGE(TAG, "Writing failed");
return false;
}
s_flash_dirty = false;
bool ESP8266Preferences::sync() {
if (!s_flash_dirty)
return true;
if (s_prevent_write)
return false;
ESP_LOGD(TAG, "Saving");
SpiFlashOpResult erase_res, write_res = SPI_FLASH_RESULT_OK;
{
InterruptLock lock;
erase_res = spi_flash_erase_sector(get_esp8266_flash_sector());
if (erase_res == SPI_FLASH_RESULT_OK) {
write_res = spi_flash_write(get_esp8266_flash_address(), s_flash_storage, ESP8266_FLASH_STORAGE_SIZE * 4);
}
}
if (erase_res != SPI_FLASH_RESULT_OK) {
ESP_LOGE(TAG, "Erasing failed");
return false;
}
if (write_res != SPI_FLASH_RESULT_OK) {
ESP_LOGE(TAG, "Writing failed");
return false;
}
bool reset() override {
ESP_LOGD(TAG, "Erasing storage");
SpiFlashOpResult erase_res;
{
InterruptLock lock;
erase_res = spi_flash_erase_sector(get_esp8266_flash_sector());
}
if (erase_res != SPI_FLASH_RESULT_OK) {
ESP_LOGE(TAG, "Erasing failed");
return false;
}
s_flash_dirty = false;
return true;
}
// Protect flash from writing till restart
s_prevent_write = true;
return true;
bool ESP8266Preferences::reset() {
ESP_LOGD(TAG, "Erasing storage");
SpiFlashOpResult erase_res;
{
InterruptLock lock;
erase_res = spi_flash_erase_sector(get_esp8266_flash_sector());
}
};
if (erase_res != SPI_FLASH_RESULT_OK) {
ESP_LOGE(TAG, "Erasing failed");
return false;
}
// Protect flash from writing till restart
s_prevent_write = true;
return true;
}
static ESP8266Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
ESP8266Preferences *get_preferences() { return &s_preferences; }
void setup_preferences() {
s_preferences.setup();
global_preferences = &s_preferences;
+23 -1
View File
@@ -1,12 +1,34 @@
#pragma once
#ifdef USE_ESP8266
#include "esphome/core/preference_backend.h"
namespace esphome::esp8266 {
class ESP8266Preferences final : public PreferencesMixin<ESP8266Preferences> {
public:
using PreferencesMixin<ESP8266Preferences>::make_preference;
void setup();
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash);
ESPPreferenceObject make_preference(size_t length, uint32_t type) {
#ifdef USE_ESP8266_PREFERENCES_FLASH
return this->make_preference(length, type, true);
#else
return this->make_preference(length, type, false);
#endif
}
bool sync();
bool reset();
uint32_t current_offset = 0;
uint32_t current_flash_offset = 0; // in words
};
void setup_preferences();
void preferences_prevent_write(bool prevent);
} // namespace esphome::esp8266
DECLARE_PREFERENCE_ALIASES(esphome::esp8266::ESP8266Preferences)
#endif // USE_ESP8266
@@ -87,7 +87,8 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status)
// 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
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
// allocate() returned non-null, the queue cannot be full.
// Wake main loop immediately to process ESP-NOW send event instead of waiting for select() timeout
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
@@ -109,7 +110,8 @@ void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int
// 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
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
// allocate() returned non-null, the queue cannot be full.
// Wake main loop immediately to process ESP-NOW receive event instead of waiting for select() timeout
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
+6 -2
View File
@@ -163,10 +163,14 @@ class ESPNowComponent : public Component {
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_{};
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
// buffer that holds N-1 elements. This guarantees allocate() returns nullptr
// before push() can fail, preventing a pool slot leak.
EventPool<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE - 1> 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_{};
// Pool sized to queue capacity (SIZE-1) — see receive_packet_pool_ comment.
EventPool<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE - 1> send_packet_pool_{};
ESPNowSendPacket *current_send_packet_{nullptr}; // Currently sending packet, nullptr if none
uint8_t wifi_channel_{0};
@@ -85,7 +85,7 @@ enum eth_duplex_t { ETH_DUPLEX_HALF, ETH_DUPLEX_FULL };
enum eth_speed_t { ETH_SPEED_10M, ETH_SPEED_100M };
#endif
class EthernetComponent : public Component {
class EthernetComponent final : public Component {
public:
EthernetComponent();
void setup() override;
-4
View File
@@ -45,9 +45,5 @@ void Event::set_event_types(const std::vector<const char *> &event_types) {
this->last_event_type_ = nullptr; // Reset when types change
}
void Event::add_on_event_callback(std::function<void(StringRef event_type)> &&callback) {
this->event_callback_.add(std::move(callback));
}
} // namespace event
} // namespace esphome
+3 -1
View File
@@ -66,7 +66,9 @@ class Event : public EntityBase {
/// Check if an event has been triggered.
bool has_event() const { return this->last_event_type_ != nullptr; }
void add_on_event_callback(std::function<void(StringRef event_type)> &&callback);
template<typename F> void add_on_event_callback(F &&callback) {
this->event_callback_.add(std::forward<F>(callback));
}
protected:
LazyCallbackManager<void(StringRef event_type)> event_callback_;
+8 -12
View File
@@ -44,8 +44,8 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2
// Device Information
void get_device_information();
void add_device_infomation_callback(std::function<void(std::string)> &&callback) {
this->device_infomation_callback_.add(std::move(callback));
template<typename F> void add_device_infomation_callback(F &&callback) {
this->device_infomation_callback_.add(std::forward<F>(callback));
}
// Sleep
@@ -56,15 +56,13 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2
// Slope
void get_slope();
void add_slope_callback(std::function<void(std::string)> &&callback) {
this->slope_callback_.add(std::move(callback));
}
template<typename F> void add_slope_callback(F &&callback) { this->slope_callback_.add(std::forward<F>(callback)); }
// T
void get_t();
void set_t(float value);
void set_tempcomp_value(float temp); // For backwards compatibility
void add_t_callback(std::function<void(std::string)> &&callback) { this->t_callback_.add(std::move(callback)); }
template<typename F> void add_t_callback(F &&callback) { this->t_callback_.add(std::forward<F>(callback)); }
// Calibration
void get_calibration();
@@ -73,20 +71,18 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2
void set_calibration_point_high(float value);
void set_calibration_generic(float value);
void clear_calibration();
void add_calibration_callback(std::function<void(std::string)> &&callback) {
this->calibration_callback_.add(std::move(callback));
template<typename F> void add_calibration_callback(F &&callback) {
this->calibration_callback_.add(std::forward<F>(callback));
}
// LED
void get_led_state();
void set_led_state(bool on);
void add_led_state_callback(std::function<void(bool)> &&callback) { this->led_callback_.add(std::move(callback)); }
template<typename F> void add_led_state_callback(F &&callback) { this->led_callback_.add(std::forward<F>(callback)); }
// Custom
void send_custom(const std::string &to_send);
void add_custom_callback(std::function<void(std::string)> &&callback) {
this->custom_callback_.add(std::move(callback));
}
template<typename F> void add_custom_callback(F &&callback) { this->custom_callback_.add(std::forward<F>(callback)); }
protected:
std::deque<std::unique_ptr<EzoCommand>> commands_;
@@ -17,8 +17,8 @@ class FactoryResetComponent : public Component {
void dump_config() override;
void setup() override;
void add_increment_callback(std::function<void(uint8_t, uint8_t)> &&callback) {
this->increment_callback_.add(std::move(callback));
template<typename F> void add_increment_callback(F &&callback) {
this->increment_callback_.add(std::forward<F>(callback));
}
protected:
-1
View File
@@ -193,7 +193,6 @@ void Fan::apply_preset_mode_(const FanCall &call) {
}
}
void Fan::add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
void Fan::publish_state() {
auto traits = this->get_traits();
+3 -1
View File
@@ -122,7 +122,9 @@ class Fan : public EntityBase {
FanCall make_call();
/// Register a callback that will be called each time the state changes.
void add_on_state_callback(std::function<void()> &&callback);
template<typename F> void add_on_state_callback(F &&callback) {
this->state_callback_.add(std::forward<F>(callback));
}
void publish_state();
@@ -127,30 +127,30 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic
void set_enrolling_binary_sensor(binary_sensor::BinarySensor *enrolling_binary_sensor) {
this->enrolling_binary_sensor_ = enrolling_binary_sensor;
}
void add_on_finger_scan_start_callback(std::function<void()> callback) {
this->finger_scan_start_callback_.add(std::move(callback));
template<typename F> void add_on_finger_scan_start_callback(F &&callback) {
this->finger_scan_start_callback_.add(std::forward<F>(callback));
}
void add_on_finger_scan_matched_callback(std::function<void(uint16_t, uint16_t)> callback) {
this->finger_scan_matched_callback_.add(std::move(callback));
template<typename F> void add_on_finger_scan_matched_callback(F &&callback) {
this->finger_scan_matched_callback_.add(std::forward<F>(callback));
}
void add_on_finger_scan_unmatched_callback(std::function<void()> callback) {
this->finger_scan_unmatched_callback_.add(std::move(callback));
template<typename F> void add_on_finger_scan_unmatched_callback(F &&callback) {
this->finger_scan_unmatched_callback_.add(std::forward<F>(callback));
}
void add_on_finger_scan_misplaced_callback(std::function<void()> callback) {
this->finger_scan_misplaced_callback_.add(std::move(callback));
template<typename F> void add_on_finger_scan_misplaced_callback(F &&callback) {
this->finger_scan_misplaced_callback_.add(std::forward<F>(callback));
}
void add_on_finger_scan_invalid_callback(std::function<void()> callback) {
this->finger_scan_invalid_callback_.add(std::move(callback));
template<typename F> void add_on_finger_scan_invalid_callback(F &&callback) {
this->finger_scan_invalid_callback_.add(std::forward<F>(callback));
}
void add_on_enrollment_scan_callback(std::function<void(uint8_t, uint16_t)> callback) {
this->enrollment_scan_callback_.add(std::move(callback));
template<typename F> void add_on_enrollment_scan_callback(F &&callback) {
this->enrollment_scan_callback_.add(std::forward<F>(callback));
}
void add_on_enrollment_done_callback(std::function<void(uint16_t)> callback) {
this->enrollment_done_callback_.add(std::move(callback));
template<typename F> void add_on_enrollment_done_callback(F &&callback) {
this->enrollment_done_callback_.add(std::forward<F>(callback));
}
void add_on_enrollment_failed_callback(std::function<void(uint16_t)> callback) {
this->enrollment_failed_callback_.add(std::move(callback));
template<typename F> void add_on_enrollment_failed_callback(F &&callback) {
this->enrollment_failed_callback_.add(std::forward<F>(callback));
}
void enroll_fingerprint(uint16_t finger_id, uint8_t num_buffers);
+1
View File
@@ -552,6 +552,7 @@ async def to_code(config):
"""
# get the codepoints from glyphsets and flatten to a set of chrs.
cg.add_define("USE_FONT")
point_set: set[str] = {
chr(x)
for x in flatten(
+80 -5
View File
@@ -9,13 +9,87 @@ namespace font {
static const char *const TAG = "font";
#ifdef USE_LVGL_FONT
const uint8_t *Font::get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) {
auto *fe = (Font *) font->dsc;
const auto *gd = fe->get_glyph_data_(unicode_letter);
static const uint8_t OPA4_TABLE[16] = {0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255};
static const uint8_t OPA2_TABLE[4] = {0, 85, 170, 255};
const void *Font::get_glyph_bitmap(lv_font_glyph_dsc_t *dsc, lv_draw_buf_t *draw_buf) {
const auto *font = dsc->resolved_font;
auto *const fe = (Font *) font->dsc;
const auto *gd = fe->get_glyph_data_(dsc->gid.index);
if (gd == nullptr) {
return nullptr;
}
return gd->data;
const uint8_t *bitmap_in = gd->data;
uint8_t *bitmap_out_tmp = draw_buf->data;
int32_t i = 0;
int32_t x, y;
uint32_t stride = lv_draw_buf_width_to_stride(gd->width, LV_COLOR_FORMAT_A8);
switch (fe->get_bpp()) {
case 1: {
uint8_t mask = 0;
uint8_t byte = 0;
for (y = 0; y != gd->height; y++) {
for (x = 0; x != gd->width; x++) {
if (mask == 0) {
mask = 0x80;
byte = *bitmap_in++;
}
bitmap_out_tmp[x] = byte & mask ? 255 : 0;
mask >>= 1;
}
bitmap_out_tmp += stride;
}
} break;
case 2:
for (y = 0; y != gd->height; y++) {
for (x = 0; x != gd->width; x++, i++) {
switch (i & 0x3) {
default:
bitmap_out_tmp[x] = OPA2_TABLE[(*bitmap_in) >> 6];
break;
case 1:
bitmap_out_tmp[x] = OPA2_TABLE[((*bitmap_in) >> 4) & 0x3];
break;
case 2:
bitmap_out_tmp[x] = OPA2_TABLE[((*bitmap_in) >> 2) & 0x3];
break;
case 3:
bitmap_out_tmp[x] = OPA2_TABLE[((*bitmap_in) >> 0) & 0x3];
bitmap_in++;
}
}
bitmap_out_tmp += stride;
}
break;
case 4:
for (y = 0; y != gd->height; y++) {
for (x = 0; x != gd->width; x++, i++) {
i = i & 0x1;
if (i == 0) {
bitmap_out_tmp[x] = OPA4_TABLE[(*bitmap_in) >> 4];
} else if (i == 1) {
bitmap_out_tmp[x] = OPA4_TABLE[(*bitmap_in) & 0xF];
bitmap_in++;
}
}
bitmap_out_tmp += stride;
}
break;
case 8:
memcpy(bitmap_out_tmp, bitmap_in, gd->width * gd->height);
break;
default:
ESP_LOGD(TAG, "Unknown bpp: %d", fe->get_bpp());
break;
}
return draw_buf;
}
bool Font::get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) {
@@ -30,7 +104,8 @@ bool Font::get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uin
dsc->box_w = gd->width;
dsc->box_h = gd->height;
dsc->is_placeholder = 0;
dsc->bpp = fe->get_bpp();
dsc->format = (lv_font_glyph_format_t) fe->get_bpp();
dsc->gid.index = unicode_letter;
return true;
}
+1 -1
View File
@@ -90,7 +90,7 @@ class Font
uint8_t bpp_; // bits per pixel
#ifdef USE_LVGL_FONT
lv_font_t lv_font_{};
static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter);
static const void *get_glyph_bitmap(lv_font_glyph_dsc_t *dsc, lv_draw_buf_t *draw_buf);
static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next);
const Glyph *get_glyph_data_(uint32_t unicode_letter);
uint32_t last_letter_{};
@@ -44,7 +44,7 @@ class GraphicalDisplayMenu : public display_menu_base::DisplayMenuComponent {
void set_foreground_color(Color foreground_color);
void set_background_color(Color background_color);
void add_on_redraw_callback(std::function<void()> &&cb) { this->on_redraw_callbacks_.add(std::move(cb)); }
template<typename F> void add_on_redraw_callback(F &&cb) { this->on_redraw_callbacks_.add(std::forward<F>(cb)); }
void draw(display::Display *display, const display::Rect *bounds);
+1 -8
View File
@@ -87,19 +87,12 @@ void GreeClimate::transmit_state() {
// Calculate the checksum
if (this->model_ == GREE_YAN || this->model_ == GREE_YX1FF) {
remote_state[7] = ((remote_state[0] << 4) + (remote_state[1] << 4) + 0xC0);
} else if (this->model_ == GREE_YAG) {
} else {
remote_state[7] =
((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) +
((remote_state[4] & 0xF0) >> 4) + ((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + 0x0A) &
0x0F)
<< 4);
} else {
remote_state[7] =
((((remote_state[0] & 0x0F) + (remote_state[1] & 0x0F) + (remote_state[2] & 0x0F) + (remote_state[3] & 0x0F) +
((remote_state[5] & 0xF0) >> 4) + ((remote_state[6] & 0xF0) >> 4) + ((remote_state[7] & 0xF0) >> 4) + 0x0A) &
0x0F)
<< 4) |
(remote_state[7] & 0x0F);
}
auto transmit = this->transmitter_->transmit();
+1 -5
View File
@@ -197,10 +197,6 @@ void HaierClimateBase::send_custom_command(const haier_protocol::HaierMessage &m
this->action_request_ = PendingAction({ActionRequest::SEND_CUSTOM_COMMAND, message});
}
void HaierClimateBase::add_status_message_callback(std::function<void(const char *, size_t)> &&callback) {
this->status_message_callback_.add(std::move(callback));
}
haier_protocol::HandlerError HaierClimateBase::answer_preprocess_(
haier_protocol::FrameType request_message_type, haier_protocol::FrameType expected_request_message_type,
haier_protocol::FrameType answer_message_type, haier_protocol::FrameType expected_answer_message_type,
@@ -246,7 +242,7 @@ void HaierClimateBase::setup() {
this->last_request_timestamp_ = std::chrono::steady_clock::now();
this->set_phase(ProtocolPhases::SENDING_INIT_1);
this->haier_protocol_.set_default_timeout_handler(
std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1));
[this](haier_protocol::FrameType type) { return this->timeout_default_handler_(type); });
this->set_handlers();
this->initialization();
}
+3 -1
View File
@@ -74,7 +74,9 @@ class HaierClimateBase : public esphome::Component,
void set_answer_timeout(uint32_t timeout);
void set_send_wifi(bool send_wifi);
void send_custom_command(const haier_protocol::HaierMessage &message);
void add_status_message_callback(std::function<void(const char *, size_t)> &&callback);
template<typename F> void add_status_message_callback(F &&callback) {
this->status_message_callback_.add(std::forward<F>(callback));
}
protected:
enum class ProtocolPhases {
+22 -24
View File
@@ -114,14 +114,6 @@ void HonClimate::start_steri_cleaning() {
}
}
void HonClimate::add_alarm_start_callback(std::function<void(uint8_t, const char *)> &&callback) {
this->alarm_start_callback_.add(std::move(callback));
}
void HonClimate::add_alarm_end_callback(std::function<void(uint8_t, const char *)> &&callback) {
this->alarm_end_callback_.add(std::move(callback));
}
haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size) {
@@ -309,32 +301,38 @@ void HonClimate::set_handlers() {
// Set handlers
this->haier_protocol_.set_answer_handler(
haier_protocol::FrameType::GET_DEVICE_VERSION,
std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
[this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
return this->get_device_version_answer_handler_(req, msg, data, size);
});
this->haier_protocol_.set_answer_handler(
haier_protocol::FrameType::GET_DEVICE_ID,
std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
[this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
return this->get_device_id_answer_handler_(req, msg, data, size);
});
this->haier_protocol_.set_answer_handler(
haier_protocol::FrameType::CONTROL,
std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3,
std::placeholders::_4));
[this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
return this->status_handler_(req, msg, data, size);
});
this->haier_protocol_.set_answer_handler(
haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION,
std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
[this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
return this->get_management_information_answer_handler_(req, msg, data, size);
});
this->haier_protocol_.set_answer_handler(
haier_protocol::FrameType::GET_ALARM_STATUS,
std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
[this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
return this->get_alarm_status_answer_handler_(req, msg, data, size);
});
this->haier_protocol_.set_answer_handler(
haier_protocol::FrameType::REPORT_NETWORK_STATUS,
std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_message_handler(
haier_protocol::FrameType::ALARM_STATUS,
std::bind(&HonClimate::alarm_status_message_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3));
[this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
return this->report_network_status_answer_handler_(req, msg, data, size);
});
this->haier_protocol_.set_message_handler(haier_protocol::FrameType::ALARM_STATUS,
[this](haier_protocol::FrameType type, const uint8_t *data, size_t size) {
return this->alarm_status_message_handler_(type, data, size);
});
}
void HonClimate::dump_config() {
+6 -2
View File
@@ -124,8 +124,12 @@ class HonClimate : public HaierClimateBase {
void set_extra_sensors_packet_bytes_size(size_t size) { this->extra_sensors_packet_bytes_ = size; };
void set_status_message_header_size(size_t size) { this->status_message_header_size_ = size; };
void set_control_method(HonControlMethod method) { this->control_method_ = method; };
void add_alarm_start_callback(std::function<void(uint8_t, const char *)> &&callback);
void add_alarm_end_callback(std::function<void(uint8_t, const char *)> &&callback);
template<typename F> void add_alarm_start_callback(F &&callback) {
this->alarm_start_callback_.add(std::forward<F>(callback));
}
template<typename F> void add_alarm_end_callback(F &&callback) {
this->alarm_end_callback_.add(std::forward<F>(callback));
}
float get_active_alarm_count() const { return this->active_alarm_count_; }
protected:
+10 -7
View File
@@ -106,18 +106,21 @@ void Smartair2Climate::set_handlers() {
// Set handlers
this->haier_protocol_.set_answer_handler(
haier_protocol::FrameType::GET_DEVICE_VERSION,
std::bind(&Smartair2Climate::get_device_version_answer_handler_, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
[this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
return this->get_device_version_answer_handler_(req, msg, data, size);
});
this->haier_protocol_.set_answer_handler(
haier_protocol::FrameType::CONTROL,
std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4));
[this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
return this->status_handler_(req, msg, data, size);
});
this->haier_protocol_.set_answer_handler(
haier_protocol::FrameType::REPORT_NETWORK_STATUS,
std::bind(&Smartair2Climate::report_network_status_answer_handler_, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
[this](haier_protocol::FrameType req, haier_protocol::FrameType msg, const uint8_t *data, size_t size) {
return this->report_network_status_answer_handler_(req, msg, data, size);
});
this->haier_protocol_.set_default_timeout_handler(
std::bind(&Smartair2Climate::messages_timeout_handler_with_cycle_for_init_, this, std::placeholders::_1));
[this](haier_protocol::FrameType type) { return this->messages_timeout_handler_with_cycle_for_init_(type); });
}
void Smartair2Climate::dump_config() {
+31 -45
View File
@@ -7,50 +7,36 @@ namespace hdc2010 {
static const char *const TAG = "hdc2010";
static const uint8_t HDC2010_ADDRESS = 0x40; // 0b1000000 or 0b1000001 from datasheet
static const uint8_t HDC2010_CMD_CONFIGURATION_MEASUREMENT = 0x8F;
static const uint8_t HDC2010_CMD_START_MEASUREMENT = 0xF9;
static const uint8_t HDC2010_CMD_TEMPERATURE_LOW = 0x00;
static const uint8_t HDC2010_CMD_TEMPERATURE_HIGH = 0x01;
static const uint8_t HDC2010_CMD_HUMIDITY_LOW = 0x02;
static const uint8_t HDC2010_CMD_HUMIDITY_HIGH = 0x03;
static const uint8_t CONFIG = 0x0E;
static const uint8_t MEASUREMENT_CONFIG = 0x0F;
// Register addresses
static constexpr uint8_t REG_TEMPERATURE_LOW = 0x00;
static constexpr uint8_t REG_TEMPERATURE_HIGH = 0x01;
static constexpr uint8_t REG_HUMIDITY_LOW = 0x02;
static constexpr uint8_t REG_HUMIDITY_HIGH = 0x03;
static constexpr uint8_t REG_RESET_DRDY_INT_CONF = 0x0E;
static constexpr uint8_t REG_MEASUREMENT_CONF = 0x0F;
// REG_MEASUREMENT_CONF (0x0F) bit masks
static constexpr uint8_t MEAS_TRIG = 0x01; // Bit 0: measurement trigger
static constexpr uint8_t MEAS_CONF_MASK = 0x06; // Bits 2:1: measurement mode
static constexpr uint8_t HRES_MASK = 0x30; // Bits 5:4: humidity resolution
static constexpr uint8_t TRES_MASK = 0xC0; // Bits 7:6: temperature resolution
// REG_RESET_DRDY_INT_CONF (0x0E) bit masks
static constexpr uint8_t AMM_MASK = 0x70; // Bits 6:4: auto measurement mode
void HDC2010Component::setup() {
ESP_LOGCONFIG(TAG, "Running setup");
const uint8_t data[2] = {
0b00000000, // resolution 14bit for both humidity and temperature
0b00000000 // reserved
};
if (!this->write_bytes(HDC2010_CMD_CONFIGURATION_MEASUREMENT, data, 2)) {
ESP_LOGW(TAG, "Initial config instruction error");
this->status_set_warning();
return;
}
// Set measurement mode to temperature and humidity
// Set 14-bit resolution for both sensors and measurement mode to temp + humidity
uint8_t config_contents;
this->read_register(MEASUREMENT_CONFIG, &config_contents, 1);
config_contents = (config_contents & 0xF9); // Always set to TEMP_AND_HUMID mode
this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1);
this->read_register(REG_MEASUREMENT_CONF, &config_contents, 1);
config_contents &= ~(TRES_MASK | HRES_MASK | MEAS_CONF_MASK); // 14-bit temp, 14-bit humidity, temp+humidity mode
this->write_bytes(REG_MEASUREMENT_CONF, &config_contents, 1);
// Set rate to manual
this->read_register(CONFIG, &config_contents, 1);
config_contents &= 0x8F;
this->write_bytes(CONFIG, &config_contents, 1);
// Set temperature resolution to 14bit
this->read_register(CONFIG, &config_contents, 1);
config_contents &= 0x3F;
this->write_bytes(CONFIG, &config_contents, 1);
// Set humidity resolution to 14bit
this->read_register(CONFIG, &config_contents, 1);
config_contents &= 0xCF;
this->write_bytes(CONFIG, &config_contents, 1);
// Set auto measurement rate to manual (on-demand via MEAS_TRIG)
this->read_register(REG_RESET_DRDY_INT_CONF, &config_contents, 1);
config_contents &= ~AMM_MASK;
this->write_bytes(REG_RESET_DRDY_INT_CONF, &config_contents, 1);
}
void HDC2010Component::dump_config() {
@@ -67,9 +53,9 @@ void HDC2010Component::dump_config() {
void HDC2010Component::update() {
// Trigger measurement
uint8_t config_contents;
this->read_register(CONFIG, &config_contents, 1);
config_contents |= 0x01;
this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1);
this->read_register(REG_MEASUREMENT_CONF, &config_contents, 1);
config_contents |= MEAS_TRIG;
this->write_bytes(REG_MEASUREMENT_CONF, &config_contents, 1);
// 1ms delay after triggering the sample
set_timeout(1, [this]() {
@@ -90,8 +76,8 @@ void HDC2010Component::update() {
float HDC2010Component::read_temp() {
uint8_t byte[2];
this->read_register(HDC2010_CMD_TEMPERATURE_LOW, &byte[0], 1);
this->read_register(HDC2010_CMD_TEMPERATURE_HIGH, &byte[1], 1);
this->read_register(REG_TEMPERATURE_LOW, &byte[0], 1);
this->read_register(REG_TEMPERATURE_HIGH, &byte[1], 1);
uint16_t temp = encode_uint16(byte[1], byte[0]);
return (float) temp * 0.0025177f - 40.0f;
@@ -100,8 +86,8 @@ float HDC2010Component::read_temp() {
float HDC2010Component::read_humidity() {
uint8_t byte[2];
this->read_register(HDC2010_CMD_HUMIDITY_LOW, &byte[0], 1);
this->read_register(HDC2010_CMD_HUMIDITY_HIGH, &byte[1], 1);
this->read_register(REG_HUMIDITY_LOW, &byte[0], 1);
this->read_register(REG_HUMIDITY_HIGH, &byte[1], 1);
uint16_t humidity = encode_uint16(byte[1], byte[0]);
return (float) humidity * 0.001525879f;
+12 -13
View File
@@ -91,24 +91,23 @@ class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice {
void set_version_text_sensor(text_sensor::TextSensor *version_text_sensor) {
this->version_text_sensor_ = version_text_sensor;
}
void add_on_face_scan_matched_callback(std::function<void(int16_t, std::string)> callback) {
this->face_scan_matched_callback_.add(std::move(callback));
template<typename F> void add_on_face_scan_matched_callback(F &&callback) {
this->face_scan_matched_callback_.add(std::forward<F>(callback));
}
void add_on_face_scan_unmatched_callback(std::function<void()> callback) {
this->face_scan_unmatched_callback_.add(std::move(callback));
template<typename F> void add_on_face_scan_unmatched_callback(F &&callback) {
this->face_scan_unmatched_callback_.add(std::forward<F>(callback));
}
void add_on_face_scan_invalid_callback(std::function<void(uint8_t)> callback) {
this->face_scan_invalid_callback_.add(std::move(callback));
template<typename F> void add_on_face_scan_invalid_callback(F &&callback) {
this->face_scan_invalid_callback_.add(std::forward<F>(callback));
}
void add_on_face_info_callback(
std::function<void(int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t, int16_t)> callback) {
this->face_info_callback_.add(std::move(callback));
template<typename F> void add_on_face_info_callback(F &&callback) {
this->face_info_callback_.add(std::forward<F>(callback));
}
void add_on_enrollment_done_callback(std::function<void(int16_t, uint8_t)> callback) {
this->enrollment_done_callback_.add(std::move(callback));
template<typename F> void add_on_enrollment_done_callback(F &&callback) {
this->enrollment_done_callback_.add(std::forward<F>(callback));
}
void add_on_enrollment_failed_callback(std::function<void(uint8_t)> callback) {
this->enrollment_failed_callback_.add(std::move(callback));
template<typename F> void add_on_enrollment_failed_callback(F &&callback) {
this->enrollment_failed_callback_.add(std::forward<F>(callback));
}
void enroll_face(const std::string &name, HlkFm22xFaceDirection direction);
@@ -55,15 +55,15 @@ void HomeassistantNumber::step_retrieved_(StringRef step) {
}
void HomeassistantNumber::setup() {
api::global_api_server->subscribe_home_assistant_state(
this->entity_id_, nullptr, std::bind(&HomeassistantNumber::state_changed_, this, std::placeholders::_1));
api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullptr,
[this](StringRef state) { this->state_changed_(state); });
api::global_api_server->get_home_assistant_state(
this->entity_id_, "min", std::bind(&HomeassistantNumber::min_retrieved_, this, std::placeholders::_1));
api::global_api_server->get_home_assistant_state(
this->entity_id_, "max", std::bind(&HomeassistantNumber::max_retrieved_, this, std::placeholders::_1));
api::global_api_server->get_home_assistant_state(
this->entity_id_, "step", std::bind(&HomeassistantNumber::step_retrieved_, this, std::placeholders::_1));
api::global_api_server->get_home_assistant_state(this->entity_id_, "min",
[this](StringRef min) { this->min_retrieved_(min); });
api::global_api_server->get_home_assistant_state(this->entity_id_, "max",
[this](StringRef max) { this->max_retrieved_(max); });
api::global_api_server->get_home_assistant_state(this->entity_id_, "step",
[this](StringRef step) { this->step_retrieved_(step); });
}
void HomeassistantNumber::dump_config() {
-3
View File
@@ -58,9 +58,6 @@ void HOT arch_feed_wdt() {
// pass
}
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
const char *progmem_read_ptr(const char *const *addr) { return *addr; }
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
uint32_t arch_get_cpu_cycle_count() {
struct timespec spec;
clock_gettime(CLOCK_MONOTONIC, &spec);
@@ -0,0 +1,29 @@
#pragma once
#ifdef USE_HOST
#include <cstddef>
#include <cstdint>
namespace esphome::host {
class HostPreferenceBackend final {
public:
explicit HostPreferenceBackend(uint32_t key) : key_(key) {}
bool save(const uint8_t *data, size_t len);
bool load(uint8_t *data, size_t len);
protected:
uint32_t key_{};
};
class HostPreferences;
HostPreferences *get_preferences();
} // namespace esphome::host
namespace esphome {
using PreferenceBackend = host::HostPreferenceBackend;
} // namespace esphome
#endif // USE_HOST
+7 -4
View File
@@ -6,8 +6,7 @@
#include "esphome/core/application.h"
#include "esphome/core/log.h"
namespace esphome {
namespace host {
namespace esphome::host {
namespace fs = std::filesystem;
static const char *const TAG = "host.preferences";
@@ -77,6 +76,8 @@ ESPPreferenceObject HostPreferences::make_preference(size_t length, uint32_t typ
static HostPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
HostPreferences *get_preferences() { return &s_preferences; }
void setup_preferences() {
host_preferences = &s_preferences;
global_preferences = &s_preferences;
@@ -88,9 +89,11 @@ bool HostPreferenceBackend::save(const uint8_t *data, size_t len) {
bool HostPreferenceBackend::load(uint8_t *data, size_t len) { return host_preferences->load(this->key_, data, len); }
HostPreferences *host_preferences;
} // namespace host
HostPreferences *host_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome::host
namespace esphome {
ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome
+15 -24
View File
@@ -1,33 +1,22 @@
#pragma once
#ifdef USE_HOST
#include "esphome/core/preferences.h"
#include "esphome/core/preference_backend.h"
#include <cstring>
#include <map>
#include <string>
#include <vector>
namespace esphome {
namespace host {
namespace esphome::host {
class HostPreferenceBackend : public ESPPreferenceBackend {
class HostPreferences final : public PreferencesMixin<HostPreferences> {
public:
explicit HostPreferenceBackend(uint32_t key) { this->key_ = key; }
using PreferencesMixin<HostPreferences>::make_preference;
bool sync();
bool reset();
bool save(const uint8_t *data, size_t len) override;
bool load(uint8_t *data, size_t len) override;
protected:
uint32_t key_{};
};
class HostPreferences : public ESPPreferences {
public:
bool sync() override;
bool reset() override;
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override;
ESPPreferenceObject make_preference(size_t length, uint32_t type) override {
return make_preference(length, type, false);
}
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash);
ESPPreferenceObject make_preference(size_t length, uint32_t type) { return make_preference(length, type, false); }
bool save(uint32_t key, const uint8_t *data, size_t len) {
if (len > 255)
@@ -58,10 +47,12 @@ class HostPreferences : public ESPPreferences {
std::string filename_{};
std::map<uint32_t, std::vector<uint8_t>> data{};
};
void setup_preferences();
extern HostPreferences *host_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace host
} // namespace esphome
} // namespace esphome::host
DECLARE_PREFERENCE_ALIASES(esphome::host::HostPreferences)
#endif // USE_HOST
@@ -487,12 +487,10 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
body = this->body_.value(x...);
}
if (!this->json_.empty()) {
auto f = std::bind(&HttpRequestSendAction<Ts...>::encode_json_, this, x..., std::placeholders::_1);
body = json::build_json(f);
body = json::build_json([this, x...](JsonObject root) { this->encode_json_(x..., root); });
}
if (this->json_func_ != nullptr) {
auto f = std::bind(&HttpRequestSendAction<Ts...>::encode_json_func_, this, x..., std::placeholders::_1);
body = json::build_json(f);
body = json::build_json([this, x...](JsonObject root) { this->json_func_(x..., root); });
}
std::vector<Header> request_headers;
request_headers.reserve(this->request_headers_.size());
@@ -561,7 +559,6 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
root[item.first] = val.value(x...);
}
}
void encode_json_func_(Ts... x, JsonObject root) { this->json_func_(x..., root); }
HttpRequestComponent *parent_;
FixedVector<std::pair<const char *, TemplatableValue<const char *, Ts...>>> request_headers_{};
std::vector<std::string> lower_case_collect_headers_{"content-type", "content-length"};
@@ -23,6 +23,12 @@ namespace http_request {
static const char *const TAG = "http_request.update";
// Wraps UpdateInfo + error for the task→main-loop handoff.
struct TaskResult {
update::UpdateInfo info;
const LogString *error_str{nullptr};
};
static const size_t MAX_READ_SIZE = 256;
static constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0;
static constexpr uint32_t INITIAL_CHECK_INTERVAL_MS = 10000;
@@ -68,6 +74,10 @@ void HttpRequestUpdate::update() {
}
this->cancel_interval(INITIAL_CHECK_INTERVAL_ID);
#ifdef USE_ESP32
if (this->update_task_handle_ != nullptr) {
ESP_LOGW(TAG, "Update check already in progress");
return;
}
xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_);
#else
this->update_task(this);
@@ -77,134 +87,151 @@ void HttpRequestUpdate::update() {
void HttpRequestUpdate::update_task(void *params) {
HttpRequestUpdate *this_update = (HttpRequestUpdate *) params;
// Allocate once — every path below returns via the single defer at the end.
// On failure, error_str is set; on success it is nullptr.
auto *result = new TaskResult();
auto *info = &result->info;
auto container = this_update->request_parent_->get(this_update->source_url_);
if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_.c_str());
// Defer to main loop to avoid race condition on component_state_ read-modify-write
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to fetch manifest")); });
UPDATE_RETURN;
if (container != nullptr)
container->end();
result->error_str = LOG_STR("Failed to fetch manifest");
goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
}
RAMAllocator<uint8_t> allocator;
uint8_t *data = allocator.allocate(container->content_length);
if (data == nullptr) {
ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length);
// Defer to main loop to avoid race condition on component_state_ read-modify-write
this_update->defer(
[this_update]() { this_update->status_set_error(LOG_STR("Failed to allocate memory for manifest")); });
container->end();
UPDATE_RETURN;
}
auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE,
this_update->request_parent_->get_timeout());
if (read_result.status != HttpReadStatus::OK) {
if (read_result.status == HttpReadStatus::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading manifest");
} else {
ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code);
{
RAMAllocator<uint8_t> allocator;
uint8_t *data = allocator.allocate(container->content_length);
if (data == nullptr) {
ESP_LOGE(TAG, "Failed to allocate %zu bytes for manifest", container->content_length);
container->end();
result->error_str = LOG_STR("Failed to allocate memory for manifest");
goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
}
// Defer to main loop to avoid race condition on component_state_ read-modify-write
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to read manifest")); });
allocator.deallocate(data, container->content_length);
container->end();
UPDATE_RETURN;
}
size_t read_index = container->get_bytes_read();
size_t content_length = container->content_length;
container->end();
container.reset(); // Release ownership of the container's shared_ptr
bool valid = false;
{ // Scope to ensure JsonDocument is destroyed before deallocating buffer
valid = json::parse_json(data, read_index, [this_update](JsonObject root) -> bool {
if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
!root[ESPHOME_F("builds")].is<JsonArray>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
auto read_result = http_read_fully(container.get(), data, container->content_length, MAX_READ_SIZE,
this_update->request_parent_->get_timeout());
if (read_result.status != HttpReadStatus::OK) {
if (read_result.status == HttpReadStatus::TIMEOUT) {
ESP_LOGE(TAG, "Timeout reading manifest");
} else {
ESP_LOGE(TAG, "Error reading manifest: %d", read_result.error_code);
}
this_update->update_info_.title = root[ESPHOME_F("name")].as<std::string>();
this_update->update_info_.latest_version = root[ESPHOME_F("version")].as<std::string>();
allocator.deallocate(data, container->content_length);
container->end();
result->error_str = LOG_STR("Failed to read manifest");
goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
}
size_t read_index = container->get_bytes_read();
size_t content_length = container->content_length;
auto builds_array = root[ESPHOME_F("builds")].as<JsonArray>();
for (auto build : builds_array) {
if (!build[ESPHOME_F("chipFamily")].is<const char *>()) {
container->end();
container.reset(); // Release ownership of the container's shared_ptr
bool valid = false;
{ // Scope to ensure JsonDocument is destroyed before deallocating buffer
valid = json::parse_json(data, read_index, [info](JsonObject root) -> bool {
if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
!root[ESPHOME_F("builds")].is<JsonArray>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) {
if (!build[ESPHOME_F("ota")].is<JsonObject>()) {
info->title = root[ESPHOME_F("name")].as<std::string>();
info->latest_version = root[ESPHOME_F("version")].as<std::string>();
auto builds_array = root[ESPHOME_F("builds")].as<JsonArray>();
for (auto build : builds_array) {
if (!build[ESPHOME_F("chipFamily")].is<const char *>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
JsonObject ota = build[ESPHOME_F("ota")].as<JsonObject>();
if (!ota[ESPHOME_F("path")].is<const char *>() || !ota[ESPHOME_F("md5")].is<const char *>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
if (build[ESPHOME_F("chipFamily")] == ESPHOME_VARIANT) {
if (!build[ESPHOME_F("ota")].is<JsonObject>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
JsonObject ota = build[ESPHOME_F("ota")].as<JsonObject>();
if (!ota[ESPHOME_F("path")].is<const char *>() || !ota[ESPHOME_F("md5")].is<const char *>()) {
ESP_LOGE(TAG, "Manifest does not contain required fields");
return false;
}
info->firmware_url = ota[ESPHOME_F("path")].as<std::string>();
info->md5 = ota[ESPHOME_F("md5")].as<std::string>();
if (ota[ESPHOME_F("summary")].is<const char *>())
info->summary = ota[ESPHOME_F("summary")].as<std::string>();
if (ota[ESPHOME_F("release_url")].is<const char *>())
info->release_url = ota[ESPHOME_F("release_url")].as<std::string>();
return true;
}
this_update->update_info_.firmware_url = ota[ESPHOME_F("path")].as<std::string>();
this_update->update_info_.md5 = ota[ESPHOME_F("md5")].as<std::string>();
if (ota[ESPHOME_F("summary")].is<const char *>())
this_update->update_info_.summary = ota[ESPHOME_F("summary")].as<std::string>();
if (ota[ESPHOME_F("release_url")].is<const char *>())
this_update->update_info_.release_url = ota[ESPHOME_F("release_url")].as<std::string>();
return true;
}
}
return false;
});
}
allocator.deallocate(data, content_length);
return false;
});
}
allocator.deallocate(data, content_length);
if (!valid) {
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
// Defer to main loop to avoid race condition on component_state_ read-modify-write
this_update->defer([this_update]() { this_update->status_set_error(LOG_STR("Failed to parse manifest JSON")); });
UPDATE_RETURN;
}
if (!valid) {
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
result->error_str = LOG_STR("Failed to parse manifest JSON");
goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
}
// Merge source_url_ and this_update->update_info_.firmware_url
if (this_update->update_info_.firmware_url.find("http") == std::string::npos) {
std::string path = this_update->update_info_.firmware_url;
if (path[0] == '/') {
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8));
this_update->update_info_.firmware_url = domain + path;
} else {
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1);
this_update->update_info_.firmware_url = domain + path;
// Merge source_url_ and firmware_url
if (!info->firmware_url.empty() && info->firmware_url.find("http") == std::string::npos) {
std::string path = info->firmware_url;
if (path[0] == '/') {
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.find('/', 8));
info->firmware_url = domain + path;
} else {
std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1);
info->firmware_url = domain + path;
}
}
}
#ifdef ESPHOME_PROJECT_VERSION
this_update->update_info_.current_version = ESPHOME_PROJECT_VERSION;
info->current_version = ESPHOME_PROJECT_VERSION;
#else
this_update->update_info_.current_version = ESPHOME_VERSION;
info->current_version = ESPHOME_VERSION;
#endif
bool trigger_update_available = false;
if (this_update->update_info_.latest_version.empty() ||
this_update->update_info_.latest_version == this_update->update_info_.current_version) {
this_update->state_ = update::UPDATE_STATE_NO_UPDATE;
} else {
if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) {
trigger_update_available = true;
}
this_update->state_ = update::UPDATE_STATE_AVAILABLE;
}
// Defer to main loop to ensure thread-safe execution of:
// - status_clear_error() performs non-atomic read-modify-write on component_state_
// - publish_state() triggers API callbacks that write to the shared protobuf buffer
// which can be corrupted if accessed concurrently from task and main loop threads
// - update_available trigger to ensure consistent state when the trigger fires
this_update->defer([this_update, trigger_update_available]() {
this_update->update_info_.has_progress = false;
this_update->update_info_.progress = 0.0f;
defer:
// Release container before vTaskDelete (which doesn't call destructors)
container.reset();
// Defer to the main loop so all update_info_ and state_ writes happen on the
// same thread as readers (API, MQTT, web server). This is a single defer for
// both success and error paths to avoid multiple std::function instantiations.
// Lambda captures only 2 pointers (8 bytes) — fits in std::function SBO on supported toolchains.
this_update->defer([this_update, result]() {
#ifdef USE_ESP32
this_update->update_task_handle_ = nullptr;
#endif
if (result->error_str != nullptr) {
this_update->status_set_error(result->error_str);
delete result;
return;
}
// Determine new state on main loop (avoids extra lambda captures from task)
bool trigger_update_available = false;
update::UpdateState new_state;
if (result->info.latest_version.empty() || result->info.latest_version == result->info.current_version) {
new_state = update::UPDATE_STATE_NO_UPDATE;
} else {
new_state = update::UPDATE_STATE_AVAILABLE;
if (this_update->state_ != update::UPDATE_STATE_AVAILABLE) {
trigger_update_available = true;
}
}
this_update->update_info_ = std::move(result->info);
this_update->state_ = new_state;
delete result; // Safe: moved-from state is valid for destruction
this_update->status_clear_error();
this_update->publish_state();
+1 -1
View File
@@ -587,7 +587,7 @@ def _build_config_struct(
async def to_code(config: ConfigType) -> None:
add_idf_component(
name="esphome/esp-hub75",
ref="0.3.4",
ref="0.3.5",
)
# Set compile-time configuration via build flags (so external library sees them)
+17 -84
View File
@@ -53,8 +53,6 @@ CONF_RIGHT = "right"
CONF_STEREO = "stereo"
CONF_BOTH = "both"
CONF_USE_LEGACY = "use_legacy"
i2s_audio_ns = cg.esphome_ns.namespace("i2s_audio")
I2SAudioComponent = i2s_audio_ns.class_("I2SAudioComponent", cg.Component)
I2SAudioBase = i2s_audio_ns.class_(
@@ -154,20 +152,6 @@ def validate_mclk_divisible_by_3(config):
return config
# Key for storing legacy driver setting in CORE.data
I2S_USE_LEGACY_DRIVER_KEY = "i2s_use_legacy_driver"
def _get_use_legacy_driver():
"""Get the legacy driver setting from CORE.data."""
return CORE.data.get(I2S_USE_LEGACY_DRIVER_KEY)
def _set_use_legacy_driver(value: bool) -> None:
"""Set the legacy driver setting in CORE.data."""
CORE.data[I2S_USE_LEGACY_DRIVER_KEY] = value
def i2s_audio_component_schema(
class_: MockObjClass,
*,
@@ -192,10 +176,6 @@ def i2s_audio_component_schema(
*I2S_MODE_OPTIONS, lower=True
),
cv.Optional(CONF_USE_APLL, default=False): cv.boolean,
cv.Optional(CONF_BITS_PER_CHANNEL, default="default"): cv.All(
cv.Any(cv.float_with_unit("bits", "bit"), "default"),
cv.one_of(*I2S_BITS_PER_CHANNEL),
),
cv.Optional(CONF_MCLK_MULTIPLE, default=256): cv.one_of(*I2S_MCLK_MULTIPLE),
}
)
@@ -203,59 +183,28 @@ def i2s_audio_component_schema(
async def register_i2s_audio_component(var, config):
await cg.register_parented(var, config[CONF_I2S_AUDIO_ID])
if use_legacy():
cg.add(var.set_i2s_mode(I2S_MODE_OPTIONS[config[CONF_I2S_MODE]]))
cg.add(var.set_channel(I2S_CHANNELS[config[CONF_CHANNEL]]))
cg.add(
var.set_bits_per_sample(I2S_BITS_PER_SAMPLE[config[CONF_BITS_PER_SAMPLE]])
)
cg.add(
var.set_bits_per_channel(
I2S_BITS_PER_CHANNEL[config[CONF_BITS_PER_CHANNEL]]
)
)
else:
cg.add(var.set_i2s_role(I2S_ROLE_OPTIONS[config[CONF_I2S_MODE]]))
slot_mode = config[CONF_CHANNEL]
if slot_mode != CONF_STEREO:
slot_mode = CONF_MONO
slot_mask = config[CONF_CHANNEL]
if slot_mask not in [CONF_LEFT, CONF_RIGHT]:
slot_mask = CONF_BOTH
cg.add(var.set_slot_mode(I2S_SLOT_MODE[slot_mode]))
cg.add(var.set_std_slot_mask(I2S_STD_SLOT_MASK[slot_mask]))
cg.add(var.set_slot_bit_width(I2S_SLOT_BIT_WIDTH[config[CONF_BITS_PER_SAMPLE]]))
cg.add(var.set_i2s_role(I2S_ROLE_OPTIONS[config[CONF_I2S_MODE]]))
slot_mode = config[CONF_CHANNEL]
if slot_mode != CONF_STEREO:
slot_mode = CONF_MONO
slot_mask = config[CONF_CHANNEL]
if slot_mask not in [CONF_LEFT, CONF_RIGHT]:
slot_mask = CONF_BOTH
cg.add(var.set_slot_mode(I2S_SLOT_MODE[slot_mode]))
cg.add(var.set_std_slot_mask(I2S_STD_SLOT_MASK[slot_mask]))
cg.add(var.set_slot_bit_width(I2S_SLOT_BIT_WIDTH[config[CONF_BITS_PER_SAMPLE]]))
cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE]))
cg.add(var.set_use_apll(config[CONF_USE_APLL]))
cg.add(var.set_mclk_multiple(I2S_MCLK_MULTIPLE[config[CONF_MCLK_MULTIPLE]]))
def validate_use_legacy(value):
if CONF_USE_LEGACY in value:
existing_value = _get_use_legacy_driver()
if (existing_value is not None) and (existing_value != value[CONF_USE_LEGACY]):
raise cv.Invalid(
f"All i2s_audio components must set {CONF_USE_LEGACY} to the same value."
)
if (not value[CONF_USE_LEGACY]) and (CORE.using_arduino):
raise cv.Invalid("Arduino supports only the legacy i2s driver")
_set_use_legacy_driver(value[CONF_USE_LEGACY])
elif CORE.using_arduino:
_set_use_legacy_driver(True)
return value
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(I2SAudioComponent),
cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_I2S_MCLK_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_USE_LEGACY): cv.boolean,
},
),
validate_use_legacy,
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(I2SAudioComponent),
cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_I2S_MCLK_PIN): pins.internal_gpio_output_pin_number,
},
)
@@ -311,13 +260,6 @@ def _assign_ports() -> None:
def _final_validate(_):
from esphome.components.esp32 import idf_version
if use_legacy() and idf_version() >= cv.Version(6, 0, 0):
raise cv.Invalid(
"The legacy I2S driver is not available in ESP-IDF 6.0+. "
"Set 'use_legacy: false' in i2s_audio configuration."
)
i2s_audio_configs = fv.full_config.get()[CONF_I2S_AUDIO]
variant = get_esp32_variant()
if variant not in I2S_PORTS:
@@ -329,10 +271,6 @@ def _final_validate(_):
_assign_ports()
def use_legacy():
return _get_use_legacy_driver()
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -349,11 +287,6 @@ async def to_code(config):
# Re-enable ESP-IDF's I2S driver (excluded by default to save compile time)
include_builtin_idf_component("esp_driver_i2s")
if use_legacy():
cg.add_define("USE_I2S_LEGACY")
# Legacy I2S API lives in the "driver" shim component (driver/i2s.h)
include_builtin_idf_component("driver")
# Helps avoid callbacks being skipped due to processor load
add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True)
-35
View File
@@ -6,11 +6,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include <esp_idf_version.h>
#ifdef USE_I2S_LEGACY
#include <driver/i2s.h>
#else
#include <driver/i2s_std.h>
#endif
namespace esphome {
namespace i2s_audio {
@@ -19,33 +15,19 @@ class I2SAudioComponent;
class I2SAudioBase : public Parented<I2SAudioComponent> {
public:
#ifdef USE_I2S_LEGACY
void set_i2s_mode(i2s_mode_t mode) { this->i2s_mode_ = mode; }
void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; }
void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; }
void set_bits_per_channel(i2s_bits_per_chan_t bits_per_channel) { this->bits_per_channel_ = bits_per_channel; }
#else
void set_i2s_role(i2s_role_t role) { this->i2s_role_ = role; }
void set_slot_mode(i2s_slot_mode_t slot_mode) { this->slot_mode_ = slot_mode; }
void set_std_slot_mask(i2s_std_slot_mask_t std_slot_mask) { this->std_slot_mask_ = std_slot_mask; }
void set_slot_bit_width(i2s_slot_bit_width_t slot_bit_width) { this->slot_bit_width_ = slot_bit_width; }
#endif
void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; }
void set_use_apll(uint32_t use_apll) { this->use_apll_ = use_apll; }
void set_mclk_multiple(i2s_mclk_multiple_t mclk_multiple) { this->mclk_multiple_ = mclk_multiple; }
protected:
#ifdef USE_I2S_LEGACY
i2s_mode_t i2s_mode_{};
i2s_channel_fmt_t channel_;
i2s_bits_per_sample_t bits_per_sample_;
i2s_bits_per_chan_t bits_per_channel_;
#else
i2s_role_t i2s_role_{};
i2s_slot_mode_t slot_mode_;
i2s_std_slot_mask_t std_slot_mask_;
i2s_slot_bit_width_t slot_bit_width_;
#endif
uint32_t sample_rate_;
bool use_apll_;
i2s_mclk_multiple_t mclk_multiple_;
@@ -57,17 +39,6 @@ class I2SAudioOut : public I2SAudioBase {};
class I2SAudioComponent : public Component {
public:
#ifdef USE_I2S_LEGACY
i2s_pin_config_t get_pin_config() const {
return {
.mck_io_num = this->mclk_pin_,
.bck_io_num = this->bclk_pin_,
.ws_io_num = this->lrclk_pin_,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = I2S_PIN_NO_CHANGE,
};
}
#else
i2s_std_gpio_config_t get_pin_config() const {
return {.mclk = (gpio_num_t) this->mclk_pin_,
.bclk = (gpio_num_t) this->bclk_pin_,
@@ -80,7 +51,6 @@ class I2SAudioComponent : public Component {
.ws_inv = false,
}};
}
#endif
void set_mclk_pin(int pin) { this->mclk_pin_ = pin; }
void set_bclk_pin(int pin) { this->bclk_pin_ = pin; }
@@ -101,13 +71,8 @@ class I2SAudioComponent : public Component {
I2SAudioIn *audio_in_{nullptr};
I2SAudioOut *audio_out_{nullptr};
#ifdef USE_I2S_LEGACY
int mclk_pin_{I2S_PIN_NO_CHANGE};
int bclk_pin_{I2S_PIN_NO_CHANGE};
#else
int mclk_pin_{I2S_GPIO_UNUSED};
int bclk_pin_{I2S_GPIO_UNUSED};
#endif
int lrclk_pin_;
int port_{};
};
@@ -1,121 +1,7 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import esp32, media_player
import esphome.config_validation as cv
from esphome.const import CONF_MODE
from .. import (
CONF_I2S_AUDIO_ID,
CONF_I2S_DOUT_PIN,
CONF_LEFT,
CONF_MONO,
CONF_RIGHT,
CONF_STEREO,
I2SAudioComponent,
I2SAudioOut,
i2s_audio_ns,
use_legacy,
CONFIG_SCHEMA = cv.invalid(
"The I2S audio media player has been removed. "
"Use the speaker media player component instead. "
"See https://esphome.io/components/media_player/speaker.html for details."
)
CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["i2s_audio"]
I2SAudioMediaPlayer = i2s_audio_ns.class_(
"I2SAudioMediaPlayer", cg.Component, media_player.MediaPlayer, I2SAudioOut
)
i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t")
CONF_MUTE_PIN = "mute_pin"
CONF_AUDIO_ID = "audio_id"
CONF_DAC_TYPE = "dac_type"
CONF_I2S_COMM_FMT = "i2s_comm_fmt"
INTERNAL_DAC_OPTIONS = {
CONF_LEFT: i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN,
CONF_RIGHT: i2s_dac_mode_t.I2S_DAC_CHANNEL_RIGHT_EN,
CONF_STEREO: i2s_dac_mode_t.I2S_DAC_CHANNEL_BOTH_EN,
}
EXTERNAL_DAC_OPTIONS = [CONF_MONO, CONF_STEREO]
NO_INTERNAL_DAC_VARIANTS = [esp32.VARIANT_ESP32S2]
I2C_COMM_FMT_OPTIONS = ["lsb", "msb"]
def validate_esp32_variant(config):
if config[CONF_DAC_TYPE] != "internal":
return config
variant = esp32.get_esp32_variant()
if variant in NO_INTERNAL_DAC_VARIANTS:
raise cv.Invalid(f"{variant} does not have an internal DAC")
return config
CONFIG_SCHEMA = cv.All(
cv.typed_schema(
{
"internal": media_player.media_player_schema(I2SAudioMediaPlayer)
.extend(
{
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
cv.Required(CONF_MODE): cv.enum(INTERNAL_DAC_OPTIONS, lower=True),
}
)
.extend(cv.COMPONENT_SCHEMA),
"external": media_player.media_player_schema(I2SAudioMediaPlayer)
.extend(
{
cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent),
cv.Required(
CONF_I2S_DOUT_PIN
): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_MUTE_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_MODE, default="mono"): cv.one_of(
*EXTERNAL_DAC_OPTIONS, lower=True
),
cv.Optional(CONF_I2S_COMM_FMT, default="msb"): cv.one_of(
*I2C_COMM_FMT_OPTIONS, lower=True
),
}
)
.extend(cv.COMPONENT_SCHEMA),
},
key=CONF_DAC_TYPE,
),
cv.only_with_arduino,
validate_esp32_variant,
)
def _final_validate(_):
if not use_legacy():
raise cv.Invalid("I2S media player is only compatible with legacy i2s driver")
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
var = await media_player.new_media_player(config)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_I2S_AUDIO_ID])
if config[CONF_DAC_TYPE] == "internal":
cg.add(var.set_internal_dac_mode(config[CONF_MODE]))
else:
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
if CONF_MUTE_PIN in config:
pin = await cg.gpio_pin_expression(config[CONF_MUTE_PIN])
cg.add(var.set_mute_pin(pin))
cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1))
cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb"))
cg.add_library("WiFi", None)
cg.add_library("NetworkClientSecure", None)
cg.add_library("HTTPClient", None)
cg.add_library("esphome/ESP32-audioI2S", "2.3.0")
cg.add_build_flag("-DAUDIO_NO_SD_FS")
@@ -1,260 +0,0 @@
#include "i2s_audio_media_player.h"
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
#include "esphome/core/log.h"
namespace esphome {
namespace i2s_audio {
static const char *const TAG = "audio";
void I2SAudioMediaPlayer::control(const media_player::MediaPlayerCall &call) {
media_player::MediaPlayerState play_state = media_player::MEDIA_PLAYER_STATE_PLAYING;
auto announcement = call.get_announcement();
if (announcement.has_value()) {
play_state = *announcement ? media_player::MEDIA_PLAYER_STATE_ANNOUNCING : media_player::MEDIA_PLAYER_STATE_PLAYING;
}
auto media_url = call.get_media_url();
if (media_url.has_value()) {
this->current_url_ = media_url;
if (this->i2s_state_ != I2S_STATE_STOPPED && this->audio_ != nullptr) {
if (this->audio_->isRunning()) {
this->audio_->stopSong();
}
this->audio_->connecttohost(media_url->c_str());
this->state = play_state;
} else {
this->start();
}
}
if (play_state == media_player::MEDIA_PLAYER_STATE_ANNOUNCING) {
this->is_announcement_ = true;
}
auto vol = call.get_volume();
if (vol.has_value()) {
this->volume = *vol;
this->set_volume_(volume);
this->unmute_();
}
auto cmd = call.get_command();
if (cmd.has_value()) {
switch (*cmd) {
case media_player::MEDIA_PLAYER_COMMAND_MUTE:
this->mute_();
break;
case media_player::MEDIA_PLAYER_COMMAND_UNMUTE:
this->unmute_();
break;
case media_player::MEDIA_PLAYER_COMMAND_VOLUME_UP: {
float new_volume = this->volume + 0.1f;
if (new_volume > 1.0f)
new_volume = 1.0f;
this->set_volume_(new_volume);
this->unmute_();
break;
}
case media_player::MEDIA_PLAYER_COMMAND_VOLUME_DOWN: {
float new_volume = this->volume - 0.1f;
if (new_volume < 0.0f)
new_volume = 0.0f;
this->set_volume_(new_volume);
this->unmute_();
break;
}
default:
break;
}
if (this->i2s_state_ != I2S_STATE_RUNNING) {
return;
}
switch (*cmd) {
case media_player::MEDIA_PLAYER_COMMAND_PLAY:
if (!this->audio_->isRunning())
this->audio_->pauseResume();
this->state = play_state;
break;
case media_player::MEDIA_PLAYER_COMMAND_PAUSE:
if (this->audio_->isRunning())
this->audio_->pauseResume();
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
break;
case media_player::MEDIA_PLAYER_COMMAND_STOP:
this->stop();
break;
case media_player::MEDIA_PLAYER_COMMAND_TOGGLE:
this->audio_->pauseResume();
if (this->audio_->isRunning()) {
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
} else {
this->state = media_player::MEDIA_PLAYER_STATE_PAUSED;
}
break;
default:
break;
}
}
this->publish_state();
}
void I2SAudioMediaPlayer::mute_() {
if (this->mute_pin_ != nullptr) {
this->mute_pin_->digital_write(true);
} else {
this->set_volume_(0.0f, false);
}
this->muted_ = true;
}
void I2SAudioMediaPlayer::unmute_() {
if (this->mute_pin_ != nullptr) {
this->mute_pin_->digital_write(false);
} else {
this->set_volume_(this->volume, false);
}
this->muted_ = false;
}
void I2SAudioMediaPlayer::set_volume_(float volume, bool publish) {
if (this->audio_ != nullptr)
this->audio_->setVolume(remap<uint8_t, float>(volume, 0.0f, 1.0f, 0, 21));
if (publish)
this->volume = volume;
}
void I2SAudioMediaPlayer::setup() { this->state = media_player::MEDIA_PLAYER_STATE_IDLE; }
void I2SAudioMediaPlayer::loop() {
switch (this->i2s_state_) {
case I2S_STATE_STARTING:
this->start_();
break;
case I2S_STATE_RUNNING:
this->play_();
break;
case I2S_STATE_STOPPING:
this->stop_();
break;
case I2S_STATE_STOPPED:
break;
}
}
void I2SAudioMediaPlayer::play_() {
this->audio_->loop();
if ((this->state == media_player::MEDIA_PLAYER_STATE_PLAYING ||
this->state == media_player::MEDIA_PLAYER_STATE_ANNOUNCING) &&
!this->audio_->isRunning()) {
this->stop();
}
}
void I2SAudioMediaPlayer::start() { this->i2s_state_ = I2S_STATE_STARTING; }
void I2SAudioMediaPlayer::start_() {
if (!this->parent_->try_lock()) {
return; // Waiting for another i2s to return lock
}
#if SOC_I2S_SUPPORTS_DAC
if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) {
this->audio_ = make_unique<Audio>(true, this->internal_dac_mode_, this->parent_->get_port());
} else {
#endif
this->audio_ = make_unique<Audio>(false, 3, this->parent_->get_port());
i2s_pin_config_t pin_config = this->parent_->get_pin_config();
pin_config.data_out_num = this->dout_pin_;
i2s_set_pin(this->parent_->get_port(), &pin_config);
this->audio_->setI2SCommFMT_LSB(this->i2s_comm_fmt_lsb_);
this->audio_->forceMono(this->external_dac_channels_ == 1);
if (this->mute_pin_ != nullptr) {
this->mute_pin_->setup();
this->mute_pin_->digital_write(false);
}
#if SOC_I2S_SUPPORTS_DAC
}
#endif
this->i2s_state_ = I2S_STATE_RUNNING;
this->high_freq_.start();
this->audio_->setVolume(remap<uint8_t, float>(this->volume, 0.0f, 1.0f, 0, 21));
if (this->current_url_.has_value()) {
this->audio_->connecttohost(this->current_url_.value().c_str());
this->state = media_player::MEDIA_PLAYER_STATE_PLAYING;
if (this->is_announcement_) {
this->state = media_player::MEDIA_PLAYER_STATE_ANNOUNCING;
}
this->publish_state();
}
}
void I2SAudioMediaPlayer::stop() {
if (this->i2s_state_ == I2S_STATE_STOPPED) {
return;
}
if (this->i2s_state_ == I2S_STATE_STARTING) {
this->i2s_state_ = I2S_STATE_STOPPED;
return;
}
this->i2s_state_ = I2S_STATE_STOPPING;
}
void I2SAudioMediaPlayer::stop_() {
if (this->audio_->isRunning()) {
this->audio_->stopSong();
return;
}
this->audio_ = nullptr;
this->current_url_ = {};
this->parent_->unlock();
this->i2s_state_ = I2S_STATE_STOPPED;
this->high_freq_.stop();
this->state = media_player::MEDIA_PLAYER_STATE_IDLE;
this->publish_state();
this->is_announcement_ = false;
}
media_player::MediaPlayerTraits I2SAudioMediaPlayer::get_traits() {
auto traits = media_player::MediaPlayerTraits();
traits.set_supports_pause(true);
return traits;
};
void I2SAudioMediaPlayer::dump_config() {
ESP_LOGCONFIG(TAG, "Audio:");
if (this->is_failed()) {
ESP_LOGCONFIG(TAG, "Audio failed to initialize!");
return;
}
#if SOC_I2S_SUPPORTS_DAC
if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) {
switch (this->internal_dac_mode_) {
case I2S_DAC_CHANNEL_LEFT_EN:
ESP_LOGCONFIG(TAG, " Internal DAC mode: Left");
break;
case I2S_DAC_CHANNEL_RIGHT_EN:
ESP_LOGCONFIG(TAG, " Internal DAC mode: Right");
break;
case I2S_DAC_CHANNEL_BOTH_EN:
ESP_LOGCONFIG(TAG, " Internal DAC mode: Left & Right");
break;
default:
break;
}
} else {
#endif
ESP_LOGCONFIG(TAG,
" External DAC channels: %d\n"
" I2S DOUT Pin: %d",
this->external_dac_channels_, this->dout_pin_);
LOG_PIN(" Mute Pin: ", this->mute_pin_);
#if SOC_I2S_SUPPORTS_DAC
}
#endif
}
} // namespace i2s_audio
} // namespace esphome
#endif // USE_ESP32_FRAMEWORK_ARDUINO
@@ -1,87 +0,0 @@
#pragma once
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
#include "../i2s_audio.h"
#include <driver/i2s.h>
#include "esphome/components/media_player/media_player.h"
#include "esphome/core/component.h"
#include "esphome/core/gpio.h"
#include "esphome/core/helpers.h"
#include <Audio.h>
namespace esphome {
namespace i2s_audio {
enum I2SState : uint8_t {
I2S_STATE_STOPPED = 0,
I2S_STATE_STARTING,
I2S_STATE_RUNNING,
I2S_STATE_STOPPING,
};
class I2SAudioMediaPlayer : public Component, public Parented<I2SAudioComponent>, public media_player::MediaPlayer {
public:
void setup() override;
float get_setup_priority() const override { return esphome::setup_priority::LATE; }
void loop() override;
void dump_config() override;
void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; }
void set_mute_pin(GPIOPin *mute_pin) { this->mute_pin_ = mute_pin; }
#if SOC_I2S_SUPPORTS_DAC
void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; }
#endif
void set_external_dac_channels(uint8_t channels) { this->external_dac_channels_ = channels; }
void set_i2s_comm_fmt_lsb(bool lsb) { this->i2s_comm_fmt_lsb_ = lsb; }
media_player::MediaPlayerTraits get_traits() override;
bool is_muted() const override { return this->muted_; }
void start();
void stop();
protected:
void control(const media_player::MediaPlayerCall &call) override;
void mute_();
void unmute_();
void set_volume_(float volume, bool publish = true);
void start_();
void stop_();
void play_();
I2SState i2s_state_{I2S_STATE_STOPPED};
std::unique_ptr<Audio> audio_;
uint8_t dout_pin_{0};
GPIOPin *mute_pin_{nullptr};
bool muted_{false};
float unmuted_volume_{0};
#if SOC_I2S_SUPPORTS_DAC
i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE};
#endif
uint8_t external_dac_channels_;
bool i2s_comm_fmt_lsb_;
HighFrequencyLoopRequester high_freq_;
optional<std::string> current_url_{};
bool is_announcement_{false};
};
} // namespace i2s_audio
} // namespace esphome
#endif // USE_ESP32_FRAMEWORK_ARDUINO
@@ -1,14 +1,13 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components import audio, esp32, microphone
from esphome.components.adc import ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, validate_adc_pin
from esphome.components.adc import validate_adc_pin
import esphome.config_validation as cv
from esphome.const import (
CONF_BITS_PER_SAMPLE,
CONF_CHANNEL,
CONF_ID,
CONF_NUM_CHANNELS,
CONF_NUMBER,
CONF_SAMPLE_RATE,
)
@@ -23,7 +22,6 @@ from .. import (
i2s_audio_component_schema,
i2s_audio_ns,
register_i2s_audio_component,
use_legacy,
validate_mclk_divisible_by_3,
)
@@ -127,8 +125,10 @@ CONFIG_SCHEMA = cv.All(
def _final_validate(config):
if not use_legacy() and config[CONF_ADC_TYPE] == "internal":
raise cv.Invalid("Internal ADC is only compatible with legacy i2s driver")
if config[CONF_ADC_TYPE] == "internal":
raise cv.Invalid(
"Internal ADC is no longer supported. Use an external I2S microphone instead."
)
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -140,13 +140,7 @@ async def to_code(config):
await register_i2s_audio_component(var, config)
await microphone.register_microphone(var, config)
if config[CONF_ADC_TYPE] == "internal":
variant = esp32.get_esp32_variant()
pin_num = config[CONF_ADC_PIN][CONF_NUMBER]
channel = ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant][pin_num]
cg.add(var.set_adc_channel(channel))
else:
cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN]))
cg.add(var.set_pdm(config[CONF_PDM]))
cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN]))
cg.add(var.set_pdm(config[CONF_PDM]))
cg.add(var.set_correct_dc_offset(config[CONF_CORRECT_DC_OFFSET]))
@@ -2,12 +2,8 @@
#ifdef USE_ESP32
#ifdef USE_I2S_LEGACY
#include <driver/i2s.h>
#else
#include <driver/i2s_std.h>
#include <driver/i2s_pdm.h>
#endif
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
@@ -65,13 +61,6 @@ void I2SAudioMicrophone::dump_config() {
void I2SAudioMicrophone::configure_stream_settings_() {
uint8_t channel_count = 1;
#ifdef USE_I2S_LEGACY
uint8_t bits_per_sample = this->bits_per_sample_;
if (this->channel_ == I2S_CHANNEL_FMT_RIGHT_LEFT) {
channel_count = 2;
}
#else
uint8_t bits_per_sample = 16;
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) {
bits_per_sample = this->slot_bit_width_;
@@ -80,7 +69,6 @@ void I2SAudioMicrophone::configure_stream_settings_() {
if (this->slot_mode_ == I2S_SLOT_MODE_STEREO) {
channel_count = 2;
}
#endif
#ifdef USE_ESP32_VARIANT_ESP32
// ESP32 reads audio aligned to a multiple of 2 bytes. For example, if configured for 24 bits per sample, then it will
@@ -114,65 +102,6 @@ bool I2SAudioMicrophone::start_driver_() {
this->locked_driver_ = true;
esp_err_t err;
#ifdef USE_I2S_LEGACY
i2s_driver_config_t config = {
.mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_RX),
.sample_rate = this->sample_rate_,
.bits_per_sample = this->bits_per_sample_,
.channel_format = this->channel_,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 4,
.dma_buf_len = 240, // Must be divisible by 3 to support 24 bits per sample on old driver and newer variants
.use_apll = this->use_apll_,
.tx_desc_auto_clear = false,
.fixed_mclk = 0,
.mclk_multiple = this->mclk_multiple_,
.bits_per_chan = this->bits_per_channel_,
};
#if SOC_I2S_SUPPORTS_ADC
if (this->adc_) {
config.mode = (i2s_mode_t) (config.mode | I2S_MODE_ADC_BUILT_IN);
err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error installing driver: %s", esp_err_to_name(err));
return false;
}
err = i2s_set_adc_mode(ADC_UNIT_1, this->adc_channel_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error setting ADC mode: %s", esp_err_to_name(err));
return false;
}
err = i2s_adc_enable(this->parent_->get_port());
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error enabling ADC: %s", esp_err_to_name(err));
return false;
}
} else
#endif
{
if (this->pdm_)
config.mode = (i2s_mode_t) (config.mode | I2S_MODE_PDM);
err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error installing driver: %s", esp_err_to_name(err));
return false;
}
i2s_pin_config_t pin_config = this->parent_->get_pin_config();
pin_config.data_in_num = this->din_pin_;
err = i2s_set_pin(this->parent_->get_port(), &pin_config);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error setting pin: %s", esp_err_to_name(err));
return false;
}
}
#else
i2s_chan_config_t chan_cfg = {
.id = this->parent_->get_port(),
.role = this->i2s_role_,
@@ -265,7 +194,6 @@ bool I2SAudioMicrophone::start_driver_() {
ESP_LOGE(TAG, "Enabling failed: %s", esp_err_to_name(err));
return false;
}
#endif
this->configure_stream_settings_(); // redetermine the settings in case some settings were changed after compilation
@@ -284,24 +212,6 @@ void I2SAudioMicrophone::stop_driver_() {
// ensures that we stop/unload the driver when it only partially starts.
esp_err_t err;
#ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_ADC
if (this->adc_) {
err = i2s_adc_disable(this->parent_->get_port());
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error disabling ADC: %s", esp_err_to_name(err));
}
}
#endif
err = i2s_stop(this->parent_->get_port());
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error stopping: %s", esp_err_to_name(err));
}
err = i2s_driver_uninstall(this->parent_->get_port());
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error uninstalling driver: %s", esp_err_to_name(err));
}
#else
if (this->rx_handle_ != nullptr) {
/* Have to stop the channel before deleting it */
err = i2s_channel_disable(this->rx_handle_);
@@ -315,7 +225,6 @@ void I2SAudioMicrophone::stop_driver_() {
}
this->rx_handle_ = nullptr;
}
#endif
if (this->locked_driver_) {
this->parent_->unlock();
this->locked_driver_ = false;
@@ -412,12 +321,8 @@ void I2SAudioMicrophone::fix_dc_offset_(std::vector<uint8_t> &data) {
size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait) {
size_t bytes_read = 0;
#ifdef USE_I2S_LEGACY
esp_err_t err = i2s_read(this->parent_->get_port(), buf, len, &bytes_read, ticks_to_wait);
#else
// i2s_channel_read expects the timeout value in ms, not ticks
esp_err_t err = i2s_channel_read(this->rx_handle_, buf, len, &bytes_read, pdTICKS_TO_MS(ticks_to_wait));
#endif
if ((err != ESP_OK) && ((err != ESP_ERR_TIMEOUT) || (ticks_to_wait != 0))) {
// Ignore ESP_ERR_TIMEOUT if ticks_to_wait = 0, as it will read the data on the next call
if (!this->status_has_warning()) {
@@ -432,7 +337,7 @@ size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_w
return 0;
}
this->status_clear_warning();
#if defined(USE_ESP32_VARIANT_ESP32) and not defined(USE_I2S_LEGACY)
#ifdef USE_ESP32_VARIANT_ESP32
// For ESP32 16-bit standard mono mode, adjacent samples need to be swapped.
if (this->slot_mode_ == I2S_SLOT_MODE_MONO && this->slot_bit_width_ == I2S_SLOT_BIT_WIDTH_16BIT && !this->pdm_) {
int16_t *samples = reinterpret_cast<int16_t *>(buf);
@@ -26,23 +26,10 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
void set_correct_dc_offset(bool correct_dc_offset) { this->correct_dc_offset_ = correct_dc_offset; }
#ifdef USE_I2S_LEGACY
void set_din_pin(int8_t pin) { this->din_pin_ = pin; }
#else
void set_din_pin(int8_t pin) { this->din_pin_ = (gpio_num_t) pin; }
#endif
void set_pdm(bool pdm) { this->pdm_ = pdm; }
#ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_ADC
void set_adc_channel(adc_channel_t channel) {
this->adc_channel_ = (adc1_channel_t) channel;
this->adc_ = true;
}
#endif
#endif
protected:
/// @brief Starts the I2S driver. Updates the ``audio_stream_info_`` member variable with the current setttings.
/// @return True if succesful, false otherwise
@@ -68,16 +55,8 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
TaskHandle_t task_handle_{nullptr};
#ifdef USE_I2S_LEGACY
int8_t din_pin_{I2S_PIN_NO_CHANGE};
#if SOC_I2S_SUPPORTS_ADC
adc1_channel_t adc_channel_{ADC1_CHANNEL_MAX};
bool adc_{false};
#endif
#else
gpio_num_t din_pin_{I2S_GPIO_UNUSED};
i2s_chan_handle_t rx_handle_;
#endif
bool pdm_{false};
bool correct_dc_offset_;
@@ -26,7 +26,6 @@ from .. import (
i2s_audio_component_schema,
i2s_audio_ns,
register_i2s_audio_component,
use_legacy,
validate_mclk_divisible_by_3,
)
@@ -166,13 +165,12 @@ CONFIG_SCHEMA = cv.All(
def _final_validate(config):
if not use_legacy():
if config[CONF_DAC_TYPE] == "internal":
raise cv.Invalid("Internal DAC is only compatible with legacy i2s driver")
if config[CONF_I2S_COMM_FMT] == "stand_max":
raise cv.Invalid(
"I2S standard max format only implemented with legacy i2s driver."
)
if config[CONF_DAC_TYPE] == "internal":
raise cv.Invalid(
"Internal DAC is no longer supported. Use an external I2S DAC instead."
)
if config[CONF_I2S_COMM_FMT] == "stand_max":
raise cv.Invalid("I2S standard max format is no longer supported.")
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -184,21 +182,13 @@ async def to_code(config):
await register_i2s_audio_component(var, config)
await speaker.register_speaker(var, config)
if config[CONF_DAC_TYPE] == "internal":
cg.add(var.set_internal_dac_mode(config[CONF_MODE]))
else:
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
if use_legacy():
cg.add(
var.set_i2s_comm_fmt(I2C_COMM_FMT_OPTIONS[config[CONF_I2S_COMM_FMT]])
)
else:
fmt = "std" # equals stand_i2s, stand_pcm_long, i2s_msb, pcm_long
if config[CONF_I2S_COMM_FMT] in ["stand_msb", "i2s_lsb"]:
fmt = "msb"
elif config[CONF_I2S_COMM_FMT] in ["stand_pcm_short", "pcm_short", "pcm"]:
fmt = "pcm"
cg.add(var.set_i2s_comm_fmt(fmt))
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
fmt = "std" # equals stand_i2s, stand_pcm_long, i2s_msb, pcm_long
if config[CONF_I2S_COMM_FMT] in ["stand_msb", "i2s_lsb"]:
fmt = "msb"
elif config[CONF_I2S_COMM_FMT] in ["stand_pcm_short", "pcm_short", "pcm"]:
fmt = "pcm"
cg.add(var.set_i2s_comm_fmt(fmt))
if config[CONF_TIMEOUT] != CONF_NEVER:
cg.add(var.set_timeout(config[CONF_TIMEOUT]))
cg.add(var.set_buffer_duration(config[CONF_BUFFER_DURATION]))
@@ -2,11 +2,7 @@
#ifdef USE_ESP32
#ifdef USE_I2S_LEGACY
#include <driver/i2s.h>
#else
#include <driver/i2s_std.h>
#endif
#include "esphome/components/audio/audio.h"
#include "esphome/components/audio/audio_transfer_buffer.h"
@@ -79,14 +75,7 @@ void I2SAudioSpeaker::dump_config() {
if (this->timeout_.has_value()) {
ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 " ms", this->timeout_.value());
}
#ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_DAC
ESP_LOGCONFIG(TAG, " Internal DAC mode: %d", static_cast<int8_t>(this->internal_dac_mode_));
#endif
ESP_LOGCONFIG(TAG, " Communication format: %d", static_cast<int8_t>(this->i2s_comm_fmt_));
#else
ESP_LOGCONFIG(TAG, " Communication format: %s", this->i2s_comm_fmt_.c_str());
#endif
}
void I2SAudioSpeaker::loop() {
@@ -300,14 +289,6 @@ void I2SAudioSpeaker::speaker_task(void *params) {
// Audio stream info changed, stop the speaker task so it will restart with the proper settings.
break;
}
#ifdef USE_I2S_LEGACY
i2s_event_t i2s_event;
while (xQueueReceive(this_speaker->i2s_event_queue_, &i2s_event, 0)) {
if (i2s_event.type == I2S_EVENT_TX_Q_OVF) {
tx_dma_underflow = true;
}
}
#else
int64_t write_timestamp;
while (xQueueReceive(this_speaker->i2s_event_queue_, &write_timestamp, 0)) {
// Receives timing events from the I2S on_sent callback. If actual audio data was sent in this event, it passes
@@ -326,7 +307,6 @@ void I2SAudioSpeaker::speaker_task(void *params) {
this_speaker->audio_output_callback_(frames_sent, write_timestamp);
}
}
#endif
if (this_speaker->pause_state_) {
// Pause state is accessed atomically, so thread safe
@@ -393,17 +373,6 @@ void I2SAudioSpeaker::speaker_task(void *params) {
vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS / 2));
} else {
size_t bytes_written = 0;
#ifdef USE_I2S_LEGACY
if (this_speaker->current_stream_info_.get_bits_per_sample() == (uint8_t) this_speaker->bits_per_sample_) {
i2s_write(this_speaker->parent_->get_port(), transfer_buffer->get_buffer_start(),
transfer_buffer->available(), &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS));
} else if (this_speaker->current_stream_info_.get_bits_per_sample() <
(uint8_t) this_speaker->bits_per_sample_) {
i2s_write_expand(this_speaker->parent_->get_port(), transfer_buffer->get_buffer_start(),
transfer_buffer->available(), this_speaker->current_stream_info_.get_bits_per_sample(),
this_speaker->bits_per_sample_, &bytes_written, pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS));
}
#else
if (tx_dma_underflow) {
// Temporarily disable channel and callback to reset the I2S driver's internal DMA buffer queue so timing
// callbacks are accurate. Preload the data.
@@ -420,14 +389,12 @@ void I2SAudioSpeaker::speaker_task(void *params) {
i2s_channel_write(this_speaker->tx_handle_, transfer_buffer->get_buffer_start(), transfer_buffer->available(),
&bytes_written, DMA_BUFFER_DURATION_MS);
}
#endif
if (bytes_written > 0) {
last_data_received_time = millis();
frames_written += this_speaker->current_stream_info_.bytes_to_frames(bytes_written);
transfer_buffer->decrease_buffer_length(bytes_written);
if (tx_dma_underflow) {
tx_dma_underflow = false;
#ifndef USE_I2S_LEGACY
// Reset the event queue timestamps
// Enable the on_sent callback to accurately track the timestamps of played audio
// Enable the I2S channel to start sending the preloaded audio
@@ -440,14 +407,7 @@ void I2SAudioSpeaker::speaker_task(void *params) {
i2s_channel_register_event_callback(this_speaker->tx_handle_, &callbacks, this_speaker);
i2s_channel_enable(this_speaker->tx_handle_);
#endif
}
#ifdef USE_I2S_LEGACY
// The legacy driver doesn't easily support the callback approach for timestamps, so fall back to a direct but
// less accurate approach.
this_speaker->audio_output_callback_(this_speaker->current_stream_info_.bytes_to_frames(bytes_written),
esp_timer_get_time() + dma_buffers_duration_ms * 1000);
#endif
}
}
}
@@ -496,22 +456,14 @@ void I2SAudioSpeaker::stop_(bool wait_on_empty) {
esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info) {
this->current_stream_info_ = audio_stream_info; // store the stream info settings the driver will use
#ifdef USE_I2S_LEGACY
if ((this->i2s_mode_ & I2S_MODE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT
#else
if ((this->i2s_role_ & I2S_ROLE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT
#endif
// Can't reconfigure I2S bus, so the sample rate must match the configured value
ESP_LOGE(TAG, "Audio stream settings are not compatible with this I2S configuration");
return ESP_ERR_NOT_SUPPORTED;
}
#ifdef USE_I2S_LEGACY
if ((i2s_bits_per_sample_t) audio_stream_info.get_bits_per_sample() > this->bits_per_sample_) {
#else
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO &&
(i2s_slot_bit_width_t) audio_stream_info.get_bits_per_sample() > this->slot_bit_width_) {
#endif
// Currently can't handle the case when the incoming audio has more bits per sample than the configured value
ESP_LOGE(TAG, "Audio streams with more bits per sample than the I2S speaker's configuration is not supported");
return ESP_ERR_NOT_SUPPORTED;
@@ -524,77 +476,6 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
uint32_t dma_buffer_length = audio_stream_info.ms_to_frames(DMA_BUFFER_DURATION_MS);
#ifdef USE_I2S_LEGACY
i2s_channel_fmt_t channel = this->channel_;
if (audio_stream_info.get_channels() == 1) {
if (this->channel_ == I2S_CHANNEL_FMT_ONLY_LEFT) {
channel = I2S_CHANNEL_FMT_ONLY_LEFT;
} else {
channel = I2S_CHANNEL_FMT_ONLY_RIGHT;
}
} else if (audio_stream_info.get_channels() == 2) {
channel = I2S_CHANNEL_FMT_RIGHT_LEFT;
}
i2s_driver_config_t config = {
.mode = (i2s_mode_t) (this->i2s_mode_ | I2S_MODE_TX),
.sample_rate = audio_stream_info.get_sample_rate(),
.bits_per_sample = this->bits_per_sample_,
.channel_format = channel,
.communication_format = this->i2s_comm_fmt_,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = DMA_BUFFERS_COUNT,
.dma_buf_len = (int) dma_buffer_length,
.use_apll = this->use_apll_,
.tx_desc_auto_clear = true,
.fixed_mclk = I2S_PIN_NO_CHANGE,
.mclk_multiple = this->mclk_multiple_,
.bits_per_chan = this->bits_per_channel_,
#if SOC_I2S_SUPPORTS_TDM
.chan_mask = (i2s_channel_t) (I2S_TDM_ACTIVE_CH0 | I2S_TDM_ACTIVE_CH1),
.total_chan = 2,
.left_align = false,
.big_edin = false,
.bit_order_msb = false,
.skip_msk = false,
#endif
};
#if SOC_I2S_SUPPORTS_DAC
if (this->internal_dac_mode_ != I2S_DAC_CHANNEL_DISABLE) {
config.mode = (i2s_mode_t) (config.mode | I2S_MODE_DAC_BUILT_IN);
}
#endif
esp_err_t err =
i2s_driver_install(this->parent_->get_port(), &config, I2S_EVENT_QUEUE_COUNT, &this->i2s_event_queue_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to install I2S legacy driver");
// Failed to install the driver, so unlock the I2S port
this->parent_->unlock();
return err;
}
#if SOC_I2S_SUPPORTS_DAC
if (this->internal_dac_mode_ == I2S_DAC_CHANNEL_DISABLE) {
#endif
i2s_pin_config_t pin_config = this->parent_->get_pin_config();
pin_config.data_out_num = this->dout_pin_;
err = i2s_set_pin(this->parent_->get_port(), &pin_config);
#if SOC_I2S_SUPPORTS_DAC
} else {
i2s_set_dac_mode(this->internal_dac_mode_);
}
#endif
if (err != ESP_OK) {
// Failed to set the data out pin, so uninstall the driver and unlock the I2S port
ESP_LOGE(TAG, "Failed to set the data out pin");
i2s_driver_uninstall(this->parent_->get_port());
this->parent_->unlock();
}
#else
i2s_chan_config_t chan_cfg = {
.id = this->parent_->get_port(),
.role = this->i2s_role_,
@@ -683,12 +564,10 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
}
i2s_channel_enable(this->tx_handle_);
#endif
return err;
}
#ifndef USE_I2S_LEGACY
bool IRAM_ATTR I2SAudioSpeaker::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) {
int64_t now = esp_timer_get_time();
@@ -709,16 +588,11 @@ bool IRAM_ATTR I2SAudioSpeaker::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_eve
return need_yield1 | need_yield2 | need_yield3;
}
#endif
void I2SAudioSpeaker::stop_i2s_driver_() {
#ifdef USE_I2S_LEGACY
i2s_driver_uninstall(this->parent_->get_port());
#else
i2s_channel_disable(this->tx_handle_);
i2s_del_channel(this->tx_handle_);
this->tx_handle_ = nullptr;
#endif
this->parent_->unlock();
}
@@ -29,16 +29,8 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; }
void set_timeout(uint32_t ms) { this->timeout_ = ms; }
#ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_DAC
void set_internal_dac_mode(i2s_dac_mode_t mode) { this->internal_dac_mode_ = mode; }
#endif
void set_dout_pin(uint8_t pin) { this->dout_pin_ = pin; }
void set_i2s_comm_fmt(i2s_comm_format_t mode) { this->i2s_comm_fmt_ = mode; }
#else
void set_dout_pin(uint8_t pin) { this->dout_pin_ = (gpio_num_t) pin; }
void set_i2s_comm_fmt(std::string mode) { this->i2s_comm_fmt_ = std::move(mode); }
#endif
void start() override;
void stop() override;
@@ -83,14 +75,12 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
/// @param wait_on_empty If false, sends the COMMAND_STOP signal. If true, sends the COMMAND_STOP_GRACEFULLY signal.
void stop_(bool wait_on_empty);
#ifndef USE_I2S_LEGACY
/// @brief Callback function used to send playback timestamps the to the speaker task.
/// @param handle (i2s_chan_handle_t)
/// @param event (i2s_event_data_t)
/// @param user_ctx (void*) User context pointer that the callback accesses
/// @return True if a higher priority task was interrupted
static bool i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx);
#endif
/// @brief Starts the ESP32 I2S driver.
/// Attempts to lock the I2S port, starts the I2S driver using the passed in stream information, and sets the data out
@@ -110,7 +100,7 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
TaskHandle_t speaker_task_handle_{nullptr};
EventGroupHandle_t event_group_{nullptr};
QueueHandle_t i2s_event_queue_;
QueueHandle_t i2s_event_queue_{nullptr};
std::weak_ptr<RingBuffer> audio_ring_buffer_;
@@ -124,17 +114,9 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
audio::AudioStreamInfo current_stream_info_; // The currently loaded driver's stream info
#ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_DAC
i2s_dac_mode_t internal_dac_mode_{I2S_DAC_CHANNEL_DISABLE};
#endif
uint8_t dout_pin_;
i2s_comm_format_t i2s_comm_fmt_;
#else
gpio_num_t dout_pin_;
std::string i2s_comm_fmt_;
i2s_chan_handle_t tx_handle_;
#endif
};
} // namespace i2s_audio

Some files were not shown because too many files have changed in this diff Show More