Compare commits

..
Author SHA1 Message Date
Jesse Hills e7c9c5ad0b Merge pull request #19270 from esphome/bump-2026.9.0b4
2026.9.0b4
2026-09-14 15:27:03 +12:00
J. Nick Koston 82b608706c [bluetooth_connection] Keep USE_BLUETOOTH_PROXY out of the shared host test binary (#19271) 2026-09-14 14:18:38 +12:00
Jesse Hills 7fe0689fb8 Bump version to 2026.9.0b4 2026-09-14 13:12:27 +12:00
J. Nick Koston 93fa95c833 [api] Reuse overflow buffer storage instead of allocating per stalled write (#19093) 2026-09-14 13:12:26 +12:00
Jesse Hills 501009073d [core] Clear loaded_platforms on CORE.reset() (#19268) 2026-09-14 13:12:26 +12:00
J. Nick Koston 81a54ea9db [ota] Allocate the signature block through RAMAllocator (#19251) 2026-09-14 13:12:26 +12:00
J. Nick Koston e28b4eb2a0 [ethernet] Keep the W5500 SPI context in a static instance instead of the heap (#19248) 2026-09-14 13:12:26 +12:00
J. Nick Koston ebe72c3cef [core] Resolve file paths against the YAML file that declares them (#19259) 2026-09-14 13:12:26 +12:00
J. Nick Koston 977542061d [esphome] Allocate the OTA noise session and auth buffer through RAMAllocator (#19249) 2026-09-14 13:12:26 +12:00
J. Nick Koston b0b75f705a [nextion] Allocate queue components through RAMAllocator and free entries the way they were allocated (#19246) 2026-09-14 13:12:25 +12:00
J. Nick Koston be2c3dda94 [esp32_ble_tracker] Revert coexistence preference to balanced when OTA starts (#19082) 2026-09-14 13:12:25 +12:00
Kevin Ahrendt 64fd87f436 [i2s_audio][router] Loop thread controls all state changes (#19089) 2026-09-14 13:12:25 +12:00
J. Nick Koston cd5d4ff254 [core] Add RAMAllocator::make_unique for objects whose allocation may fail (#19245) 2026-09-14 13:12:25 +12:00
J. Nick Koston 1a555d5848 [esp32_ble_tracker] Re-register GATT clients after ble.disable and ble.enable (#19068) 2026-09-14 13:12:25 +12:00
J. Nick Koston b17cd89469 [wifi] Drop a scan instead of aborting when its results cannot be allocated, filter ESP32 scans by SSID in the driver (#19254) 2026-09-14 13:12:25 +12:00
esphome[bot] 9064bfcc85 Bump bundled esphome-device-builder to 1.14.9 (#19263) 2026-09-14 13:12:25 +12:00
J. Nick Koston 0803d7b37c [core] Add FixedVector::try_init so callers can handle an exhausted heap (#19253) 2026-09-14 13:12:25 +12:00
J. Nick Koston 9814966fe7 [noise] Bump noise-c to 0.1.30 and libsodium to 1.10021.11 (#19062) 2026-09-14 13:12:25 +12:00
esphome[bot] 3eda3060b8 Bump bundled esphome-device-builder to 1.14.8 (#19250) 2026-09-14 13:12:25 +12:00
Clyde Stubbs 6cc2b9bf17 [lvgl] Fix crash when using lvgl.list.add (#19177) 2026-09-14 13:12:25 +12:00
esphome[bot] 1975b17eac Bump bundled esphome-device-builder to 1.14.7 (#19096) 2026-09-14 13:12:25 +12:00
J. Nick Koston 3b499ecb3e [core] Support set_internal() during setup, log error after setup (#19069) 2026-09-14 13:12:25 +12:00
esphome[bot] 6bd6603d52 Bump bundled esphome-device-builder to 1.14.6 (#19072) 2026-09-14 13:12:25 +12:00
Robin ThoniandJesse Hills ad4ee1d34e [network] Improve network::is_connected() to better handle multiple interfaces (#18999)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-09-14 13:12:25 +12:00
Remco van Essen ccec6e72bf [sendspin] Fix codec enum codegen when codecs is not set (#19055) 2026-09-14 13:12:25 +12:00
187 changed files with 3077 additions and 6042 deletions

No files matched your search

+2 -11
View File
@@ -244,20 +244,11 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Read prek version from requirements_test.txt
id: prek
# requirements_test.txt is the only place the version is pinned, so a
# Dependabot bump there is picked up here without a second edit.
run: |
if ! version=$(sed -nE 's/^prek==([^[:space:]#]+).*/\1/p' requirements_test.txt) || [ -z "$version" ]; then
echo "::error::No prek== pin found in requirements_test.txt."
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Run prek
uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0
with:
prek-version: ${{ steps.prek.outputs.version }}
# Keep in sync with requirements_test.txt.
prek-version: "0.4.11"
# This job only runs on pull requests, so nothing ever populates
# the cache on dev. Every run would miss and then write a per-pull
# request copy, which is what the old seed-cache job existed to
+1 -1
View File
@@ -33,7 +33,7 @@ jobs:
and will be closed if no further activity occurs within 7 days.
If you are the author of this PR, please leave a comment if you want
to keep it open. Also, please merge the latest dev branch into your
to keep it open. Also, please rebase your PR onto the latest dev
branch to ensure that it's up to date with the latest changes.
Thank you for your contribution!
@@ -1,94 +0,0 @@
# Keeps pre-commit hook revs in sync with the requirements files.
#
# Dependabot only bumps the pins in requirements*.txt. Some of those tools
# are pinned again as hook revs in .pre-commit-config.yaml. This workflow
# runs script/sync_dependency_versions.py against the pull request branch
# and pushes a commit with the revs updated.
name: Sync dependency versions
on:
# pull_request_target rather than pull_request so the App secret is
# available on Dependabot pull requests (pull_request runs opened by
# Dependabot only see Dependabot secrets). The job below only touches
# branches in this repository and only ever executes the script from the
# base branch checkout, so fork code never runs with the token.
pull_request_target:
types: [opened, synchronize, reopened]
paths:
- requirements_dev.txt
- requirements_test.txt
- .pre-commit-config.yaml
- script/sync_dependency_versions.py
# The push to the pull request branch uses the App token minted below, so
# the workflow's GITHUB_TOKEN does not need any scopes.
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
sync:
name: Sync pinned versions
runs-on: ubuntu-latest
# Same-repository branches only: a push to a fork is not possible with
# this token, and it keeps untrusted heads out of a privileged job.
if: >-
github.repository == 'esphome/esphome'
&& github.event.pull_request.head.repo.full_name == github.repository
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
# A push made with the workflow's own GITHUB_TOKEN would not start
# CI on the new commit; a push with the App token does.
permission-contents: write # git push of the sync commit to the pull request branch
- name: Check out base branch
# Provides the script that runs below. Deliberately the base branch
# so the pull request cannot change what executes here.
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.base.sha }}
persist-credentials: false
- name: Check out pull request branch
# No allow-unsafe-pr-checkout here on purpose: checkout v7 only
# refuses heads that live in a different repository, and the job
# condition above already limits runs to same-repository branches.
# Leaving it off keeps that refusal as a backstop for fork heads.
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.ref }}
path: pull-request
token: ${{ steps.generate-token.outputs.token }}
- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Install yamlrocks
# The script edits YAML through yamlrocks. Take the pin from the
# base branch requirements so this workflow has no copy of its own.
run: pip install "$(grep -E '^yamlrocks==' requirements_test.txt | cut -d'#' -f1)"
- name: Sync pinned versions
run: python script/sync_dependency_versions.py --root pull-request
- name: Push changes
working-directory: pull-request
run: |
if git diff --quiet; then
echo "All pinned versions already match the requirements files."
exit 0
fi
git config user.name "esphome[bot]"
git config user.email "115708604+esphome[bot]@users.noreply.github.com"
git commit -am "Sync pinned tool versions with requirements files"
git push
+3 -2
View File
@@ -1,6 +1,7 @@
---
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
ci:
autoupdate_commit_msg: 'pre-commit: autoupdate'
autoupdate_schedule: off # Disabled until ruff versions are synced between deps and pre-commit
@@ -10,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.16.6
rev: v0.16.3
hooks:
# Run the linter.
- id: ruff
@@ -41,7 +42,7 @@ repos:
- id: pyupgrade
args: [--py312-plus]
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.38.0
rev: v1.37.1
hooks:
- id: yamllint
exclude: ^(\.clang-format|\.clang-tidy)$
+1 -2
View File
@@ -553,7 +553,6 @@ file does, and it is the authority when they disagree. The most useful starting
4. **Lint:** Run `prek` to ensure code is compliant.
5. **Commit:** Commit your changes. There is no strict format for commit messages.
6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title must start with a `[tag]` prefix. For component work, use the component name (e.g., `[display] Fix bug`, `[abc123] Add new component`); for changes to shared/core code that isn't tied to a single component, use `[core]` (e.g., `[core] Add validator`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template.
7. **Comments:** When commenting on GitHub PRs or issues, don't tag contributors, especially bots. Avoid referring to list items (e.g. from reviews) with the form #nn - this will be interpreted by GitHub as a reference to issue or PR nn. Keep comments short and exclude irrelevant details, backstories, restatement of previous comments and anything that is already obvious to the reader.
* **Documentation Contributions:**
* Documentation is hosted in the separate `esphome/esphome.io` repository.
@@ -840,7 +839,7 @@ file does, and it is the authority when they disagree. The most useful starting
cv.rename_key(
CONF_OLD_KEY, CONF_NEW_KEY, removed_in="2026.6.0", component="my_component"
),
cv.Schema({...}),
cv.Schema({ ... }),
)
```
For other deprecations, warn manually during validation:
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.10.0-dev
PROJECT_NUMBER = 2026.9.0b4
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.6
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.9
RUN \
platformio settings set enable_telemetry No \
+3 -1
View File
@@ -23,7 +23,9 @@ from esphome.util import safe_print
if TYPE_CHECKING:
from collections.abc import Callable
from aioesphomeapi.api_pb2 import SubscribeLogsResponse # pylint: disable=no-name-in-module
from aioesphomeapi.api_pb2 import (
SubscribeLogsResponse, # pylint: disable=no-name-in-module
)
_LOGGER = logging.getLogger(__name__)
+2 -3
View File
@@ -350,10 +350,9 @@ CONFIG_SCHEMA = cv.All(
ln882x=5, # Moderate RAM
nrf52=4, # ~256KB RAM, BSD sockets, Thread (single HA controller)
): cv.int_range(min=1, max=20),
# Maximum queued send buffers per connection before dropping connection
# Each buffer uses ~8-12 bytes overhead plus actual message size
# Max queued messages per connection, and 2 KB of backlog per slot up
# to 64 KB (a lone message is exempt), before the connection is dropped
# 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=4, # Limited RAM, need to fail fast
+4 -77
View File
@@ -76,9 +76,7 @@ service APIConnection {
rpc serial_proxy_write(SerialProxyWriteRequest) returns (void) {}
rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {}
rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {}
rpc serial_proxy_get_usb_info(SerialProxyGetUsbInfoRequest) returns (void) {}
rpc serial_proxy_request(SerialProxyRequest) returns (void) {}
rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {}
}
@@ -229,11 +227,6 @@ enum SerialProxyPortType {
SERIAL_PROXY_PORT_TYPE_TTL = 0;
SERIAL_PROXY_PORT_TYPE_RS232 = 1;
SERIAL_PROXY_PORT_TYPE_RS485 = 2;
// A serial device attached through a USB bridge. Set by the device configuration, never
// by the user; identifies ports whose USB identity can be read with
// SerialProxyGetUsbInfoRequest. Deliberately not a USB endpoint type: serial_proxy
// carries serial devices only, whatever bridge chip connects them.
SERIAL_PROXY_PORT_TYPE_USB_SERIAL = 3;
}
message SerialProxyInfo {
@@ -2733,8 +2726,7 @@ enum SerialProxyParity {
SERIAL_PROXY_PARITY_ODD = 2;
}
// Configure UART parameters for a serial proxy instance. Only the subscribed client may
// configure the port; others are refused with PORT_IN_USE (since API 1.17).
// Configure UART parameters for a serial proxy instance
message SerialProxyConfigureRequest {
option (id) = 138;
option (source) = SOURCE_CLIENT;
@@ -2760,8 +2752,7 @@ message SerialProxyDataReceived {
bytes data = 2; // Raw data received from the serial device
}
// Write data to a serial device. Only the subscribed client may write; writes from
// others are ignored (since API 1.17).
// Write data to a serial device
message SerialProxyWriteRequest {
option (id) = 140;
option (source) = SOURCE_CLIENT;
@@ -2772,8 +2763,7 @@ message SerialProxyWriteRequest {
bytes data = 2; // Raw data to write to the serial device
}
// Set modem control pin states (RTS and DTR). Only the subscribed client may set them;
// others are refused with PORT_IN_USE (since API 1.17).
// Set modem control pin states (RTS and DTR)
message SerialProxySetModemPinsRequest {
option (id) = 141;
option (source) = SOURCE_CLIENT;
@@ -2812,7 +2802,6 @@ enum SerialProxyRequestType {
// error the device answers with INVALID_ARGUMENT.
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3; // Acknowledges a SerialProxyConfigureRequest
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4; // Acknowledges a SerialProxySetModemPinsRequest
SERIAL_PROXY_REQUEST_TYPE_SET_MODE = 5; // Acknowledges a SerialProxySetModeRequest (since API 1.17)
}
enum SerialProxyStatus {
@@ -2825,8 +2814,7 @@ enum SerialProxyStatus {
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6; // Invalid instance index or parameter value
}
// Generic request message for simple serial proxy operations. FLUSH requires an active
// subscription; it is refused with PORT_IN_USE otherwise (since API 1.17).
// Generic request message for simple serial proxy operations
message SerialProxyRequest {
option (id) = 144;
option (source) = SOURCE_CLIENT;
@@ -2850,67 +2838,6 @@ message SerialProxyRequestResponse {
string error_message = 4; // Additional detail on failure (optional)
}
// How a port treats the bytes passing through it. RAW is a plain byte pipe; PROTOCOL
// activates the port's protocol-aware tap (if one is configured), letting it observe
// traffic and inject protocol bytes such as acknowledgements. Which protocol the tap
// speaks is a property of the device configuration, discoverable from the tap
// component's own API surface. A client that is about to flash firmware selects RAW
// first, which definitively disables that injection.
enum SerialProxyMode {
SERIAL_PROXY_MODE_RAW = 0;
SERIAL_PROXY_MODE_PROTOCOL = 1;
}
// Only the subscribed client may change the mode; any other caller -- including one that
// never subscribed -- is refused with PORT_IN_USE. PROTOCOL is refused with NOT_SUPPORTED
// when the port has no protocol-aware tap configured.
message SerialProxySetModeRequest {
option (id) = 152;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1;
SerialProxyMode mode = 2;
}
// Ask for the USB identity of the device behind a USB_SERIAL port. Read-only, so no
// subscription is required -- a client typically uses this to decide which port to
// subscribe to. Answered with NOT_SUPPORTED on ports that are not USB_SERIAL.
message SerialProxyGetUsbInfoRequest {
option (id) = 153;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1;
}
// The USB identity of the device currently behind a port, read live from the cached
// USB descriptors. Fields are zero/empty while no device is connected.
//
// Sent in reply to SerialProxyGetUsbInfoRequest, and also unsolicited to every connected
// client whenever a USB device is attached to or removed from a USB_SERIAL port. A client
// therefore subscribes to this message before querying the current state, so the
// connect-time query and later hotplug events are handled by one path. The port's
// subscription is untouched by a hotplug: the session belongs to the client, which decides
// whether to keep it.
message SerialProxyUsbInfo {
option (id) = 154;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1;
SerialProxyStatus status = 2; // NOT_SUPPORTED when the port is not USB_SERIAL
bool connected = 3; // True when a USB device is currently attached
uint32 vendor_id = 4;
uint32 product_id = 5;
uint32 bcd_device = 6;
uint32 interface_number = 7; // bInterfaceNumber a host driver binds to, as Linux reports it
string manufacturer = 8;
string product = 9;
string serial_number = 10;
string interface_description = 11; // iInterface string of that interface; empty when the device has none
}
// ==================== BLUETOOTH CONNECTION PARAMS ====================
message BluetoothSetConnectionParamsRequest {
option (id) = 145;
+26 -9
View File
@@ -1,20 +1,37 @@
#include "api_buffer.h"
#include <new>
#ifdef ESPHOME_DEBUG_API
#include "esphome/core/log.h"
#endif
namespace esphome::api {
#ifdef ESPHOME_DEBUG_API
void APIBuffer::debug_check_drop_(size_t drop) const {
if (drop > this->size_) {
ESP_LOGE("api.buffer", "drop_front: drop=%zu size=%u", drop, this->size_);
abort();
}
}
#endif
bool APIBuffer::grow_(size_t n) {
// nothrow (no zero-fill) so OOM is reportable; plain new aborts instead
// (NEW_OOM_ABORT on ESP8266 Arduino, exception stub on ESP-IDF).
// RAMAllocator is no fit here: unique_ptr needs delete[]-compatible memory.
std::unique_ptr<uint8_t[]> new_data(new (std::nothrow) uint8_t[n]);
if (new_data == nullptr)
if (n > MAX_SIZE)
return false;
if (this->size_)
std::memcpy(new_data.get(), this->data_.get(), this->size_);
this->data_ = std::move(new_data);
// realloc extends in place when it can, avoiding the copy
uint8_t *grown = RAMAllocator<uint8_t>().reallocate(this->data_.get(), n);
if (grown == nullptr)
return false;
(void) this->data_.release(); // realloc already freed or reused the old block
this->data_.reset(grown);
this->capacity_ = n;
return true;
}
uint8_t *APIBuffer::append(size_t n) {
const size_t old_size = this->size_;
if (!this->resize(old_size + n))
return nullptr;
return this->data_.get() + old_size;
}
} // namespace esphome::api
+20 -4
View File
@@ -25,6 +25,7 @@ namespace esphome::api {
/// writes in debug builds.
class APIBuffer {
public:
static constexpr size_t MAX_SIZE = UINT16_MAX; // API frames carry 16 bit lengths
void clear() { this->size_ = 0; }
/// Returns false if allocation fails; the buffer is left unchanged.
[[nodiscard]] inline bool reserve(size_t n) ESPHOME_ALWAYS_INLINE { return n <= this->capacity_ || this->grow_(n); }
@@ -36,9 +37,19 @@ class APIBuffer {
[[nodiscard]] inline bool reserve_and_resize(size_t reserve_size, size_t new_size) ESPHOME_ALWAYS_INLINE {
if (!this->reserve(std::max(reserve_size, new_size)))
return false;
this->size_ = new_size;
this->size_ = static_cast<uint16_t>(new_size);
return true;
}
/// Grow by n bytes; returns the new bytes, or nullptr on allocation failure.
[[nodiscard]] uint8_t *append(size_t n);
/// Drop the first `drop` bytes, sliding the rest down. Precondition: drop <= size().
void drop_front(size_t drop) {
#ifdef ESPHOME_DEBUG_API
this->debug_check_drop_(drop);
#endif
this->size_ -= drop;
std::memmove(this->data_.get(), this->data_.get() + drop, this->size_);
}
uint8_t *data() { return this->data_.get(); }
const uint8_t *data() const { return this->data_.get(); }
size_t size() const { return this->size_; }
@@ -55,9 +66,14 @@ class APIBuffer {
protected:
bool grow_(size_t n);
std::unique_ptr<uint8_t[]> data_;
size_t size_{0};
size_t capacity_{0};
#ifdef ESPHOME_DEBUG_API
void debug_check_drop_(size_t drop) const;
#endif
// RAMAllocator: PSRAM when available, and it reports failure where
// new (std::nothrow) still aborts on ESP-IDF without exceptions
RAMUniquePtr<uint8_t[]> data_;
uint16_t size_{0};
uint16_t capacity_{0};
};
} // namespace esphome::api
+5 -40
View File
@@ -48,9 +48,6 @@
#ifdef USE_ZWAVE_PROXY
#include "esphome/components/zwave_proxy/zwave_proxy.h"
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
#include "esphome/components/usb_host/usb_host.h"
#endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
@@ -367,7 +364,10 @@ void APIConnection::check_keepalive_(uint32_t now) {
ESP_LOGVV(TAG, "Sending keepalive PING");
PingRequest req;
this->flags_.sent_ping = this->send_message(req);
if (!this->flags_.sent_ping) {
if (this->flags_.sent_ping) {
// Quiet for a keepalive period and the ping is on its way: a one-off stall's storage can go
this->helper_->release_overflow_buffer();
} else {
// If we can't send the ping request directly (tx_buffer full),
// schedule it at the front of the batch so it will be sent with priority
ESP_LOGW(TAG, "Buffer full, ping queued");
@@ -1645,27 +1645,6 @@ void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetM
}
}
void APIConnection::on_serial_proxy_get_usb_info_request(const SerialProxyGetUsbInfoRequest &msg) {
auto &proxies = App.get_serial_proxies();
SerialProxyUsbInfo resp{};
resp.instance = msg.instance;
if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
resp.status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
} else {
#ifdef USE_SERIAL_PROXY_USB_INFO
// The message's strings are views into this buffer, which outlives the send below
usb_host::UsbDeviceInfo info;
proxies[msg.instance]->get_usb_info(info, resp);
#else
resp.status = enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED;
#endif
}
if (!this->send_message(resp)) {
API_LOG_MSG_DROPPED(TAG, "Serial proxy response");
}
}
void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) {
@@ -1685,7 +1664,6 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
break;
case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE:
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE:
// Response-only discriminators; never valid in a request
ESP_LOGW(TAG, "Response-only serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type));
status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
@@ -1698,19 +1676,6 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
send_serial_proxy_ack(this, msg.instance, msg.type, status);
}
void APIConnection::on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg) {
auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE,
enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
return;
}
serial_proxy::SerialProxyResult result = proxies[msg.instance]->set_mode_from_client(this, msg.mode);
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE,
serial_proxy_result_to_status(result));
}
void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) {
if (!this->send_message(msg)) {
ESP_LOGV(TAG, "Serial proxy data dropped, TCP buffer full");
@@ -1837,7 +1802,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
HelloResponse resp;
resp.api_version_major = 1;
resp.api_version_minor = 17;
resp.api_version_minor = 16;
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
resp.server_info = ESPHOME_VERSION_REF;
resp.name = StringRef(App.get_name());
-2
View File
@@ -243,9 +243,7 @@ class APIConnection final : public APIServerConnectionBase {
void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg);
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg);
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg);
void on_serial_proxy_get_usb_info_request(const SerialProxyGetUsbInfoRequest &msg);
void on_serial_proxy_request(const SerialProxyRequest &msg);
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg);
void send_serial_proxy_data(const SerialProxyDataReceived &msg);
#endif
+1 -1
View File
@@ -171,7 +171,7 @@ APIError APIFrameHelper::write_raw_iov_(const struct iovec *iov, int iovcnt, uin
return APIError::OK;
// Queue unsent data into overflow buffer
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent))) {
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, sent)) {
HELPER_LOG("Overflow buffer full or out of memory, dropping connection");
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
@@ -219,7 +219,10 @@ class APIFrameHelper {
if (this->rx_buf_len_ == 0) {
this->rx_buf_.release();
}
this->release_overflow_buffer();
}
// Free the send backlog storage once it has drained
void release_overflow_buffer() { this->overflow_buf_.release(); }
protected:
// Drain backlogged overflow data to the socket and handle errors.
@@ -67,15 +67,15 @@ APIError APINoiseFrameHelper::init() {
}
// init prologue
size_t old_size = prologue_.size();
if (!prologue_.resize(old_size + PROLOGUE_INIT_LEN)) [[unlikely]] {
uint8_t *dst = prologue_.append(PROLOGUE_INIT_LEN);
if (dst == nullptr) [[unlikely]] {
state_ = State::FAILED;
return APIError::OUT_OF_MEMORY;
}
#ifdef USE_ESP8266
memcpy_P(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
memcpy_P(dst, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
#else
std::memcpy(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
std::memcpy(dst, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
#endif
state_ = State::CLIENT_HELLO;
@@ -272,17 +272,17 @@ APIError APINoiseFrameHelper::state_action_client_hello_() {
return handle_handshake_frame_error_(aerr);
}
// ignore contents, may be used in future for flags
// Resize for: existing prologue + 2 size bytes + frame data
size_t old_size = this->prologue_.size();
// Append 2 size bytes + frame data to the prologue
size_t rx_size = this->rx_buf_.size();
if (!this->prologue_.resize(old_size + 2 + rx_size)) [[unlikely]] {
uint8_t *dst = this->prologue_.append(2 + rx_size);
if (dst == nullptr) [[unlikely]] {
state_ = State::FAILED;
return APIError::OUT_OF_MEMORY;
}
this->prologue_[old_size] = (uint8_t) (rx_size >> 8);
this->prologue_[old_size + 1] = (uint8_t) rx_size;
dst[0] = (uint8_t) (rx_size >> 8);
dst[1] = (uint8_t) rx_size;
if (rx_size > 0) {
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size);
std::memcpy(dst + 2, this->rx_buf_.data(), rx_size);
}
state_ = State::SERVER_HELLO;
+57 -64
View File
@@ -1,98 +1,91 @@
#include "api_overflow_buffer.h"
#ifdef USE_API
#include <cstring>
#include <new>
namespace esphome::api {
APIOverflowBuffer::~APIOverflowBuffer() {
for (auto *entry : this->queue_) {
if (entry != nullptr)
Entry::destroy(entry);
}
}
ssize_t APIOverflowBuffer::try_drain(socket::Socket *socket) {
// socket->write() can re-enter this function: a log message emitted from an
// lwip callback during the write goes out over the API and lands back in the
// frame helper's write/drain path. If a nested drain ran here it would send
// and free the entry the outer drain is still holding, causing a double free.
// Report "no progress" instead; the outer drain keeps draining, and the
// nested send is enqueued behind the existing backlog.
// Nested call from inside socket->write(); see draining_
if (this->draining_)
return 0;
// RAII so the flag is cleared on every return path
struct DrainGuard {
explicit DrainGuard(bool &flag) : flag_(flag) { flag_ = true; }
~DrainGuard() { this->flag_ = false; }
bool &flag_;
} guard(this->draining_);
APIOverflowBuffer &owner;
~DrainGuard() { this->owner.draining_ = false; }
} guard{*this};
this->draining_ = true;
while (this->count_ > 0) {
Entry *front = this->queue_[this->head_];
uint8_t *msg = this->buf_.data() + this->head_;
size_t len = msg[0] | (msg[1] << 8);
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)
ssize_t sent = socket->write(msg + LEN_PREFIX, len);
if (sent <= 0)
return sent;
if (static_cast<size_t>(sent) < len) {
// Step past the sent bytes and rewrite the prefix there; it lands on bytes already sent
this->head_ += sent;
len -= sent;
msg += sent;
msg[0] = len;
msg[1] = len >> 8;
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 — unlink it before freeing so a freed pointer is never
// reachable from the queue
this->queue_[this->head_] = nullptr;
this->head_ = (this->head_ + 1) % API_MAX_SEND_QUEUE;
this->head_ += LEN_PREFIX + len;
this->count_--;
Entry::destroy(front);
}
return 0; // All drained
this->head_ = 0;
if (this->release_when_drained_) {
this->release_when_drained_ = false;
this->buf_.release();
} else {
this->buf_.clear();
}
return 0;
}
bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip) {
bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, size_t total_len, size_t skip) {
if (this->count_ >= API_MAX_SEND_QUEUE)
return false;
uint16_t buffer_size = total_len - skip;
// nothrow: a failed allocation returns nullptr so the connection is dropped
// cleanly instead of plain new's crash or abort on OOM
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
auto *data = new (std::nothrow) uint8_t[buffer_size];
if (data == nullptr)
return false;
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
auto *entry = new (std::nothrow) Entry{data, buffer_size, 0};
if (entry == nullptr) {
delete[] data;
const size_t new_len = total_len - skip;
const size_t new_bytes = LEN_PREFIX + new_len;
const size_t live = this->buf_.size() - this->head_;
// A lone message is only bound by the buffer; refusing it would just drop the connection
if (live + new_bytes > (this->count_ > 0 ? MAX_BYTES : MAX_LONE_BYTES))
return false;
if (this->buf_.size() + new_bytes > this->buf_.capacity()) {
// Storage would move under an outer drain's write()
if (this->draining_)
return false;
if (this->head_ > 0) {
// Reclaim the sent prefix before growing
this->buf_.drop_front(this->head_);
this->head_ = 0;
}
if (!this->buf_.reserve(reserve_for(live + new_bytes)))
return false;
}
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);
uint8_t *dst = this->buf_.append(new_bytes);
if (dst == nullptr)
return false;
dst[0] = new_len;
dst[1] = new_len >> 8;
dst += LEN_PREFIX;
for (const struct iovec *end = iov + iovcnt; iov != end; iov++) {
if (skip >= iov->iov_len) {
skip -= iov->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;
const size_t len = iov->iov_len - skip;
std::memcpy(dst, static_cast<const uint8_t *>(iov->iov_base) + skip, len);
dst += len;
skip = 0;
}
}
// Publish only after the copy completes so a half-built entry is never reachable
this->queue_[this->tail_] = entry;
this->tail_ = (this->tail_ + 1) % API_MAX_SEND_QUEUE;
this->count_++;
return true;
}
+40 -53
View File
@@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <sys/types.h>
@@ -8,71 +9,57 @@
#include "esphome/components/socket/headers.h"
#include "esphome/components/socket/socket.h"
#include "esphome/core/helpers.h"
#include "api_buffer.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.
/// TCP send backlog, only used when the socket send buffer is full.
/// One contiguous buffer per connection, allocated on the first stall and
/// kept at its high-water mark so a lossy link does not churn the heap.
/// Messages are stored as a 2 byte length prefix plus payload.
/// API_MAX_SEND_QUEUE bounds queued messages and, at 2 KB per slot, queued
/// bytes; exceeding either fails the connection.
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.
/// Drain queued messages to the socket.
/// Returns bytes written, 0 for a re-entrant call, -1 on error (check errno
/// for EWOULDBLOCK); callers only need to act on -1.
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 or allocation fails (caller should fail the connection).
bool enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip);
/// Queue iov data from byte offset `skip` as one message.
/// Returns false when a limit is hit, allocation fails, or storage would move
/// during a drain; the caller should fail the connection.
bool enqueue_iov(const struct iovec *iov, int iovcnt, size_t total_len, size_t skip);
/// Free the retained storage, now if empty, otherwise once it has drained.
void release() {
if (this->count_ == 0) {
this->buf_.release();
} else {
this->release_when_drained_ = true;
}
}
protected:
std::array<Entry *, API_MAX_SEND_QUEUE> queue_{};
uint8_t head_{0};
uint8_t tail_{0};
static constexpr size_t LEN_PREFIX = 2;
static constexpr size_t BYTES_PER_SLOT = 2048;
// Reserve in 256 byte steps so a creeping high-water mark settles quickly
static constexpr size_t GROW_QUANTUM = 256;
// Lone message ceiling, rounded down so reserve_for() never exceeds the buffer limit
static constexpr size_t MAX_LONE_BYTES = APIBuffer::MAX_SIZE & ~(GROW_QUANTUM - 1);
static constexpr size_t MAX_BYTES = std::min(API_MAX_SEND_QUEUE * BYTES_PER_SLOT, MAX_LONE_BYTES);
static constexpr size_t reserve_for(size_t want) { return (want + GROW_QUANTUM - 1) & ~(GROW_QUANTUM - 1); }
APIBuffer buf_;
uint16_t head_{0}; // offset of the front message's length prefix; bytes before it are sent
uint8_t count_{0};
// Guards against re-entrant drains: socket->write() can re-enter the API
// send path (e.g. a log message emitted from an lwip callback), and a nested
// drain would free the entry the outer drain is still holding.
bool draining_{false};
// socket->write() can re-enter the send path (log from an lwip callback):
// a nested drain makes no progress and a nested enqueue never moves storage
bool draining_ : 1 {false};
bool release_when_drained_ : 1 {false};
};
} // namespace esphome::api
-53
View File
@@ -4253,59 +4253,6 @@ uint32_t SerialProxyRequestResponse::calculate_size() const {
size += ProtoSize::calc_length(1, this->error_message.size());
return size;
}
bool SerialProxySetModeRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
switch (field_id) {
case 1:
this->instance = value;
break;
case 2:
this->mode = static_cast<enums::SerialProxyMode>(value);
break;
default:
return false;
}
return true;
}
bool SerialProxyGetUsbInfoRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
switch (field_id) {
case 1:
this->instance = value;
break;
default:
return false;
}
return true;
}
uint8_t *SerialProxyUsbInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->instance);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->status));
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 3, this->connected);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, this->vendor_id);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, this->product_id);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, this->bcd_device);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, this->interface_number);
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->manufacturer);
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 9, this->product);
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 10, this->serial_number);
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 11, this->interface_description);
return pos;
}
uint32_t SerialProxyUsbInfo::calculate_size() const {
uint32_t size = 0;
size += ProtoSize::calc_uint32(1, this->instance);
size += this->status ? 2 : 0;
size += ProtoSize::calc_bool(1, this->connected);
size += ProtoSize::calc_uint32(1, this->vendor_id);
size += ProtoSize::calc_uint32(1, this->product_id);
size += ProtoSize::calc_uint32(1, this->bcd_device);
size += ProtoSize::calc_uint32(1, this->interface_number);
size += ProtoSize::calc_length(1, this->manufacturer.size());
size += ProtoSize::calc_length(1, this->product.size());
size += ProtoSize::calc_length(1, this->serial_number.size());
size += ProtoSize::calc_length(1, this->interface_description.size());
return size;
}
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
-63
View File
@@ -23,7 +23,6 @@ enum SerialProxyPortType : uint32_t {
SERIAL_PROXY_PORT_TYPE_TTL = 0,
SERIAL_PROXY_PORT_TYPE_RS232 = 1,
SERIAL_PROXY_PORT_TYPE_RS485 = 2,
SERIAL_PROXY_PORT_TYPE_USB_SERIAL = 3,
};
enum EntityCategory : uint32_t {
ENTITY_CATEGORY_NONE = 0,
@@ -357,7 +356,6 @@ enum SerialProxyRequestType : uint32_t {
SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2,
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3,
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4,
SERIAL_PROXY_REQUEST_TYPE_SET_MODE = 5,
};
enum SerialProxyStatus : uint32_t {
SERIAL_PROXY_STATUS_OK = 0,
@@ -368,10 +366,6 @@ enum SerialProxyStatus : uint32_t {
SERIAL_PROXY_STATUS_PORT_IN_USE = 5,
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6,
};
enum SerialProxyMode : uint32_t {
SERIAL_PROXY_MODE_RAW = 0,
SERIAL_PROXY_MODE_PROTOCOL = 1,
};
#endif
} // namespace enums
@@ -3409,63 +3403,6 @@ class SerialProxyRequestResponse final : public ProtoMessage {
protected:
};
class SerialProxySetModeRequest final : public ProtoDecodableMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 152;
static constexpr uint8_t ESTIMATED_SIZE = 6;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("serial_proxy_set_mode_request"); }
#endif
uint32_t instance{0};
enums::SerialProxyMode mode{};
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class SerialProxyGetUsbInfoRequest final : public ProtoDecodableMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 153;
static constexpr uint8_t ESTIMATED_SIZE = 4;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("serial_proxy_get_usb_info_request"); }
#endif
uint32_t instance{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
};
class SerialProxyUsbInfo final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 154;
static constexpr uint8_t ESTIMATED_SIZE = 60;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("serial_proxy_usb_info"); }
#endif
uint32_t instance{0};
enums::SerialProxyStatus status{};
bool connected{false};
uint32_t vendor_id{0};
uint32_t product_id{0};
uint32_t bcd_device{0};
uint32_t interface_number{0};
StringRef manufacturer{};
StringRef product{};
StringRef serial_number{};
StringRef interface_description{};
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage {
-40
View File
@@ -143,8 +143,6 @@ template<> const char *proto_enum_to_string<enums::SerialProxyPortType>(enums::S
return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_RS232");
case enums::SERIAL_PROXY_PORT_TYPE_RS485:
return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_RS485");
case enums::SERIAL_PROXY_PORT_TYPE_USB_SERIAL:
return ESPHOME_PSTR("SERIAL_PROXY_PORT_TYPE_USB_SERIAL");
default:
return ESPHOME_PSTR("UNKNOWN");
}
@@ -856,8 +854,6 @@ template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_CONFIGURE");
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS");
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE:
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODE");
default:
return ESPHOME_PSTR("UNKNOWN");
}
@@ -882,16 +878,6 @@ template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::Ser
return ESPHOME_PSTR("UNKNOWN");
}
}
template<> const char *proto_enum_to_string<enums::SerialProxyMode>(enums::SerialProxyMode value) {
switch (value) {
case enums::SERIAL_PROXY_MODE_RAW:
return ESPHOME_PSTR("SERIAL_PROXY_MODE_RAW");
case enums::SERIAL_PROXY_MODE_PROTOCOL:
return ESPHOME_PSTR("SERIAL_PROXY_MODE_PROTOCOL");
default:
return ESPHOME_PSTR("UNKNOWN");
}
}
#endif
const char *HelloRequest::dump_to(DumpBuffer &out) const {
@@ -2819,32 +2805,6 @@ const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("error_message"), this->error_message);
return out.c_str();
}
const char *SerialProxySetModeRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxySetModeRequest"));
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
dump_field(out, ESPHOME_PSTR("mode"), static_cast<enums::SerialProxyMode>(this->mode));
return out.c_str();
}
const char *SerialProxyGetUsbInfoRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetUsbInfoRequest"));
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
return out.c_str();
}
const char *SerialProxyUsbInfo::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyUsbInfo"));
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
dump_field(out, ESPHOME_PSTR("status"), static_cast<enums::SerialProxyStatus>(this->status));
dump_field(out, ESPHOME_PSTR("connected"), this->connected);
dump_field(out, ESPHOME_PSTR("vendor_id"), this->vendor_id);
dump_field(out, ESPHOME_PSTR("product_id"), this->product_id);
dump_field(out, ESPHOME_PSTR("bcd_device"), this->bcd_device);
dump_field(out, ESPHOME_PSTR("interface_number"), this->interface_number);
dump_field(out, ESPHOME_PSTR("manufacturer"), this->manufacturer);
dump_field(out, ESPHOME_PSTR("product"), this->product);
dump_field(out, ESPHOME_PSTR("serial_number"), this->serial_number);
dump_field(out, ESPHOME_PSTR("interface_description"), this->interface_description);
return out.c_str();
}
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const {
@@ -712,28 +712,6 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
this->on_device_capabilities_request();
break;
}
#ifdef USE_SERIAL_PROXY
case SerialProxySetModeRequest::MESSAGE_TYPE: {
SerialProxySetModeRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_set_mode_request"), msg);
#endif
this->on_serial_proxy_set_mode_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxyGetUsbInfoRequest::MESSAGE_TYPE: {
SerialProxyGetUsbInfoRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_get_usb_info_request"), msg);
#endif
this->on_serial_proxy_get_usb_info_request(msg);
break;
}
#endif
default:
break;
}
-7
View File
@@ -235,13 +235,6 @@ class APIServerConnectionBase {
void on_serial_proxy_request(const SerialProxyRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_get_usb_info_request(const SerialProxyGetUsbInfoRequest &value){};
#endif
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
#endif
-12
View File
@@ -404,18 +404,6 @@ void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
}
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
void APIServer::send_serial_proxy_usb_info(const SerialProxyUsbInfo &msg) {
// Unsolicited: a hotplug is rare and the message small, so every client hears of it
// rather than maintaining a subscription for the one client there normally is
for (auto &c : this->active_clients()) {
if (!c->send_message(msg)) {
API_LOG_MSG_DROPPED(TAG, "USB info notification");
}
}
}
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key,
const std::vector<int32_t> *timings) {
-4
View File
@@ -194,10 +194,6 @@ class APIServer final : public Component,
#ifdef USE_ZWAVE_PROXY
void on_zwave_proxy_request(const ZWaveProxyRequest &msg);
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
/// Tell every client that the USB device behind a serial proxy port changed
void send_serial_proxy_usb_info(const SerialProxyUsbInfo &msg);
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
#endif
@@ -45,15 +45,7 @@ void BluedroidGattClient::setup() {
void BluedroidGattClient::loop() {
if (!esp32_ble::global_ble->is_active()) {
// Stack down: no CLOSE_EVT will come. Settle a live link so the consumer
// frees its slot, then re-register the app on the next enable.
auto down_st = this->state();
if (down_st != ClientState::IDLE && down_st != ClientState::INIT) {
this->release_services();
this->set_idle_();
this->listener_->on_connection_state(false, 0, ble_device_base::GATT_ERR_NOT_CONNECTED);
}
this->set_state(ClientState::INIT);
// ble_before_disabled_event_handler() settles the slot.
return;
}
auto st = this->state();
@@ -65,7 +57,7 @@ void BluedroidGattClient::loop() {
ESP_LOGE(TAG, "gattc app register failed: app_id=%d code=%d", this->app_id, ret);
this->mark_failed();
}
// Do not wait for REG_EVT; a dropped event must not wedge the slot.
// Do not wait for REG_EVT; connect() rejects until it lands.
this->set_idle_();
} else if (st == ClientState::DISCONNECTING || this->disconnect_pending()) {
// The one teardown safety net: a lost CLOSE_EVT, or a scheduled
@@ -78,8 +70,8 @@ void BluedroidGattClient::loop() {
this->listener_->on_connection_state(false, 0, ESP_GATT_CONN_TIMEOUT);
}
} else {
// The loop stays on while a link exists (stack-down watch, pre-started
// search flush); it settles only back at IDLE.
// The loop stays on while a link exists (pre-started search flush); it
// settles only back at IDLE.
this->deliver_pending_search_();
if (this->state() == ClientState::IDLE) {
this->disable_loop();
@@ -87,6 +79,22 @@ void BluedroidGattClient::loop() {
}
}
// Stack down: no CLOSE_EVT will come. Settle a live link so the consumer
// frees its slot, then register the app again on the next enable.
void BluedroidGattClient::ble_before_disabled_event_handler() {
auto st = this->state();
if (st != ClientState::IDLE && st != ClientState::INIT) {
this->release_services();
this->set_idle_();
this->listener_->on_connection_state(false, 0, ble_device_base::GATT_ERR_NOT_CONNECTED);
}
// The interface belongs to the torn-down stack.
this->gattc_if_ = ESP_GATT_IF_NONE;
this->set_state(ClientState::INIT);
// An idle slot runs no loop; the INIT branch must run to register again.
this->enable_loop();
}
void BluedroidGattClient::dump_config() {
ESP_LOGCONFIG(TAG, "Bluedroid GATT client %d", this->connection_index_);
if (this->is_failed()) {
@@ -97,6 +105,11 @@ void BluedroidGattClient::dump_config() {
// ---- contract ops ----
int BluedroidGattClient::connect(uint64_t address, uint8_t addr_type) {
if (this->gattc_if_ == ESP_GATT_IF_NONE) {
// Bluedroid drops an open on an unknown interface without any event.
ESP_LOGW(TAG, "[%d] Connect rejected, GATT app not registered", this->connection_index_);
return ble_device_base::GATT_ERR_NOT_CONNECTED;
}
// Only from idle: clobbering DISCONNECTING would open a new link the
// stale CLOSE_EVT then tears down.
if (this->state() != ClientState::IDLE) {
@@ -56,6 +56,7 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
void connect() override;
void disconnect() override;
void ble_before_disabled_event_handler() override;
bool wants_parsed_advertisements() override { return false; }
void on_scan_end() override {}
bool parse_device(const ble_device_base::ESPBTDevice &device) override { return false; }
+5 -1
View File
@@ -23,7 +23,11 @@ from esphome.const import (
)
from esphome.types import ConfigType
from . import CONF_DEBUG_ID, FILTER_SOURCE_FILES, DebugComponent # noqa: F401 pylint: disable=unused-import
from . import ( # noqa: F401 pylint: disable=unused-import
CONF_DEBUG_ID,
FILTER_SOURCE_FILES,
DebugComponent,
)
DEPENDENCIES = ["debug"]
+5 -1
View File
@@ -9,7 +9,11 @@ from esphome.const import (
)
from esphome.types import ConfigType
from . import CONF_DEBUG_ID, FILTER_SOURCE_FILES, DebugComponent # noqa: F401 pylint: disable=unused-import
from . import ( # noqa: F401 pylint: disable=unused-import
CONF_DEBUG_ID,
FILTER_SOURCE_FILES,
DebugComponent,
)
DEPENDENCIES = ["debug"]
@@ -44,7 +44,7 @@ bool DeepSleepComponent::prepare_to_sleep_() {
this->status_set_warning();
ESP_LOGV(TAG, "Waiting for pin to switch state to enter deep sleep...");
}
this->defer_sleep_();
this->next_enter_deep_sleep_ = true;
return false;
}
}
@@ -17,7 +17,6 @@ void DeepSleepComponent::setup() {
void DeepSleepComponent::schedule_sleep_() {
this->next_enter_deep_sleep_ = false;
this->disable_loop();
const optional<uint32_t> run_duration = get_run_duration_();
if (run_duration.has_value()) {
ESP_LOGI(TAG, "Scheduling in %" PRIu32 " ms", *run_duration);
@@ -46,7 +45,7 @@ void DeepSleepComponent::loop() {
void DeepSleepComponent::begin_sleep(bool manual) {
if (this->prevent_ && !manual) {
this->defer_sleep_();
this->next_enter_deep_sleep_ = true;
return;
}
@@ -190,11 +190,6 @@ class DeepSleepComponent final : public Component {
void schedule_sleep_();
bool should_teardown_();
void defer_sleep_() {
this->next_enter_deep_sleep_ = true;
this->enable_loop();
}
#ifdef USE_BK72XX
bool pin_prevents_sleep_(WakeUpPinItem &pin_item) const;
bool get_real_pin_state_(InternalGPIOPin &pin) const { return (pin.digital_read() ^ pin.is_inverted()); }
@@ -100,7 +100,7 @@ bool DeepSleepComponent::prepare_to_sleep_() {
this->status_set_warning();
ESP_LOGW(TAG, "Waiting for wakeup pin state change");
}
this->defer_sleep_();
this->next_enter_deep_sleep_ = true;
return false;
}
return true;
+2 -3
View File
@@ -153,14 +153,13 @@ bool ES7210::configure_mic_gain_() {
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x0f, regv));
// Configure mic 3
// MIC3 uses the ADC3/4 and MIC3/4 clock domains (bits 2 and 4), not the MIC1/2 domains.
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x15, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x10, 0x10));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x0f, regv));
// Configure mic 4
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x15, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00));
ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x10, 0x10));
ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x0f, regv));
+9 -2
View File
@@ -3,11 +3,18 @@ import esphome.codegen as cg
# Re-exported for the many esp32-side users; defined in esphome.const
# and esphome.espidf so the upload/logs fast path can use them without
# importing this package.
from esphome.const import KEY_ESP32, KEY_FLASH_SIZE, KEY_IDF_VERSION, KEY_VARIANT # noqa: F401 # pylint: disable=unused-import
from esphome.const import ( # noqa: F401 # pylint: disable=unused-import
KEY_ESP32,
KEY_FLASH_SIZE,
KEY_IDF_VERSION,
KEY_VARIANT,
)
# Back compat for external components only; in-tree callers import it
# from esphome.espidf directly.
from esphome.espidf import variant_to_idf_target # noqa: F401 # pylint: disable=unused-import
from esphome.espidf import ( # noqa: F401 # pylint: disable=unused-import
variant_to_idf_target,
)
KEY_BOARD = "board"
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
+22 -13
View File
@@ -83,18 +83,23 @@ void ESP32BLE::setup() {
}
}
void ESP32BLE::enable() {
if (this->state_ != BLE_COMPONENT_STATE_DISABLED)
return;
this->state_ = BLE_COMPONENT_STATE_ENABLE;
}
void ESP32BLE::disable() {
if (this->state_ == BLE_COMPONENT_STATE_DISABLED)
return;
this->state_ = BLE_COMPONENT_STATE_DISABLE;
// Queue the transition for loop(). A pending transition the other way is
// cancelled instead, since nothing was torn down or brought up yet; any other
// state is already there or on its way.
void ESP32BLE::request_state_(bool enable) {
if (enable) {
if (this->state_ == BLE_COMPONENT_STATE_DISABLED) {
this->state_ = BLE_COMPONENT_STATE_ENABLE;
} else if (this->state_ == BLE_COMPONENT_STATE_DISABLE) {
this->state_ = BLE_COMPONENT_STATE_ACTIVE;
}
} else {
if (this->state_ == BLE_COMPONENT_STATE_ACTIVE) {
this->state_ = BLE_COMPONENT_STATE_DISABLE;
} else if (this->state_ == BLE_COMPONENT_STATE_ENABLE) {
this->state_ = BLE_COMPONENT_STATE_DISABLED;
}
}
}
#ifdef USE_ESP32_BLE_ADVERTISING
@@ -580,7 +585,11 @@ void ESP32BLE::loop_handle_state_transition_not_active_() {
this->mark_failed();
return;
}
this->state_ = BLE_COMPONENT_STATE_DISABLED;
this->drain_ble_events_();
// A status callback may have asked for BLE back; the stack is down now, so
// that request becomes a bring-up.
this->state_ =
this->state_ == BLE_COMPONENT_STATE_ACTIVE ? BLE_COMPONENT_STATE_ENABLE : BLE_COMPONENT_STATE_DISABLED;
} else if (this->state_ == BLE_COMPONENT_STATE_ENABLE) {
ESP_LOGD(TAG, "Enabling");
this->state_ = BLE_COMPONENT_STATE_OFF;
+11 -2
View File
@@ -102,8 +102,8 @@ class ESP32BLE final : public Component {
}
uint32_t get_advertising_cycle_time() const { return this->advertising_cycle_time_; }
void enable();
void disable();
void enable() { this->request_state_(true); }
void disable() { this->request_state_(false); }
ESPHOME_ALWAYS_INLINE bool is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; }
void setup() override;
void loop() override;
@@ -176,6 +176,15 @@ class ESP32BLE final : public Component {
bool ble_setup_();
bool ble_dismantle_();
void request_state_(bool enable);
// Drop what the old stack queued; the next stack reuses the same interface ids.
void drain_ble_events_() {
BLEEvent *ble_event;
while ((ble_event = this->ble_events_.pop()) != nullptr) {
this->ble_event_pool_.release(ble_event);
}
this->ble_events_.get_and_reset_dropped_count();
}
bool ble_pre_setup_();
#ifdef USE_ESP32_BLE_ADVERTISING
void advertising_init_();
@@ -42,7 +42,7 @@ void BLEClientBase::set_state(espbt::ClientState st) {
void BLEClientBase::loop() {
if (!esp32_ble::global_ble->is_active()) {
this->set_state(espbt::ClientState::INIT);
// ble_before_disabled_event_handler() resets the client.
return;
}
if (this->state() == espbt::ClientState::INIT) {
@@ -72,6 +72,21 @@ void BLEClientBase::loop() {
float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
void BLEClientBase::ble_before_disabled_event_handler() {
auto st = this->state();
if (st != espbt::ClientState::IDLE && st != espbt::ClientState::INIT) {
// No CLOSE_EVT will come: free the services and settle the link.
this->release_services();
this->set_idle_();
this->on_disconnect_complete(ESP_GATT_CONN_TERMINATE_LOCAL_HOST);
}
// The interface belongs to the torn-down stack.
this->gattc_if_ = ESP_GATT_IF_NONE;
this->set_state(espbt::ClientState::INIT);
// An idle client runs no loop; the INIT branch must run to register again.
this->enable_loop();
}
void BLEClientBase::dump_config() {
ESP_LOGCONFIG(TAG,
" Address: %s\n"
@@ -93,6 +108,10 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
return false;
if (this->state() != espbt::ClientState::IDLE)
return false;
// Not registered on this stack yet; promoting now would stop the scan for a
// connect that connect() rejects anyway.
if (this->gattc_if_ == ESP_GATT_IF_NONE)
return false;
this->log_event_("Found device");
if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG)
@@ -117,6 +136,15 @@ void BLEClientBase::connect() {
this->connection_index_, this->address_str_);
return;
}
if (this->gattc_if_ == ESP_GATT_IF_NONE) {
// Bluedroid drops an open on an unknown interface without any event.
this->log_warning_("Connect rejected, GATT app not registered");
// INIT stays so loop() still registers; only a promoted client goes back.
if (this->state() == espbt::ClientState::DISCOVERED) {
this->set_state(espbt::ClientState::IDLE);
}
return;
}
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_);
this->paired_ = false;
// A registration whose event never arrived must not block this connection's release.
@@ -199,7 +227,10 @@ void BLEClientBase::release_services() {
#ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH
// Only the cache clean makes the stack's database unsafe to walk.
this->services_released_ = true;
esp_ble_gattc_cache_clean(this->remote_bda_);
// A stack on its way down frees its own cache.
if (esp32_ble::global_ble->is_active()) {
esp_ble_gattc_cache_clean(this->remote_bda_);
}
#endif
}
@@ -41,6 +41,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void connect() override;
esp_err_t pair();
void disconnect() override;
void ble_before_disabled_event_handler() override;
void unconditional_disconnect();
void release_services();
@@ -114,7 +115,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
#endif
// Group 3: 4-byte types
int gattc_if_;
int gattc_if_{ESP_GATT_IF_NONE};
esp_gatt_status_t status_{ESP_GATT_OK};
// Group 4: Arrays
@@ -139,7 +140,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
uint8_t pending_notify_regs_{0};
bool auto_connect_{false};
bool paired_{false};
// Set only when release_services() cleans the stack's GATT cache, which no API may then walk
// Set by release_services() on RAM-cache builds; the stack's GATT database must not be walked after it
bool services_released_{false};
// 8 bytes used, no padding
@@ -155,10 +156,11 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
void log_connection_params_(const char *param_type);
void handle_connection_result_(esp_err_t ret);
/// Hook called once a connection has been fully torn down (after release_services() and
/// set_idle_()), from both the CLOSE_EVT handler and the DISCONNECTING safety timeout.
/// set_idle_()): CLOSE_EVT, the DISCONNECTING safety timeout, or the BLE stack going down.
/// Subclasses with extra per-connection accounting (e.g. bluetooth_proxy slot state)
/// override this to release that state. `reason` is the controller reason code, or
/// ESP_GATT_CONN_TIMEOUT for the safety-timeout path.
/// override this to release that state. `reason` is the controller reason code,
/// ESP_GATT_CONN_TIMEOUT for the safety timeout, or ESP_GATT_CONN_TERMINATE_LOCAL_HOST
/// for the stack going down.
virtual void on_disconnect_complete(esp_err_t reason) {}
/// Transition to IDLE and reset conn_id — call when the connection is fully dead.
void set_idle_() {
@@ -62,6 +62,11 @@ void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, u
for (auto *client : this->clients_) {
client->disconnect();
}
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
// The OTA transfer blocks the main loop, so the revert in loop() cannot run. No
// active-connection gate here: every client was just told to disconnect.
this->update_coex_preference_(false);
#endif
#endif
} else if ((state == ota::OTA_ERROR || state == ota::OTA_ABORT) && this->scan_continuous_before_ota_) {
this->scan_continuous_before_ota_ = false;
@@ -74,11 +79,11 @@ void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, u
void ESP32BLETracker::loop() {
if (!this->parent_->is_active()) {
this->ble_was_disabled_ = true;
return;
} else if (this->ble_was_disabled_) {
}
if (this->ble_was_disabled_) {
this->ble_was_disabled_ = false;
// If the BLE stack was disabled, we need to start the scan again.
// First start after boot or after the stack came back.
if (this->scan_continuous_) {
this->start_scan();
}
@@ -218,7 +223,27 @@ void ESP32BLETracker::stop_scan() {
this->stop_scan_();
}
void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_(); }
void ESP32BLETracker::ble_before_disabled_event_handler() {
// Tell the controller to stop; a scan still starting has nothing to stop yet.
if (this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::FAILED) {
this->stop_scan_();
}
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
for (auto *client : this->clients_) {
client->ble_before_disabled_event_handler();
}
this->skip_next_scan_end_ = false;
#endif
// The stop above never completes (stack torn down, events dropped); settle
// here so start_scan_() sees IDLE once the stack is back.
if (this->scanner_state_ != ScannerState::IDLE) {
this->cleanup_scan_state_(true);
}
// A failure latched by the old stack must not be handled against the next.
this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS;
this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
this->ble_was_disabled_ = true;
}
bool ESP32BLETracker::stop_scan_() {
if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
@@ -113,6 +113,9 @@ class ESPBTClient : public ESPBTDeviceListener {
virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0;
virtual void connect() = 0;
virtual void disconnect() = 0;
/// Called right before the BLE stack is dismantled. Nothing in flight will
/// complete, and the GATT app must register again once the stack is back.
virtual void ble_before_disabled_event_handler() {}
bool disconnect_pending() const { return this->want_disconnect_; }
void cancel_pending_disconnect() { this->want_disconnect_ = false; }
@@ -842,7 +842,14 @@ bool ESPHomeOTAComponent::handle_auth_send_() {
const size_t hex_size = hasher.get_size() * 2;
const size_t nonce_len = hasher.get_size() / 4;
const size_t auth_buf_size = 1 + 3 * hex_size;
this->auth_buf_ = std::make_unique<uint8_t[]>(auth_buf_size);
// Internal RAM first: 128 of these bytes go straight into the hardware SHA engine
this->auth_buf_ =
RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::PREFER_INTERNAL).make_unique_array_for_overwrite(auth_buf_size);
if (!this->auth_buf_) {
this->log_auth_warning_(LOG_STR("No memory"));
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN);
return false;
}
this->auth_buf_pos_ = 0;
char *buf = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
+2 -2
View File
@@ -145,13 +145,13 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
#ifdef USE_OTA_PASSWORD
std::string password_;
std::unique_ptr<uint8_t[]> auth_buf_;
RAMUniquePtr<uint8_t[]> auth_buf_;
#endif // USE_OTA_PASSWORD
#ifdef USE_OTA_ENCRYPTION
#ifndef USE_OTA_ENCRYPTION_FROM_API
noise::NoiseContext noise_ctx_;
#endif
std::unique_ptr<NoiseSession> noise_;
RAMUniquePtr<NoiseSession> noise_;
#endif // USE_OTA_ENCRYPTION
socket::ListenSocket *server_{nullptr};
@@ -7,7 +7,6 @@
#include "esphome/core/log.h"
#include <cstring>
#include <new>
#ifdef USE_ESP8266
#include <pgmspace.h>
@@ -43,9 +42,8 @@ ESPHomeOTAComponent::NoiseSession::~NoiseSession() {
bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) {
// A provisioned key cleared between the offer and here is not guarded: the
// session runs on the zero key load_psk fills in and fails the client's MAC.
// Default-init: the frame buffer is written before it is read
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
this->noise_ = std::unique_ptr<NoiseSession>(new (std::nothrow) NoiseSession);
// Default placement, PSRAM first where present: the session only lives for one upload
this->noise_ = RAMAllocator<NoiseSession>().make_unique();
static constexpr size_t PROLOGUE_ACK_LEN = 2; // OTA_RESPONSE_OK + version
static constexpr size_t PROLOGUE_CLIENT_FEATURES_LEN = 1;
static constexpr size_t PROLOGUE_FEATURE_ACK_LEN = 2; // OTA_RESPONSE_FEATURE_FLAGS + server flags
@@ -6,17 +6,21 @@
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <cstring>
#include <new>
namespace esphome::ethernet {
namespace {
// Per-device context returned by init() and handed back to read/write/deinit.
// Context returned by init() and handed back to read/write/deinit. There is one W5500 per device, so a
// single static instance replaces a heap allocation that could fail. It is always clear when init() runs:
// esp_eth_mac_new_w5500() calls deinit() on every failure after init() succeeded, and nothing else
// uninstalls the driver
struct W5500CustomSpiContext {
spi_device_handle_t handle;
SemaphoreHandle_t lock;
};
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) - intentional mutable state
W5500CustomSpiContext w5500_context{};
// Transfers up to the ESP32 SPI hardware FIFO size (64 bytes) stay on the polling path; larger
// transfers (the frame payloads) use the blocking, DMA-backed transmit.
@@ -25,23 +29,20 @@ constexpr uint32_t W5500_SPI_LOCK_TIMEOUT_MS = 50;
void *w5500_custom_spi_init(const void *spi_config) {
const auto *config = static_cast<const eth_w5500_config_t *>(spi_config);
auto *ctx = new (std::nothrow) W5500CustomSpiContext{};
if (ctx == nullptr) {
return nullptr;
}
auto *ctx = &w5500_context;
// The W5500 SPI frame carries the 16-bit address in the command phase and the 8-bit control
// byte in the address phase; mirror what the stock driver configures.
spi_device_interface_config_t devcfg = *config->spi_devcfg;
devcfg.command_bits = 16;
devcfg.address_bits = 8;
if (spi_bus_add_device(config->spi_host_id, &devcfg, &ctx->handle) != ESP_OK) {
delete ctx;
ctx->handle = nullptr;
return nullptr;
}
ctx->lock = xSemaphoreCreateMutex();
if (ctx->lock == nullptr) {
spi_bus_remove_device(ctx->handle);
delete ctx;
ctx->handle = nullptr;
return nullptr;
}
return ctx;
@@ -51,7 +52,7 @@ esp_err_t w5500_custom_spi_deinit(void *spi_ctx) {
auto *ctx = static_cast<W5500CustomSpiContext *>(spi_ctx);
spi_bus_remove_device(ctx->handle);
vSemaphoreDelete(ctx->lock);
delete ctx;
*ctx = {};
return ESP_OK;
}
@@ -53,6 +53,13 @@ void I2SAudioSpeakerBase::dump_config() {
void I2SAudioSpeakerBase::loop() {
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
// A stop that arrives while stopped cancels any start that has not been processed yet
constexpr uint32_t stop_bits = SpeakerEventGroupBits::COMMAND_STOP | SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY;
if ((event_group_bits & stop_bits) && (this->state_ == speaker::STATE_STOPPED)) {
xEventGroupClearBits(this->event_group_, stop_bits | SpeakerEventGroupBits::COMMAND_START);
event_group_bits &= ~(stop_bits | SpeakerEventGroupBits::COMMAND_START);
}
if ((event_group_bits & SpeakerEventGroupBits::COMMAND_START) && (this->state_ == speaker::STATE_STOPPED)) {
this->state_ = speaker::STATE_STARTING;
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START);
@@ -239,8 +246,6 @@ void I2SAudioSpeakerBase::start() {
if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING))
return;
// Mark STARTING immediately to avoid transient STOPPED observations before loop() processes COMMAND_START.
this->state_ = speaker::STATE_STARTING;
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START);
}
@@ -249,11 +254,10 @@ void I2SAudioSpeakerBase::stop() { this->stop_(false); }
void I2SAudioSpeakerBase::finish() { this->stop_(true); }
void I2SAudioSpeakerBase::stop_(bool wait_on_empty) {
if (this->is_failed())
return;
if (this->state_ == speaker::STATE_STOPPED)
if (!this->is_ready() || this->is_failed())
return;
// Always set the bit, even when stopped, so loop() can cancel a start that is still pending
if (wait_on_empty) {
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY);
} else {
+2 -11
View File
@@ -3,7 +3,6 @@
#include "esphome/components/esp32/crash_handler.h"
#include <esp_log.h>
#include <esp_idf_version.h>
#include <driver/uart.h>
#include <soc/soc_caps.h>
@@ -17,10 +16,8 @@
#include <driver/usb_serial_jtag_vfs.h>
#endif
#endif
#if defined(CONFIG_PM_ENABLE) && defined(CONFIG_FREERTOS_USE_TICKLESS_IDLE) && \
(ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0))
#include "esp_sleep.h"
#endif
#include "esp_idf_version.h"
#include "freertos/FreeRTOS.h"
#include <fcntl.h>
@@ -90,12 +87,6 @@ void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) {
// ESP-IDF requires rx_buffer_size > UART_HW_FIFO_LEN (128 bytes).
const int min_rx_buffer_size = UART_HW_FIFO_LEN(uart_num) + 1;
uart_driver_install(uart_num, min_rx_buffer_size, tx_buffer_size, 0, nullptr, 0);
#if defined(CONFIG_PM_ENABLE) && defined(CONFIG_FREERTOS_USE_TICKLESS_IDLE) && \
(ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0))
// Always flush before going to light sleep. Could be disabled for devices
// without TOP_PD or if source_clk = UART_SCLK_RTC
esp_sleep_set_console_uart_handling_mode(ESP_SLEEP_ALWAYS_FLUSH_UART);
#endif
}
void Logger::pre_setup() {
@@ -227,6 +227,7 @@ LIST_ID_SCHEMA = cv.Schema({cv.Required(CONF_ID): cv.use_id(lv_list_t)})
)
async def list_add_text_to_code(config, action_id, template_arg, args):
widgets = await get_widgets(config)
await _wait_list_triggers_completed()
async def do_add_text(w: Widget):
text = await lv_text.process(config[CONF_TEXT])
@@ -370,6 +371,7 @@ async def list_add_to_code(config, action_id, template_arg, args):
_register_lv_uses(w_type_name, w_conf)
_register_dynamic_widget_style_uses(w_conf)
widgets = await get_widgets(config)
await _wait_list_triggers_completed()
async def do_add(w: Widget):
index = None
@@ -503,6 +505,7 @@ LIST_REMOVE_SCHEMA = LIST_ID_SCHEMA.extend(
)
async def list_remove_to_code(config, action_id, template_arg, args):
widgets = await get_widgets(config)
await _wait_list_triggers_completed()
async def do_remove(w: Widget):
index = await lv_int.process(config[CONF_INDEX])
@@ -536,6 +539,7 @@ async def list_remove_to_code(config, action_id, template_arg, args):
)
async def list_clear_to_code(config, action_id, template_arg, args):
widgets = await get_widgets(config)
await _wait_list_triggers_completed()
async def do_clear(w: Widget):
await _wait_list_triggers_completed()
+2 -2
View File
@@ -35,8 +35,8 @@ void MipiDsi::setup() {
.bus_id = 0, // index from 0, specify the DSI host to use
.num_data_lanes =
this->lanes_, // Number of data lanes to use, can't set a value that exceeds the chip's capability
// phy_clk_src left at 0 to enable runtime auto-select.
.lane_bit_rate_mbps = this->lane_bit_rate_, // Bit rate of the data lanes, in Mbps
.phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT, // Clock source for the DPHY
.lane_bit_rate_mbps = this->lane_bit_rate_, // Bit rate of the data lanes, in Mbps
};
auto err = esp_lcd_new_dsi_bus(&bus_config, &this->bus_handle_);
if (err != ESP_OK) {
+42 -135
View File
@@ -26,129 +26,44 @@ static const uint8_t MLX90614_ID4 = 0x3F;
static const char *const TAG = "mlx90614";
// The EEPROM cell has a limited number of write cycles, so stop retrying after a few failures
static constexpr uint8_t EMISSIVITY_WRITE_ATTEMPTS = 3;
// SMBus packet error code: CRC-8 with polynomial 0x07, MSB first
static uint8_t crc8_pec(const uint8_t *data, uint8_t len) { return crc8(data, len, 0x00, 0x07, true); }
void MLX90614Component::setup() {
if (std::isnan(this->emissivity_)) {
if (!this->write_emissivity_()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
this->mark_failed();
return;
}
this->emissivity_write_attempts_ = EMISSIVITY_WRITE_ATTEMPTS;
this->try_write_emissivity_();
if (this->emissivity_write_attempts_ != 0) {
this->status_set_warning(LOG_STR("Failed to write emissivity, will retry"));
}
}
void MLX90614Component::try_write_emissivity_() {
if (this->emissivity_write_attempts_ == 0) {
return;
}
if (this->write_emissivity_()) {
this->emissivity_write_attempts_ = 0;
return;
}
if (--this->emissivity_write_attempts_ == 0) {
ESP_LOGE(TAG, "Giving up on writing emissivity after %u attempts", EMISSIVITY_WRITE_ATTEMPTS);
this->emissivity_write_failed_ = true;
}
}
bool MLX90614Component::write_emissivity_() {
// Skip the write when the EEPROM already holds the desired value to save write cycles
uint16_t current_emissivity;
if (this->read_register_(MLX90614_EMISSIVITY, current_emissivity) != i2c::ERROR_OK) {
return false;
}
const auto desired_emissivity = static_cast<uint16_t>(this->emissivity_ * 0xFFFF);
if (current_emissivity == desired_emissivity) {
if (std::isnan(this->emissivity_))
return true;
}
return this->write_register_(MLX90614_EMISSIVITY, desired_emissivity);
}
bool MLX90614Component::write_register_(uint8_t reg, uint16_t data) {
// The PEC covers the whole write transaction: SLA+W, command, data low, data high
uint8_t buf[5];
buf[0] = this->address_ << 1;
buf[1] = reg;
// See datasheet 8.3.3.1 EEPROM write sequence
// 1. Write 0x0000 into the cell of interest (erases the cell)
buf[2] = buf[3] = 0;
buf[4] = crc8_pec(buf, 4);
auto ec = this->write_register(reg, buf + 2, 3);
if (ec != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Can't erase register 0x%02X, error %d", reg, ec);
uint16_t value = (uint16_t) (this->emissivity_ * 65535);
if (!this->write_bytes_(MLX90614_EMISSIVITY, 0)) {
return false;
}
// 2. Wait at least 5ms
delay(10);
// 3. Write the new value
if (data != 0) {
buf[2] = data & 0xFF;
buf[3] = data >> 8;
buf[4] = crc8_pec(buf, 4);
ec = this->write_register(reg, buf + 2, 3);
if (ec != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Can't write register 0x%02X, error %d", reg, ec);
return false;
}
// 4. Wait at least 5ms
delay(10);
}
// 5. Read back to confirm the value was stored
uint16_t read_back;
ec = this->read_register_(reg, read_back);
if (ec != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Can't check register 0x%02X value, error %d", reg, ec);
if (!this->write_bytes_(MLX90614_EMISSIVITY, value)) {
return false;
}
if (read_back != data) {
ESP_LOGW(TAG, "Read back mismatch on register 0x%02X. Expected 0x%04X, got 0x%04X", reg, data, read_back);
return false;
}
delay(10);
return true;
}
i2c::ErrorCode MLX90614Component::read_register_(uint8_t reg, uint16_t &data) {
// The PEC covers the whole read transaction: SLA+W, command, SLA+R, data low, data high
uint8_t buf[6];
bool MLX90614Component::write_bytes_(uint8_t reg, uint16_t data) {
uint8_t buf[5];
buf[0] = this->address_ << 1;
buf[1] = reg;
buf[2] = (this->address_ << 1) | 0x01;
const auto ec = this->read_register(reg, buf + 3, 3);
if (ec != i2c::ERROR_OK) {
ESP_LOGW(TAG, "i2c read error %d", ec);
return ec;
}
const auto expected_pec = crc8_pec(buf, 5);
if (buf[5] != expected_pec) {
ESP_LOGW(TAG, "i2c CRC error. Expected 0x%02X, got 0x%02X", expected_pec, buf[5]);
return i2c::ERROR_CRC;
}
data = encode_uint16(buf[4], buf[3]);
return i2c::ERROR_OK;
buf[2] = data & 0xFF;
buf[3] = data >> 8;
buf[4] = crc8(buf, 4, 0x00, 0x07, true);
return this->write_bytes(reg, buf + 2, 3);
}
void MLX90614Component::dump_config() {
ESP_LOGCONFIG(TAG, "MLX90614:");
LOG_I2C_DEVICE(this);
if (this->emissivity_write_attempts_ != 0) {
ESP_LOGW(TAG, " Emissivity not written yet, will retry");
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Ambient", this->ambient_sensor_);
@@ -156,41 +71,33 @@ void MLX90614Component::dump_config() {
}
void MLX90614Component::update() {
// Temperature reads run regardless of the emissivity state so a failure still shows up as NAN
this->try_write_emissivity_();
// Publishes NAN on a bus or CRC failure so a stuck reading is visible instead of silently stale
auto publish_sensor = [this](sensor::Sensor *sensor, uint8_t reg) {
if (sensor == nullptr) {
return i2c::ERROR_OK;
}
uint16_t raw;
const auto ec = this->read_register_(reg, raw);
if (ec != i2c::ERROR_OK) {
sensor->publish_state(NAN);
return ec;
}
// Bit 15 set means the device flagged the reading as invalid
const float temperature = (raw & 0x8000) ? NAN : raw * 0.02f - 273.15f;
ESP_LOGD(TAG, "'%s': Got temperature=%.1f°C", sensor->get_name().c_str(), temperature);
sensor->publish_state(temperature);
return ec;
};
const auto object_ec = publish_sensor(this->object_sensor_, MLX90614_TEMPERATURE_OBJECT_1);
const auto ambient_ec = publish_sensor(this->ambient_sensor_, MLX90614_TEMPERATURE_AMBIENT);
if (object_ec != i2c::ERROR_OK || ambient_ec != i2c::ERROR_OK) {
this->status_set_warning(LOG_STR("Failed to read some sensors"));
} else if (this->emissivity_write_failed_) {
this->status_set_warning(LOG_STR("Failed to write emissivity"));
} else if (this->emissivity_write_attempts_ != 0) {
this->status_set_warning(LOG_STR("Failed to write emissivity, will retry"));
} else {
this->status_clear_warning();
uint8_t emissivity[3];
if (this->read_register(MLX90614_EMISSIVITY, emissivity, 3) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
uint8_t raw_object[3];
if (this->read_register(MLX90614_TEMPERATURE_OBJECT_1, raw_object, 3) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
uint8_t raw_ambient[3];
if (this->read_register(MLX90614_TEMPERATURE_AMBIENT, raw_ambient, 3) != i2c::ERROR_OK) {
this->status_set_warning();
return;
}
float ambient = raw_ambient[1] & 0x80 ? NAN : encode_uint16(raw_ambient[1], raw_ambient[0]) * 0.02f - 273.15f;
float object = raw_object[1] & 0x80 ? NAN : encode_uint16(raw_object[1], raw_object[0]) * 0.02f - 273.15f;
ESP_LOGD(TAG, "Got Temperature=%.1f°C Ambient=%.1f°C", object, ambient);
if (this->ambient_sensor_ != nullptr && !std::isnan(ambient))
this->ambient_sensor_->publish_state(ambient);
if (this->object_sensor_ != nullptr && !std::isnan(object))
this->object_sensor_->publish_state(object);
this->status_clear_warning();
}
} // namespace esphome::mlx90614
+1 -6
View File
@@ -18,18 +18,13 @@ class MLX90614Component final : public PollingComponent, public i2c::I2CDevice {
void set_emissivity(float emissivity) { emissivity_ = emissivity; }
protected:
void try_write_emissivity_();
bool write_emissivity_();
bool write_register_(uint8_t reg, uint16_t data);
i2c::ErrorCode read_register_(uint8_t reg, uint16_t &data);
bool write_bytes_(uint8_t reg, uint16_t data);
sensor::Sensor *ambient_sensor_{nullptr};
sensor::Sensor *object_sensor_{nullptr};
float emissivity_{NAN};
// Remaining attempts to program the emissivity EEPROM cell, bounded to limit cell wear
uint8_t emissivity_write_attempts_{0};
bool emissivity_write_failed_{false};
};
} // namespace esphome::mlx90614
+6 -1
View File
@@ -14,7 +14,12 @@ from esphome.const import (
)
from esphome.core import CORE, TimePeriod
from . import FILTER_SOURCE_FILES, Nextion, nextion_ns, nextion_ref # noqa: F401 pylint: disable=unused-import
from . import ( # noqa: F401 pylint: disable=unused-import
FILTER_SOURCE_FILES,
Nextion,
nextion_ns,
nextion_ref,
)
from .base_component import (
CONF_AUTO_WAKE_ON_TOUCH,
CONF_COMMAND_SPACING,
+74 -78
View File
@@ -13,6 +13,11 @@ namespace esphome::nextion {
static const char *const TAG = "nextion";
// A user entity may be named sleep_wake too; only the internal NO_RESULT command clears the sleeping flag
static bool is_sleep_wake_command(const NextionComponentBase *component) {
return component->get_queue_type() == NextionQueueType::NO_RESULT && component->get_variable_name() == "sleep_wake";
}
// Nextion command terminator: three consecutive 0xFF bytes (per Nextion Instruction Set v1.1).
static constexpr uint8_t COMMAND_DELIMITER[3] = {0xFF, 0xFF, 0xFF};
static constexpr size_t DELIMITER_SIZE = sizeof(COMMAND_DELIMITER);
@@ -163,6 +168,17 @@ bool Nextion::check_connect_() {
#endif // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
}
// NO_RESULT components are owned by their entry; every other component is a user entity. Entry and
// component storage comes from RAMAllocator, so delete is not valid for either.
void Nextion::release_queue_entry_(NextionQueue *nb) {
if (nb->component != nullptr && nb->component->get_queue_type() == NextionQueueType::NO_RESULT) {
nb->component->~NextionComponentBase();
RAMAllocator<NextionComponentBase>().deallocate(nb->component, 1);
}
nb->~NextionQueue();
RAMAllocator<NextionQueue>().deallocate(nb, 1);
}
void Nextion::reset_(bool reset_nextion) {
uint8_t d;
@@ -170,15 +186,12 @@ void Nextion::reset_(bool reset_nextion) {
this->read_byte(&d);
}
for (auto *entry : this->nextion_queue_) {
if (entry->component != nullptr && entry->component->get_queue_type() == NextionQueueType::NO_RESULT) {
delete entry->component; // NOLINT(cppcoreguidelines-owning-memory)
}
delete entry; // NOLINT(cppcoreguidelines-owning-memory)
this->release_queue_entry_(entry);
}
this->nextion_queue_.clear();
#ifdef USE_NEXTION_WAVEFORM
for (auto *entry : this->waveform_queue_) {
delete entry; // NOLINT(cppcoreguidelines-owning-memory)
this->release_queue_entry_(entry);
}
this->waveform_queue_.clear();
#endif // USE_NEXTION_WAVEFORM
@@ -421,6 +434,9 @@ bool Nextion::remove_from_q_(bool report_empty) {
NextionQueue *nb = this->nextion_queue_.front();
if (!nb || !nb->component) {
ESP_LOGE(TAG, "Invalid queue");
if (nb != nullptr) {
this->release_queue_entry_(nb);
}
this->nextion_queue_.pop_front();
return false;
}
@@ -428,13 +444,10 @@ bool Nextion::remove_from_q_(bool report_empty) {
ESP_LOGN(TAG, "Removed: %s", component->get_variable_name().c_str());
if (component->get_queue_type() == NextionQueueType::NO_RESULT) {
if (component->get_variable_name() == "sleep_wake") {
this->is_sleeping_ = false;
}
delete component; // NOLINT(cppcoreguidelines-owning-memory)
if (is_sleep_wake_command(component)) {
this->is_sleeping_ = false;
}
delete nb; // NOLINT(cppcoreguidelines-owning-memory)
this->release_queue_entry_(nb);
this->nextion_queue_.pop_front();
return true;
}
@@ -544,7 +557,7 @@ void Nextion::process_nextion_commands_() {
ESP_LOGW(TAG, "Invalid waveform ID %d/ch %d", component->get_component_id(),
component->get_wave_channel_id());
ESP_LOGN(TAG, "Remove waveform ID %d/ch %d", component->get_component_id(), component->get_wave_channel_id());
delete nb; // NOLINT(cppcoreguidelines-owning-memory)
this->release_queue_entry_(nb);
this->waveform_queue_.pop();
}
#else // USE_NEXTION_WAVEFORM
@@ -647,6 +660,9 @@ void Nextion::process_nextion_commands_() {
NextionQueue *nb = this->nextion_queue_.front();
if (!nb || !nb->component) {
ESP_LOGE(TAG, "Invalid queue entry");
if (nb != nullptr) {
this->release_queue_entry_(nb);
}
this->nextion_queue_.pop_front();
return;
}
@@ -660,7 +676,7 @@ void Nextion::process_nextion_commands_() {
component->set_state_from_string(to_process, true, false);
}
delete nb; // NOLINT(cppcoreguidelines-owning-memory)
this->release_queue_entry_(nb);
this->nextion_queue_.pop_front();
break;
@@ -687,6 +703,9 @@ void Nextion::process_nextion_commands_() {
NextionQueue *nb = this->nextion_queue_.front();
if (!nb || !nb->component) {
ESP_LOGE(TAG, "Invalid queue");
if (nb != nullptr) {
this->release_queue_entry_(nb);
}
this->nextion_queue_.pop_front();
return;
}
@@ -703,7 +722,7 @@ void Nextion::process_nextion_commands_() {
component->set_state_from_int(value, true, false);
}
delete nb; // NOLINT(cppcoreguidelines-owning-memory)
this->release_queue_entry_(nb);
this->nextion_queue_.pop_front();
break;
@@ -890,7 +909,7 @@ void Nextion::process_nextion_commands_() {
ESP_LOGN(TAG, "Send waveform: component id %d, waveform id %d, size %zu", component->get_component_id(),
component->get_wave_channel_id(), buffer_to_send);
component->clear_wave_buffer(buffer_to_send);
delete nb; // NOLINT(cppcoreguidelines-owning-memory)
this->release_queue_entry_(nb);
this->waveform_queue_.pop();
#else // USE_NEXTION_WAVEFORM
ESP_LOGW(TAG, "Waveform transmit ready but waveform not enabled");
@@ -920,14 +939,10 @@ void Nextion::purge_stale_queue_entries_() {
ESP_LOGV(TAG, "Remove old queue '%s':'%s'", component->get_queue_type_string(),
component->get_variable_name().c_str());
if (component->get_queue_type() == NextionQueueType::NO_RESULT) {
if (component->get_variable_name() == "sleep_wake") {
this->is_sleeping_ = false;
}
delete component; // NOLINT(cppcoreguidelines-owning-memory)
if (is_sleep_wake_command(component)) {
this->is_sleeping_ = false;
}
delete *it; // NOLINT(cppcoreguidelines-owning-memory)
this->release_queue_entry_(*it);
it = this->nextion_queue_.erase(it);
} else {
@@ -1079,6 +1094,34 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool
return response.length();
}
// Allocates a queue entry owning a bare NO_RESULT component; nullptr when the queue is full or memory is out
NextionQueue *Nextion::make_no_result_entry_(const std::string &variable_name) {
#ifdef USE_NEXTION_MAX_QUEUE_SIZE
if (this->max_queue_size_ > 0 && this->nextion_queue_.size() >= this->max_queue_size_) {
ESP_LOGW(TAG, "Queue full (%zu), drop: %s", this->nextion_queue_.size(), variable_name.c_str());
return nullptr;
}
#endif
auto *nextion_queue = RAMAllocator<nextion::NextionQueue>().allocate(1);
if (nextion_queue == nullptr) {
ESP_LOGW(TAG, "Queue alloc failed");
return nullptr;
}
new (nextion_queue) nextion::NextionQueue;
nextion_queue->component = RAMAllocator<nextion::NextionComponentBase>().allocate(1);
if (nextion_queue->component == nullptr) {
ESP_LOGW(TAG, "Component alloc failed");
this->release_queue_entry_(nextion_queue);
return nullptr;
}
new (nextion_queue->component) nextion::NextionComponentBase;
nextion_queue->component->set_variable_name(variable_name);
nextion_queue->queue_time = App.get_loop_component_start_time();
return nextion_queue;
}
/**
* @brief Add a command to the Nextion queue that expects no response.
*
@@ -1090,36 +1133,11 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool
* @param variable_name Name of the variable or component associated with the command.
*/
void Nextion::add_no_result_to_queue_(const std::string &variable_name) {
#ifdef USE_NEXTION_MAX_QUEUE_SIZE
if (this->max_queue_size_ > 0 && this->nextion_queue_.size() >= this->max_queue_size_) {
ESP_LOGW(TAG, "Queue full (%zu), drop: %s", this->nextion_queue_.size(), variable_name.c_str());
auto *nextion_queue = this->make_no_result_entry_(variable_name);
if (nextion_queue == nullptr)
return;
}
#endif
RAMAllocator<nextion::NextionQueue> allocator;
nextion::NextionQueue *nextion_queue = allocator.allocate(1);
if (nextion_queue == nullptr) {
ESP_LOGW(TAG, "Queue alloc failed");
return;
}
new (nextion_queue) nextion::NextionQueue();
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
nextion_queue->component = new (std::nothrow) nextion::NextionComponentBase;
if (nextion_queue->component == nullptr) {
ESP_LOGW(TAG, "Component alloc failed");
nextion_queue->~NextionQueue();
allocator.deallocate(nextion_queue, 1);
return;
}
nextion_queue->component->set_variable_name(variable_name);
nextion_queue->queue_time = App.get_loop_component_start_time();
this->nextion_queue_.push_back(nextion_queue);
ESP_LOGN(TAG, "Queue NORESULT: %s", nextion_queue->component->get_variable_name().c_str());
ESP_LOGN(TAG, "Queue NORESULT: %s", variable_name.c_str());
}
/**
@@ -1153,32 +1171,10 @@ void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_n
#ifdef USE_NEXTION_COMMAND_SPACING
void Nextion::add_no_result_to_queue_with_pending_command_(const std::string &variable_name,
const std::string &command) {
#ifdef USE_NEXTION_MAX_QUEUE_SIZE
if (this->max_queue_size_ > 0 && this->nextion_queue_.size() >= this->max_queue_size_) {
ESP_LOGW(TAG, "Queue full (%zu), drop: %s", this->nextion_queue_.size(), variable_name.c_str());
auto *nextion_queue = this->make_no_result_entry_(variable_name);
if (nextion_queue == nullptr)
return;
}
#endif
RAMAllocator<nextion::NextionQueue> allocator;
nextion::NextionQueue *nextion_queue = allocator.allocate(1);
if (nextion_queue == nullptr) {
ESP_LOGW(TAG, "Queue alloc failed");
return;
}
new (nextion_queue) nextion::NextionQueue();
nextion_queue->component = new (std::nothrow) nextion::NextionComponentBase;
if (nextion_queue->component == nullptr) {
ESP_LOGW(TAG, "Component alloc failed");
nextion_queue->~NextionQueue();
allocator.deallocate(nextion_queue, 1);
return;
}
nextion_queue->component->set_variable_name(variable_name);
nextion_queue->queue_time = App.get_loop_component_start_time();
nextion_queue->pending_command = command; // Store command for retry
this->nextion_queue_.push_back(nextion_queue);
ESP_LOGVV(TAG, "Queue with pending command: %s", variable_name.c_str());
}
@@ -1312,7 +1308,7 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) {
ESP_LOGW(TAG, "Queue alloc failed");
return;
}
new (nextion_queue) nextion::NextionQueue();
new (nextion_queue) nextion::NextionQueue;
nextion_queue->component = component;
nextion_queue->queue_time = App.get_loop_component_start_time();
@@ -1334,7 +1330,7 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) {
if (this->send_command_(command)) {
this->nextion_queue_.push_back(nextion_queue);
} else {
delete nextion_queue; // NOLINT(cppcoreguidelines-owning-memory)
this->release_queue_entry_(nextion_queue);
}
#endif // USE_NEXTION_COMMAND_SPACING
}
@@ -1355,14 +1351,14 @@ void Nextion::add_addt_command_to_queue(NextionComponentBase *component) {
ESP_LOGW(TAG, "Queue alloc failed");
return;
}
new (nextion_queue) nextion::NextionQueue();
new (nextion_queue) nextion::NextionQueue;
nextion_queue->component = component;
nextion_queue->queue_time = App.get_loop_component_start_time();
if (!this->waveform_queue_.push(nextion_queue)) {
ESP_LOGW(TAG, "Waveform queue full, drop");
delete nextion_queue; // NOLINT(cppcoreguidelines-owning-memory)
this->release_queue_entry_(nextion_queue);
return;
}
if (this->waveform_queue_.size() == 1)
+2
View File
@@ -1469,6 +1469,8 @@ class Nextion final : public NextionBase, public PollingComponent, public uart::
void all_components_send_state_(bool force_update = false);
uint32_t comok_sent_ = 0;
bool remove_from_q_(bool report_empty = true);
void release_queue_entry_(NextionQueue *nb);
NextionQueue *make_no_result_entry_(const std::string &variable_name);
/**
* @brief Status flags for Nextion display state management
@@ -23,8 +23,7 @@ class NextionComponentBase;
class NextionQueue {
public:
virtual ~NextionQueue() = default;
NextionComponentBase *component;
NextionComponentBase *component{nullptr};
uint32_t queue_time = 0;
// Store command for retry if spacing blocked it
@@ -105,6 +104,6 @@ class NextionComponentBase {
int wave_max_length_ = 255;
#endif // USE_NEXTION_WAVEFORM
bool needs_to_send_update_;
bool needs_to_send_update_{false};
};
} // namespace esphome::nextion
+2 -2
View File
@@ -88,12 +88,12 @@ def encryption_schema(config: ConfigType | None) -> ConfigType:
async def to_code(config: ConfigType) -> None:
cg.add_define("USE_NOISE")
cg.add_library("esphome/noise-c", "0.1.26")
cg.add_library("esphome/noise-c", "0.1.30")
# noise-c depends on libsodium, but declaring it here too lets the
# library manager see the full set up front instead of discovering
# libsodium only after noise-c has downloaded, so the two can download
# in parallel. The version must match noise-c's library.json.
cg.add_library("esphome/libsodium", "1.10021.8")
cg.add_library("esphome/libsodium", "1.10021.11")
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
cg.add_build_flag("-DHAVE_INLINE_ASM=1")
@@ -9,7 +9,6 @@
#include <array>
#include <cstring>
#include <memory>
#include <new>
#include <esp_image_format.h>
#include <esp_partition.h>
#include <esp_rom_crc.h>
@@ -235,9 +234,11 @@ bool IDFOTABackend::verify_signed_image_(const esp_partition_t *incoming) {
// runs mid-OTA on the loop task, on top of the caller's live 1 KB OTA buffer
// and mbedtls's own ~1 KB verify scratch, so keeping it off the stack widens
// a thin margin. One short-lived allocation right before reboot is not the
// fragmentation pattern the project guards against. nothrow so an OOM here
// fails closed like every other error path, rather than aborting.
std::unique_ptr<uint8_t[]> block(new (std::nothrow) uint8_t[SIG_BLOCK_SIZE]);
// fragmentation pattern the project guards against. An OOM returns nullptr
// and fails closed like every other error path. Internal RAM first: the
// block is an esp_partition_read target.
auto block =
RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::PREFER_INTERNAL).make_unique_array_for_overwrite(SIG_BLOCK_SIZE);
if (!block) {
OTA_IDF_SIG_LOG(ESP_LOGE, "out of memory");
return false;
@@ -2,6 +2,8 @@
#ifdef USE_ESP32
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esp_timer.h"
@@ -12,6 +14,9 @@ namespace esphome::router {
static const char *const TAG = "router.speaker";
// Maximum time to wait for the active output to report running after start() before giving up
static const uint32_t STATE_TRANSITION_TIMEOUT_MS = 5000;
static inline uint32_t atomic_subtract_clamped(std::atomic<uint32_t> &var, uint32_t amount) {
uint32_t current = var.load(std::memory_order_acquire);
uint32_t subtracted = 0;
@@ -72,6 +77,7 @@ void Router::loop() {
this->apply_cached_state_to_active_();
this->state_ = speaker::STATE_STARTING;
this->state_start_ms_ = App.get_loop_component_start_time();
active->start();
}
return;
@@ -86,10 +92,17 @@ void Router::loop() {
// set_audio_stream_info() and never reaches the output on its own; if the format
// changed while stopped, only start()'s apply_cached_state_to_active_() pushes it
// down before the output's play()-side auto-start locks in the stale format.
if (active->is_stopped()) {
// While STARTING, ignore a transient stopped report as speaker running state
// is set asynchronously from start(). Timeout if the speaker never transitions.
if (this->state_ == speaker::STATE_STARTING) {
if (active->is_running()) {
this->state_ = speaker::STATE_RUNNING;
} else if ((App.get_loop_component_start_time() - this->state_start_ms_) > STATE_TRANSITION_TIMEOUT_MS) {
ESP_LOGW(TAG, "Active output did not start; giving up");
this->state_ = speaker::STATE_STOPPED;
}
} else if (active->is_stopped()) {
this->state_ = speaker::STATE_STOPPED;
} else if (this->state_ == speaker::STATE_STARTING && active->is_running()) {
this->state_ = speaker::STATE_RUNNING;
}
}
@@ -133,6 +146,8 @@ void Router::start() {
this->frames_in_pipeline_.store(0, std::memory_order_release);
this->apply_cached_state_to_active_();
this->state_ = speaker::STATE_STARTING;
// May run on a producer task, so the cached loop timestamp is not usable here
this->state_start_ms_ = millis();
this->get_active_output()->start();
}
@@ -59,6 +59,9 @@ class Router final : public Component, public speaker::Speaker {
// frames_in_pipeline_.
std::atomic<uint32_t> frames_in_pipeline_{0};
// Set when entering STATE_STARTING; used to time out a start the output never acts on
uint32_t state_start_ms_{0};
bool cached_pause_{false};
void apply_cached_state_to_active_();
-32
View File
@@ -6,17 +6,12 @@ from esphome.components import esp32, network, psram, socket, wifi
import esphome.config_validation as cv
from esphome.const import (
CONF_BUFFER_SIZE,
CONF_ESPHOME,
CONF_FORMAT,
CONF_HEIGHT,
CONF_ID,
CONF_MODEL,
CONF_NAME,
CONF_PROJECT,
CONF_SAMPLE_RATE,
CONF_SOURCE,
CONF_TASK_STACK_IN_PSRAM,
CONF_VERSION,
CONF_WIDTH,
)
from esphome.core import CORE, ID
@@ -32,14 +27,6 @@ DOMAIN = "sendspin"
CONF_DISPLAY_OFFSET = "display_offset"
CONF_SENDSPIN_ID = "sendspin_id"
CONF_FIRMWARE_VERSION = "firmware_version"
CONF_MANUFACTURER = "manufacturer"
# An empty device information string would be sent to the server as an empty value rather than
# falling back, so reject it instead of silently substituting the fallback. The 127 byte cap keeps
# the length prefix of a protobuf string field to a single byte, matching `esphome: project:`.
DEVICE_INFO_STRING = cv.All(cv.string_strict, cv.Length(min=1), cv.ByteLength(max=127))
CONF_INITIAL_STATIC_DELAY = "initial_static_delay"
CONF_FIXED_DELAY = "fixed_delay"
CONF_DECODE_MEMORY = "decode_memory"
@@ -211,9 +198,6 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(SendspinHub),
cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram,
cv.Optional(CONF_MANUFACTURER): DEVICE_INFO_STRING,
cv.Optional(CONF_MODEL): DEVICE_INFO_STRING,
cv.Optional(CONF_FIRMWARE_VERSION): DEVICE_INFO_STRING,
}
),
cv.only_on_esp32,
@@ -264,22 +248,6 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_task_stack_in_psram(True))
psram.request_external_task_stack()
# Device information for the server's client/hello message. Falls back to the project
# information, which is written as `manufacturer.model`. Anything still unset keeps the
# default the hub itself applies: the ESPHome name and version.
project = CORE.config[CONF_ESPHOME].get(CONF_PROJECT, {})
project_manufacturer, _, project_model = project.get(CONF_NAME, "").partition(".")
for value, setter in (
(config.get(CONF_MANUFACTURER) or project_manufacturer, var.set_manufacturer),
(config.get(CONF_MODEL) or project_model, var.set_model),
(
config.get(CONF_FIRMWARE_VERSION) or project.get(CONF_VERSION),
var.set_firmware_version,
),
):
if value:
cg.add(setter(value))
# sendspin-cpp library
esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.7.2")
+4 -12
View File
@@ -76,12 +76,8 @@ void SendspinHub::dump_config() {
ESP_LOGCONFIG(TAG,
"Sendspin Hub:\n"
" Client ID: %s\n"
" Manufacturer: %s\n"
" Model: %s\n"
" Firmware version: %s\n"
" Task stack in PSRAM: %s",
get_client_id_into_buffer(mac_buf), this->manufacturer_, this->get_product_name_(),
this->firmware_version_, YESNO(this->task_stack_in_psram_));
get_client_id_into_buffer(mac_buf), YESNO(this->task_stack_in_psram_));
#ifdef USE_SENDSPIN_ARTWORK
// Slot indices come from the order the image platform entries were declared, so the log is the
@@ -131,19 +127,15 @@ const char *SendspinHub::get_client_id_into_buffer(std::span<char, MAC_ADDRESS_P
return get_mac_address_pretty_into_buffer(buf);
}
const char *SendspinHub::get_product_name_() const {
return this->model_ != nullptr ? this->model_ : App.get_name().c_str();
}
sendspin::SendspinClientConfig SendspinHub::build_client_config_() {
sendspin::SendspinClientConfig config;
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
config.client_id = SendspinHub::get_client_id_into_buffer(mac_buf);
config.name = App.get_friendly_name();
config.product_name = this->get_product_name_();
config.manufacturer = this->manufacturer_;
config.software_version = this->firmware_version_;
config.product_name = App.get_name();
config.manufacturer = "ESPHome";
config.software_version = ESPHOME_VERSION;
config.httpd_psram_stack = this->task_stack_in_psram_;
return config;
@@ -8,7 +8,6 @@
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "esphome/core/version.h"
#include <sendspin/client.h>
#include <sendspin/config.h>
@@ -126,15 +125,6 @@ class SendspinHub final : public Component,
void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; }
/// @brief Sets the device information reported to the server in the `client/hello` message.
///
/// Each takes a pointer to a string literal emitted by codegen, so it must stay valid for the
/// lifetime of the hub. Only called for values the configuration overrides; anything left alone
/// keeps the default described on the member below.
void set_manufacturer(const char *manufacturer) { this->manufacturer_ = manufacturer; }
void set_model(const char *model) { this->model_ = model; }
void set_firmware_version(const char *firmware_version) { this->firmware_version_ = firmware_version; }
// --- Sendspin role specific methods ---
#ifdef USE_SENDSPIN_ARTWORK
@@ -197,9 +187,6 @@ class SendspinHub final : public Component,
/// @brief Builds the SendspinClientConfig from ESPHome configuration and platform info.
sendspin::SendspinClientConfig build_client_config_();
/// @brief Returns the product name reported to the server: the configured model, or the device name.
const char *get_product_name_() const;
/// @brief Writes the active network interface's MAC into @p buf and returns its data pointer.
/// Uses the ethernet MAC if ethernet is configured, otherwise the base MAC (used by wifi).
static const char *get_client_id_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
@@ -281,12 +268,6 @@ class SendspinHub final : public Component,
CallbackManager<void(const sendspin::GroupUpdateObject &)> group_update_callbacks_{};
bool task_stack_in_psram_{false};
// Device information sent in the `client/hello` message. Defaults apply when neither the
// sendspin configuration nor the project information supplies a value.
const char *manufacturer_{"ESPHome"};
const char *model_{nullptr}; // nullptr reports the device name instead
const char *firmware_version_{ESPHOME_VERSION};
};
/// @brief Base class for all sendspin subcomponents.
+3 -34
View File
@@ -18,10 +18,9 @@ from esphome import pins
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NAME, CONF_UART_ID
from esphome.const import CONF_ID, CONF_NAME
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@kbx81"]
@@ -31,18 +30,14 @@ MULTI_CONF = True
serial_proxy_ns = cg.esphome_ns.namespace("serial_proxy")
SerialProxy = serial_proxy_ns.class_("SerialProxy", cg.Component, uart.UARTDevice)
SerialProxyTap = serial_proxy_ns.class_("SerialProxyTap")
api_enums_ns = cg.esphome_ns.namespace("api").namespace("enums")
SerialProxyPortType = api_enums_ns.enum("SerialProxyPortType")
# User-selectable electrical types. USB_SERIAL is deliberately absent: it is derived
# from the uart_id pointing at a usb_uart channel, never set by the user.
SERIAL_PROXY_PORT_TYPES = {
"TTL": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_TTL,
"RS232": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_RS232,
"RS485": SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_RS485,
}
PORT_TYPE_USB_SERIAL = SerialProxyPortType.SERIAL_PROXY_PORT_TYPE_USB_SERIAL
CONF_DTR_PIN = "dtr_pin"
CONF_PORT_TYPE = "port_type"
@@ -67,7 +62,7 @@ CONFIG_SCHEMA = (
{
cv.GenerateID(): cv.declare_id(SerialProxy),
cv.Required(CONF_NAME): cv.string_strict,
cv.Optional(CONF_PORT_TYPE): cv.enum(SERIAL_PROXY_PORT_TYPES, upper=True),
cv.Required(CONF_PORT_TYPE): cv.enum(SERIAL_PROXY_PORT_TYPES, upper=True),
cv.Optional(CONF_RTS_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_DTR_PIN): pins.gpio_output_pin_schema,
}
@@ -77,26 +72,6 @@ CONFIG_SCHEMA = (
)
def _uses_usb_uart(config: ConfigType, full_config: ConfigType) -> bool:
from esphome.components.usb_uart import is_usb_uart_channel
return is_usb_uart_channel(config[CONF_UART_ID], full_config)
def _final_validate(config: ConfigType) -> ConfigType:
if _uses_usb_uart(config, fv.full_config.get()):
if CONF_PORT_TYPE in config:
raise cv.Invalid(
f"{CONF_PORT_TYPE} is set automatically for USB serial ports"
)
elif CONF_PORT_TYPE not in config:
raise cv.Invalid(f"{CONF_PORT_TYPE} is required")
return config
FINAL_VALIDATE_SCHEMA = _final_validate
@coroutine_with_priority(CoroPriority.FINAL)
async def _add_serial_proxy_count_define() -> None:
"""Emit the SERIAL_PROXY_COUNT define once with the final instance count."""
@@ -111,13 +86,7 @@ async def to_code(config: ConfigType) -> None:
await uart.register_uart_device(var, config)
cg.add(cg.App.register_serial_proxy(var))
cg.add(var.set_name(config[CONF_NAME]))
if _uses_usb_uart(config, CORE.config):
cg.add(var.set_port_type(PORT_TYPE_USB_SERIAL))
channel = await cg.get_variable(config[CONF_UART_ID])
cg.add(var.set_usb_channel(channel))
cg.add_define("USE_SERIAL_PROXY_USB_INFO")
else:
cg.add(var.set_port_type(config[CONF_PORT_TYPE]))
cg.add(var.set_port_type(config[CONF_PORT_TYPE]))
cg.add_define("USE_SERIAL_PROXY")
# Track instance count for the FINAL priority define
+34 -230
View File
@@ -12,10 +12,6 @@
#include "esphome/components/api/api_server.h"
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
#include "esphome/components/usb_uart/usb_uart.h"
#endif
namespace esphome::serial_proxy {
static const char *const TAG = "serial_proxy";
@@ -33,64 +29,26 @@ void SerialProxy::setup() {
#ifdef USE_API
// instance_index_ is fixed at registration time; pre-set it so loop() only needs to update data
this->outgoing_msg_.instance = this->instance_index_;
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
// The define is global, so a hardware UART port in the same config also gets here
if (this->usb_channel_ != nullptr) {
this->usb_channel_->get_parent()->add_on_connection_callback(
[this](bool connected) { this->on_usb_connection_changed_(connected); });
}
#endif
#ifdef USE_SERIAL_PROXY_TAP
// A tap sets itself up before this runs (its setup priority is higher), so it may
// already be waiting on the port -- a boot-time handshake with the device, say. Leaving
// the loop enabled is what lets that finish; without it the tap would stall until a
// client happened to subscribe.
if (this->tap_ != nullptr && this->tap_->tap_needs_port()) {
return;
}
#endif
// No subscriber at startup; disable loop until a client subscribes
this->disable_loop();
}
#ifdef USE_SERIAL_PROXY_TAP
void SerialProxy::reset_mode_() {
// The mode belongs to a session, not to the port. Carrying a departed client's choice
// over to the next one would inject protocol bytes into a stream that never asked for
// them -- a firmware upload, or any client built before this request existed and so
// unable to turn it off. Guessing RAW is the safe direction: a client that wanted
// protocol handling and did not ask for it merely sends its own acknowledgements.
if (this->mode_ == api::enums::SERIAL_PROXY_MODE_RAW) {
return;
}
ESP_LOGD(TAG, "Session ended, returning serial proxy [%" PRIu32 "] to RAW mode", this->instance_index_);
this->mode_ = api::enums::SERIAL_PROXY_MODE_RAW;
}
#endif
void SerialProxy::loop() {
#ifdef USE_API
// Detect subscriber disconnect
if (this->api_connection_ != nullptr && (this->api_connection_->is_marked_for_removal() ||
!this->api_connection_->is_connection_setup() || !api_is_connected())) {
ESP_LOGW(TAG, "Subscriber disconnected");
this->api_connection_ = nullptr;
this->reset_mode_();
}
// With no subscriber there is normally nothing to do, but a tap may still need the port
// read -- it does its protocol work precisely while nobody else is listening.
// Safety check — loop should only run when subscribed, but guard against races
if (this->api_connection_ == nullptr) [[unlikely]] {
#ifdef USE_SERIAL_PROXY_TAP
if (this->tap_ == nullptr || !this->tap_->tap_needs_port()) {
this->disable_loop();
return;
}
#else
this->disable_loop();
return;
#endif
}
// Detect subscriber disconnect
if (this->api_connection_->is_marked_for_removal() || !this->api_connection_->is_connection_setup() ||
!api_is_connected()) {
ESP_LOGW(TAG, "Subscriber disconnected");
this->api_connection_ = nullptr;
this->disable_loop();
return;
}
// Read available data from UART and forward to subscribed client
@@ -111,54 +69,11 @@ void __attribute__((noinline)) SerialProxy::read_and_send_(size_t available) {
if (!this->read_array(buffer, to_read))
return;
#ifdef USE_SERIAL_PROXY_TAP
// Before forwarding, so a tap that answers the device (an acknowledgement, say) is not
// waiting on the network round trip to a subscriber that may not even exist.
if (this->tap_observing_()) {
this->tap_->on_device_rx(buffer, to_read);
}
#endif
if (this->api_connection_ == nullptr) {
return;
}
this->outgoing_msg_.set_data(buffer, to_read);
this->api_connection_->send_serial_proxy_data(this->outgoing_msg_);
}
#endif
#ifdef USE_SERIAL_PROXY_TAP
bool SerialProxy::tap_observing_() const {
if (this->tap_ == nullptr) {
return false;
}
// With no subscriber, a tap doing its own protocol work (the boot-time handshake with
// the device, say) is served regardless of mode -- nobody has chosen one yet. Once a
// subscriber holds the port, the mode alone decides, so RAW stays inert.
if (this->api_connection_ == nullptr && this->tap_->tap_needs_port()) {
return true;
}
// Otherwise the mode decides. RAW must be inert: a client that flips to RAW before
// flashing firmware is entitled to a byte pipe with nothing injecting protocol bytes
// into it, and "the tap turned out not to recognise the stream" is not good enough.
return this->mode_ == api::enums::SERIAL_PROXY_MODE_PROTOCOL;
}
void SerialProxy::tap_pump() {
#ifdef USE_API
// Nothing would consume the bytes; leave them in the FIFO
if (!this->tap_observing_() && this->api_connection_ == nullptr) {
return;
}
const size_t available = this->available();
if (available > 0) {
this->read_and_send_(available);
}
#endif
}
#endif
void SerialProxy::dump_config() {
ESP_LOGCONFIG(TAG,
"Serial Proxy [%" PRIu32 "]:\n"
@@ -167,10 +82,9 @@ void SerialProxy::dump_config() {
" RTS Pin: %s\n"
" DTR Pin: %s",
this->instance_index_, this->name_ != nullptr ? this->name_ : "",
this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS485 ? LOG_STR_LITERAL("RS485")
: this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? LOG_STR_LITERAL("RS232")
: this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_USB_SERIAL ? LOG_STR_LITERAL("USB_SERIAL")
: LOG_STR_LITERAL("TTL"),
this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS485 ? LOG_STR_LITERAL("RS485")
: this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? LOG_STR_LITERAL("RS232")
: LOG_STR_LITERAL("TTL"),
this->rts_pin_ != nullptr ? LOG_STR_LITERAL("configured") : LOG_STR_LITERAL("not configured"),
this->dtr_pin_ != nullptr ? LOG_STR_LITERAL("configured") : LOG_STR_LITERAL("not configured"));
}
@@ -178,9 +92,8 @@ void SerialProxy::dump_config() {
SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control,
uint8_t parity, uint8_t stop_bits, uint8_t data_size) {
#ifdef USE_API
if (!this->is_subscriber_(api_connection)) {
ESP_LOGW(TAG, "Ignoring configure request from client without port subscription [%" PRIu32 "]",
this->instance_index_);
if (this->port_claimed_by_other_(api_connection)) {
ESP_LOGW(TAG, "Ignoring configure request from client without port access [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
}
#endif
@@ -212,6 +125,11 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
ESP_LOGW(TAG, "Invalid parity: %u (must be 0-2)", parity);
return SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT;
}
if (flow_control) {
ESP_LOGW(TAG, "Hardware flow control requested but is not yet supported");
return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED;
}
// Skip a no-op reconfigure. Clients routinely re-send identical settings on every
// port open, and on a USB UART each apply is a CDC SET_LINE_CODING control transfer.
// Some bridges watch line-coding changes as a signalling channel (a magic baud
@@ -222,8 +140,7 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
uart::UART_CONFIG_PARITY_ODD,
};
if (uart_comp->get_baud_rate() == baudrate && uart_comp->get_stop_bits() == stop_bits &&
uart_comp->get_data_bits() == data_size && uart_comp->get_parity() == PARITY_MAP[parity] &&
uart_comp->get_flow_control() == flow_control) {
uart_comp->get_data_bits() == data_size && uart_comp->get_parity() == PARITY_MAP[parity]) {
ESP_LOGV(TAG, "Settings unchanged, skipping reconfigure [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
@@ -232,7 +149,6 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
uart_comp->set_baud_rate(baudrate);
uart_comp->set_stop_bits(stop_bits);
uart_comp->set_data_bits(data_size);
uart_comp->set_flow_control(flow_control);
uart_comp->set_parity(PARITY_MAP[parity]);
@@ -243,80 +159,24 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
SerialProxyResult SerialProxy::set_mode_from_client(api::APIConnection *api_connection,
api::enums::SerialProxyMode mode) {
#ifdef USE_API
// Only the live subscriber may change the mode, so the mode cannot outlive a session
if (!this->is_subscriber_(api_connection)) {
ESP_LOGW(TAG, "Ignoring mode request from client without port subscription [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
}
#endif
// Values come from a remote client
if (mode != api::enums::SERIAL_PROXY_MODE_RAW && mode != api::enums::SERIAL_PROXY_MODE_PROTOCOL) {
ESP_LOGW(TAG, "Invalid mode: %" PRIu32, static_cast<uint32_t>(mode));
return SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT;
}
// PROTOCOL on a port with no tap would be a silent no-op; refuse so the client knows
#ifdef USE_SERIAL_PROXY_TAP
const bool has_tap = this->tap_ != nullptr;
#else
const bool has_tap = false;
#endif
if (mode == api::enums::SERIAL_PROXY_MODE_PROTOCOL && !has_tap) {
ESP_LOGW(TAG, "No tap on serial proxy [%" PRIu32 "]; PROTOCOL mode unavailable", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED;
}
ESP_LOGD(TAG, "Serial proxy [%" PRIu32 "] mode set to %s", this->instance_index_,
mode == api::enums::SERIAL_PROXY_MODE_PROTOCOL ? LOG_STR_LITERAL("PROTOCOL") : LOG_STR_LITERAL("RAW"));
#ifdef USE_SERIAL_PROXY_TAP
const bool leaving_protocol_mode =
this->mode_ != api::enums::SERIAL_PROXY_MODE_RAW && mode == api::enums::SERIAL_PROXY_MODE_RAW;
this->mode_ = mode;
// Only for an explicit client request, not for reset_mode_() at the end of a session:
// an ordinary disconnect says nothing about the device, whereas a client deliberately
// asking for raw bytes usually precedes changing what the device is.
if (leaving_protocol_mode && this->tap_ != nullptr) {
this->tap_->on_protocol_disabled();
}
#endif
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
void SerialProxy::write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len) {
#ifdef USE_API
// Bytes from anyone but the live subscriber would interleave with the subscriber's
// traffic -- or with an active tap's -- on the wire
if (!this->is_subscriber_(api_connection)) {
if (this->api_connection_ != nullptr) {
ESP_LOGW(TAG, "Ignoring write from client that does not hold serial proxy [%" PRIu32 "]", this->instance_index_);
} else {
// A legacy client streaming writes without subscribing would flood WARN, one per
// request; writes are the only high-rate, unacknowledged operation, so keep this
// visible without drowning the log
ESP_LOGV(TAG, "Ignoring write from client without port subscription [%" PRIu32 "]", this->instance_index_);
}
// Bytes from a client other than the live subscriber would interleave with the
// subscriber's traffic on the wire
if (this->port_claimed_by_other_(api_connection)) {
ESP_LOGW(TAG, "Ignoring write from client without port access [%" PRIu32 "]", this->instance_index_);
return;
}
#endif
if (data == nullptr || len == 0)
return;
this->write_array(data, len);
#ifdef USE_SERIAL_PROXY_TAP
// After the write, so the tap observes the same ordering the device does
if (this->tap_observing_()) {
this->tap_->on_client_tx(data, len);
}
#endif
}
SerialProxyResult SerialProxy::set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) {
#ifdef USE_API
if (!this->is_subscriber_(api_connection)) {
ESP_LOGW(TAG, "Ignoring modem pin request from client without port subscription [%" PRIu32 "]",
this->instance_index_);
if (this->port_claimed_by_other_(api_connection)) {
ESP_LOGW(TAG, "Ignoring modem pin request from client without port access [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
}
#endif
@@ -342,55 +202,6 @@ SerialProxyResult SerialProxy::set_modem_pins(api::APIConnection *api_connection
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
#ifdef USE_SERIAL_PROXY_USB_INFO
void SerialProxy::on_usb_connection_changed_(bool connected) {
ESP_LOGD(TAG, "USB device %s serial proxy [%" PRIu32 "]",
connected ? LOG_STR_LITERAL("attached to") : LOG_STR_LITERAL("removed from"), this->instance_index_);
#ifdef USE_SERIAL_PROXY_TAP
// Before telling clients, so a tap never acknowledges a frame from the old device after
// a client has been told it is gone. The subscriber and the mode stay: both belong to
// the client's session, and only the client knows whether that session is over.
if (!connected && this->tap_ != nullptr) {
this->tap_->on_device_disconnected();
}
#endif
#ifdef USE_API
if (api::global_api_server == nullptr) {
return;
}
// The message's strings are views into this buffer, which outlives the send below
usb_host::UsbDeviceInfo info;
api::SerialProxyUsbInfo msg{};
msg.instance = this->instance_index_;
this->get_usb_info(info, msg);
api::global_api_server->send_serial_proxy_usb_info(msg);
#endif
}
#ifdef USE_API
void SerialProxy::get_usb_info(usb_host::UsbDeviceInfo &info, api::SerialProxyUsbInfo &msg) const {
if (this->usb_channel_ == nullptr) {
msg.status = api::enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED;
return;
}
if (!this->usb_channel_->get_parent()->get_device_info(info)) {
// No device attached right now; not an error
return;
}
msg.connected = true;
msg.vendor_id = info.vendor_id;
msg.product_id = info.product_id;
msg.bcd_device = info.bcd_device;
msg.interface_number = this->usb_channel_->get_interface_number();
msg.manufacturer = StringRef(info.manufacturer);
msg.product = StringRef(info.product);
msg.serial_number = StringRef(info.serial_number);
// Lives in the channel for as long as the device is attached
msg.interface_description = StringRef(this->usb_channel_->get_interface_string());
}
#endif
#endif
uint32_t SerialProxy::get_modem_pins() const {
return (this->rts_state_ ? static_cast<uint32_t>(SERIAL_PROXY_LINE_STATE_FLAG_RTS) : 0u) |
(this->dtr_state_ ? static_cast<uint32_t>(SERIAL_PROXY_LINE_STATE_FLAG_DTR) : 0u);
@@ -399,8 +210,8 @@ uint32_t SerialProxy::get_modem_pins() const {
SerialProxyResult SerialProxy::flush_port(api::APIConnection *api_connection) {
#ifdef USE_API
// Flushing stalls the port, so it gets the same ownership check as writes
if (!this->is_subscriber_(api_connection)) {
ESP_LOGW(TAG, "Ignoring flush from client without port subscription [%" PRIu32 "]", this->instance_index_);
if (this->port_claimed_by_other_(api_connection)) {
ESP_LOGW(TAG, "Ignoring flush from client without port access [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
}
#endif
@@ -419,6 +230,11 @@ SerialProxyResult SerialProxy::flush_port(api::APIConnection *api_connection) {
}
#ifdef USE_API
bool SerialProxy::port_claimed_by_other_(api::APIConnection *api_connection) const {
return this->api_connection_ != nullptr && this->api_connection_ != api_connection &&
this->api_connection_->is_connection_setup();
}
SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_connection,
api::enums::SerialProxyRequestType type) {
switch (type) {
@@ -436,10 +252,6 @@ SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_conn
return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE;
}
ESP_LOGW(TAG, "Previous subscriber disconnected; taking over subscription");
// End the dead client's session before starting the new one, so its mode
// cannot leak into a session that never asked for it
this->api_connection_ = nullptr;
this->reset_mode_();
}
this->api_connection_ = api_connection;
this->enable_loop();
@@ -452,15 +264,7 @@ SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_conn
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
this->api_connection_ = nullptr;
this->reset_mode_();
#ifdef USE_SERIAL_PROXY_TAP
// Keep the loop alive for a tap that still needs the port (mirrors loop())
if (this->tap_ == nullptr || !this->tap_->tap_needs_port()) {
this->disable_loop();
}
#else
this->disable_loop();
#endif
ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
default:
+4 -137
View File
@@ -20,22 +20,12 @@
#include "esphome/components/api/api_pb2.h"
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
namespace esphome::usb_uart {
class USBUartChannel;
} // namespace esphome::usb_uart
namespace esphome::usb_host {
struct UsbDeviceInfo;
} // namespace esphome::usb_host
#endif
// Forward-declare types needed outside the USE_API guard.
namespace esphome::api {
class APIConnection;
namespace enums {
enum SerialProxyPortType : uint32_t;
enum SerialProxyRequestType : uint32_t;
enum SerialProxyMode : uint32_t;
} // namespace enums
} // namespace esphome::api
@@ -62,41 +52,6 @@ enum class SerialProxyResult : uint8_t {
/// Maximum bytes to read from UART in a single loop iteration
inline constexpr size_t SERIAL_PROXY_MAX_READ_SIZE = 256;
#ifdef USE_SERIAL_PROXY_TAP
/// Observes a port's traffic without owning it, and may inject bytes of its own.
///
/// This exists so protocol-aware behaviour can be layered onto a plain byte pipe without
/// the pipe knowing anything about the protocol: the tap is compiled in only when some
/// component asks for one, so a proxy carrying an RS485 meter pays nothing for it.
///
/// A tap is an observer, never a gatekeeper -- it cannot suppress or alter the bytes
/// flowing in either direction, so a misbehaving tap cannot corrupt the stream.
class SerialProxyTap {
public:
/// Bytes read from the device, before they are forwarded to any subscriber.
virtual void on_device_rx(const uint8_t *data, size_t len) = 0;
/// Bytes a subscriber sent towards the device, after they have been written.
virtual void on_client_tx(const uint8_t *data, size_t len) = 0;
/// True when the port must keep reading even with no subscriber attached, so a tap can
/// do its own protocol work while nobody is listening. Honoured only while no
/// subscriber holds the port; with one attached, the port mode alone decides.
virtual bool tap_needs_port() const = 0;
/// A client explicitly turned protocol handling off for this port. Distinct from the
/// automatic reset when a session ends: this one means a client intends to do something
/// else with the device -- reflash it, most likely -- so anything the tap believes about
/// it should be treated as suspect.
virtual void on_protocol_disabled() = 0;
/// The device behind the port went away -- unplugged, or its power was cut. Whatever
/// session the tap had observed ended with it; the device that appears next starts from
/// scratch and may not even be the same one. Only a USB UART can report this.
virtual void on_device_disconnected() = 0;
};
#endif
class SerialProxy final : public uart::UARTDevice, public Component {
public:
void setup() override;
@@ -122,13 +77,10 @@ class SerialProxy final : public uart::UARTDevice, public Component {
/// Get the port type
api::enums::SerialProxyPortType get_port_type() const { return this->port_type_; }
/// Handle a mode change requested by an API client
SerialProxyResult set_mode_from_client(api::APIConnection *api_connection, api::enums::SerialProxyMode mode);
/// Configure UART parameters and apply them
/// @param api_connection The API connection requesting the change
/// @param baudrate Baud rate in bits per second
/// @param flow_control True to request hardware flow control
/// @param flow_control True to enable hardware flow control
/// @param parity Parity setting (0=none, 1=even, 2=odd)
/// @param stop_bits Number of stop bits (1 or 2)
/// @param data_size Number of data bits (5-8)
@@ -169,84 +121,13 @@ class SerialProxy final : public uart::UARTDevice, public Component {
/// Set the DTR GPIO pin (from YAML configuration)
void set_dtr_pin(GPIOPin *pin) { this->dtr_pin_ = pin; }
#ifdef USE_SERIAL_PROXY_USB_INFO
/// Attach the USB UART channel behind this port (from code generation)
void set_usb_channel(usb_uart::USBUartChannel *channel) { this->usb_channel_ = channel; }
#ifdef USE_API
/// Fill a USB info message for this port. The message's strings are views into info,
/// so info must outlive the send.
void get_usb_info(usb_host::UsbDeviceInfo &info, api::SerialProxyUsbInfo &msg) const;
#endif
#endif
#ifdef USE_SERIAL_PROXY_TAP
/// Attach a traffic observer. At most one, set once at setup time.
void set_tap(SerialProxyTap *tap) { this->tap_ = tap; }
/// Write bytes originating from the tap rather than from a client. Bypasses the
/// subscriber ownership check, but only while the tap is being served bytes -- so a
/// port in RAW mode with a subscriber attached stays inert. Returns false when the
/// bytes were dropped for that reason.
bool write_from_tap(const uint8_t *data, size_t len) {
if (!this->tap_observing_()) {
return false;
}
this->write_array(data, len);
return true;
}
/// Whether the tap is currently being served bytes. Can flip false with no callback
/// (a subscriber attaching in RAW mode, say), so a tap should check before starting
/// protocol work and when a reply seems overdue.
bool tap_is_observed() const { return this->tap_observing_(); }
/// Resume reading after a tap's needs change. loop() disables itself when there is
/// neither a subscriber nor a tap that wants the port, so a tap starting fresh work
/// must ask for it back. Must be called from the main loop.
void tap_request_port() { this->enable_loop(); }
/// Whether the underlying device is present. On a USB UART this tracks enumeration, so
/// a tap can notice the device being unplugged and plugged back in.
bool is_device_connected() const { return this->parent_->is_connected(); }
/// Run one read-and-dispatch cycle immediately. Lets a tap make progress before the
/// main loop is running -- during setup, for instance, while a component is still
/// blocking on can_proceed(). Must not be called from on_device_rx() or
/// on_client_tx(): each nested cycle costs a 256-byte stack frame.
void tap_pump();
#endif
protected:
#ifdef USE_API
/// Read from UART, hand the bytes to any tap, and forward them to a subscriber
/// (slow path with a 256-byte stack buffer)
/// Read from UART and send to API client (slow path with 256-byte stack buffer)
void read_and_send_(size_t available);
/// True when the given connection is the live subscriber. Every port operation
/// (write, configure, modem pins, flush, mode) requires this, so an unsubscribed
/// client can never share the wire with the subscriber or an active tap.
bool is_subscriber_(api::APIConnection *api_connection) const { return this->api_connection_ == api_connection; }
#endif
#ifdef USE_SERIAL_PROXY_TAP
/// Return the port to RAW when a subscriber goes away, so the mode never outlives it
void reset_mode_();
#else
/// Without a tap, PROTOCOL is refused, so the mode is fixed at RAW and there is
/// nothing to reset
void reset_mode_() {}
#endif
#ifdef USE_SERIAL_PROXY_TAP
/// True when the tap should be shown the traffic passing through this port
bool tap_observing_() const;
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
/// The USB device behind this port was attached or removed. Reports the port's new USB
/// identity to every API client, and ends the tap's view of the old device.
void on_usb_connection_changed_(bool connected);
/// True when a live subscriber other than the given connection holds the port
bool port_claimed_by_other_(api::APIConnection *api_connection) const;
#endif
/// Instance index for identifying this proxy in API messages
@@ -266,11 +147,6 @@ class SerialProxy final : public uart::UARTDevice, public Component {
/// Port type
api::enums::SerialProxyPortType port_type_{};
#ifdef USE_SERIAL_PROXY_TAP
/// How the bytes passing through are treated; zero is SERIAL_PROXY_MODE_RAW
api::enums::SerialProxyMode mode_{};
#endif
/// Optional GPIO pins for modem control
GPIOPin *rts_pin_{nullptr};
GPIOPin *dtr_pin_{nullptr};
@@ -278,15 +154,6 @@ class SerialProxy final : public uart::UARTDevice, public Component {
/// Current modem pin states
bool rts_state_{false};
bool dtr_state_{false};
#ifdef USE_SERIAL_PROXY_TAP
SerialProxyTap *tap_{nullptr};
#endif
#ifdef USE_SERIAL_PROXY_USB_INFO
/// The USB UART channel behind this port; nullptr on non-USB ports
usb_uart::USBUartChannel *usb_channel_{nullptr};
#endif
};
} // namespace esphome::serial_proxy
@@ -2,13 +2,7 @@ from esphome import automation
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_CONDITION,
CONF_DEVICE_CLASS,
CONF_ID,
CONF_LAMBDA,
CONF_STATE,
)
from esphome.const import CONF_CONDITION, CONF_ID, CONF_LAMBDA, CONF_STATE
from esphome.cpp_generator import LambdaExpression
from .. import template_ns
@@ -18,11 +12,7 @@ TemplateBinarySensor = template_ns.class_(
)
CONFIG_SCHEMA = (
cv.with_visibility(
binary_sensor.binary_sensor_schema(TemplateBinarySensor),
cv.Visibility.UI,
CONF_DEVICE_CLASS,
)
binary_sensor.binary_sensor_schema(TemplateBinarySensor)
.extend(
{
cv.Exclusive(CONF_LAMBDA, CONF_CONDITION): cv.returning_lambda,
@@ -1,14 +1,10 @@
from esphome.components import button
import esphome.config_validation as cv
from esphome.const import CONF_DEVICE_CLASS
from .. import template_ns
TemplateButton = template_ns.class_("TemplateButton", button.Button)
CONFIG_SCHEMA = cv.with_visibility(
button.button_schema(TemplateButton), cv.Visibility.UI, CONF_DEVICE_CLASS
)
CONFIG_SCHEMA = button.button_schema(TemplateButton)
async def to_code(config):
@@ -6,7 +6,6 @@ from esphome.const import (
CONF_ASSUMED_STATE,
CONF_CLOSE_ACTION,
CONF_CURRENT_OPERATION,
CONF_DEVICE_CLASS,
CONF_ID,
CONF_LAMBDA,
CONF_OPEN_ACTION,
@@ -39,11 +38,7 @@ CONF_HAS_POSITION = "has_position"
CONF_TOGGLE_ACTION = "toggle_action"
CONFIG_SCHEMA = (
cv.with_visibility(
cover.cover_schema(TemplateCover),
cv.Visibility.UI,
CONF_DEVICE_CLASS,
)
cover.cover_schema(TemplateCover)
.extend(
{
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
@@ -1,7 +1,7 @@
import esphome.codegen as cg
from esphome.components import event
import esphome.config_validation as cv
from esphome.const import CONF_DEVICE_CLASS, CONF_EVENT_TYPES
from esphome.const import CONF_EVENT_TYPES
from .. import template_ns
@@ -9,9 +9,7 @@ CODEOWNERS = ["@nohat"]
TemplateEvent = template_ns.class_("TemplateEvent", event.Event, cg.Component)
CONFIG_SCHEMA = cv.with_visibility(
event.event_schema(TemplateEvent), cv.Visibility.UI, CONF_DEVICE_CLASS
).extend(
CONFIG_SCHEMA = event.event_schema(TemplateEvent).extend(
{
cv.Required(CONF_EVENT_TYPES): cv.ensure_list(cv.string_strict),
}
@@ -3,7 +3,6 @@ import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
from esphome.const import (
CONF_DEVICE_CLASS,
CONF_ID,
CONF_INITIAL_VALUE,
CONF_LAMBDA,
@@ -13,7 +12,6 @@ from esphome.const import (
CONF_RESTORE_VALUE,
CONF_SET_ACTION,
CONF_STEP,
CONF_UNIT_OF_MEASUREMENT,
)
from .. import template_ns
@@ -48,12 +46,7 @@ def validate(config):
CONFIG_SCHEMA = cv.All(
cv.with_visibility(
number.number_schema(TemplateNumber),
cv.Visibility.UI,
CONF_DEVICE_CLASS,
CONF_UNIT_OF_MEASUREMENT,
)
number.number_schema(TemplateNumber)
.extend(
{
cv.Required(CONF_MAX_VALUE): cv.float_,
+4 -18
View File
@@ -2,16 +2,7 @@ from esphome import automation
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ACCURACY_DECIMALS,
CONF_DEVICE_CLASS,
CONF_FORCE_UPDATE,
CONF_ID,
CONF_LAMBDA,
CONF_STATE,
CONF_STATE_CLASS,
CONF_UNIT_OF_MEASUREMENT,
)
from esphome.const import CONF_ID, CONF_LAMBDA, CONF_STATE
from .. import template_ns
@@ -20,14 +11,9 @@ TemplateSensor = template_ns.class_(
)
CONFIG_SCHEMA = (
cv.with_visibility(
sensor.sensor_schema(TemplateSensor, accuracy_decimals=1),
cv.Visibility.UI,
CONF_UNIT_OF_MEASUREMENT,
CONF_ACCURACY_DECIMALS,
CONF_DEVICE_CLASS,
CONF_STATE_CLASS,
CONF_FORCE_UPDATE,
sensor.sensor_schema(
TemplateSensor,
accuracy_decimals=1,
)
.extend(
{
@@ -4,7 +4,6 @@ from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import (
CONF_ASSUMED_STATE,
CONF_DEVICE_CLASS,
CONF_ID,
CONF_LAMBDA,
CONF_OPTIMISTIC,
@@ -32,11 +31,7 @@ def validate(config):
CONFIG_SCHEMA = cv.All(
cv.with_visibility(
switch.switch_schema(TemplateSwitch),
cv.Visibility.UI,
CONF_DEVICE_CLASS,
)
switch.switch_schema(TemplateSwitch)
.extend(
{
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
@@ -3,7 +3,7 @@ import esphome.codegen as cg
from esphome.components import text_sensor
from esphome.components.text_sensor import TextSensorPublishAction
import esphome.config_validation as cv
from esphome.const import CONF_DEVICE_CLASS, CONF_ID, CONF_LAMBDA, CONF_STATE
from esphome.const import CONF_ID, CONF_LAMBDA, CONF_STATE
from .. import template_ns
@@ -12,11 +12,7 @@ TemplateTextSensor = template_ns.class_(
)
CONFIG_SCHEMA = (
cv.with_visibility(
text_sensor.text_sensor_schema(),
cv.Visibility.UI,
CONF_DEVICE_CLASS,
)
text_sensor.text_sensor_schema()
.extend(
{
cv.GenerateID(): cv.declare_id(TemplateTextSensor),
@@ -6,7 +6,6 @@ from esphome.const import (
CONF_ASSUMED_STATE,
CONF_CLOSE_ACTION,
CONF_CURRENT_OPERATION,
CONF_DEVICE_CLASS,
CONF_ID,
CONF_LAMBDA,
CONF_OPEN_ACTION,
@@ -37,11 +36,7 @@ CONF_HAS_POSITION = "has_position"
CONF_TOGGLE_ACTION = "toggle_action"
CONFIG_SCHEMA = (
cv.with_visibility(
valve.valve_schema(TemplateValve),
cv.Visibility.UI,
CONF_DEVICE_CLASS,
)
valve.valve_schema(TemplateValve)
.extend(
{
cv.Optional(CONF_LAMBDA): cv.returning_lambda,
-9
View File
@@ -166,14 +166,6 @@ class UARTComponent {
// @return Baud rate in bits per second.
uint32_t get_baud_rate() const { return baud_rate_; }
// Requests hardware (RTS/CTS) flow control.
// @param flow_control True to request hardware flow control.
void set_flow_control(bool flow_control) { this->flow_control_ = flow_control; }
// Gets whether hardware flow control was requested.
// @return True when hardware flow control was requested.
bool get_flow_control() const { return this->flow_control_; }
#if defined(USE_ESP8266) || defined(USE_ESP32)
/**
* Load the UART settings.
@@ -221,7 +213,6 @@ class UARTComponent {
uint8_t stop_bits_{0};
uint8_t data_bits_{0};
UARTParityOptions parity_{UART_CONFIG_PARITY_NONE};
bool flow_control_{false};
#ifdef USE_UART_DEBUGGER
CallbackManager<void(UARTDirection, uint8_t)> debug_callback_{};
#endif
+2 -49
View File
@@ -26,8 +26,6 @@ USBClient = usb_host_ns.class_("USBClient", Component)
DOMAIN = "usb_host"
CONF_VID = "vid"
CONF_PID = "pid"
CONF_MANUFACTURER = "manufacturer"
CONF_PRODUCT = "product"
CONF_ENABLE_HUBS = "enable_hubs"
CONF_MAX_TRANSFER_REQUESTS = "max_transfer_requests"
CONF_MAX_PACKET_SIZE = "max_packet_size"
@@ -49,46 +47,7 @@ def usb_device_schema(
schema = schema.extend({cv.Optional(CONF_PID, default=pid): cv.hex_uint16_t})
else:
schema = schema.extend({cv.Required(CONF_PID): cv.hex_uint16_t})
return schema.extend(
{
cv.Optional(CONF_MANUFACTURER): cv.string_strict,
cv.Optional(CONF_PRODUCT): cv.string_strict,
}
)
_validate_filters_complete = cv.has_none_or_all_keys(CONF_MANUFACTURER, CONF_PRODUCT)
def validate_usb_clients(configs: list[ConfigType]) -> list[ConfigType]:
"""Reject invalid USB configuration."""
for config in configs:
_validate_filters_complete(config)
for index, first in enumerate(configs):
# Ensure matching logic does not overlap between entries
for second in configs[index + 1 :]:
# A zero VID or PID is a wildcard for that field, so only a differing non-zero
# value separates two entries
if not all(
first[key] == 0 or second[key] == 0 or first[key] == second[key]
for key in (CONF_VID, CONF_PID)
):
continue
# An unset filter constrains nothing, so only a differing value separates them
if not all(
(a := first.get(key)) is None
or (b := second.get(key)) is None
or a == b
for key in (CONF_MANUFACTURER, CONF_PRODUCT)
):
continue
raise cv.Invalid(
f"USB configs overlap: {first[CONF_ID]!r}, {second[CONF_ID]!r}"
)
return configs
return schema
def _set_max_packet_size(config: dict) -> dict:
@@ -113,9 +72,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_MAX_PACKET_SIZE, default=64): cv.one_of(
64, 128, 256, 512, 1024, int=True
),
cv.Optional(CONF_DEVICES): cv.All(
cv.ensure_list(usb_device_schema()), validate_usb_clients
),
cv.Optional(CONF_DEVICES): cv.ensure_list(usb_device_schema()),
}
),
only_on_variant(
@@ -134,10 +91,6 @@ CONFIG_SCHEMA = cv.All(
async def register_usb_client(config: ConfigType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID], config[CONF_VID], config[CONF_PID])
await cg.register_component(var, config)
if (manufacturer := config.get(CONF_MANUFACTURER)) is not None:
cg.add(var.set_manufacturer_filter(manufacturer))
if (product := config.get(CONF_PRODUCT)) is not None:
cg.add(var.set_product_filter(product))
return var
-70
View File
@@ -5,7 +5,6 @@
defined(USE_ESP32_VARIANT_ESP32S31) || defined(USE_ESP32_VARIANT_ESP32H4)
#include "esphome/core/defines.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include <vector>
#include "usb/usb_host.h"
#include <freertos/FreeRTOS.h>
@@ -13,7 +12,6 @@
#include "esphome/core/lock_free_queue.h"
#include "esphome/core/event_pool.h"
#include <atomic>
#include <span>
namespace esphome::usb_host {
@@ -119,24 +117,6 @@ struct UsbEvent {
// callback function type.
// USB string descriptors hold at most 126 characters; one more for the terminator
static constexpr size_t DESC_STRING_BUF_SIZE = 128;
/// Identity of a connected USB device, copied out of the descriptors the USB host
/// stack caches for the lifetime of the connection
struct UsbDeviceInfo {
uint16_t vendor_id;
uint16_t product_id;
uint16_t bcd_device;
char manufacturer[DESC_STRING_BUF_SIZE];
char product[DESC_STRING_BUF_SIZE];
char serial_number[DESC_STRING_BUF_SIZE];
};
/// Copy a USB string descriptor into a NUL-terminated buffer, dropping characters outside
/// Latin-1. A missing descriptor copies as an empty string.
void copy_descriptor_string(const usb_str_desc_t *desc, std::span<char, DESC_STRING_BUF_SIZE> buffer);
enum ClientState {
USB_CLIENT_INIT = 0,
USB_CLIENT_OPEN,
@@ -164,32 +144,6 @@ class USBClient : public Component {
bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback,
const std::vector<uint8_t> &data = {});
/// Copy the connected device's identity out of the cached USB descriptors.
/// Returns false when no device is connected.
bool get_device_info(UsbDeviceInfo &info) const;
/// Read a string descriptor the host stack does not cache, an interface string say, into
/// buffer. Uses a transfer of its own: the pooled ones hold one packet, and a string
/// descriptor can be four times that. The callback runs on the USB task once buffer holds
/// the string (empty on failure). Returns false when nothing was started. One read at a
/// time; main loop only.
bool read_string_descriptor(uint8_t index, std::span<char, DESC_STRING_BUF_SIZE> buffer,
const transfer_cb_t &callback);
/// Narrow which device this client claims, beyond the VID/PID it was constructed
/// with, by requiring a descriptor string to match exactly.
void set_manufacturer_filter(const char *manufacturer) { this->manufacturer_filter_ = manufacturer; }
void set_product_filter(const char *product) { this->product_filter_ = product; }
/// Register a callback for the device this client claims being connected (true) or
/// removed (false). Fires only for a device that passed every filter and was fully
/// opened, so a device another client claims is never reported. Called from the main
/// loop: connected once the device is ready to use (a subclass may hold this back until
/// its own setup of the device has finished), removed after on_disconnected() has run.
template<typename F> void add_on_connection_callback(F &&callback) {
this->connection_callback_.add(std::forward<F>(callback));
}
// Lock-free event queue and pool for USB task to main loop communication
// Must be public for access from static callbacks
LockFreeQueue<UsbEvent, USB_EVENT_QUEUE_SIZE> event_queue;
@@ -207,23 +161,11 @@ class USBClient : public Component {
TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe)
virtual void disconnect();
virtual void on_connected() {}
/// Whether the device's descriptor strings satisfy every filter that is set.
bool descriptor_strings_match_(const usb_device_info_t &dev_info) const;
/// Whether the subclass reports the device as connected itself, once its own setup of
/// the device has finished, rather than as soon as the device has been opened
virtual bool reports_connection_itself() const { return false; }
/// Report the claimed device to the connection callbacks. Idempotent; a subclass that
/// reports itself calls this once the device is ready to use.
void report_connected_();
virtual void on_disconnected() {
// Reset all requests to available (all bits to 0)
this->trq_in_use_.store(0);
}
static void string_descriptor_callback(usb_transfer_t *xfer);
// USB task management
static void usb_task_fn(void *arg);
[[noreturn]] void usb_task_loop_() const;
@@ -231,11 +173,6 @@ class USBClient : public Component {
// Members ordered to minimize struct padding on 32-bit platforms
TransferRequest requests_[MAX_REQUESTS]{};
TaskHandle_t usb_task_handle_{nullptr};
// Dedicated transfer for read_string_descriptor(), allocated on first use and kept
usb_transfer_t *string_transfer_{nullptr};
transfer_cb_t string_callback_;
char *string_buffer_{nullptr};
std::atomic<bool> string_read_busy_{false};
usb_host_client_handle_t handle_{};
usb_device_handle_t device_handle_{};
int device_addr_{-1};
@@ -244,15 +181,8 @@ class USBClient : public Component {
// Bit i = 1: requests_[i] is in use, Bit i = 0: requests_[i] is available
// Supports multiple concurrent consumers and producers (both threads can allocate/deallocate)
std::atomic<trq_bitmask_t> trq_in_use_;
LazyCallbackManager<void(bool)> connection_callback_;
// Descriptor strings a device must report to be claimed; nullptr means no constraint
const char *manufacturer_filter_{nullptr};
const char *product_filter_{nullptr};
uint16_t vid_{};
uint16_t pid_{};
// Whether the connection callbacks were told about the current device, so a removal is
// only ever reported for a device that was reported connected
bool connection_reported_{false};
};
class USBHost final : public Component {
public:
+10 -162
View File
@@ -143,8 +143,10 @@ static void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc
} while (next_desc != NULL);
}
#endif
// bLength (uint8_t, max 255) includes the 2-byte header (bLength and bDescriptorType),
// so character count = (bLength - 2) / 2.
// USB string descriptors: bLength (uint8_t, max 255) includes the 2-byte header (bLength and bDescriptorType).
// Character count = (bLength - 2) / 2, max 126 chars + null terminator.
static constexpr size_t DESC_STRING_BUF_SIZE = 128;
static const char *get_descriptor_string(const usb_str_desc_t *desc, std::span<char, DESC_STRING_BUF_SIZE> buffer) {
if (desc == nullptr || desc->bLength < 2)
return "(unspecified)";
@@ -160,127 +162,6 @@ static const char *get_descriptor_string(const usb_str_desc_t *desc, std::span<c
return buffer.data();
}
// A missing descriptor copies as an empty string, unlike the "(unspecified)"
// placeholder the logging helper above uses
void copy_descriptor_string(const usb_str_desc_t *desc, std::span<char, DESC_STRING_BUF_SIZE> buffer) {
buffer[0] = '\0';
if (desc == nullptr || desc->bLength < 2)
return;
int char_count = (desc->bLength - 2) / 2;
char *p = buffer.data();
char *end = p + buffer.size() - 1;
for (int i = 0; i != char_count && p < end; i++) {
auto c = desc->wData[i];
if (c < 0x100)
*p++ = static_cast<char>(c);
}
*p = '\0';
}
// Descriptor strings are UTF-16, so a character above Latin-1 can never match.
static bool descriptor_string_equals(const usb_str_desc_t *desc, const char *expected) {
const int char_count = (desc == nullptr || desc->bLength < 2) ? 0 : (desc->bLength - 2) / 2;
for (int i = 0; i != char_count; i++) {
const uint16_t c = desc->wData[i];
if (c >= 0x100 || expected[i] == '\0' || static_cast<char>(c) != expected[i])
return false;
}
return expected[char_count] == '\0';
}
bool USBClient::descriptor_strings_match_(const usb_device_info_t &dev_info) const {
if (this->manufacturer_filter_ != nullptr &&
!descriptor_string_equals(dev_info.str_desc_manufacturer, this->manufacturer_filter_))
return false;
if (this->product_filter_ != nullptr && !descriptor_string_equals(dev_info.str_desc_product, this->product_filter_))
return false;
return true;
}
bool USBClient::get_device_info(UsbDeviceInfo &info) const {
if (this->state_ != USB_CLIENT_CONNECTED)
return false;
const usb_device_desc_t *desc;
if (usb_host_get_device_descriptor(this->device_handle_, &desc) != ESP_OK)
return false;
info.vendor_id = desc->idVendor;
info.product_id = desc->idProduct;
info.bcd_device = desc->bcdDevice;
usb_device_info_t dev_info;
if (usb_host_device_info(this->device_handle_, &dev_info) != ESP_OK)
return false;
copy_descriptor_string(dev_info.str_desc_manufacturer, info.manufacturer);
copy_descriptor_string(dev_info.str_desc_product, info.product);
copy_descriptor_string(dev_info.str_desc_serial_num, info.serial_number);
return true;
}
static constexpr size_t MAX_STRING_DESC_SIZE = 255; // bLength is one byte
static constexpr uint16_t LANGID_EN_US = 0x0409;
// CALLBACK CONTEXT: USB task
void USBClient::string_descriptor_callback(usb_transfer_t *xfer) {
auto *client = static_cast<USBClient *>(xfer->context);
TransferStatus status{};
status.error_code = xfer->status;
status.success = xfer->status == USB_TRANSFER_STATUS_COMPLETED;
status.endpoint = xfer->bEndpointAddress;
// The buffer starts with the setup packet; the descriptor follows it
status.data = xfer->data_buffer + SETUP_PACKET_SIZE;
status.data_len =
xfer->actual_num_bytes > static_cast<int>(SETUP_PACKET_SIZE) ? xfer->actual_num_bytes - SETUP_PACKET_SIZE : 0;
const auto *desc = reinterpret_cast<const usb_str_desc_t *>(status.data);
// A short read leaves bLength claiming bytes that never arrived; treat it as missing
const bool complete = status.success && status.data_len >= 2 && desc->bLength <= status.data_len;
copy_descriptor_string(complete ? desc : nullptr,
std::span<char, DESC_STRING_BUF_SIZE>(client->string_buffer_, DESC_STRING_BUF_SIZE));
client->string_read_busy_.store(false);
if (client->string_callback_ != nullptr) {
client->string_callback_(status);
}
}
bool USBClient::read_string_descriptor(uint8_t index, std::span<char, DESC_STRING_BUF_SIZE> buffer,
const transfer_cb_t &callback) {
buffer[0] = '\0';
if (this->state_ != USB_CLIENT_CONNECTED || index == 0) {
return false;
}
if (this->string_transfer_ == nullptr) {
if (usb_host_transfer_alloc(SETUP_PACKET_SIZE + MAX_STRING_DESC_SIZE, 0, &this->string_transfer_) != ESP_OK) {
ESP_LOGE(TAG, "String descriptor transfer alloc failed");
return false;
}
this->string_transfer_->context = this;
this->string_transfer_->callback = string_descriptor_callback;
}
bool idle = false;
if (!this->string_read_busy_.compare_exchange_strong(idle, true)) {
ESP_LOGW(TAG, "String descriptor read already in progress");
return false;
}
auto *setup = reinterpret_cast<usb_setup_packet_t *>(this->string_transfer_->data_buffer);
setup->bmRequestType = USB_DIR_IN | USB_TYPE_STANDARD | USB_RECIP_DEVICE;
setup->bRequest = USB_B_REQUEST_GET_DESCRIPTOR;
setup->wValue = (USB_B_DESCRIPTOR_TYPE_STRING << 8) | index;
// The host stack read the device strings in US English; Linux asks for it first as well,
// so all three report the same text
setup->wIndex = LANGID_EN_US;
setup->wLength = MAX_STRING_DESC_SIZE;
this->string_transfer_->num_bytes = SETUP_PACKET_SIZE + MAX_STRING_DESC_SIZE;
this->string_transfer_->bEndpointAddress = USB_DIR_IN;
this->string_transfer_->device_handle = this->device_handle_;
this->string_buffer_ = buffer.data();
this->string_callback_ = callback;
auto err = usb_host_transfer_submit_control(this->handle_, this->string_transfer_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to submit string descriptor read, err=%s", esp_err_to_name(err));
this->string_read_busy_.store(false);
return false;
}
return true;
}
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *ptr) {
auto *client = static_cast<USBClient *>(ptr);
@@ -421,10 +302,12 @@ void USBClient::handle_open_state_() {
return;
}
ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct);
if ((this->vid_ != 0 && desc->idVendor != this->vid_) || (this->pid_ != 0 && desc->idProduct != this->pid_)) {
ESP_LOGD(TAG, "Not our device, closing");
this->disconnect();
return;
if (desc->idVendor != this->vid_ || desc->idProduct != this->pid_) {
if (this->vid_ != 0 || this->pid_ != 0) {
ESP_LOGD(TAG, "Not our device, closing");
this->disconnect();
return;
}
}
usb_device_info_t dev_info;
err = usb_host_device_info(this->device_handle_, &dev_info);
@@ -433,16 +316,6 @@ void USBClient::handle_open_state_() {
this->disconnect();
return;
}
// Scoped so the buffers do not outlive this cold branch
if (!this->descriptor_strings_match_(dev_info)) {
char buf_manuf[DESC_STRING_BUF_SIZE];
char buf_product[DESC_STRING_BUF_SIZE];
ESP_LOGD(TAG, "Device does not match filter, closing. Manuf: %s; Prod: %s",
get_descriptor_string(dev_info.str_desc_manufacturer, buf_manuf),
get_descriptor_string(dev_info.str_desc_product, buf_product));
this->disconnect();
return;
}
this->state_ = USB_CLIENT_CONNECTED;
char buf_manuf[DESC_STRING_BUF_SIZE];
char buf_product[DESC_STRING_BUF_SIZE];
@@ -463,18 +336,6 @@ void USBClient::handle_open_state_() {
usb_client_print_config_descriptor(config_desc, nullptr);
#endif
this->on_connected();
// on_connected() may have rejected the device (no usable interface, say) and closed it
if (this->state_ == USB_CLIENT_CONNECTED && !this->reports_connection_itself()) {
this->report_connected_();
}
}
void USBClient::report_connected_() {
if (this->state_ != USB_CLIENT_CONNECTED || this->connection_reported_) {
return;
}
this->connection_reported_ = true;
this->connection_callback_.call(true);
}
void USBClient::on_opened(uint8_t addr) {
@@ -545,10 +406,6 @@ TransferRequest *USBClient::get_trq_() {
}
void USBClient::disconnect() {
// Also reached for a device this client opened and then declined, or lost before it was
// ready; neither was reported as connected, so neither is reported as removed
const bool was_reported = this->connection_reported_;
this->connection_reported_ = false;
this->on_disconnected();
auto err = usb_host_device_close(this->handle_, this->device_handle_);
if (err != ESP_OK) {
@@ -557,9 +414,6 @@ void USBClient::disconnect() {
this->state_ = USB_CLIENT_INIT;
this->device_handle_ = nullptr;
this->device_addr_ = -1;
if (was_reported) {
this->connection_callback_.call(false);
}
}
// THREAD CONTEXT: Called from main loop thread only
@@ -703,12 +557,6 @@ void USBClient::dump_config() {
" Vendor id %04X\n"
" Product id %04X",
this->vid_, this->pid_);
if (this->manufacturer_filter_ != nullptr) {
ESP_LOGCONFIG(TAG, " Manufacturer %s", this->manufacturer_filter_);
}
if (this->product_filter_ != nullptr) {
ESP_LOGCONFIG(TAG, " Product %s", this->product_filter_);
}
}
// THREAD CONTEXT: Called from both USB task and main loop threads
// - USB task: Immediately after transfer callback completes
+11 -25
View File
@@ -6,7 +6,6 @@ from esphome.components.usb_host import (
get_max_packet_size,
register_usb_client,
usb_device_schema,
validate_usb_clients,
)
import esphome.config_validation as cv
from esphome.const import (
@@ -17,7 +16,7 @@ from esphome.const import (
CONF_DUMMY_RECEIVER,
CONF_ID,
)
from esphome.core import CORE, ID
from esphome.core import CORE
from esphome.cpp_types import Component
from esphome.types import ConfigType
@@ -28,16 +27,6 @@ usb_uart_ns = cg.esphome_ns.namespace("usb_uart")
USBUartComponent = usb_uart_ns.class_("USBUartComponent", Component)
USBUartChannel = usb_uart_ns.class_("USBUartChannel", UARTComponent)
def is_usb_uart_channel(uart_id: ID, full_config: ConfigType) -> bool:
"""Return True if the given ID refers to a channel of a configured usb_uart device."""
return any(
channel[CONF_ID] == uart_id
for device in full_config.get("usb_uart") or []
for channel in device[CONF_CHANNELS]
)
UARTParityOptions = usb_uart_ns.enum("UARTParityOptions")
UART_PARITY_OPTIONS = {
"NONE": UARTParityOptions.UART_CONFIG_PARITY_NONE,
@@ -155,19 +144,16 @@ def channel_schema(type_: "Type") -> cv.Schema:
)
CONFIG_SCHEMA = cv.All(
cv.ensure_list(
cv.typed_schema(
{
it.name: usb_device_schema(it.cls, it.vid, it.pid).extend(
channel_schema(it)
)
for it in uart_types
},
upper=True,
)
),
validate_usb_clients,
CONFIG_SCHEMA = cv.ensure_list(
cv.typed_schema(
{
it.name: usb_device_schema(it.cls, it.vid, it.pid).extend(
channel_schema(it)
)
for it in uart_types
},
upper=True,
)
)
+2 -3
View File
@@ -88,11 +88,10 @@ std::vector<CdcEps> USBUartTypeCP210X::parse_descriptors(usb_device_handle_t dev
ESP_LOGE(TAG, "in_ep: usb_parse_endpoint_descriptor_by_index failed");
continue;
}
// No communication interface: the data interface is the one a host binds to
if (in_ep->bEndpointAddress & usb_host::USB_DIR_IN) {
cdc_devs.push_back({CdcEps{nullptr, in_ep, out_ep, data_desc->bInterfaceNumber, 0xFF, 0, data_desc->iInterface}});
cdc_devs.push_back({CdcEps{nullptr, in_ep, out_ep, data_desc->bInterfaceNumber}});
} else {
cdc_devs.push_back({CdcEps{nullptr, out_ep, in_ep, data_desc->bInterfaceNumber, 0xFF, 0, data_desc->iInterface}});
cdc_devs.push_back({CdcEps{nullptr, out_ep, in_ep, data_desc->bInterfaceNumber}});
}
}
return cdc_devs;
-3
View File
@@ -217,9 +217,6 @@ static optional<CdcEps> get_uart(const usb_config_desc_t *config_desc, uint8_t i
}
eps.bulk_interface_number = intf_desc->bInterfaceNumber;
eps.bulk_interface_string_index = intf_desc->iInterface;
// No communication interface: the data interface is the one a host binds to
eps.interrupt_interface_number = 0xFF;
return eps;
}
+1 -2
View File
@@ -189,8 +189,7 @@ std::vector<CdcEps> USBUartTypePL2303::parse_descriptors(usb_device_handle_t dev
}
if (in_ep && out_ep) {
cdc_devs.push_back(CdcEps{notify_ep, in_ep, out_ep, intf->bInterfaceNumber, intf->bInterfaceNumber,
intf->iInterface, intf->iInterface});
cdc_devs.push_back(CdcEps{notify_ep, in_ep, out_ep, intf->bInterfaceNumber, intf->bInterfaceNumber});
break; // PL2303 is single-port
}
}
+1 -53
View File
@@ -43,17 +43,14 @@ static optional<CdcEps> get_cdc(const usb_config_desc_t *config_desc, uint8_t in
if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_INT) {
eps.notify_ep = ep;
eps.interrupt_interface_number = intf_desc->bInterfaceNumber;
eps.interrupt_interface_string_index = intf_desc->iInterface;
} else if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_BULK && ep->bEndpointAddress & usb_host::USB_DIR_IN &&
(eps.bulk_interface_number == 0xFF || eps.bulk_interface_number == intf_desc->bInterfaceNumber)) {
eps.in_ep = ep;
eps.bulk_interface_number = intf_desc->bInterfaceNumber;
eps.bulk_interface_string_index = intf_desc->iInterface;
} else if (ep->bmAttributes == USB_BM_ATTRIBUTES_XFER_BULK && !(ep->bEndpointAddress & usb_host::USB_DIR_IN) &&
(eps.bulk_interface_number == 0xFF || eps.bulk_interface_number == intf_desc->bInterfaceNumber)) {
eps.out_ep = ep;
eps.bulk_interface_number = intf_desc->bInterfaceNumber;
eps.bulk_interface_string_index = intf_desc->iInterface;
} else {
ESP_LOGE(TAG, "Unexpected endpoint attributes: %02X", ep->bmAttributes);
continue;
@@ -245,7 +242,7 @@ void USBUartComponent::loop() {
this->chunk_pool_.release(chunk);
// Invoke the RX callback (if registered) immediately after data lands in the
// ring buffer. This lets consumers such as ZigbeeProxyTap process incoming bytes
// ring buffer. This lets consumers such as ZigbeeProxy process incoming bytes
// in the same loop iteration they are delivered, avoiding an extra wakeup cycle.
if (channel->rx_callback_) {
channel->rx_callback_();
@@ -484,7 +481,6 @@ void USBUartTypeCdcAcm::on_disconnected() {
channel->input_started_.store(true);
channel->output_started_.store(true);
channel->input_buffer_.clear();
channel->interface_string_[0] = '\0';
// Drain any pending output chunks and return them to the pool
{
UsbOutputChunk *chunk;
@@ -563,38 +559,11 @@ void USBUartComponent::start_config_(bool reload) {
this->cfg_step_ = 0;
this->cfg_ok_ = true;
this->cfg_in_flight_ = false;
this->cfg_string_done_ = false;
this->cfg_string_in_flight_ = false;
this->cfg_done_.store(false);
this->cfg_active_ = true;
this->enable_loop();
}
bool USBUartComponent::fetch_interface_string_(USBUartChannelBase *channel) {
const CdcEps &eps = channel->cdc_dev_;
const uint8_t index =
eps.interrupt_interface_number != 0xFF ? eps.interrupt_interface_string_index : eps.bulk_interface_string_index;
channel->interface_string_[0] = '\0';
if (index == 0) {
return false;
}
this->cfg_done_.store(false);
const bool submitted =
this->read_string_descriptor(index, channel->interface_string_, [this](const usb_host::TransferStatus &status) {
if (!status.success) {
ESP_LOGW(TAG, "Interface string read failed: %s", esp_err_to_name(status.error_code));
}
// Release: publishes the string before the loop observes cfg_done_.
this->cfg_done_.store(true, std::memory_order_release);
this->enable_loop_soon_any_context();
App.wake_loop_threadsafe();
});
if (!submitted) {
ESP_LOGW(TAG, "Interface string read submit failed");
}
return submitted;
}
void USBUartComponent::config_transfer_(uint8_t type, uint8_t request, uint16_t value, uint16_t index,
const std::vector<uint8_t> &data) {
this->cfg_done_.store(false);
@@ -628,14 +597,6 @@ bool USBUartComponent::run_config_machine_() {
if (!this->cfg_active_)
return false;
if (this->cfg_string_in_flight_) {
// Acquire: pairs with the release in fetch_interface_string_'s callback.
if (!this->cfg_done_.load(std::memory_order_acquire))
return false;
this->cfg_string_in_flight_ = false;
this->cfg_done_.store(false);
}
if (this->cfg_in_flight_) {
// Acquire: pairs with the release in config_transfer_'s callback.
if (!this->cfg_done_.load(std::memory_order_acquire))
@@ -666,16 +627,6 @@ bool USBUartComponent::run_config_machine_() {
? this->cfg_single_
: (this->cfg_channel_idx_ < this->channels_.size() ? this->channels_[this->cfg_channel_idx_] : nullptr);
// Once per channel on init, before its settings: the interface string is part of the
// identity reported to clients, and the connected report waits for this machine.
if (channel != nullptr && !this->cfg_reload_ && !this->cfg_string_done_) {
this->cfg_string_done_ = true;
if (this->fetch_interface_string_(channel)) {
this->cfg_string_in_flight_ = true;
return true;
}
}
if (channel != nullptr && channel->initialised_.load()) {
if (!this->cfg_ok_) {
// A previous step in this channel's sequence failed. Abort the rest. On a full init,
@@ -699,14 +650,11 @@ bool USBUartComponent::run_config_machine_() {
// Advance to the next channel (or finish).
this->cfg_step_ = 0;
this->cfg_ok_ = true;
this->cfg_string_done_ = false;
if (this->cfg_single_ != nullptr) {
this->cfg_active_ = false;
this->cfg_single_ = nullptr;
} else if (++this->cfg_channel_idx_ >= this->channels_.size()) {
this->cfg_active_ = false;
// Init is done and the line settings are on the wire: now the device is ready to use
this->report_connected_();
}
// If the machine just went idle and a reload was requested while it was busy, start it now.
+1 -28
View File
@@ -37,9 +37,6 @@ struct CdcEps {
// Also the wIndex target for CDC class requests (SET_LINE_CODING etc.), so it
// must remain valid even when the interface itself is not claimed.
uint8_t interrupt_interface_number;
// iInterface of each interface; 0 when the device provides no string for it
uint8_t interrupt_interface_string_index;
uint8_t bulk_interface_string_index;
bool interrupt_interface_claimed{false};
};
@@ -166,30 +163,15 @@ class USBUartChannelBase : public uart::UARTComponent, public Parented<USBUartCo
/// Register a callback invoked immediately after data is pushed to the input ring buffer.
/// Called from USBUartComponent::loop() in the main loop context.
/// Allows consumers (e.g. ZigbeeProxyTap) to process bytes in the same loop iteration
/// Allows consumers (e.g. ZigbeeProxy) to process bytes in the same loop iteration
/// they arrive, eliminating one full main-loop-wakeup cycle of latency.
void set_rx_callback(std::function<void()> cb) { this->rx_callback_ = std::move(cb); }
/// Channel index on the bridge
uint8_t get_index() const { return this->index_; }
/// USB interface number a host driver binds to for this channel: the communication
/// interface of a CDC ACM function, otherwise the data interface.
uint8_t get_interface_number() const {
return this->cdc_dev_.interrupt_interface_number != 0xFF ? this->cdc_dev_.interrupt_interface_number
: this->cdc_dev_.bulk_interface_number;
}
/// iInterface string of that interface; empty when the device has none, or until it has
/// been read after the device connected
const char *get_interface_string() const { return this->interface_string_; }
protected:
// Not directly instantiable; construct a concrete channel type instead.
USBUartChannelBase(uint8_t index, uint16_t buffer_size) : input_buffer_(RingBuffer(buffer_size)), index_(index) {}
void check_logger_conflict() override {}
// Larger structures first (8+ bytes)
char interface_string_[usb_host::DESC_STRING_BUF_SIZE]{};
RingBuffer input_buffer_;
LockFreeQueue<UsbOutputChunk, USB_OUTPUT_CHUNK_COUNT> output_queue_;
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
@@ -259,9 +241,6 @@ class USBUartComponent : public usb_host::USBClient {
void start_config_(bool reload);
// Advance the config state machine; called from loop(). Returns true if it did work.
bool run_config_machine_();
// Ask the device for the channel's interface string. Returns true when a transfer was
// submitted; its completion is signalled through cfg_done_ like a config step's.
bool fetch_interface_string_(USBUartChannelBase *channel);
// Per-subclass per-channel settings sequence. For the given zero-based step, issue the
// next control transfer via config_transfer_() and return true, or return false when the
@@ -273,10 +252,6 @@ class USBUartComponent : public usb_host::USBClient {
// (e.g. CH34x chip detection). Same contract as config_step_(). Default: no steps.
virtual bool config_device_step(uint8_t step, bool ok, const uint8_t *response) { return false; }
// The device is only usable once the config machine has applied every channel's line
// settings, so the connected report waits for run_config_machine_() to finish the init
bool reports_connection_itself() const override { return true; }
std::vector<USBUartChannelBase *> channels_{};
// Config state machine
@@ -291,8 +266,6 @@ class USBUartComponent : public usb_host::USBClient {
bool cfg_device_phase_{false};
bool cfg_in_flight_{false};
bool cfg_ok_{true};
bool cfg_string_done_{false};
bool cfg_string_in_flight_{false};
};
class USBUartTypeCdcAcm : public USBUartComponent {
+4 -15
View File
@@ -66,14 +66,13 @@ from esphome.const import (
)
from esphome.core import (
CORE,
ID,
CoroPriority,
EsphomeError,
HexInt,
coroutine_with_priority,
)
import esphome.final_validate as fv
from esphome.types import ConfigType, TemplateArgsType
from esphome.types import ConfigType
from . import wpa2_eap
@@ -209,7 +208,6 @@ WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition)
WiFiAPActiveCondition = wifi_ns.class_("WiFiAPActiveCondition", Condition)
WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action)
WiFiDisableAction = wifi_ns.class_("WiFiDisableAction", automation.Action)
WiFiRoamAction = wifi_ns.class_("WiFiRoamAction", automation.Action)
WiFiConfigureAction = wifi_ns.class_(
"WiFiConfigureAction", automation.Action, cg.Component
)
@@ -625,6 +623,9 @@ async def to_code(config):
networks = config.get(CONF_NETWORKS, [])
if networks:
cg.add(var.init_sta(len(networks)))
if len(networks) > 1:
# The ESP32 scan can filter one SSID in the driver; with several the whole list is kept
cg.add_define("USE_WIFI_MULTI_SSID")
def add_sta(ap: cg.MockObj, network: dict) -> None:
ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP))
@@ -822,18 +823,6 @@ async def wifi_disable_to_code(config, action_id, template_arg, args):
return cg.new_Pvariable(action_id, template_arg)
@automation.register_action(
"wifi.roam", WiFiRoamAction, cv.Schema({}), synchronous=True
)
async def wifi_roam_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> cg.MockObj:
return cg.new_Pvariable(action_id, template_arg)
KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results"
RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save"
RUNTIME_ROAMING_SUPPRESSION_KEY = "wifi_runtime_roaming_suppression"
-5
View File
@@ -31,11 +31,6 @@ template<typename... Ts> class WiFiDisableAction final : public Action<Ts...> {
void play(const Ts &...x) override { global_wifi_component->disable(); }
};
template<typename... Ts> class WiFiRoamAction final : public Action<Ts...> {
public:
void play(const Ts &...x) override { global_wifi_component->force_roam_check(); }
};
template<typename... Ts> class WiFiConfigureAction final : public Action<Ts...>, public Component {
public:
TEMPLATABLE_VALUE(std::string, ssid)
+14 -30
View File
@@ -846,18 +846,17 @@ void WiFiComponent::loop() {
this->notify_connect_state_listeners_();
#endif
// Post-connect roaming: check for better AP. A scan may have been started by an
// explicit force_roam_check() even when post_connect_roaming_ is disabled, so the
// scan must always be consumed here to avoid leaving roaming_state_ stuck.
if (this->is_roaming_scan_active()) {
if (this->scan_done_) {
this->process_roaming_scan_();
// Post-connect roaming: check for better AP
if (this->post_connect_roaming_) {
if (this->is_roaming_scan_active()) {
if (this->scan_done_) {
this->process_roaming_scan_();
}
// else: scan in progress, wait
} else if (this->roaming_state_ == RoamingState::IDLE && this->roaming_attempts_ < ROAMING_MAX_ATTEMPTS &&
now - this->roaming_last_check_ >= ROAMING_CHECK_INTERVAL && !this->roaming_suppressed_()) {
this->check_roaming_(now);
}
// else: scan in progress, wait
} else if (this->post_connect_roaming_ && this->roaming_state_ == RoamingState::IDLE &&
this->roaming_attempts_ < ROAMING_MAX_ATTEMPTS &&
now - this->roaming_last_check_ >= ROAMING_CHECK_INTERVAL && !this->roaming_suppressed_()) {
this->check_roaming_(now);
}
}
break;
@@ -1499,8 +1498,8 @@ void WiFiComponent::check_scanning_finished() {
return;
}
this->scan_done_ = false;
this->has_completed_scan_after_captive_portal_start_ =
true; // Track that we've done a scan since captive portal started
// A driver filtered scan saw one SSID; a portal that started during it still needs a full scan
this->has_completed_scan_after_captive_portal_start_ = !this->is_scan_driver_filtered_();
this->retry_hidden_mode_ = RetryHiddenMode::SCAN_BASED;
if (this->scan_result_.empty()) {
@@ -2416,7 +2415,7 @@ void WiFiComponent::handle_driver_roam_(const bssid_t &bssid, uint8_t channel) {
void WiFiComponent::release_scan_results_() {
if (!this->keep_scan_results_) {
ScanResultsLock lock(this);
#if defined(USE_RP2) || defined(USE_ESP32)
#if defined(USE_RP2)
// std::vector - use swap trick since shrink_to_fit is non-binding
decltype(this->scan_result_)().swap(this->scan_result_);
#else
@@ -2464,17 +2463,6 @@ void WiFiComponent::notify_scan_results_listeners_() {
}
#endif // USE_WIFI_SCAN_RESULTS_LISTENERS
void WiFiComponent::force_roam_check() {
if (!this->is_connected() || this->roaming_state_ != RoamingState::IDLE || this->roaming_suppressed_()) {
ESP_LOGD(TAG, "Roam check requested, but not able to check now");
return;
}
// Reset the attempt counter so a prior run of failed roams doesn't block this explicit request
// Note that this re-arms automatic roaming if enabled.
this->roaming_attempts_ = 0;
this->check_roaming_(millis());
}
void WiFiComponent::check_roaming_(uint32_t now) {
// Guard: not for hidden networks (may not appear in scan)
const WiFiAP *selected = this->get_selected_sta_();
@@ -2496,11 +2484,7 @@ void WiFiComponent::check_roaming_(uint32_t now) {
ESP_LOGD(TAG, "Roam scan (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
this->roaming_state_ = RoamingState::SCANNING;
if (!this->wifi_scan_start_(this->passive_scan_)) {
// Scan failed to start (e.g. busy) - don't get stuck in SCANNING forever
ESP_LOGD(TAG, "Roam scan failed to start");
this->roaming_state_ = RoamingState::IDLE;
}
this->wifi_scan_start_(this->passive_scan_);
}
void WiFiComponent::process_roaming_scan_() {
+11 -11
View File
@@ -178,12 +178,12 @@ struct EAPAuth {
using bssid_t = std::array<uint8_t, 6>;
/// Initial reserve size for filtered scan results (typical: 1-3 matching networks per SSID)
static constexpr size_t WIFI_SCAN_RESULT_FILTERED_RESERVE = 8;
// ESP32 with one configured network: the driver filters the scan by its SSID and only this many of
// its BSSIDs are kept, the strongest ones
static constexpr size_t WIFI_SCAN_RESULT_BOUND = 12;
// Use std::vector for RP2040 (callback-based) and ESP32 (destructive scan API)
// Use FixedVector for ESP8266 and LibreTiny where two-pass exact allocation is possible
#if defined(USE_RP2) || defined(USE_ESP32)
// RP2040's callback delivers results one at a time with no count, so it needs a growable vector
#if defined(USE_RP2)
template<typename T> using wifi_scan_vector_t = std::vector<T>;
#else
template<typename T> using wifi_scan_vector_t = FixedVector<T>;
@@ -565,12 +565,6 @@ class WiFiComponent final : public Component {
void set_keep_scan_results(bool keep_scan_results) { this->keep_scan_results_ = keep_scan_results; }
void set_post_connect_roaming(bool enabled) { this->post_connect_roaming_ = enabled; }
/** Force an immediate post-connect roaming check, bypassing the periodic interval and the
* per-connection attempt limit. Does nothing (besides a debug log) if not connected, if a
* roam scan or connect is already in progress, or if roaming is currently suppressed.
*/
void force_roam_check();
#ifdef USE_WIFI_CONNECT_TRIGGER
Trigger<> *get_connect_trigger() { return &this->connect_trigger_; }
#endif
@@ -954,6 +948,12 @@ class WiFiComponent final : public Component {
uint8_t num_ipv6_addresses_{0};
#endif /* USE_NETWORK_IPV6 */
bool error_from_callback_{false};
#if defined(USE_ESP32) && !defined(USE_WIFI_MULTI_SSID)
bool scan_driver_filtered_{false};
bool is_scan_driver_filtered_() const { return this->scan_driver_filtered_; }
#else
constexpr bool is_scan_driver_filtered_() const { return false; }
#endif
#if defined(USE_ESP8266) || defined(USE_LIBRETINY)
// Platform-specific STA state enum, defined in platform cpp file.
// On ESP8266, written from SDK system context (wifi_event_callback) —
@@ -773,7 +773,11 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
}
}
this->scan_result_.init(count); // Exact allocation
if (!this->scan_result_.try_init(count)) {
ESP_LOGW(TAG, "No memory for %zu scan results", count);
this->scan_done_ = true;
return;
}
// Second pass: store matching networks
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
@@ -909,7 +909,8 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
ESP_LOGV(TAG, "Scan done: status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id);
uint16_t number = it.number;
bool needs_full = this->needs_full_scan_results_();
const bool filtered = this->is_scan_driver_filtered_();
const bool needs_full = this->needs_full_scan_results_();
{
// Mutate in place under the lock; blocking a portal request is fine and
// avoids scratch buffers
@@ -926,8 +927,14 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
return;
}
// Smart reserve: full capacity if needed, small reserve otherwise
this->scan_result_.reserve(needs_full ? number : WIFI_SCAN_RESULT_FILTERED_RESERVE);
const size_t wanted = filtered ? std::min<size_t>(number, WIFI_SCAN_RESULT_BOUND) : number;
// Storage is reused across the scans of one retry cycle and freed on connect; an exhausted
// heap drops this scan and the retry logic scans again
if (this->scan_result_.capacity() < wanted && !this->scan_result_.try_init(wanted)) {
esp_wifi_clear_ap_list();
ESP_LOGW(TAG, "No memory for %zu scan results", wanted);
return;
}
#ifdef USE_ESP32_HOSTED
// getting records one at a time fails on P4 with hosted esp32 WiFi coprocessor
@@ -955,22 +962,38 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
}
#endif // USE_ESP32_HOSTED
// Check C string first - avoid std::string construction for non-matching networks
const char *ssid_cstr = reinterpret_cast<const char *>(record.ssid);
// Only construct std::string and store if needed
if (needs_full || this->matches_configured_network_(ssid_cstr, record.bssid)) {
bssid_t bssid;
std::copy(record.bssid, record.bssid + 6, bssid.begin());
if (!needs_full && !this->matches_configured_network_(ssid_cstr, record.bssid)) {
this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary);
continue;
}
bssid_t bssid;
std::copy(record.bssid, record.bssid + 6, bssid.begin());
if (this->scan_result_.size() < wanted) {
this->scan_result_.emplace_back(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi,
record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0');
} else {
this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary);
continue;
}
// Records arrive in scan order, not by signal, so a bounded store keeps the strongest by
// replacing its weakest entry. Only SSID and signal decide here; a channel or auth constrained
// network hidden behind 12 stronger APs of its own SSID is not a real deployment
WiFiScanResult *weakest = &this->scan_result_[0];
for (auto &res : this->scan_result_) {
if (res.get_rssi() < weakest->get_rssi())
weakest = &res;
}
if (record.rssi <= weakest->get_rssi()) {
this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary);
continue;
}
// Rebuilt in place rather than assigned; assignment pulls in CompactString's operators, 104 B of flash
weakest->~WiFiScanResult();
new (weakest) WiFiScanResult(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi,
record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0');
}
}
ESP_LOGV(TAG, "Scan complete: %u found, %zu stored%s", number, this->scan_result_.size(),
needs_full ? "" : " (filtered)");
filtered ? LOG_STR_LITERAL(" (driver filtered)") : LOG_STR_LITERAL(""));
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
this->notify_scan_results_listeners_();
#endif
@@ -1047,6 +1070,16 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
wifi_scan_config_t config{};
config.ssid = nullptr;
config.bssid = nullptr;
#ifndef USE_WIFI_MULTI_SSID
// One configured network with an SSID: let the driver keep only its APs, so the WiFi library
// holds fewer records during the scan. Full results (portal, provisioning, listeners) and a
// network configured by BSSID alone still scan everything
this->scan_driver_filtered_ =
!this->needs_full_scan_results_() && this->sta_.size() == 1 && !this->sta_[0].get_ssid().empty();
if (this->scan_driver_filtered_) {
config.ssid = const_cast<uint8_t *>(reinterpret_cast<const uint8_t *>(this->sta_[0].get_ssid().c_str()));
}
#endif
config.channel = 0;
config.show_hidden = true;
config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE;
@@ -709,7 +709,11 @@ void WiFiComponent::wifi_scan_done_callback_() {
}
}
this->scan_result_.init(count); // Exact allocation
if (!this->scan_result_.try_init(count)) {
ESP_LOGW(TAG, "No memory for %zu scan results", count);
WiFi.scanDelete();
return;
}
// Second pass: store matching networks
for (int i = 0; i < num; i++) {
@@ -1,7 +1,6 @@
#include "zigbee_time_zephyr.h"
#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_TIME)
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome::zigbee {
@@ -48,7 +47,6 @@ void ZigbeeTime::set_epoch_time(uint32_t epoch) {
this->synchronize_epoch_(epoch);
this->has_time_ = true;
});
App.wake_loop_threadsafe();
}
void ZigbeeTime::zcl_device_cb_(zb_bufid_t bufid) {
+1 -4
View File
@@ -49,8 +49,7 @@ void ZigbeeComponent::factory_reset() {
void ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(ezb_bdb_comm_mode_mask_t mode) {
if (!esp_zigbee_lock_acquire(10 / portTICK_PERIOD_MS)) {
global_zigbee->set_timeout("zb_init", 100, [mode]() { ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(mode); });
App.wake_loop_threadsafe();
global_zigbee->set_timeout("zb_init", 10, [mode]() { ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(mode); });
return;
}
if (ezb_bdb_start_top_level_commissioning(mode) != EZB_ERR_NONE) {
@@ -89,7 +88,6 @@ bool ZigbeeComponent::app_signal_handler(const ezb_app_signal_t *app_signal) {
global_zigbee->set_timeout("zb_init", 1000, []() {
ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(EZB_BDB_MODE_INITIALIZATION);
});
App.wake_loop_threadsafe();
}
} break;
case EZB_BDB_SIGNAL_STEERING: {
@@ -115,7 +113,6 @@ bool ZigbeeComponent::app_signal_handler(const ezb_app_signal_t *app_signal) {
ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(EZB_BDB_MODE_NETWORK_STEERING);
});
}
App.wake_loop_threadsafe();
}
} break;
case EZB_ZDO_SIGNAL_LEAVE: {
+2 -4
View File
@@ -1,10 +1,10 @@
#include "zigbee_zephyr.h"
#if defined(USE_ZIGBEE) && defined(USE_NRF52)
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <zephyr/settings/settings.h>
#include <zephyr/storage/flash_map.h>
#include "esphome/core/hal.h"
#include "esphome/core/wake.h"
extern "C" {
#include <zboss_api.h>
@@ -120,7 +120,7 @@ void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) {
/* Set default response value. */
p_device_cb_param->status = RET_OK;
App.wake_loop_threadsafe();
esphome::wake_loop_threadsafe();
// endpoints are enumerated from 1
if (global_zigbee->callbacks_.size() >= endpoint) {
@@ -138,7 +138,6 @@ void ZigbeeComponent::on_join_(bool factory_new) {
ESP_LOGD(TAG, "Joined the network");
this->join_cb_.call(factory_new);
});
App.wake_loop_threadsafe();
}
void ZigbeeComponent::on_start_() {
@@ -146,7 +145,6 @@ void ZigbeeComponent::on_start_() {
ESP_LOGD(TAG, "Started zigbee stack");
this->start_cb_.call();
});
App.wake_loop_threadsafe();
}
#ifdef USE_ZIGBEE_WIPE_ON_BOOT
@@ -1,47 +0,0 @@
import esphome.codegen as cg
from esphome.components import serial_proxy
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_POWER_SAVE_MODE, CONF_WIFI
import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@kbx81"]
DEPENDENCIES = ["serial_proxy"]
CONF_SERIAL_PROXY_ID = "serial_proxy_id"
zigbee_proxy_tap_ns = cg.esphome_ns.namespace("zigbee_proxy_tap")
ZigbeeProxyTap = zigbee_proxy_tap_ns.class_(
"ZigbeeProxyTap", cg.Component, serial_proxy.SerialProxyTap
)
def _final_validate(config: ConfigType) -> ConfigType:
full_config = fv.full_config.get()
if (wifi_conf := full_config.get(CONF_WIFI)) and (
wifi_conf.get(CONF_POWER_SAVE_MODE, "").lower() != "none"
):
raise cv.Invalid(
f"{CONF_WIFI} {CONF_POWER_SAVE_MODE} must be set to 'none' when using Zigbee proxy"
)
return config
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ZigbeeProxyTap),
cv.Required(CONF_SERIAL_PROXY_ID): cv.use_id(serial_proxy.SerialProxy),
}
).extend(cv.COMPONENT_SCHEMA)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
sp = await cg.get_variable(config[CONF_SERIAL_PROXY_ID])
var = cg.new_Pvariable(config[CONF_ID], sp)
await cg.register_component(var, config)
cg.add_define("USE_ZIGBEE_PROXY_TAP")
# Compiles the tap interface into serial_proxy; without it the port is a plain byte pipe
cg.add_define("USE_SERIAL_PROXY_TAP")
@@ -1,250 +0,0 @@
#include "ash_detector.h"
#ifdef USE_ZIGBEE_PROXY_TAP
namespace esphome::zigbee_proxy_tap {
// Control byte of an RSTACK, and the only ASH version byte that can follow it
static constexpr uint8_t ASH_RSTACK_CONTROL = 0xC1;
static constexpr uint8_t ASH_PROTOCOL_VERSION = 0x02;
static constexpr size_t ASH_RSTACK_BODY_SIZE = 3; // control, version, reset code
static constexpr size_t ASH_CRC_SIZE = 2;
// Smallest legal frame on the wire: a bare control byte plus its CRC
static constexpr size_t ASH_MIN_FRAME_SIZE = 1 + ASH_CRC_SIZE;
// The opening EZSP version command is a constant: control 0x00 (frmNum 0, ackNum 0)
// followed by [seq=0][frameControl=0][frameId=0] randomized by 0x42 0x21 0xA8. Only the
// trailing requested-version byte varies, so the first four bytes pin the frame exactly.
static constexpr uint8_t EZSP_VERSION_CMD_PREFIX[] = {0x00, 0x42, 0x21, 0xA8};
static constexpr size_t EZSP_VERSION_CMD_SIZE = 5;
// Consecutive frames we could not accept, with neither a good frame nor a retransmission
// in between, before concluding the peer is no longer speaking ASH. A real ASH peer must
// retransmit an unacknowledged frame, so the absence of one is the positive evidence
// here -- garbage on the line is not, since noise proves nothing either way.
static constexpr uint8_t MAX_UNCONFIRMED_REJECTS = 4;
static bool ash_reset_code_is_known(uint8_t code) {
switch (code) {
case 0x00: // RESET_UNKNOWN
case 0x01: // RESET_EXTERNAL
case 0x02: // RESET_POWER_ON
case 0x03: // RESET_WATCHDOG
case 0x06: // RESET_ASSERT
case 0x09: // RESET_BOOTLOADER
case 0x0B: // RESET_SOFTWARE
case 0x51: // ERROR_EXCEEDED_MAXIMUM_ACK_TIMEOUT_COUNT
case 0x80: // ERROR_CHIP_SPECIFIC
case 0x81: // RESET_CHIP_SPECIFIC
return true;
default:
return false;
}
}
void AshFrameScanner::begin_frame_() {
this->index_ = 0;
this->crc_ = ASH_CRC_INIT;
this->escaped_ = false;
this->poisoned_ = false;
}
void AshFrameScanner::reset() {
this->begin_frame_();
this->frame_length_ = 0;
this->discarding_ = false;
}
ScanResult AshFrameScanner::feed(uint8_t byte) {
if (byte == ASH_FLAG_BYTE) {
// Snapshot everything the verdict depends on: begin_frame_() clears all of it.
const bool discarding = this->discarding_;
const bool poisoned = this->poisoned_;
const bool escaped = this->escaped_;
const size_t index = this->index_;
const uint16_t crc = this->crc_;
// A FLAG always starts the next frame afresh, whatever preceded it
this->begin_frame_();
this->discarding_ = false;
if (discarding || index == 0) {
// Consecutive delimiters carry no frame at all, so there is nothing to judge
this->frame_length_ = 0;
return ScanResult::NONE;
}
// Running the CRC over the body *and* its trailing CRC bytes leaves zero when
// correct, so validity needs no second pass over the frame.
if (poisoned || escaped || index < ASH_MIN_FRAME_SIZE || crc != 0) {
this->frame_length_ = 0;
return ScanResult::INVALID;
}
this->frame_length_ = index - ASH_CRC_SIZE;
return ScanResult::FRAME;
}
if (this->discarding_) {
return ScanResult::NONE;
}
switch (byte) {
case ASH_CANCEL_BYTE:
// Everything received since the last FLAG is to be ignored
this->begin_frame_();
return ScanResult::NONE;
case ASH_SUBSTITUTE_BYTE:
// A low-level error was flagged; ignore everything up to the next FLAG
this->discarding_ = true;
return ScanResult::NONE;
case ASH_XON_BYTE:
case ASH_XOFF_BYTE:
// Transport flow control, not frame content: skip it without disturbing the frame
return ScanResult::NONE;
case ASH_ESCAPE_BYTE:
this->escaped_ = true;
return ScanResult::NONE;
default:
break;
}
uint8_t value = byte;
if (this->escaped_) {
this->escaped_ = false;
value = byte ^ ASH_XOR_BYTE;
// An escape must decode to a reserved byte; anything else is not ASH framing at all
if (!ash_is_reserved(value)) {
this->poisoned_ = true;
return ScanResult::NONE;
}
}
if (this->index_ >= sizeof(this->buffer_)) {
this->poisoned_ = true;
return ScanResult::NONE;
}
this->buffer_[this->index_++] = value;
this->crc_ = ash_crc16(&value, 1, this->crc_);
return ScanResult::NONE;
}
void AshDetector::reset() {
this->ncp_scanner_.reset();
this->host_scanner_.reset();
this->state_ = AshDetectState::IDLE;
this->rx_sequence_ = 0;
this->ack_owed_ = false;
this->unconfirmed_rejects_ = 0;
}
void AshDetector::from_ncp(uint8_t byte) {
switch (this->ncp_scanner_.feed(byte)) {
case ScanResult::FRAME:
this->handle_ncp_frame_();
break;
case ScanResult::INVALID:
// A delimited chunk that is not a frame. While armed this may be a corrupted ASH
// frame, which the peer will retransmit, or a sign the peer stopped speaking ASH.
// reject_() distinguishes the two by whether a retransmission ever arrives.
this->reject_();
break;
case ScanResult::NONE:
break;
}
}
void AshDetector::handle_ncp_frame_() {
const uint8_t *body = this->ncp_scanner_.frame();
const size_t length = this->ncp_scanner_.length();
const uint8_t control = body[0];
// RSTACK is the only way into the handshake, and the only way back after a firmware
// swap: a Spinel or bootloader NCP never emits one, so those stay unarmed forever.
if (control == ASH_RSTACK_CONTROL) {
if (length == ASH_RSTACK_BODY_SIZE && body[1] == ASH_PROTOCOL_VERSION && ash_reset_code_is_known(body[2])) {
this->state_ = AshDetectState::SAW_RSTACK;
this->rx_sequence_ = 0;
this->ack_owed_ = false;
this->unconfirmed_rejects_ = 0;
}
return;
}
if (this->state_ != AshDetectState::ARMED) {
return;
}
if ((control & 0x80) != 0) {
return; // ACK/NAK/RST/ERROR: nothing is owed for these
}
const uint8_t frame_num = (control >> 4) & ASH_MAX_SEQUENCE;
const bool re_tx = (control & 0x08) != 0;
if (frame_num != this->rx_sequence_) {
// A retransmission still proves the peer is speaking ASH even though we cannot use
// this copy, so it clears the suspicion without being acknowledged.
if (re_tx) {
this->unconfirmed_rejects_ = 0;
} else {
this->reject_();
}
return;
}
this->rx_sequence_ = (this->rx_sequence_ + 1) & ASH_MAX_SEQUENCE;
this->pending_ack_ = this->rx_sequence_;
this->ack_owed_ = true;
this->unconfirmed_rejects_ = 0;
}
void AshDetector::reject_() {
if (this->state_ != AshDetectState::ARMED) {
return;
}
if (++this->unconfirmed_rejects_ >= MAX_UNCONFIRMED_REJECTS) {
this->state_ = AshDetectState::IDLE;
this->unconfirmed_rejects_ = 0;
}
}
void AshDetector::from_host(uint8_t byte) {
if (this->host_scanner_.feed(byte) != ScanResult::FRAME) {
return;
}
if (this->state_ != AshDetectState::SAW_RSTACK) {
return;
}
const uint8_t *body = this->host_scanner_.frame();
if (this->host_scanner_.length() != EZSP_VERSION_CMD_SIZE) {
return;
}
for (size_t i = 0; i < sizeof(EZSP_VERSION_CMD_PREFIX); i++) {
if (body[i] != EZSP_VERSION_CMD_PREFIX[i]) {
return;
}
}
this->state_ = AshDetectState::ARMED;
this->rx_sequence_ = 0;
this->ack_owed_ = false;
this->unconfirmed_rejects_ = 0;
}
bool AshDetector::take_pending_ack(uint8_t &ack_num) {
if (!this->ack_owed_) {
return false;
}
this->ack_owed_ = false;
ack_num = this->pending_ack_;
return true;
}
} // namespace esphome::zigbee_proxy_tap
#endif // USE_ZIGBEE_PROXY_TAP
@@ -1,104 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ZIGBEE_PROXY_TAP
#include "ash_protocol.h"
#include <cstddef>
#include <cstdint>
namespace esphome::zigbee_proxy_tap {
// Decides when it is safe to acknowledge NCP frames on a client's behalf.
//
// The client suppresses its own ACKs, so nobody else will send them, and injecting ASH
// bytes into a stream that is not ASH would corrupt it. Detection is therefore one-sided:
// arm only on the session handshake, which is a fixed byte string, and never on frame
// validity, which non-ASH traffic can satisfy by luck.
//
// RSTACK (NCP -> host) c1 02 <reset_code> <crc> 7e
// version (host -> NCP) 00 42 21 a8 <version^0x54> <crc> 7e
//
// Requiring both, in that order, in opposite directions cannot be satisfied by a
// unidirectional byte stream whatever it contains -- which is exactly the situation
// during a firmware upload. Verified against real .gbl images and real Spinel traffic:
// zero false arms, and neither pattern occurs even as a substring.
//
// Getting it wrong in the other direction is cheap: a frame we decline to acknowledge is
// retransmitted by the NCP, so we see a clean copy and lose only the ack timeout. That
// asymmetry is why this errs towards silence everywhere.
enum class AshDetectState : uint8_t {
IDLE, // Not ASH, or not yet proven to be
SAW_RSTACK, // Handshake half-complete; watching for the version command
ARMED, // Session confirmed; acknowledging on the client's behalf
};
enum class ScanResult : uint8_t {
NONE, // Mid-frame, or a delimiter that carried nothing
FRAME, // frame()/length() hold a complete body with a verified CRC
INVALID, // A delimited chunk arrived but was not a well-formed ASH frame
};
// Reassembles one direction of the byte stream into unstuffed, CRC-checked frames.
// Mirrors bellows' AshProtocol.data_received: FLAG ends a frame, CANCEL discards what
// precedes it, SUBSTITUTE poisons everything up to the next FLAG, and XON/XOFF are
// transport flow control removed without disturbing the frame around them.
class AshFrameScanner {
public:
ScanResult feed(uint8_t byte);
void reset();
// Valid only until the next feed() call, which begins overwriting the buffer.
const uint8_t *frame() const { return this->buffer_; }
size_t length() const { return this->frame_length_; }
private:
void begin_frame_();
// Frames are bounded by the ASH maximum, so a stream carrying no delimiters cannot
// grow the buffer without limit; it just keeps failing.
uint8_t buffer_[MAX_ASH_FRAME_SIZE];
size_t index_{0}; // accumulation position for the frame being read
size_t frame_length_{0}; // body length of the last completed frame
uint16_t crc_{ASH_CRC_INIT};
bool escaped_{false};
bool discarding_{false};
bool poisoned_{false};
};
class AshDetector {
public:
void reset();
// Feed observed traffic. Neither call gates forwarding: the detector only watches.
void from_ncp(uint8_t byte);
void from_host(uint8_t byte);
bool armed() const { return this->state_ == AshDetectState::ARMED; }
// True only while the host direction can affect the state machine, i.e. while waiting
// for the version command. Lets the caller skip scanning that direction entirely the
// rest of the time -- it is the one carrying firmware uploads.
bool needs_host_scan() const { return this->state_ == AshDetectState::SAW_RSTACK; }
// An acknowledgement became owed after the last from_ncp() call. Clears the flag.
bool take_pending_ack(uint8_t &ack_num);
protected:
void handle_ncp_frame_();
void reject_();
AshFrameScanner ncp_scanner_;
AshFrameScanner host_scanner_;
AshDetectState state_{AshDetectState::IDLE};
uint8_t rx_sequence_{0};
uint8_t pending_ack_{0};
uint8_t unconfirmed_rejects_{0};
bool ack_owed_{false};
};
} // namespace esphome::zigbee_proxy_tap
#endif // USE_ZIGBEE_PROXY_TAP
@@ -1,41 +0,0 @@
#include "ash_protocol.h"
namespace esphome::zigbee_proxy_tap {
static const uint16_t CRC_NIBBLE_TABLE[16] = {0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7,
0x8108, 0x9129, 0xA14A, 0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF};
uint16_t ash_crc16(const uint8_t *data, size_t length, uint16_t init) {
uint16_t crc = init;
for (size_t i = 0; i < length; i++) {
crc = static_cast<uint16_t>(crc << 4) ^ CRC_NIBBLE_TABLE[(crc >> 12) ^ (data[i] >> 4)];
crc = static_cast<uint16_t>(crc << 4) ^ CRC_NIBBLE_TABLE[(crc >> 12) ^ (data[i] & 0x0F)];
}
return crc;
}
// Appends a byte with ASH stuffing; the caller sized `output` for the worst case.
static void append_byte_stuffed(uint8_t *output, size_t &pos, uint8_t byte) {
if (ash_is_reserved(byte)) {
output[pos++] = ASH_ESCAPE_BYTE;
output[pos++] = byte ^ ASH_XOR_BYTE;
} else {
output[pos++] = byte;
}
}
size_t ash_build_ack_frame(uint8_t *output, uint8_t ack_num) {
// ACK control byte: 100nrPPP, where PPP is the next frame number expected
const uint8_t control = 0x80 | (ack_num & ASH_MAX_SEQUENCE);
const uint16_t crc = ash_crc16(&control, 1);
size_t pos = 0;
output[pos++] = ASH_FLAG_BYTE;
append_byte_stuffed(output, pos, control);
append_byte_stuffed(output, pos, (crc >> 8) & 0xFF);
append_byte_stuffed(output, pos, crc & 0xFF);
output[pos++] = ASH_FLAG_BYTE;
return pos;
}
} // namespace esphome::zigbee_proxy_tap
@@ -1,51 +0,0 @@
#pragma once
#include <cstddef>
#include <cstdint>
namespace esphome::zigbee_proxy_tap {
// ASH Protocol Constants
static constexpr uint8_t ASH_FLAG_BYTE = 0x7E; // Frame delimiter
static constexpr uint8_t ASH_ESCAPE_BYTE = 0x7D; // Escape/substitution byte
static constexpr uint8_t ASH_XOR_BYTE = 0x20; // XOR mask for escaped bytes
static constexpr uint8_t ASH_SUBSTITUTE_BYTE = 0x18; // Substitution for invalid bytes
static constexpr uint8_t ASH_XON_BYTE = 0x11; // Resume transmission
static constexpr uint8_t ASH_XOFF_BYTE = 0x13; // Pause transmission
static constexpr uint8_t ASH_CANCEL_BYTE = 0x1A; // Discards the partial frame before it
// A reserved byte can never appear literally inside a frame; it is escaped as
// ESCAPE followed by the byte XOR 0x20. Rejecting frames that contain one is what
// eliminates most non-ASH traffic before its CRC is ever computed: real firmware
// images and Spinel payloads are dense in 0x11/0x13/0x18/0x1A.
inline bool ash_is_reserved(uint8_t byte) {
return byte == ASH_FLAG_BYTE || byte == ASH_ESCAPE_BYTE || byte == ASH_XON_BYTE || byte == ASH_XOFF_BYTE ||
byte == ASH_SUBSTITUTE_BYTE || byte == ASH_CANCEL_BYTE;
}
// CRC-CCITT (init 0xFFFF, polynomial 0x1021, transmitted big-endian). Note this is a
// different variant from the Kermit FCS that Spinel/HDLC-lite uses over the same
// 0x7E framing, so Spinel frames systematically fail this check.
uint16_t ash_crc16(const uint8_t *data, size_t length, uint16_t init = 0xFFFF);
// ASH bounds a frame's Data Field at 128 bytes, so the largest body a scanner has to
// hold is that field plus the control byte and the two CRC bytes ahead of the closing
// delimiter. Byte stuffing happens on the wire only and is undone as bytes arrive, so it
// does not enlarge this.
static constexpr size_t ASH_MAX_DATA_FIELD_SIZE = 128;
static constexpr size_t MAX_ASH_FRAME_SIZE = 1 + ASH_MAX_DATA_FIELD_SIZE + 2;
// Protocol limits
static constexpr uint8_t ASH_MAX_SEQUENCE = 7; // 3-bit sequence number (0-7)
static constexpr uint16_t ASH_CRC_INIT = 0xFFFF; // CRC-CCITT initial value
// An ACK is FLAG, control byte, two CRC bytes, FLAG. Every byte but the delimiters may
// need escaping, so the worst case is 2 + 3 * 2 = 8.
static constexpr size_t ASH_ACK_FRAME_MAX_SIZE = 8;
// Writes an ACK frame for `ack_num` into `output`, which must hold at least
// ASH_ACK_FRAME_MAX_SIZE bytes, and returns its length.
size_t ash_build_ack_frame(uint8_t *output, uint8_t ack_num);
} // namespace esphome::zigbee_proxy_tap
Loaded 100 of 187 files, more files were not shown because too many files have changed in this diff. Show more