Merge branch 'dev' into callback-drop-captures

This commit is contained in:
J. Nick Koston
2026-03-18 18:43:45 -10:00
committed by GitHub
181 changed files with 5304 additions and 2775 deletions
+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
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
+2 -2
View File
@@ -40,7 +40,7 @@ jobs:
echo "You have modified clang-tidy configuration but have not updated the hash." | tee -a $GITHUB_STEP_SUMMARY
echo "Please run 'script/clang_tidy_hash.py --update' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY
- if: failure()
- if: failure() && github.event.pull_request.head.repo.full_name == github.repository
name: Request changes
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
@@ -53,7 +53,7 @@ jobs:
body: 'You have modified clang-tidy configuration but have not updated the hash.\nPlease run `script/clang_tidy_hash.py --update` and commit the changes.'
})
- if: success()
- if: success() && github.event.pull_request.head.repo.full_name == github.repository
name: Dismiss review
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
+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') }}
+2 -2
View File
@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
with:
category: "/language:${{matrix.language}}"
+3 -3
View File
@@ -221,7 +221,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -256,7 +256,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -287,7 +287,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
+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
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
+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 -1
View File
@@ -35,7 +35,7 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent
uint8_t current_sensor_;
// The AM43 often gets into a state where it spams loads of battery update
// notifications. Here we will limit to no more than every 10s.
uint8_t last_battery_update_;
uint32_t last_battery_update_;
};
} // namespace am43
+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
+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
@@ -41,7 +41,7 @@ enum AS3935RegisterMasks {
INT_MASK = 0xF0,
THRESH_MASK = 0x0F,
R_SPIKE_MASK = 0xF0,
ENERGY_MASK = 0xF0,
ENERGY_MASK = 0xE0,
CAP_MASK = 0xF0,
LIGHT_MASK = 0xCF,
DISTURB_MASK = 0xDF,
+1 -1
View File
@@ -183,7 +183,7 @@ class BedjetCodec {
BedjetPacket packet_;
BedjetStatusPacket *status_packet_;
BedjetStatusPacket *status_packet_{nullptr};
BedjetStatusPacket buf_;
};
@@ -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",
+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"
@@ -136,6 +136,9 @@ bool DallasTemperatureSensor::check_scratch_pad_() {
float DallasTemperatureSensor::get_temp_c_() {
int16_t temp = (this->scratch_pad_[1] << 8) | this->scratch_pad_[0];
if ((this->address_ & 0xff) == DALLAS_MODEL_DS18S20) {
if (this->scratch_pad_[7] == 0) {
return NAN;
}
return (temp >> 1) + (this->scratch_pad_[7] - this->scratch_pad_[6]) / float(this->scratch_pad_[7]) - 0.25;
}
switch (this->resolution_) {
+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';
+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
-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;
@@ -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};
+31 -4
View File
@@ -158,6 +158,8 @@ _IDF6_ETHERNET_COMPONENTS: dict[str, IDFRegistryComponent] = {
}
SPI_ETHERNET_TYPES = ["W5500", "DM9051"]
# RP2040-supported SPI ethernet types
RP2040_SPI_ETHERNET_TYPES = ["W5500"]
SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10)
emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t")
@@ -273,6 +275,11 @@ def _validate(config):
f"{config[CONF_TYPE]} PHY requires RMII interface and is only supported "
f"on ESP32 classic and ESP32-P4, not {variant}"
)
elif CORE.is_rp2040 and config[CONF_TYPE] not in RP2040_SPI_ETHERNET_TYPES:
raise cv.Invalid(
f"Only {', '.join(RP2040_SPI_ETHERNET_TYPES)} are supported on RP2040, "
f"not {config[CONF_TYPE]}"
)
return config
@@ -330,18 +337,21 @@ SPI_SCHEMA = cv.All(
cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_number,
cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_CLOCK_SPEED, default="26.67MHz"): cv.All(
cv.frequency, cv.int_range(int(8e6), int(80e6))
cv.SplitDefault(CONF_CLOCK_SPEED, esp32="26.67MHz"): cv.All(
cv.only_on_esp32,
cv.frequency,
cv.int_range(int(8e6), int(80e6)),
),
# Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate()
cv.Optional(CONF_POLLING_INTERVAL): cv.All(
cv.only_on_esp32,
cv.positive_time_period_milliseconds,
cv.Range(min=TimePeriodMilliseconds(milliseconds=1)),
),
}
),
),
cv.only_on([Platform.ESP32]),
cv.only_on([Platform.ESP32, Platform.RP2040]),
)
CONFIG_SCHEMA = cv.All(
@@ -431,6 +441,8 @@ async def to_code(config):
if CORE.is_esp32:
await _to_code_esp32(var, config)
elif CORE.is_rp2040:
await _to_code_rp2040(var, config)
cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]]))
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
@@ -464,7 +476,7 @@ async def to_code(config):
CORE.add_job(final_step)
async def _to_code_esp32(var, config):
async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None:
from esphome.components.esp32 import (
add_idf_component,
add_idf_sdkconfig_option,
@@ -532,6 +544,20 @@ async def _to_code_esp32(var, config):
add_idf_component(name=component.name, ref=component.version)
async def _to_code_rp2040(var: cg.Pvariable, config: ConfigType) -> None:
cg.add(var.set_clk_pin(config[CONF_CLK_PIN]))
cg.add(var.set_miso_pin(config[CONF_MISO_PIN]))
cg.add(var.set_mosi_pin(config[CONF_MOSI_PIN]))
cg.add(var.set_cs_pin(config[CONF_CS_PIN]))
if CONF_INTERRUPT_PIN in config:
cg.add(var.set_interrupt_pin(config[CONF_INTERRUPT_PIN]))
if CONF_RESET_PIN in config:
cg.add(var.set_reset_pin(config[CONF_RESET_PIN]))
cg.add_define("USE_ETHERNET_SPI")
cg.add_library("lwIP_w5500", None)
def _final_validate_rmii_pins(config: ConfigType) -> None:
"""Validate that RMII pins are not used by other components."""
if not CORE.is_esp32:
@@ -611,6 +637,7 @@ _platform_filter = filter_source_files_from_platform(
PlatformFramework.ESP32_IDF,
PlatformFramework.ESP32_ARDUINO,
},
"ethernet_component_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
"esp_eth_phy_jl1101.c": {
PlatformFramework.ESP32_IDF,
PlatformFramework.ESP32_ARDUINO,
@@ -22,6 +22,10 @@ extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void);
#endif
#endif // USE_ESP32
#ifdef USE_RP2040
#include <W5500lwIP.h>
#endif
namespace esphome::ethernet {
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
@@ -81,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;
@@ -135,6 +139,15 @@ class EthernetComponent : public Component {
#endif // USE_ETHERNET_SPI
#endif // USE_ESP32
#ifdef USE_RP2040
void set_clk_pin(uint8_t clk_pin);
void set_miso_pin(uint8_t miso_pin);
void set_mosi_pin(uint8_t mosi_pin);
void set_cs_pin(uint8_t cs_pin);
void set_interrupt_pin(int8_t interrupt_pin);
void set_reset_pin(int8_t reset_pin);
#endif // USE_RP2040
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void add_ip_state_listener(EthernetIPStateListener *listener) { this->ip_state_listeners_.push_back(listener); }
#endif
@@ -200,6 +213,18 @@ class EthernetComponent : public Component {
esp_eth_phy_t *phy_{nullptr};
#endif // USE_ESP32
#ifdef USE_RP2040
static constexpr uint32_t LINK_CHECK_INTERVAL = 500; // ms between link/IP polls
Wiznet5500lwIP *eth_{nullptr};
uint32_t last_link_check_{0};
uint8_t clk_pin_;
uint8_t miso_pin_;
uint8_t mosi_pin_;
uint8_t cs_pin_;
int8_t interrupt_pin_{-1};
int8_t reset_pin_{-1};
#endif // USE_RP2040
// Common members
#ifdef USE_ETHERNET_MANUAL_IP
optional<ManualIP> manual_ip_{};
@@ -0,0 +1,315 @@
#include "ethernet_component.h"
#if defined(USE_ETHERNET) && defined(USE_RP2040)
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/components/rp2040/gpio.h"
#include <SPI.h>
#include <lwip/dns.h>
#include <lwip/netif.h>
namespace esphome::ethernet {
static const char *const TAG = "ethernet";
void EthernetComponent::setup() {
// Configure SPI pins
SPI.setRX(this->miso_pin_);
SPI.setTX(this->mosi_pin_);
SPI.setSCK(this->clk_pin_);
// Toggle reset pin if configured
if (this->reset_pin_ >= 0) {
rp2040::RP2040GPIOPin reset_pin;
reset_pin.set_pin(this->reset_pin_);
reset_pin.set_flags(gpio::FLAG_OUTPUT);
reset_pin.setup();
reset_pin.digital_write(false);
delay(1); // NOLINT
reset_pin.digital_write(true);
delay(10); // NOLINT - wait for W5500 to initialize after reset
}
// Create the W5500 device instance
this->eth_ = new Wiznet5500lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
// Set hostname before begin() so the LWIP netif gets it
this->eth_->hostname(App.get_name().c_str());
// Configure static IP if set (must be done before begin())
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
IPAddress ip(this->manual_ip_->static_ip);
IPAddress gateway(this->manual_ip_->gateway);
IPAddress subnet(this->manual_ip_->subnet);
IPAddress dns1(this->manual_ip_->dns1);
IPAddress dns2(this->manual_ip_->dns2);
this->eth_->config(ip, gateway, subnet, dns1, dns2);
}
#endif
// Begin with fixed MAC or auto-generated
bool success;
if (this->fixed_mac_.has_value()) {
success = this->eth_->begin(this->fixed_mac_->data());
} else {
success = this->eth_->begin();
}
if (!success) {
ESP_LOGE(TAG, "Failed to initialize W5500 Ethernet");
delete this->eth_; // NOLINT(cppcoreguidelines-owning-memory)
this->eth_ = nullptr;
this->mark_failed();
return;
}
// Make this the default interface for routing
this->eth_->setDefault(true);
// The arduino-pico LwipIntfDev automatically handles packet processing
// via __addEthernetPacketHandler when no interrupt pin is used,
// or via GPIO interrupt when one is provided.
// Don't set started_ here — let the link polling in loop() set it
// when the W5500 link is actually up. Setting it prematurely causes
// a "Starting → Stopped → Starting" log sequence because the W5500
// needs time after begin() before the PHY link is ready.
}
void EthernetComponent::loop() {
// On RP2040, we need to poll connection state since there are no events.
const uint32_t now = App.get_loop_component_start_time();
// Throttle link/IP polling to avoid excessive SPI transactions from linkStatus()
// which reads the W5500 PHY register via SPI on every call.
// connected() reads netif->ip_addr without LwIPLock, but this is a single
// 32-bit aligned read (atomic on ARM) — worst case is a one-iteration-stale
// value, which is benign for polling.
if (this->eth_ != nullptr && now - this->last_link_check_ >= LINK_CHECK_INTERVAL) {
this->last_link_check_ = now;
bool link_up = this->eth_->linkStatus() == LinkON;
bool has_ip = this->eth_->connected();
if (!link_up) {
if (this->started_) {
this->started_ = false;
this->connected_ = false;
}
} else {
if (!this->started_) {
this->started_ = true;
}
bool was_connected = this->connected_;
this->connected_ = has_ip;
if (this->connected_ && !was_connected) {
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
this->notify_ip_state_listeners_();
#endif
}
}
}
// State machine
switch (this->state_) {
case EthernetComponentState::STOPPED:
if (this->started_) {
ESP_LOGI(TAG, "Starting connection");
this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_();
}
break;
case EthernetComponentState::CONNECTING:
if (!this->started_) {
ESP_LOGI(TAG, "Stopped connection");
this->state_ = EthernetComponentState::STOPPED;
} else if (this->connected_) {
// connection established
ESP_LOGI(TAG, "Connected");
this->state_ = EthernetComponentState::CONNECTED;
this->dump_connect_params_();
this->status_clear_warning();
#ifdef USE_ETHERNET_CONNECT_TRIGGER
this->connect_trigger_.trigger();
#endif
} else if (now - this->connect_begin_ > 15000) {
ESP_LOGW(TAG, "Connecting failed; reconnecting");
this->start_connect_();
}
break;
case EthernetComponentState::CONNECTED:
if (!this->started_) {
ESP_LOGI(TAG, "Stopped connection");
this->state_ = EthernetComponentState::STOPPED;
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
this->disconnect_trigger_.trigger();
#endif
} else if (!this->connected_) {
ESP_LOGW(TAG, "Connection lost; reconnecting");
this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_();
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
this->disconnect_trigger_.trigger();
#endif
} else {
this->finish_connect_();
}
break;
}
}
void EthernetComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"Ethernet:\n"
" Type: W5500\n"
" Connected: %s\n"
" CLK Pin: %u\n"
" MISO Pin: %u\n"
" MOSI Pin: %u\n"
" CS Pin: %u\n"
" IRQ Pin: %d\n"
" Reset Pin: %d",
YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_,
this->interrupt_pin_, this->reset_pin_);
this->dump_connect_params_();
}
network::IPAddresses EthernetComponent::get_ip_addresses() {
network::IPAddresses addresses;
if (this->eth_ != nullptr) {
LwIPLock lock;
addresses[0] = network::IPAddress(this->eth_->localIP());
}
return addresses;
}
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
LwIPLock lock;
const ip_addr_t *dns_ip = dns_getserver(num);
return dns_ip;
}
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
if (this->eth_ != nullptr) {
this->eth_->macAddress(mac);
} else {
memset(mac, 0, 6);
}
}
std::string EthernetComponent::get_eth_mac_address_pretty() {
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
return std::string(this->get_eth_mac_address_pretty_into_buffer(buf));
}
const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
uint8_t mac[6];
get_eth_mac_address_raw(mac);
format_mac_addr_upper(mac, buf.data());
return buf.data();
}
eth_duplex_t EthernetComponent::get_duplex_mode() {
// W5500 is always full duplex
return ETH_DUPLEX_FULL;
}
eth_speed_t EthernetComponent::get_link_speed() {
// W5500 is always 100Mbps
return ETH_SPEED_100M;
}
bool EthernetComponent::powerdown() {
ESP_LOGI(TAG, "Powering down ethernet");
if (this->eth_ != nullptr) {
this->eth_->end();
}
this->connected_ = false;
this->started_ = false;
return true;
}
void EthernetComponent::start_connect_() {
this->got_ipv4_address_ = false;
this->connect_begin_ = millis();
this->status_set_warning(LOG_STR("waiting for IP configuration"));
// Hostname is already set in setup() via LwipIntf::setHostname()
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
// Static IP was already configured before begin() in setup()
// Set DNS servers
LwIPLock lock;
if (this->manual_ip_->dns1.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns1;
dns_setserver(0, &d);
}
if (this->manual_ip_->dns2.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns2;
dns_setserver(1, &d);
}
}
#endif
}
void EthernetComponent::finish_connect_() {
// No additional work needed on RP2040 for now
// IPv6 link-local could be added here in the future
}
void EthernetComponent::dump_connect_params_() {
if (this->eth_ == nullptr) {
return;
}
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
// Copy all lwIP state under the lock to avoid races with IRQ callbacks
ip_addr_t ip_addr, netmask, gw, dns1_addr, dns2_addr;
{
LwIPLock lock;
auto *netif = this->eth_->getNetIf();
ip_addr = netif->ip_addr;
netmask = netif->netmask;
gw = netif->gw;
dns1_addr = *dns_getserver(0);
dns2_addr = *dns_getserver(1);
}
ESP_LOGCONFIG(TAG,
" IP Address: %s\n"
" Hostname: '%s'\n"
" Subnet: %s\n"
" Gateway: %s\n"
" DNS1: %s\n"
" DNS2: %s\n"
" MAC Address: %s",
network::IPAddress(&ip_addr).str_to(ip_buf), App.get_name().c_str(),
network::IPAddress(&netmask).str_to(subnet_buf), network::IPAddress(&gw).str_to(gateway_buf),
network::IPAddress(&dns1_addr).str_to(dns1_buf), network::IPAddress(&dns2_addr).str_to(dns2_buf),
this->get_eth_mac_address_pretty_into_buffer(mac_buf));
}
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
void EthernetComponent::set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
void EthernetComponent::set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; }
} // namespace esphome::ethernet
#endif // USE_ETHERNET && USE_RP2040
@@ -131,7 +131,7 @@ uint8_t IRAM_ATTR GPIOOneWireBus::read8() {
uint64_t IRAM_ATTR GPIOOneWireBus::read64() {
InterruptLock lock;
uint64_t ret = 0;
for (uint8_t i = 0; i < 8; i++) {
for (uint8_t i = 0; i < 64; i++) {
ret |= (uint64_t(this->read_bit_()) << i);
}
return ret;
+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();
+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;
-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
@@ -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.2",
ref="0.3.5",
)
# Set compile-time configuration via build flags (so external library sees them)
+1 -1
View File
@@ -185,7 +185,7 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s
jobs[num_jobs++].command = I2C_MASTER_CMD_STOP;
ESP_LOGV(TAG, "Sending %zu jobs", num_jobs);
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 100);
if (err == ESP_ERR_INVALID_STATE) {
if (err == ESP_ERR_INVALID_STATE || err == ESP_ERR_INVALID_RESPONSE) {
ESP_LOGV(TAG, "TX to %02X failed: not acked", address);
return ERROR_NOT_ACKNOWLEDGED;
} else if (err == ESP_ERR_TIMEOUT) {
+80 -77
View File
@@ -1,3 +1,5 @@
from dataclasses import dataclass, field
from esphome import pins
import esphome.codegen as cg
from esphome.components.esp32 import (
@@ -26,6 +28,9 @@ CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["esp32"]
MULTI_CONF = True
CONF_PDM = "pdm"
CONF_ADC_TYPE = "adc_type"
CONF_I2S_DOUT_PIN = "i2s_dout_pin"
CONF_I2S_DIN_PIN = "i2s_din_pin"
CONF_I2S_MCLK_PIN = "i2s_mclk_pin"
@@ -48,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_(
@@ -149,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,
*,
@@ -187,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),
}
)
@@ -198,62 +183,82 @@ 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,
},
)
@dataclass
class I2SAudioData:
"""I2S audio component state stored in CORE.data."""
port_map: dict[str, int] = field(default_factory=dict)
def _get_data() -> I2SAudioData:
if CONF_I2S_AUDIO not in CORE.data:
CORE.data[CONF_I2S_AUDIO] = I2SAudioData()
return CORE.data[CONF_I2S_AUDIO]
def _assign_ports() -> None:
"""Assign I2S port numbers, prioritizing instances with microphone children.
Microphones (especially PDM) require port 0 on most ESP32 variants.
This runs once and stores the mapping in CORE.data.
"""
data = _get_data()
if data.port_map:
return
full_config = fv.full_config.get()
i2s_configs = full_config[CONF_I2S_AUDIO]
# Find i2s_audio instances with microphones that require port 0
# (PDM and internal ADC only work on I2S port 0)
port0_parent_id = None
for mic_config in full_config.get("microphone", []):
if CONF_I2S_AUDIO_ID not in mic_config:
continue
if mic_config.get(CONF_PDM) or mic_config.get(CONF_ADC_TYPE) == "internal":
if port0_parent_id is not None:
raise cv.Invalid(
"Only one PDM/ADC microphone is supported (requires I2S port 0)"
)
port0_parent_id = str(mic_config[CONF_I2S_AUDIO_ID])
# Assign ports: port 0 parent first (if any), rest get sequential
next_port = 0
if port0_parent_id is not None:
data.port_map[port0_parent_id] = next_port
next_port += 1
for config in i2s_configs:
config_id = str(config[CONF_ID])
if config_id != port0_parent_id:
data.port_map[config_id] = next_port
next_port += 1
def _final_validate(_):
i2s_audio_configs = fv.full_config.get()[CONF_I2S_AUDIO]
variant = get_esp32_variant()
@@ -263,10 +268,7 @@ def _final_validate(_):
raise cv.Invalid(
f"Only {I2S_PORTS[variant]} I2S audio ports are supported on {variant}"
)
def use_legacy():
return _get_use_legacy_driver()
_assign_ports()
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -276,14 +278,15 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
# Assign I2S port from _final_validate computed mapping
data = _get_data()
if (port := data.port_map.get(str(config[CONF_ID]))) is None:
raise ValueError(f"No I2S port assigned for {config[CONF_ID]}")
cg.add(var.set_port(port))
# 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)
@@ -1,27 +0,0 @@
#include "i2s_audio.h"
#ifdef USE_ESP32
#include "esphome/core/log.h"
namespace esphome {
namespace i2s_audio {
static const char *const TAG = "i2s_audio";
void I2SAudioComponent::setup() {
static i2s_port_t next_port_num = I2S_NUM_0;
if (next_port_num >= SOC_I2S_NUM) {
ESP_LOGE(TAG, "Too many components");
this->mark_failed();
return;
}
this->port_ = next_port_num;
next_port_num = (i2s_port_t) (next_port_num + 1);
}
} // namespace i2s_audio
} // namespace esphome
#endif // USE_ESP32
+8 -40
View File
@@ -5,11 +5,8 @@
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#ifdef USE_I2S_LEGACY
#include <driver/i2s.h>
#else
#include <esp_idf_version.h>
#include <driver/i2s_std.h>
#endif
namespace esphome {
namespace i2s_audio {
@@ -18,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_;
@@ -56,19 +39,6 @@ class I2SAudioOut : public I2SAudioBase {};
class I2SAudioComponent : public Component {
public:
void setup() override;
#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_,
@@ -81,32 +51,30 @@ 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; }
void set_lrclk_pin(int pin) { this->lrclk_pin_ = pin; }
void set_port(int port) { this->port_ = port; }
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
int get_port() const { return this->port_; }
#else
i2s_port_t get_port() const { return static_cast<i2s_port_t>(this->port_); }
#endif
void lock() { this->lock_.lock(); }
bool try_lock() { return this->lock_.try_lock(); }
void unlock() { this->lock_.unlock(); }
i2s_port_t get_port() const { return this->port_; }
protected:
Mutex lock_;
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_;
i2s_port_t port_{};
int port_{};
};
} // namespace i2s_audio
@@ -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,27 +1,27 @@
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,
)
from .. import (
CONF_ADC_TYPE,
CONF_I2S_DIN_PIN,
CONF_LEFT,
CONF_MONO,
CONF_PDM,
CONF_RIGHT,
I2SAudioIn,
i2s_audio_component_schema,
i2s_audio_ns,
register_i2s_audio_component,
use_legacy,
validate_mclk_divisible_by_3,
)
@@ -29,9 +29,7 @@ CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["i2s_audio"]
CONF_ADC_PIN = "adc_pin"
CONF_ADC_TYPE = "adc_type"
CONF_CORRECT_DC_OFFSET = "correct_dc_offset"
CONF_PDM = "pdm"
I2SAudioMicrophone = i2s_audio_ns.class_(
"I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component
@@ -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"
@@ -37,27 +33,6 @@ enum MicrophoneEventGroupBits : uint32_t {
};
void I2SAudioMicrophone::setup() {
#ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_ADC
if (this->adc_) {
if (this->parent_->get_port() != I2S_NUM_0) {
ESP_LOGE(TAG, "Internal ADC only works on I2S0");
this->mark_failed();
return;
}
} else
#endif
#endif
{
if (this->pdm_) {
if (this->parent_->get_port() != I2S_NUM_0) {
ESP_LOGE(TAG, "PDM only works on I2S0");
this->mark_failed();
return;
}
}
}
this->active_listeners_semaphore_ = xSemaphoreCreateCounting(MAX_LISTENERS, MAX_LISTENERS);
if (this->active_listeners_semaphore_ == nullptr) {
ESP_LOGE(TAG, "Creating semaphore failed");
@@ -86,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_;
@@ -101,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
@@ -135,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_,
@@ -286,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
@@ -305,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_);
@@ -336,7 +225,6 @@ void I2SAudioMicrophone::stop_driver_() {
}
this->rx_handle_ = nullptr;
}
#endif
if (this->locked_driver_) {
this->parent_->unlock();
this->locked_driver_ = false;
@@ -433,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()) {
@@ -453,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
+1 -1
View File
@@ -229,7 +229,7 @@ void Inkplate::eink_off_() {
this->oe_pin_->digital_write(false);
this->gmod_pin_->digital_write(false);
GPIO.out &= ~(this->get_data_pin_mask_() | (1 << this->cl_pin_->get_pin()) | (1 << this->le_pin_->get_pin()));
GPIO.out &= ~(this->get_data_pin_mask_() | (1UL << this->cl_pin_->get_pin()) | (1UL << this->le_pin_->get_pin()));
this->ckv_pin_->digital_write(false);
this->sph_pin_->digital_write(false);
this->spv_pin_->digital_write(false);
+10 -10
View File
@@ -152,16 +152,16 @@ class Inkplate : public display::DisplayBuffer, public i2c::I2CDevice {
size_t get_buffer_length_();
int get_data_pin_mask_() {
int data = 0;
data |= (1 << this->display_data_0_pin_->get_pin());
data |= (1 << this->display_data_1_pin_->get_pin());
data |= (1 << this->display_data_2_pin_->get_pin());
data |= (1 << this->display_data_3_pin_->get_pin());
data |= (1 << this->display_data_4_pin_->get_pin());
data |= (1 << this->display_data_5_pin_->get_pin());
data |= (1 << this->display_data_6_pin_->get_pin());
data |= (1 << this->display_data_7_pin_->get_pin());
uint32_t get_data_pin_mask_() {
uint32_t data = 0;
data |= (1UL << this->display_data_0_pin_->get_pin());
data |= (1UL << this->display_data_1_pin_->get_pin());
data |= (1UL << this->display_data_2_pin_->get_pin());
data |= (1UL << this->display_data_3_pin_->get_pin());
data |= (1UL << this->display_data_4_pin_->get_pin());
data |= (1UL << this->display_data_5_pin_->get_pin());
data |= (1UL << this->display_data_6_pin_->get_pin());
data |= (1UL << this->display_data_7_pin_->get_pin());
return data;
}
+15 -33
View File
@@ -455,12 +455,10 @@ void LD2412Component::handle_periodic_data_() {
}
#ifdef USE_NUMBER
std::function<void(void)> set_number_value(number::Number *n, float value) {
void set_number_value(number::Number *n, float value) {
if (n != nullptr && (!n->has_state() || n->state != value)) {
n->state = value;
return [n, value]() { n->publish_state(value); };
n->publish_state(value);
}
return []() {};
}
#endif
@@ -504,6 +502,9 @@ bool LD2412Component::handle_ack_data_() {
break;
case CMD_QUERY_VERSION: {
if (this->buffer_pos_ < 12 + sizeof(this->version_)) {
return false;
}
std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_));
char version_s[20];
ld24xx::format_version_str(this->version_, version_s);
@@ -596,13 +597,8 @@ bool LD2412Component::handle_ack_data_() {
case CMD_QUERY_MOTION_GATE_SENS: {
#ifdef USE_NUMBER
std::vector<std::function<void(void)>> updates;
updates.reserve(this->gate_still_threshold_numbers_.size());
for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) {
updates.push_back(set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[10 + i]));
}
for (auto &update : updates) {
update();
for (size_t i = 0; i < this->gate_move_threshold_numbers_.size() && (10 + i) < this->buffer_pos_; i++) {
set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[10 + i]);
}
#endif
break;
@@ -610,13 +606,8 @@ bool LD2412Component::handle_ack_data_() {
case CMD_QUERY_STATIC_GATE_SENS: {
#ifdef USE_NUMBER
std::vector<std::function<void(void)>> updates;
updates.reserve(this->gate_still_threshold_numbers_.size());
for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) {
updates.push_back(set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[10 + i]));
}
for (auto &update : updates) {
update();
for (size_t i = 0; i < this->gate_still_threshold_numbers_.size() && (10 + i) < this->buffer_pos_; i++) {
set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[10 + i]);
}
#endif
break;
@@ -625,20 +616,21 @@ bool LD2412Component::handle_ack_data_() {
case CMD_QUERY_BASIC_CONF: // Query parameters response
{
#ifdef USE_NUMBER
if (this->buffer_pos_ < 15) {
return false;
}
/*
Moving distance range: 9th byte
Still distance range: 10th byte
*/
std::vector<std::function<void(void)>> updates;
updates.push_back(set_number_value(this->min_distance_gate_number_, this->buffer_data_[10]));
updates.push_back(set_number_value(this->max_distance_gate_number_, this->buffer_data_[11] - 1));
set_number_value(this->min_distance_gate_number_, this->buffer_data_[10]);
set_number_value(this->max_distance_gate_number_, this->buffer_data_[11] - 1);
ESP_LOGV(TAG, "min_distance_gate_number_: %u, max_distance_gate_number_ %u", this->buffer_data_[10],
this->buffer_data_[11]);
/*
None Duration: 11~12th bytes
*/
updates.push_back(
set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[13], this->buffer_data_[12])));
set_number_value(this->timeout_number_, encode_uint16(this->buffer_data_[13], this->buffer_data_[12]));
ESP_LOGV(TAG, "timeout_number_: %u", encode_uint16(this->buffer_data_[13], this->buffer_data_[12]));
/*
Output pin configuration: 13th bytes
@@ -650,9 +642,6 @@ bool LD2412Component::handle_ack_data_() {
this->out_pin_level_select_->publish_state(out_pin_level_str);
}
#endif
for (auto &update : updates) {
update();
}
#endif
} break;
default:
@@ -836,13 +825,6 @@ void LD2412Component::get_gate_threshold() {
this->send_command_(CMD_QUERY_STATIC_GATE_SENS, nullptr, 0);
}
void LD2412Component::set_gate_still_threshold_number(uint8_t gate, number::Number *n) {
this->gate_still_threshold_numbers_[gate] = n;
}
void LD2412Component::set_gate_move_threshold_number(uint8_t gate, number::Number *n) {
this->gate_move_threshold_numbers_[gate] = n;
}
#endif
void LD2412Component::set_light_out_control() {
+4 -2
View File
@@ -85,8 +85,10 @@ class LD2412Component : public Component, public uart::UARTDevice {
void set_light_out_control();
void set_basic_config();
#ifdef USE_NUMBER
void set_gate_move_threshold_number(uint8_t gate, number::Number *n);
void set_gate_still_threshold_number(uint8_t gate, number::Number *n);
void set_gate_move_threshold_number(uint8_t gate, number::Number *n) { this->gate_move_threshold_numbers_[gate] = n; }
void set_gate_still_threshold_number(uint8_t gate, number::Number *n) {
this->gate_still_threshold_numbers_[gate] = n;
}
void set_gate_threshold();
void get_gate_threshold();
#endif
+12 -8
View File
@@ -170,14 +170,18 @@ static uint8_t calc_checksum(void *data, size_t size) {
return checksum;
}
static int get_firmware_int(const char *version_string) {
std::string version_str = version_string;
if (version_str[0] == 'v') {
version_str.erase(0, 1);
static int32_t get_firmware_int(const char *version_string) {
// Convert "v1.5.4" -> 154 by skipping 'v' and '.', accumulating digits
const char *p = (*version_string == 'v') ? version_string + 1 : version_string;
int32_t result = 0;
for (; *p != '\0'; p++) {
if (*p == '.')
continue;
if (*p < '0' || *p > '9')
return 0;
result = result * 10 + (*p - '0');
}
version_str.erase(remove(version_str.begin(), version_str.end(), '.'), version_str.end());
int version_integer = stoi(version_str);
return version_integer;
return result;
}
float LD2420Component::get_setup_priority() const { return setup_priority::BUS; }
@@ -683,7 +687,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) {
retry = 0;
}
if (this->cmd_reply_.error > 0) {
this->handle_cmd_error(error);
this->handle_cmd_error(this->cmd_reply_.error);
}
}
return error;
-3
View File
@@ -54,9 +54,6 @@ void arch_restart() {
void HOT arch_feed_wdt() { lt_wdt_feed(); }
uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); }
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; }
} // namespace esphome
@@ -0,0 +1,32 @@
#pragma once
#ifdef USE_LIBRETINY
#include <cstddef>
#include <cstdint>
// Forward declare FlashDB types to avoid pulling in flashdb.h
struct fdb_kvdb;
struct fdb_blob;
namespace esphome::libretiny {
class LibreTinyPreferenceBackend final {
public:
bool save(const uint8_t *data, size_t len);
bool load(uint8_t *data, size_t len);
uint32_t key;
struct fdb_kvdb *db;
struct fdb_blob *blob;
};
class LibreTinyPreferences;
LibreTinyPreferences *get_preferences();
} // namespace esphome::libretiny
namespace esphome {
using PreferenceBackend = libretiny::LibreTinyPreferenceBackend;
} // namespace esphome
#endif // USE_LIBRETINY
+125 -144
View File
@@ -1,12 +1,11 @@
#ifdef USE_LIBRETINY
#include "preferences.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
#include <flashdb.h>
#include <cinttypes>
#include <cstring>
#include <memory>
#include <vector>
namespace esphome::libretiny {
@@ -22,163 +21,147 @@ struct NVSData {
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
public:
uint32_t key;
fdb_kvdb_t db;
fdb_blob_t blob;
bool LibreTinyPreferenceBackend::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 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 LibreTinyPreferenceBackend::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;
}
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;
}
}
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key);
fdb_blob_make(this->blob, data, len);
size_t actual_len = fdb_kv_get_blob(this->db, key_str, this->blob);
if (actual_len != len) {
ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len);
return false;
} else {
ESP_LOGVV(TAG, "fdb_kv_get_blob: key: %s, len: %zu", key_str, len);
}
return true;
}
void LibreTinyPreferences::open() {
//
fdb_err_t err = fdb_kvdb_init(&this->db, "esphome", "kvs", NULL, NULL);
if (err != FDB_NO_ERR) {
LT_E("fdb_kvdb_init(...) failed: %d", err);
} else {
LT_I("Preferences initialized");
}
}
ESPPreferenceObject LibreTinyPreferences::make_preference(size_t length, uint32_t type) {
auto *pref = new LibreTinyPreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->db = &this->db;
pref->blob = &this->blob;
pref->key = type;
return ESPPreferenceObject(pref);
}
bool LibreTinyPreferences::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;
fdb_err_t last_err = FDB_NO_ERR;
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);
fdb_blob_make(this->blob, data, len);
size_t actual_len = fdb_kv_get_blob(this->db, key_str, this->blob);
if (actual_len != len) {
ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len);
return false;
} else {
ESP_LOGVV(TAG, "fdb_kv_get_blob: key: %s, len: %zu", key_str, len);
}
return true;
}
};
class LibreTinyPreferences : public ESPPreferences {
public:
struct fdb_kvdb db;
struct fdb_blob blob;
void open() {
//
fdb_err_t err = fdb_kvdb_init(&db, "esphome", "kvs", NULL, NULL);
if (err != FDB_NO_ERR) {
LT_E("fdb_kvdb_init(...) failed: %d", err);
} else {
LT_I("Preferences initialized");
}
}
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 LibreTinyPreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->db = &this->db;
pref->blob = &this->blob;
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;
fdb_err_t last_err = FDB_NO_ERR;
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 FDB data %s has changed", key_str);
if (this->is_changed_(&this->db, save, key_str)) {
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
fdb_blob_make(&this->blob, save.data.data(), save.data.size());
fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob);
if (err != FDB_NO_ERR) {
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.data.size(), err);
failed++;
last_err = err;
last_key = save.key;
continue;
}
written++;
} else {
ESP_LOGD(TAG, "FDB 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 FDB data %s has changed", key_str);
if (this->is_changed_(&this->db, save, key_str)) {
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
fdb_blob_make(&this->blob, save.data.data(), save.data.size());
fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob);
if (err != FDB_NO_ERR) {
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.data.size(), err);
failed++;
last_err = err;
last_key = save.key;
continue;
}
written++;
} else {
ESP_LOGD(TAG, "FDB 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=%d for key=%" PRIu32, failed, last_err, last_key);
}
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=%d for key=%" PRIu32, failed, last_err, last_key);
}
protected:
bool is_changed_(fdb_kvdb_t db, const NVSData &to_save, const char *key_str) {
struct fdb_kv kv;
fdb_kv_t kvp = fdb_kv_get_obj(db, key_str, &kv);
if (kvp == nullptr) {
ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", key_str);
return true;
}
return failed == 0;
}
// Check size first - if different, data has changed
if (kv.value_len != to_save.data.size()) {
return true;
}
// Most preferences are small, use stack buffer with heap fallback for large ones
SmallBufferWithHeapFallback<256> stored_data(kv.value_len);
fdb_blob_make(&this->blob, stored_data.get(), kv.value_len);
size_t actual_len = fdb_kv_get_blob(db, key_str, &this->blob);
if (actual_len != kv.value_len) {
ESP_LOGV(TAG, "fdb_kv_get_blob('%s') len mismatch: %u != %u", key_str, actual_len, kv.value_len);
return true;
}
// Compare the actual data
return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0;
}
bool reset() override {
ESP_LOGD(TAG, "Erasing storage");
s_pending_save.clear();
fdb_kv_set_default(&db);
fdb_kvdb_deinit(&db);
bool LibreTinyPreferences::is_changed_(fdb_kvdb_t db, const NVSData &to_save, const char *key_str) {
struct fdb_kv kv;
fdb_kv_t kvp = fdb_kv_get_obj(db, key_str, &kv);
if (kvp == nullptr) {
ESP_LOGV(TAG, "fdb_kv_get_obj('%s'): nullptr - the key might not be set yet", key_str);
return true;
}
};
// Check size first - if different, data has changed
if (kv.value_len != to_save.data.size()) {
return true;
}
// Most preferences are small, use stack buffer with heap fallback for large ones
SmallBufferWithHeapFallback<256> stored_data(kv.value_len);
fdb_blob_make(&this->blob, stored_data.get(), kv.value_len);
size_t actual_len = fdb_kv_get_blob(db, key_str, &this->blob);
if (actual_len != kv.value_len) {
ESP_LOGV(TAG, "fdb_kv_get_blob('%s') len mismatch: %zu != %zu", key_str, actual_len, (size_t) kv.value_len);
return true;
}
// Compare the actual data
return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0;
}
bool LibreTinyPreferences::reset() {
ESP_LOGD(TAG, "Erasing storage");
s_pending_save.clear();
fdb_kv_set_default(&this->db);
fdb_kvdb_deinit(&this->db);
return true;
}
static LibreTinyPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
LibreTinyPreferences *get_preferences() { return &s_preferences; }
void setup_preferences() {
s_preferences.open();
global_preferences = &s_preferences;
@@ -187,9 +170,7 @@ void setup_preferences() {
} // namespace esphome::libretiny
namespace esphome {
ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome
#endif // USE_LIBRETINY
+25 -1
View File
@@ -1,11 +1,35 @@
#pragma once
#ifdef USE_LIBRETINY
#include "esphome/core/preference_backend.h"
#include <flashdb.h>
namespace esphome::libretiny {
struct NVSData;
class LibreTinyPreferences final : public PreferencesMixin<LibreTinyPreferences> {
public:
using PreferencesMixin<LibreTinyPreferences>::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();
struct fdb_kvdb db;
struct fdb_blob blob;
protected:
bool is_changed_(fdb_kvdb_t db, const NVSData &to_save, const char *key_str);
};
void setup_preferences();
} // namespace esphome::libretiny
DECLARE_PREFERENCE_ALIASES(esphome::libretiny::LibreTinyPreferences)
#endif // USE_LIBRETINY
+1 -1
View File
@@ -392,7 +392,7 @@ async def addressable_lambda_effect_to_code(config, effect_id):
"Rainbow",
{
cv.Optional(CONF_SPEED, default=10): cv.uint32_t,
cv.Optional(CONF_WIDTH, default=50): cv.uint32_t,
cv.Optional(CONF_WIDTH, default=50): cv.int_range(min=1, max=65535),
},
)
async def addressable_rainbow_effect_to_code(config, effect_id):
@@ -42,7 +42,7 @@ void LilygoT547Touchscreen::setup() {
this->x_raw_max_ = this->display_->get_native_width();
}
if (this->y_raw_max_ == this->y_raw_min_) {
this->x_raw_max_ = this->display_->get_native_height();
this->y_raw_max_ = this->display_->get_native_height();
}
}
}
@@ -64,6 +64,10 @@ void LilygoT547Touchscreen::update_touches() {
}
point = buffer[5] & 0xF;
if (point > 2) {
ESP_LOGW(TAG, "Invalid touch point count: %d", point);
point = 2;
}
if (point == 1) {
err = this->write_register(TOUCH_REGISTER, READ_TOUCH, 1);
@@ -153,6 +153,31 @@ class MediaSource {
}
}
// === Listener request helpers for smart sources ===
// Smart sources (those with internal playlists or external control) use these to request
// the orchestrator to take actions. Simple sources never call these.
/// @brief Request the orchestrator to play a new URI
void request_play_uri_(const std::string &uri) {
if (this->listener_ != nullptr) {
this->listener_->request_play_uri(uri);
}
}
/// @brief Request the orchestrator to change volume
void request_volume_(float volume) {
if (this->listener_ != nullptr) {
this->listener_->request_volume(volume);
}
}
/// @brief Request the orchestrator to change mute state
void request_mute_(bool is_muted) {
if (this->listener_ != nullptr) {
this->listener_->request_mute(is_muted);
}
}
private:
// Private to enforce the invariant that listener notifications always fire on state changes.
// All state transitions must go through set_state_() which couples the update with notification.
@@ -80,6 +80,7 @@ bool StreamingModel::load_model_() {
TfLiteTensor *output = this->interpreter_->output(0);
if ((output->dims->size != 2) || (output->dims->data[0] != 1) || (output->dims->data[1] != 1)) {
ESP_LOGE(TAG, "Streaming model tensor output dimension is not 1x1.");
return false;
}
if (output->type != kTfLiteUInt8) {
+12 -6
View File
@@ -194,8 +194,12 @@ def model_schema(config):
CONF_DE_PIN, cv.UNDEFINED
): pins.internal_gpio_output_pin_schema,
model.option(CONF_PCLK_PIN): pins.internal_gpio_output_pin_schema,
model.option(CONF_HSYNC_PIN): pins.internal_gpio_output_pin_schema,
model.option(CONF_VSYNC_PIN): pins.internal_gpio_output_pin_schema,
model.option(
CONF_HSYNC_PIN, cv.UNDEFINED
): pins.internal_gpio_output_pin_schema,
model.option(
CONF_VSYNC_PIN, cv.UNDEFINED
): pins.internal_gpio_output_pin_schema,
model.option(CONF_RESET_PIN, cv.UNDEFINED): pins.gpio_output_pin_schema,
}
)
@@ -307,10 +311,12 @@ async def to_code(config):
cg.add(var.set_de_pin(pin))
pin = await cg.gpio_pin_expression(config[CONF_PCLK_PIN])
cg.add(var.set_pclk_pin(pin))
pin = await cg.gpio_pin_expression(config[CONF_HSYNC_PIN])
cg.add(var.set_hsync_pin(pin))
pin = await cg.gpio_pin_expression(config[CONF_VSYNC_PIN])
cg.add(var.set_vsync_pin(pin))
if hsync_pin := config.get(CONF_HSYNC_PIN):
pin = await cg.gpio_pin_expression(hsync_pin)
cg.add(var.set_hsync_pin(pin))
if vsync_pin := config.get(CONF_VSYNC_PIN):
pin = await cg.gpio_pin_expression(vsync_pin)
cg.add(var.set_vsync_pin(pin))
await display.register_display(var, config)
if lamb := config.get(CONF_LAMBDA):
+10 -2
View File
@@ -158,8 +158,16 @@ void MipiRgb::common_setup_() {
}
config.data_width = data_pin_count;
config.disp_gpio_num = GPIO_NUM_NC;
config.hsync_gpio_num = static_cast<gpio_num_t>(this->hsync_pin_->get_pin());
config.vsync_gpio_num = static_cast<gpio_num_t>(this->vsync_pin_->get_pin());
if (this->hsync_pin_) {
config.hsync_gpio_num = static_cast<gpio_num_t>(this->hsync_pin_->get_pin());
} else {
config.hsync_gpio_num = GPIO_NUM_NC;
}
if (this->vsync_pin_) {
config.vsync_gpio_num = static_cast<gpio_num_t>(this->vsync_pin_->get_pin());
} else {
config.vsync_gpio_num = GPIO_NUM_NC;
}
if (this->de_pin_) {
config.de_gpio_num = static_cast<gpio_num_t>(this->de_pin_->get_pin());
} else {
@@ -178,7 +178,7 @@ template<typename N> N mask_and_shift_by_rightbit(N data, uint32_t mask) {
return result;
}
for (size_t pos = 0; pos < sizeof(N) << 3; pos++) {
if ((mask & (1 << pos)) != 0)
if ((mask & (1UL << pos)) != 0)
return result >> pos;
}
return 0;
+24 -10
View File
@@ -82,10 +82,16 @@ bool MQTTBackendESP32::initialize_() {
void MQTTBackendESP32::loop() {
// process new events
// handle only 1 message per loop iteration
if (!mqtt_events_.empty()) {
auto &event = mqtt_events_.front();
mqtt_event_handler_(event);
mqtt_events_.pop();
Event *event = this->mqtt_event_queue_.pop();
if (event != nullptr) {
this->mqtt_event_handler_(*event);
this->mqtt_event_pool_.release(event);
}
// Log dropped inbound events (check is cheap - single atomic load in common case)
uint16_t inbound_dropped = this->mqtt_event_queue_.get_and_reset_dropped_count();
if (inbound_dropped > 0) {
ESP_LOGW(TAG, "Dropped %u inbound MQTT events", inbound_dropped);
}
#if defined(USE_MQTT_IDF_ENQUEUE)
@@ -183,10 +189,18 @@ void MQTTBackendESP32::mqtt_event_handler_(const Event &event) {
void MQTTBackendESP32::mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id,
void *event_data) {
MQTTBackendESP32 *instance = static_cast<MQTTBackendESP32 *>(handler_args);
// queue event to decouple processing
// queue event to decouple processing from ESP-IDF MQTT task to main loop
if (instance) {
auto event = *static_cast<esp_mqtt_event_t *>(event_data);
instance->mqtt_events_.emplace(event);
auto *event = instance->mqtt_event_pool_.allocate();
if (event == nullptr) {
// Pool exhausted, drop event (counted via queue's dropped counter)
instance->mqtt_event_queue_.increment_dropped_count();
return;
}
event->populate(*static_cast<esp_mqtt_event_t *>(event_data));
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
// allocate() returned non-null, the queue cannot be full.
instance->mqtt_event_queue_.push(event);
// Wake main loop immediately to process MQTT event instead of waiting for select() timeout
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
@@ -226,14 +240,14 @@ void MQTTBackendESP32::esphome_mqtt_task(void *params) {
break;
}
}
this_mqtt->mqtt_event_pool_.release(elem);
this_mqtt->mqtt_outbound_pool_.release(elem);
}
}
}
bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos, bool retain, const char *payload,
size_t len) {
auto *elem = this->mqtt_event_pool_.allocate();
auto *elem = this->mqtt_outbound_pool_.allocate();
if (!elem) {
// Queue is full - increment counter but don't log immediately.
@@ -253,7 +267,7 @@ bool MQTTBackendESP32::enqueue_(MqttQueueTypeT type, const char *topic, int qos,
// Use the helper to allocate and copy data
if (!elem->set_data(topic, payload, len)) {
// Allocation failed, return elem to pool
this->mqtt_event_pool_.release(elem);
this->mqtt_outbound_pool_.release(elem);
// Increment counter without logging to avoid cascade effect during memory pressure
this->mqtt_queue_.increment_dropped_count();
return false;
+42 -27
View File
@@ -5,7 +5,6 @@
#ifdef USE_ESP32
#include <string>
#include <queue>
#include <cstring>
#include <mqtt_client.h>
#include <freertos/FreeRTOS.h>
@@ -18,32 +17,39 @@
namespace esphome::mqtt {
struct Event {
esp_mqtt_event_id_t event_id;
esp_mqtt_event_id_t event_id{};
std::vector<char> data;
int total_data_len;
int current_data_offset;
int total_data_len{0};
int current_data_offset{0};
std::string topic;
int msg_id;
bool retain;
int qos;
bool dup;
bool session_present;
esp_mqtt_error_codes_t error_handle;
int msg_id{0};
bool retain{false};
int qos{0};
bool dup{false};
bool session_present{false};
esp_mqtt_error_codes_t error_handle{};
// Construct from esp_mqtt_event_t
// Any pointer values that are unsafe to keep are converted to safe copies
Event(const esp_mqtt_event_t &event)
: event_id(event.event_id),
data(event.data, event.data + event.data_len),
total_data_len(event.total_data_len),
current_data_offset(event.current_data_offset),
topic(event.topic, event.topic_len),
msg_id(event.msg_id),
retain(event.retain),
qos(event.qos),
dup(event.dup),
session_present(event.session_present),
error_handle(*event.error_handle) {}
// Populate from esp_mqtt_event_t
// Copies pointer-based data to owned storage for safe cross-thread transfer
void populate(const esp_mqtt_event_t &event) {
this->event_id = event.event_id;
this->data.assign(event.data, event.data + event.data_len);
this->total_data_len = event.total_data_len;
this->current_data_offset = event.current_data_offset;
this->topic.assign(event.topic, event.topic_len);
this->msg_id = event.msg_id;
this->retain = event.retain;
this->qos = event.qos;
this->dup = event.dup;
this->session_present = event.session_present;
this->error_handle = *event.error_handle;
}
// Release owned resources for pool reuse (keeps allocated capacity for efficiency)
void release() {
this->data.clear();
this->topic.clear();
}
};
enum MqttQueueTypeT : uint8_t {
@@ -118,7 +124,8 @@ class MQTTBackendESP32 final : public MQTTBackend {
static constexpr size_t TASK_STACK_SIZE = 3072;
static constexpr size_t TASK_STACK_SIZE_TLS = 4096; // Larger stack for TLS operations
static constexpr ssize_t TASK_PRIORITY = 5;
static constexpr uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360
static constexpr uint8_t MQTT_QUEUE_LENGTH = 30; // 30*12 bytes = 360
static constexpr uint8_t MQTT_EVENT_QUEUE_LENGTH = 32; // Inbound events from broker
void set_keep_alive(uint16_t keep_alive) final { this->keep_alive_ = keep_alive; }
void set_client_id(const char *client_id) final { this->client_id_ = client_id; }
@@ -251,7 +258,8 @@ class MQTTBackendESP32 final : public MQTTBackend {
bool skip_cert_cn_check_{false};
#if defined(USE_MQTT_IDF_ENQUEUE)
static void esphome_mqtt_task(void *params);
EventPool<struct QueueElement, MQTT_QUEUE_LENGTH> mqtt_event_pool_;
// Pool sized to queue capacity (SIZE-1) — see mqtt_event_pool_ comment.
EventPool<struct QueueElement, MQTT_QUEUE_LENGTH - 1> mqtt_outbound_pool_;
NotifyingLockFreeQueue<struct QueueElement, MQTT_QUEUE_LENGTH> mqtt_queue_;
TaskHandle_t task_handle_{nullptr};
bool enqueue_(MqttQueueTypeT type, const char *topic, int qos = 0, bool retain = false, const char *payload = NULL,
@@ -266,7 +274,14 @@ class MQTTBackendESP32 final : public MQTTBackend {
CallbackManager<on_message_callback_t> on_message_;
CallbackManager<on_publish_user_callback_t> on_publish_;
std::string cached_topic_;
std::queue<Event> mqtt_events_;
// 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
EventPool<Event, MQTT_EVENT_QUEUE_LENGTH - 1> mqtt_event_pool_;
LockFreeQueue<Event, MQTT_EVENT_QUEUE_LENGTH> mqtt_event_queue_;
#if defined(USE_MQTT_IDF_ENQUEUE)
uint32_t last_dropped_log_time_{0};
+1 -4
View File
@@ -81,10 +81,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: dict[str, Any]) -> None:
if CORE.is_esp32:
# Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time)
# Provides driver/timer.h header for hardware timer API
# TODO: Remove this once opentherm migrates to GPTimer API (driver/gptimer.h)
include_builtin_idf_component("driver")
include_builtin_idf_component("esp_driver_gptimer")
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
+28 -61
View File
@@ -8,10 +8,7 @@
#include "opentherm.h"
#include "esphome/core/helpers.h"
#include <cinttypes>
// TODO: Migrate from legacy timer API (driver/timer.h) to GPTimer API (driver/gptimer.h)
// The legacy timer API is deprecated in ESP-IDF 5.x. See opentherm.h for details.
#ifdef USE_ESP32
#include "driver/timer.h"
#include "esp_err.h"
#endif
#ifdef ESP8266
@@ -33,10 +30,6 @@ OpenTherm *OpenTherm::instance = nullptr;
OpenTherm::OpenTherm(InternalGPIOPin *in_pin, InternalGPIOPin *out_pin, int32_t device_timeout)
: in_pin_(in_pin),
out_pin_(out_pin),
#ifdef USE_ESP32
timer_group_(TIMER_GROUP_0),
timer_idx_(TIMER_0),
#endif
mode_(OperationMode::IDLE),
error_type_(ProtocolErrorType::NO_ERROR),
capture_(0),
@@ -134,7 +127,12 @@ void IRAM_ATTR OpenTherm::read_() {
// period in OpenTherm.
}
#ifdef USE_ESP32
bool IRAM_ATTR OpenTherm::timer_isr(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx) {
auto *arg = static_cast<OpenTherm *>(user_ctx);
#else
bool IRAM_ATTR OpenTherm::timer_isr(OpenTherm *arg) {
#endif
if (arg->mode_ == OperationMode::LISTEN) {
if (arg->timeout_counter_ == 0) {
arg->mode_ = OperationMode::ERROR_TIMEOUT;
@@ -243,67 +241,35 @@ void IRAM_ATTR OpenTherm::write_bit_(uint8_t high, uint8_t clock) {
#ifdef USE_ESP32
bool OpenTherm::init_esp32_timer_() {
// Search for a free timer. Maybe unstable, we'll see.
int cur_timer = 0;
timer_group_t timer_group = TIMER_GROUP_0;
timer_idx_t timer_idx = TIMER_0;
bool timer_found = false;
for (; cur_timer < SOC_TIMER_GROUP_TOTAL_TIMERS; cur_timer++) {
timer_config_t temp_config;
timer_group = cur_timer < 2 ? TIMER_GROUP_0 : TIMER_GROUP_1;
timer_idx = cur_timer < 2 ? (timer_idx_t) cur_timer : (timer_idx_t) (cur_timer - 2);
auto err = timer_get_config(timer_group, timer_idx, &temp_config);
if (err == ESP_ERR_INVALID_ARG) {
// Error means timer was not initialized (or other things, but we are careful with our args)
timer_found = true;
break;
}
ESP_LOGD(TAG, "Timer %d:%d seems to be occupied, will try another", timer_group, timer_idx);
}
if (!timer_found) {
ESP_LOGE(TAG, "No free timer was found! OpenTherm cannot function without a timer.");
return false;
}
ESP_LOGD(TAG, "Found free timer %d:%d", timer_group, timer_idx);
this->timer_group_ = timer_group;
this->timer_idx_ = timer_idx;
timer_config_t const config = {
.alarm_en = TIMER_ALARM_EN,
.counter_en = TIMER_PAUSE,
.intr_type = TIMER_INTR_LEVEL,
.counter_dir = TIMER_COUNT_UP,
.auto_reload = TIMER_AUTORELOAD_EN,
.clk_src = TIMER_SRC_CLK_DEFAULT,
.divider = 80,
// 80MHz / 80 = 1MHz resolution (1µs per tick)
gptimer_config_t config = {
.clk_src = GPTIMER_CLK_SRC_DEFAULT,
.direction = GPTIMER_COUNT_UP,
.resolution_hz = 1000000,
};
esp_err_t result;
result = timer_init(this->timer_group_, this->timer_idx_, &config);
esp_err_t result = gptimer_new_timer(&config, &this->timer_handle_);
if (result != ESP_OK) {
const auto *error = esp_err_to_name(result);
ESP_LOGE(TAG, "Failed to init timer. Error: %s", error);
ESP_LOGE(TAG, "Failed to create timer: %s", esp_err_to_name(result));
return false;
}
result = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0);
gptimer_event_callbacks_t cbs = {
.on_alarm = OpenTherm::timer_isr,
};
result = gptimer_register_event_callbacks(this->timer_handle_, &cbs, this);
if (result != ESP_OK) {
const auto *error = esp_err_to_name(result);
ESP_LOGE(TAG, "Failed to set counter value. Error: %s", error);
ESP_LOGE(TAG, "Failed to register timer callback: %s", esp_err_to_name(result));
gptimer_del_timer(this->timer_handle_);
this->timer_handle_ = nullptr;
return false;
}
result = timer_isr_callback_add(this->timer_group_, this->timer_idx_, reinterpret_cast<bool (*)(void *)>(timer_isr),
this, 0);
result = gptimer_enable(this->timer_handle_);
if (result != ESP_OK) {
const auto *error = esp_err_to_name(result);
ESP_LOGE(TAG, "Failed to register timer interrupt. Error: %s", error);
ESP_LOGE(TAG, "Failed to enable timer: %s", esp_err_to_name(result));
gptimer_del_timer(this->timer_handle_);
this->timer_handle_ = nullptr;
return false;
}
@@ -315,12 +281,13 @@ void IRAM_ATTR OpenTherm::start_esp32_timer_(uint64_t alarm_value) {
this->timer_error_ = ESP_OK;
this->timer_error_type_ = TimerErrorType::NO_TIMER_ERROR;
this->timer_error_ = timer_set_alarm_value(this->timer_group_, this->timer_idx_, alarm_value);
this->alarm_config_.alarm_count = alarm_value;
this->timer_error_ = gptimer_set_alarm_action(this->timer_handle_, &this->alarm_config_);
if (this->timer_error_ != ESP_OK) {
this->timer_error_type_ = TimerErrorType::SET_ALARM_VALUE_ERROR;
return;
}
this->timer_error_ = timer_start(this->timer_group_, this->timer_idx_);
this->timer_error_ = gptimer_start(this->timer_handle_);
if (this->timer_error_ != ESP_OK) {
this->timer_error_type_ = TimerErrorType::TIMER_START_ERROR;
}
@@ -356,12 +323,12 @@ void IRAM_ATTR OpenTherm::stop_timer_() {
this->timer_error_ = ESP_OK;
this->timer_error_type_ = TimerErrorType::NO_TIMER_ERROR;
this->timer_error_ = timer_pause(this->timer_group_, this->timer_idx_);
this->timer_error_ = gptimer_stop(this->timer_handle_);
if (this->timer_error_ != ESP_OK) {
this->timer_error_type_ = TimerErrorType::TIMER_PAUSE_ERROR;
return;
}
this->timer_error_ = timer_set_counter_value(this->timer_group_, this->timer_idx_, 0);
this->timer_error_ = gptimer_set_raw_count(this->timer_handle_, 0);
if (this->timer_error_ != ESP_OK) {
this->timer_error_type_ = TimerErrorType::SET_COUNTER_VALUE_ERROR;
}
+11 -7
View File
@@ -12,12 +12,8 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
// TODO: Migrate from legacy timer API (driver/timer.h) to GPTimer API (driver/gptimer.h)
// The legacy timer API is deprecated in ESP-IDF 5.x. Migration would allow removing the
// "driver" IDF component dependency. See:
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/migration-guides/release-5.x/5.0/peripherals.html#id4
#ifdef USE_ESP32
#include "driver/timer.h"
#include "driver/gptimer.h"
#endif
namespace esphome {
@@ -348,7 +344,11 @@ class OpenTherm {
const char *operation_mode_to_str(OperationMode mode);
const char *message_id_to_str(MessageId id);
#ifdef USE_ESP32
static bool timer_isr(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx);
#else
static bool timer_isr(OpenTherm *arg);
#endif
#ifdef ESP8266
static void esp8266_timer_isr();
@@ -361,8 +361,12 @@ class OpenTherm {
ISRInternalGPIOPin isr_out_pin_;
#ifdef USE_ESP32
timer_group_t timer_group_;
timer_idx_t timer_idx_;
gptimer_handle_t timer_handle_{nullptr};
gptimer_alarm_config_t alarm_config_{
.alarm_count = 0,
.reload_count = 0,
.flags = {.auto_reload_on_alarm = true},
};
#endif
OperationMode mode_;
@@ -11,6 +11,7 @@
#include <openthread/instance.h>
#include <openthread/thread.h>
#include <atomic>
#include <optional>
#include <vector>
@@ -28,6 +29,8 @@ class OpenThreadComponent : public Component {
float get_setup_priority() const override { return setup_priority::WIFI; }
bool is_connected() const { return this->connected_; }
/// Returns true once esp_openthread_init() has completed and the OT lock is usable.
bool is_lock_initialized() const { return this->lock_initialized_; }
network::IPAddresses get_ip_addresses();
std::optional<otIp6Address> get_omr_address();
void ot_main();
@@ -51,6 +54,7 @@ class OpenThreadComponent : public Component {
uint32_t poll_period_{0};
#endif
std::optional<int8_t> output_power_{};
std::atomic<bool> lock_initialized_{false};
bool teardown_started_{false};
bool teardown_complete_{false};
bool connected_{false};
@@ -8,6 +8,7 @@
#include "esp_openthread_lock.h"
#include "esp_task_wdt.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -81,6 +82,9 @@ void OpenThreadComponent::ot_main() {
// Initialize the OpenThread stack
// otLoggingSetLevel(OT_LOG_LEVEL_DEBG);
ESP_ERROR_CHECK(esp_openthread_init(&config));
// Mark lock as initialized so InstanceLock callers know it's safe to acquire.
// Must be set after esp_openthread_init() which creates the internal semaphore.
this->lock_initialized_ = true;
// Fetch OT instance once to avoid repeated call into OT stack
otInstance *instance = esp_openthread_get_instance();
@@ -180,7 +184,8 @@ void OpenThreadComponent::ot_main() {
esp_openthread_launch_mainloop();
// Clean up
// Clean up - reset lock flag before deinit destroys the semaphore
this->lock_initialized_ = false;
esp_openthread_deinit();
esp_openthread_netif_glue_deinit();
esp_netif_destroy(openthread_netif);
@@ -210,6 +215,9 @@ network::IPAddresses OpenThreadComponent::get_ip_addresses() {
otInstance *OpenThreadComponent::get_openthread_instance_() { return esp_openthread_get_instance(); }
std::optional<InstanceLock> InstanceLock::try_acquire(int delay) {
if (!global_openthread_component->is_lock_initialized()) {
return {};
}
if (esp_openthread_lock_acquire(delay)) {
return InstanceLock();
}
@@ -217,6 +225,18 @@ std::optional<InstanceLock> InstanceLock::try_acquire(int delay) {
}
InstanceLock InstanceLock::acquire() {
// Wait for the lock to be created by ot_main() before attempting to acquire it.
// esp_openthread_lock_acquire() will assert-crash if called before esp_openthread_init().
constexpr uint32_t lock_init_timeout_ms = 10000;
uint32_t start = millis();
while (!global_openthread_component->is_lock_initialized()) {
if (millis() - start > lock_init_timeout_ms) {
ESP_LOGE(TAG, "OpenThread lock not initialized after %" PRIu32 "ms, aborting", lock_init_timeout_ms);
abort();
}
delay(10);
esp_task_wdt_reset();
}
while (!esp_openthread_lock_acquire(100)) {
esp_task_wdt_reset();
}
+13
View File
@@ -13,6 +13,19 @@ OTAGlobalCallback *get_global_ota_callback() {
return global_ota_callback;
}
void OTAComponent::notify_state_deferred_(OTAState state, float progress, uint8_t error) {
// Pack state, error, and progress into a single uint32_t so the lambda
// captures only [this, packed] (8 bytes) — fits in std::function SBO.
// Layout: [state:8][error:8][progress_fixed:16] where progress is 010000 (0.01% resolution)
static_assert(OTA_ERROR <= 0xFF, "OTAState must fit in 8 bits for packing");
uint32_t packed = (static_cast<uint32_t>(state) << 24) | (static_cast<uint32_t>(error) << 16) |
static_cast<uint16_t>(progress * 100.0f);
this->defer([this, packed]() {
this->notify_state_(static_cast<OTAState>(packed >> 24), static_cast<float>(packed & 0xFFFF) / 100.0f,
static_cast<uint8_t>(packed >> 16));
});
}
void OTAComponent::notify_state_(OTAState state, float progress, uint8_t error) {
for (auto *listener : this->state_listeners_) {
listener->on_ota_state(state, progress, error);
+1 -3
View File
@@ -73,9 +73,7 @@ class OTAComponent : public Component {
* This should be used by OTA implementations that run in separate tasks
* (like web_server OTA) to ensure listeners execute in the main loop.
*/
void notify_state_deferred_(OTAState state, float progress, uint8_t error) {
this->defer([this, state, progress, error]() { this->notify_state_(state, progress, error); });
}
void notify_state_deferred_(OTAState state, float progress, uint8_t error);
std::vector<OTAStateListener *> state_listeners_;
#endif
-7
View File
@@ -37,13 +37,6 @@ void arch_init() {
void HOT arch_feed_wdt() { watchdog_update(); }
uint8_t progmem_read_byte(const uint8_t *addr) {
return pgm_read_byte(addr); // NOLINT
}
const char *progmem_read_ptr(const char *const *addr) {
return reinterpret_cast<const char *>(pgm_read_ptr(addr)); // NOLINT
}
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
uint32_t HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); }
uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); }
+17 -6
View File
@@ -8,6 +8,8 @@
#if defined(USE_WIFI)
#include <WiFi.h>
#include <pico/cyw43_arch.h> // For cyw43_arch_lwip_begin/end (LwIPLock)
#elif defined(USE_ETHERNET)
#include <LwipEthernet.h> // For ethernet_arch_lwip_begin/end (LwIPLock)
#endif
#include <hardware/structs/rosc.h>
#include <hardware/sync.h>
@@ -40,18 +42,27 @@ bool random_bytes(uint8_t *data, size_t len) {
IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); }
IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
// On RP2040 (Pico W), arduino-pico sets PICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1.
// This means lwip callbacks run from a low-priority user IRQ context, not the
// On RP2040, lwip callbacks run from a low-priority user IRQ context, not the
// main loop (see low_priority_irq_handler() in pico-sdk
// async_context_threadsafe_background.c). cyw43_arch_lwip_begin/end acquires the
// async_context recursive mutex to prevent IRQ callbacks from firing during
// critical sections. See esphome#10681.
// async_context_threadsafe_background.c). This applies to both WiFi (CYW43) and
// Ethernet (W5500) — both use async_context_threadsafe_background.
//
// When CYW43 is not available (non-WiFi RP2040 boards), this is a no-op since
// Without locking, recv_fn() from IRQ context races with read_locked_() on the
// main loop, corrupting the shared rx_buf_ pbuf chain (use-after-free, pbuf_cat
// assertion failures). See esphome#10681.
//
// WiFi uses cyw43_arch_lwip_begin/end; Ethernet uses ethernet_arch_lwip_begin/end.
// Both acquire the async_context recursive mutex to prevent IRQ callbacks from
// firing during critical sections.
//
// When neither WiFi nor Ethernet is configured, this is a no-op since
// there's no network stack and no lwip callbacks to race with.
#if defined(USE_WIFI)
LwIPLock::LwIPLock() { cyw43_arch_lwip_begin(); }
LwIPLock::~LwIPLock() { cyw43_arch_lwip_end(); }
#elif defined(USE_ETHERNET)
LwIPLock::LwIPLock() { ethernet_arch_lwip_begin(); }
LwIPLock::~LwIPLock() { ethernet_arch_lwip_end(); }
#else
LwIPLock::LwIPLock() {}
LwIPLock::~LwIPLock() {}
@@ -0,0 +1,27 @@
#pragma once
#ifdef USE_RP2040
#include <cstddef>
#include <cstdint>
namespace esphome::rp2040 {
class RP2040PreferenceBackend final {
public:
bool save(const uint8_t *data, size_t len);
bool load(uint8_t *data, size_t len);
size_t offset = 0;
uint32_t type = 0;
};
class RP2040Preferences;
RP2040Preferences *get_preferences();
} // namespace esphome::rp2040
namespace esphome {
using PreferenceBackend = rp2040::RP2040PreferenceBackend;
} // namespace esphome
#endif // USE_RP2040
+87 -102
View File
@@ -11,10 +11,8 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
namespace esphome {
namespace rp2040 {
namespace esphome::rp2040 {
static const char *const TAG = "rp2040.preferences";
@@ -39,129 +37,116 @@ template<class It> uint8_t calculate_crc(It first, It last, uint32_t type) {
return crc;
}
class RP2040PreferenceBackend : public ESPPreferenceBackend {
public:
size_t offset = 0;
uint32_t type = 0;
bool RP2040PreferenceBackend::save(const uint8_t *data, size_t len) {
const size_t buffer_size = len + 1;
if (buffer_size > PREF_MAX_BUFFER_SIZE)
return false;
uint8_t buffer[PREF_MAX_BUFFER_SIZE];
memcpy(buffer, data, len);
buffer[len] = calculate_crc(buffer, buffer + len, this->type);
bool save(const uint8_t *data, size_t len) override {
const size_t buffer_size = len + 1;
if (buffer_size > PREF_MAX_BUFFER_SIZE)
for (size_t i = 0; i < buffer_size; i++) {
uint32_t j = this->offset + i;
if (j >= RP2040_FLASH_STORAGE_SIZE)
return false;
uint8_t buffer[PREF_MAX_BUFFER_SIZE];
memcpy(buffer, data, len);
buffer[len] = calculate_crc(buffer, buffer + len, this->type);
for (size_t i = 0; i < buffer_size; i++) {
uint32_t j = this->offset + i;
if (j >= RP2040_FLASH_STORAGE_SIZE)
return false;
uint8_t v = buffer[i];
uint8_t *ptr = &s_flash_storage[j];
if (*ptr != v)
s_flash_dirty = true;
*ptr = v;
}
return true;
uint8_t v = buffer[i];
uint8_t *ptr = &s_flash_storage[j];
if (*ptr != v)
s_flash_dirty = true;
*ptr = v;
}
bool load(uint8_t *data, size_t len) override {
const size_t buffer_size = len + 1;
if (buffer_size > PREF_MAX_BUFFER_SIZE)
return true;
}
bool RP2040PreferenceBackend::load(uint8_t *data, size_t len) {
const size_t buffer_size = len + 1;
if (buffer_size > PREF_MAX_BUFFER_SIZE)
return false;
uint8_t buffer[PREF_MAX_BUFFER_SIZE];
for (size_t i = 0; i < buffer_size; i++) {
uint32_t j = this->offset + i;
if (j >= RP2040_FLASH_STORAGE_SIZE)
return false;
uint8_t buffer[PREF_MAX_BUFFER_SIZE];
buffer[i] = s_flash_storage[j];
}
for (size_t i = 0; i < buffer_size; i++) {
uint32_t j = this->offset + i;
if (j >= RP2040_FLASH_STORAGE_SIZE)
return false;
buffer[i] = s_flash_storage[j];
}
uint8_t crc = calculate_crc(buffer, buffer + len, this->type);
if (buffer[len] != crc) {
return false;
}
uint8_t crc = calculate_crc(buffer, buffer + len, this->type);
if (buffer[len] != crc) {
return false;
}
memcpy(data, buffer, len);
return true;
}
memcpy(data, buffer, len);
RP2040Preferences::RP2040Preferences() : eeprom_sector_(&_EEPROM_start) {}
void RP2040Preferences::setup() {
ESP_LOGVV(TAG, "Loading preferences from flash");
memcpy(s_flash_storage, this->eeprom_sector_, RP2040_FLASH_STORAGE_SIZE);
}
ESPPreferenceObject RP2040Preferences::make_preference(size_t length, uint32_t type) {
uint32_t start = this->current_flash_offset;
uint32_t end = start + length + 1;
if (end > RP2040_FLASH_STORAGE_SIZE) {
return {};
}
auto *pref = new RP2040PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->offset = start;
pref->type = type;
this->current_flash_offset = end;
return ESPPreferenceObject(pref);
}
bool RP2040Preferences::sync() {
if (!s_flash_dirty)
return true;
}
};
if (s_prevent_write)
return false;
class RP2040Preferences : public ESPPreferences {
public:
uint32_t current_flash_offset = 0;
ESP_LOGD(TAG, "Saving");
RP2040Preferences() : eeprom_sector_(&_EEPROM_start) {}
void setup() {
ESP_LOGVV(TAG, "Loading preferences from flash");
memcpy(s_flash_storage, this->eeprom_sector_, RP2040_FLASH_STORAGE_SIZE);
{
InterruptLock lock;
::rp2040.idleOtherCore();
flash_range_erase((intptr_t) this->eeprom_sector_ - (intptr_t) XIP_BASE, 4096);
flash_range_program((intptr_t) this->eeprom_sector_ - (intptr_t) XIP_BASE, s_flash_storage,
RP2040_FLASH_STORAGE_SIZE);
::rp2040.resumeOtherCore();
}
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override {
return make_preference(length, type);
s_flash_dirty = false;
return true;
}
bool RP2040Preferences::reset() {
ESP_LOGD(TAG, "Erasing storage");
{
InterruptLock lock;
::rp2040.idleOtherCore();
flash_range_erase((intptr_t) this->eeprom_sector_ - (intptr_t) XIP_BASE, 4096);
::rp2040.resumeOtherCore();
}
ESPPreferenceObject make_preference(size_t length, uint32_t type) override {
uint32_t start = this->current_flash_offset;
uint32_t end = start + length + 1;
if (end > RP2040_FLASH_STORAGE_SIZE) {
return {};
}
auto *pref = new RP2040PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->offset = start;
pref->type = type;
current_flash_offset = end;
return {pref};
}
bool sync() override {
if (!s_flash_dirty)
return true;
if (s_prevent_write)
return false;
ESP_LOGD(TAG, "Saving");
{
InterruptLock lock;
::rp2040.idleOtherCore();
flash_range_erase((intptr_t) eeprom_sector_ - (intptr_t) XIP_BASE, 4096);
flash_range_program((intptr_t) eeprom_sector_ - (intptr_t) XIP_BASE, s_flash_storage, RP2040_FLASH_STORAGE_SIZE);
::rp2040.resumeOtherCore();
}
s_flash_dirty = false;
return true;
}
bool reset() override {
ESP_LOGD(TAG, "Erasing storage");
{
InterruptLock lock;
::rp2040.idleOtherCore();
flash_range_erase((intptr_t) eeprom_sector_ - (intptr_t) XIP_BASE, 4096);
::rp2040.resumeOtherCore();
}
s_prevent_write = true;
return true;
}
protected:
uint8_t *eeprom_sector_;
};
s_prevent_write = true;
return true;
}
static RP2040Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
RP2040Preferences *get_preferences() { return &s_preferences; }
void setup_preferences() {
s_preferences.setup();
global_preferences = &s_preferences;
}
void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; }
} // namespace rp2040
} // namespace esphome::rp2040
namespace esphome {
ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome
#endif // USE_RP2040
+24 -5
View File
@@ -1,14 +1,33 @@
#pragma once
#ifdef USE_RP2040
namespace esphome {
namespace rp2040 {
#include "esphome/core/preference_backend.h"
namespace esphome::rp2040 {
class RP2040Preferences final : public PreferencesMixin<RP2040Preferences> {
public:
using PreferencesMixin<RP2040Preferences>::make_preference;
RP2040Preferences();
void setup();
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 current_flash_offset = 0;
protected:
uint8_t *eeprom_sector_;
};
void setup_preferences();
void preferences_prevent_write(bool prevent);
} // namespace rp2040
} // namespace esphome
} // namespace esphome::rp2040
DECLARE_PREFERENCE_ALIASES(esphome::rp2040::RP2040Preferences)
#endif // USE_RP2040

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