Merge branch 'dev' into rp2040-socket-wake

This commit is contained in:
J. Nick Koston
2026-03-05 13:21:29 -10:00
committed by GitHub
129 changed files with 1624 additions and 491 deletions
+1 -1
View File
@@ -1 +1 @@
b97e16a84153b2a4cfc51137cd6121db3c32374504b2bea55144413b3e573052
b6f8c16c1ddd222134bf4a71910b4c832e764e23caf49f9bce3280b079955fcf
+1 -1
View File
@@ -49,7 +49,7 @@ jobs:
with:
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Set TAG
run: |
+6 -6
View File
@@ -99,15 +99,15 @@ jobs:
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log in to docker hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -178,17 +178,17 @@ jobs:
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr'
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
+2
View File
@@ -54,6 +54,8 @@ esphome/components/atm90e32/* @circuitsetup @descipher
esphome/components/audio/* @kahrendt
esphome/components/audio_adc/* @kbx81
esphome/components/audio_dac/* @kbx81
esphome/components/audio_file/* @kahrendt
esphome/components/audio_file/media_source/* @kahrendt
esphome/components/axs15231/* @clydebarrow
esphome/components/b_parasite/* @rbaron
esphome/components/ballu/* @bazuchan
+2 -13
View File
@@ -173,19 +173,8 @@ float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1
}
if (resolution == ADS1015_12_BITS) {
bool negative = (raw_conversion >> 15) == 1;
// shift raw_conversion as it's only 12-bits, left justified
raw_conversion = raw_conversion >> (16 - ADS1015_12_BITS);
// check if number was negative in order to keep the sign
if (negative) {
// the number was negative
// 1) set the negative bit back
raw_conversion |= 0x8000;
// 2) reset the former (shifted) negative bit
raw_conversion &= 0xF7FF;
}
// ADS1015 returns 12-bit value left-justified in 16 bits; shift right and sign-extend
raw_conversion = static_cast<uint16_t>(static_cast<int16_t>(raw_conversion) >> (16 - ADS1015_12_BITS));
}
auto signed_conversion = static_cast<int16_t>(raw_conversion);
+1 -1
View File
@@ -125,7 +125,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc
this->current_sensor_->publish_state(NAN);
if (this->speed_sensor_ != nullptr)
this->speed_sensor_->publish_state(NAN);
if (this->speed_sensor_ != nullptr)
if (this->voltage_sensor_ != nullptr)
this->voltage_sensor_->publish_state(NAN);
break;
}
+3 -2
View File
@@ -114,9 +114,10 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *pa
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
}
#elif defined(USE_API_PLAINTEXT)
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
this->helper_ = std::unique_ptr<APIPlaintextFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
#elif defined(USE_API_NOISE)
this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())};
this->helper_ =
std::unique_ptr<APINoiseFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())};
#else
#error "No frame helper defined"
#endif
+12
View File
@@ -3,6 +3,12 @@
#include "esphome/core/defines.h"
#ifdef USE_API
#include "api_frame_helper.h"
#ifdef USE_API_NOISE
#include "api_frame_helper_noise.h"
#endif
#ifdef USE_API_PLAINTEXT
#include "api_frame_helper_plaintext.h"
#endif
#include "api_pb2.h"
#include "api_pb2_service.h"
#include "api_server.h"
@@ -489,7 +495,13 @@ class APIConnection final : public APIServerConnectionBase {
// === Optimal member ordering for 32-bit systems ===
// Group 1: Pointers (4 bytes each on 32-bit)
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
std::unique_ptr<APIFrameHelper> helper_;
#elif defined(USE_API_NOISE)
std::unique_ptr<APINoiseFrameHelper> helper_;
#elif defined(USE_API_PLAINTEXT)
std::unique_ptr<APIPlaintextFrameHelper> helper_;
#endif
APIServer *parent_;
// Group 2: Iterator union (saves ~16 bytes vs separate iterators)
@@ -61,6 +61,10 @@ optional<ParseResult> ATCMiThermometer::parse_header_(const esp32_ble_tracker::S
}
auto raw = service_data.data;
if (raw.size() < 13) {
ESP_LOGVV(TAG, "parse_header_(): service data too short (%zu).", raw.size());
return {};
}
static uint8_t last_frame_count = 0;
if (last_frame_count == raw[12]) {
+1 -1
View File
@@ -197,7 +197,7 @@ float ATM90E26Component::get_reactive_power_() {
float ATM90E26Component::get_power_factor_() {
const uint16_t val = this->read16_(ATM90E26_REGISTER_POWERF); // signed
if (val & 0x8000) {
return -(val & 0x7FF) / 1000.0f;
return -(val & 0x7FFF) / 1000.0f;
} else {
return val / 1000.0f;
}
+56
View File
@@ -1,5 +1,9 @@
#include "audio.h"
#include "esphome/core/helpers.h"
#include <cstring>
namespace esphome {
namespace audio {
@@ -58,6 +62,58 @@ const char *audio_file_type_to_string(AudioFileType file_type) {
}
}
AudioFileType detect_audio_file_type(const char *content_type, const char *url) {
// Try Content-Type header first
if (content_type != nullptr && content_type[0] != '\0') {
#ifdef USE_AUDIO_MP3_SUPPORT
if (strcasecmp(content_type, "mp3") == 0 || strcasecmp(content_type, "audio/mp3") == 0 ||
strcasecmp(content_type, "audio/mpeg") == 0) {
return AudioFileType::MP3;
}
#endif
if (strcasecmp(content_type, "audio/wav") == 0) {
return AudioFileType::WAV;
}
#ifdef USE_AUDIO_FLAC_SUPPORT
if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) {
return AudioFileType::FLAC;
}
#endif
#ifdef USE_AUDIO_OPUS_SUPPORT
// Match "audio/ogg" with a codecs parameter containing "opus"
// Valid forms: audio/ogg;codecs=opus, audio/ogg; codecs="opus", etc.
// Plain "audio/ogg" without opus is not matched (almost always Ogg Vorbis)
if (strncasecmp(content_type, "audio/ogg", 9) == 0 && strcasestr(content_type + 9, "opus") != nullptr) {
return AudioFileType::OPUS;
}
#endif
}
// Fallback to URL extension
if (url != nullptr && url[0] != '\0') {
if (str_endswith_ignore_case(url, ".wav")) {
return AudioFileType::WAV;
}
#ifdef USE_AUDIO_MP3_SUPPORT
if (str_endswith_ignore_case(url, ".mp3")) {
return AudioFileType::MP3;
}
#endif
#ifdef USE_AUDIO_FLAC_SUPPORT
if (str_endswith_ignore_case(url, ".flac")) {
return AudioFileType::FLAC;
}
#endif
#ifdef USE_AUDIO_OPUS_SUPPORT
if (str_endswith_ignore_case(url, ".opus")) {
return AudioFileType::OPUS;
}
#endif
}
return AudioFileType::NONE;
}
void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
size_t samples_to_scale) {
// Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same.
+7
View File
@@ -130,6 +130,13 @@ struct AudioFile {
/// @return const char pointer to the readable file type
const char *audio_file_type_to_string(AudioFileType file_type);
/// @brief Detect audio file type from a Content-Type header value and/or URL extension.
/// Tries Content-Type first, then falls back to URL extension. Either parameter may be null.
/// @param content_type Content-Type header value (may be null or empty)
/// @param url URL to inspect for file extension (may be null or empty)
/// @return The detected AudioFileType, or NONE if unknown
AudioFileType detect_audio_file_type(const char *content_type, const char *url);
/// @brief Scales Q15 fixed point audio samples. Scales in place if audio_samples == output_buffer.
/// @param audio_samples PCM int16 audio samples
/// @param output_buffer Buffer to store the scaled samples
+3 -47
View File
@@ -185,26 +185,8 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
return err;
}
if (str_endswith_ignore_case(url, ".wav")) {
file_type = AudioFileType::WAV;
}
#ifdef USE_AUDIO_MP3_SUPPORT
else if (str_endswith_ignore_case(url, ".mp3")) {
file_type = AudioFileType::MP3;
}
#endif
#ifdef USE_AUDIO_FLAC_SUPPORT
else if (str_endswith_ignore_case(url, ".flac")) {
file_type = AudioFileType::FLAC;
}
#endif
#ifdef USE_AUDIO_OPUS_SUPPORT
else if (str_endswith_ignore_case(url, ".opus")) {
file_type = AudioFileType::OPUS;
}
#endif
else {
file_type = AudioFileType::NONE;
file_type = detect_audio_file_type(nullptr, url);
if (file_type == AudioFileType::NONE) {
this->cleanup_connection_();
return ESP_ERR_NOT_SUPPORTED;
}
@@ -232,32 +214,6 @@ AudioReaderState AudioReader::read() {
return AudioReaderState::FAILED;
}
AudioFileType AudioReader::get_audio_type(const char *content_type) {
#ifdef USE_AUDIO_MP3_SUPPORT
if (strcasecmp(content_type, "mp3") == 0 || strcasecmp(content_type, "audio/mp3") == 0 ||
strcasecmp(content_type, "audio/mpeg") == 0) {
return AudioFileType::MP3;
}
#endif
if (strcasecmp(content_type, "audio/wav") == 0) {
return AudioFileType::WAV;
}
#ifdef USE_AUDIO_FLAC_SUPPORT
if (strcasecmp(content_type, "audio/flac") == 0 || strcasecmp(content_type, "audio/x-flac") == 0) {
return AudioFileType::FLAC;
}
#endif
#ifdef USE_AUDIO_OPUS_SUPPORT
// Match "audio/ogg" with a codecs parameter containing "opus"
// Valid forms: audio/ogg;codecs=opus, audio/ogg; codecs="opus", etc.
// Plain "audio/ogg" without a codecs parameter is not matched, as those are almost always Ogg Vorbis streams
if (strncasecmp(content_type, "audio/ogg", 9) == 0 && strcasestr(content_type + 9, "opus") != nullptr) {
return AudioFileType::OPUS;
}
#endif
return AudioFileType::NONE;
}
esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) {
// Based on https://github.com/maroc81/WeatherLily/tree/main/main/net accessed 20241224
AudioReader *this_reader = (AudioReader *) evt->user_data;
@@ -265,7 +221,7 @@ esp_err_t AudioReader::http_event_handler(esp_http_client_event_t *evt) {
switch (evt->event_id) {
case HTTP_EVENT_ON_HEADER:
if (strcasecmp(evt->header_key, "Content-Type") == 0) {
this_reader->audio_file_type_ = get_audio_type(evt->header_value);
this_reader->audio_file_type_ = detect_audio_file_type(evt->header_value, nullptr);
}
break;
default:
-5
View File
@@ -58,11 +58,6 @@ class AudioReader {
/// @brief Monitors the http client events to attempt determining the file type from the Content-Type header
static esp_err_t http_event_handler(esp_http_client_event_t *evt);
/// @brief Determines the audio file type from the http header's Content-Type key
/// @param content_type string with the Content-Type key
/// @return AudioFileType of the url, if it can be determined. If not, return AudioFileType::NONE.
static AudioFileType get_audio_type(const char *content_type);
AudioReaderState file_read_();
AudioReaderState http_read_();
+255
View File
@@ -0,0 +1,255 @@
from dataclasses import dataclass, field
import hashlib
import logging
from pathlib import Path
import puremagic
from esphome import external_files
import esphome.codegen as cg
from esphome.components import audio
import esphome.config_validation as cv
from esphome.const import (
CONF_FILE,
CONF_ID,
CONF_PATH,
CONF_RAW_DATA_ID,
CONF_TYPE,
CONF_URL,
)
from esphome.core import CORE, ID, HexInt
from esphome.cpp_generator import MockObj
from esphome.external_files import download_content
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@kahrendt"]
AUTO_LOAD = ["audio"]
DOMAIN = "audio_file"
audio_file_ns = cg.esphome_ns.namespace("audio_file")
TYPE_LOCAL = "local"
TYPE_WEB = "web"
@dataclass
class AudioFileData:
file_ids: dict[str, ID] = field(default_factory=dict)
file_cache: dict[str, tuple[bytes, MockObj]] = field(default_factory=dict)
def _get_data() -> AudioFileData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = AudioFileData()
return CORE.data[DOMAIN]
def get_audio_file_ids() -> dict[str, ID]:
"""Get all registered audio file IDs for cross-component access."""
return _get_data().file_ids
def _compute_local_file_path(value: ConfigType) -> Path:
url = value[CONF_URL]
h = hashlib.new("sha256")
h.update(url.encode())
key = h.hexdigest()[:8]
base_dir = external_files.compute_local_file_dir(DOMAIN)
_LOGGER.debug("_compute_local_file_path: base_dir=%s", base_dir / key)
return base_dir / key
def _download_web_file(value: ConfigType) -> ConfigType:
url = value[CONF_URL]
path = _compute_local_file_path(value)
download_content(url, path)
_LOGGER.debug("download_web_file: path=%s", path)
return value
def _file_schema(value: ConfigType | str) -> ConfigType:
if isinstance(value, str):
return _validate_file_shorthand(value)
return TYPED_FILE_SCHEMA(value)
def _validate_file_shorthand(value: str) -> ConfigType:
value = cv.string_strict(value)
if value.startswith("http://") or value.startswith("https://"):
return _file_schema(
{
CONF_TYPE: TYPE_WEB,
CONF_URL: value,
}
)
return _file_schema(
{
CONF_TYPE: TYPE_LOCAL,
CONF_PATH: value,
}
)
def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
"""Read an audio file and determine its type. Used by this component and media_source platform."""
conf_file = file_config[CONF_FILE]
file_source = conf_file[CONF_TYPE]
if file_source == TYPE_LOCAL:
path = CORE.relative_config_path(conf_file[CONF_PATH])
elif file_source == TYPE_WEB:
path = _compute_local_file_path(conf_file)
else:
raise cv.Invalid("Unsupported file source")
with open(path, "rb") as f:
data = f.read()
try:
file_type: str = puremagic.from_string(data)
file_type = file_type.removeprefix(".")
except puremagic.PureError as e:
raise cv.Invalid(
f"Unable to determine audio file type of '{path}'. "
f"Try re-encoding the file into a supported format. Details: {e}"
)
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
if file_type == "wav":
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["WAV"]
elif file_type in ("mp3", "mpeg", "mpga"):
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["MP3"]
elif file_type == "flac":
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["FLAC"]
elif (
file_type == "ogg"
and len(data) >= 36
and data.startswith(b"OggS")
and data[28:36] == b"OpusHead"
):
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["OPUS"]
return data, media_file_type
LOCAL_SCHEMA = cv.Schema(
{
cv.Required(CONF_PATH): cv.file_,
}
)
WEB_SCHEMA = cv.All(
{
cv.Required(CONF_URL): cv.url,
},
_download_web_file,
)
TYPED_FILE_SCHEMA = cv.typed_schema(
{
TYPE_LOCAL: LOCAL_SCHEMA,
TYPE_WEB: WEB_SCHEMA,
},
)
MEDIA_FILE_TYPE_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(audio.AudioFile),
cv.Required(CONF_FILE): _file_schema,
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
}
)
MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB
def _validate_supported_local_file(config: list[ConfigType]) -> list[ConfigType]:
for file_config in config:
data, media_file_type = read_audio_file_and_type(file_config)
if len(data) > MAX_FILE_SIZE:
file_info = file_config.get(CONF_FILE, {})
source = (
file_info.get(CONF_PATH) or file_info.get(CONF_URL) or "unknown source"
)
raise cv.Invalid(
f"Audio file {source!r} is too large ({len(data)} bytes, max {MAX_FILE_SIZE} bytes)"
)
if str(media_file_type) == str(audio.AUDIO_FILE_TYPE_ENUM["NONE"]):
file_info = file_config.get(CONF_FILE, {})
source = (
file_info.get(CONF_PATH) or file_info.get(CONF_URL) or "unknown source"
)
raise cv.Invalid(
f"Unsupported media file from {source!r} (detected type: {media_file_type})"
)
# Cache the file data so to_code() doesn't need to re-read it
_get_data().file_cache[str(file_config[CONF_ID])] = (data, media_file_type)
media_file_type_str = str(media_file_type)
if media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["FLAC"]):
audio.request_flac_support()
elif media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["MP3"]):
audio.request_mp3_support()
elif media_file_type_str == str(audio.AUDIO_FILE_TYPE_ENUM["OPUS"]):
audio.request_opus_support()
return config
CONFIG_SCHEMA = cv.All(
cv.only_on_esp32,
cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA),
_validate_supported_local_file,
)
async def to_code(config: list[ConfigType]) -> None:
cache = _get_data().file_cache
for file_config in config:
file_id = str(file_config[CONF_ID])
data, media_file_type = cache[file_id]
rhs = [HexInt(x) for x in data]
prog_arr = cg.progmem_array(file_config[CONF_RAW_DATA_ID], rhs)
media_files_struct = cg.StructInitializer(
audio.AudioFile,
(
"data",
prog_arr,
),
(
"length",
len(rhs),
),
(
"file_type",
media_file_type,
),
)
cg.new_Pvariable(
file_config[CONF_ID],
media_files_struct,
)
# Store file ID for cross-component access
_get_data().file_ids[file_id] = file_config[CONF_ID]
# Register all files in the shared C++ registry
cg.add_define("AUDIO_FILE_MAX_FILES", len(config))
for file_config in config:
file_id = str(file_config[CONF_ID])
file_var = await cg.get_variable(file_config[CONF_ID])
cg.add(audio_file_ns.add_named_audio_file(file_var, file_id))
@@ -0,0 +1,28 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef AUDIO_FILE_MAX_FILES
#include "esphome/components/audio/audio.h"
#include "esphome/core/helpers.h"
namespace esphome::audio_file {
struct NamedAudioFile {
audio::AudioFile *file;
const char *file_id;
};
inline StaticVector<NamedAudioFile, AUDIO_FILE_MAX_FILES>
named_audio_files; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
inline void add_named_audio_file(audio::AudioFile *file, const char *file_id) {
named_audio_files.push_back({file, file_id});
}
inline const StaticVector<NamedAudioFile, AUDIO_FILE_MAX_FILES> &get_named_audio_files() { return named_audio_files; }
} // namespace esphome::audio_file
#endif // AUDIO_FILE_MAX_FILES
@@ -0,0 +1,38 @@
import esphome.codegen as cg
from esphome.components import media_source, psram
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_TASK_STACK_IN_PSRAM
from esphome.types import ConfigType
CODEOWNERS = ["@kahrendt"]
AUTO_LOAD = ["audio"]
DEPENDENCIES = ["audio_file"]
audio_file_ns = cg.esphome_ns.namespace("audio_file")
AudioFileMediaSource = audio_file_ns.class_(
"AudioFileMediaSource", cg.Component, media_source.MediaSource
)
CONFIG_SCHEMA = cv.All(
media_source.media_source_schema(
AudioFileMediaSource,
)
.extend(
{
cv.Optional(CONF_TASK_STACK_IN_PSRAM): cv.All(
cv.boolean, cv.requires_component(psram.DOMAIN)
),
}
)
.extend(cv.COMPONENT_SCHEMA),
cv.only_on_esp32,
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await media_source.register_media_source(var, config)
if CONF_TASK_STACK_IN_PSRAM in config:
cg.add(var.set_task_stack_in_psram(config[CONF_TASK_STACK_IN_PSRAM]))
@@ -0,0 +1,283 @@
#include "audio_file_media_source.h"
#ifdef USE_ESP32
#include "esphome/components/audio/audio_decoder.h"
#include <cstring>
namespace esphome::audio_file {
namespace { // anonymous namespace for internal linkage
struct AudioSinkAdapter : public audio::AudioSinkCallback {
media_source::MediaSource *source;
audio::AudioStreamInfo stream_info;
size_t audio_sink_write(uint8_t *data, size_t length, TickType_t ticks_to_wait) override {
return this->source->write_output(data, length, pdTICKS_TO_MS(ticks_to_wait), this->stream_info);
}
};
} // namespace
#if defined(USE_AUDIO_OPUS_SUPPORT)
static constexpr uint32_t DECODE_TASK_STACK_SIZE = 5 * 1024;
#else
static constexpr uint32_t DECODE_TASK_STACK_SIZE = 3 * 1024;
#endif
static const char *const TAG = "audio_file_media_source";
enum EventGroupBits : uint32_t {
// Requests to start playback (set by play_uri, handled by loop)
REQUEST_START = (1 << 0),
// Commands from main loop to decode task
COMMAND_STOP = (1 << 1),
COMMAND_PAUSE = (1 << 2),
// Decode task lifecycle signals (one-shot, cleared by loop)
TASK_STARTING = (1 << 7),
TASK_RUNNING = (1 << 8),
TASK_STOPPING = (1 << 9),
TASK_STOPPED = (1 << 10),
TASK_ERROR = (1 << 11),
// Decode task state (level-triggered, set/cleared by decode task)
TASK_PAUSED = (1 << 12),
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
};
void AudioFileMediaSource::dump_config() {
ESP_LOGCONFIG(TAG, "Audio File Media Source:");
ESP_LOGCONFIG(TAG, " Task Stack in PSRAM: %s", this->task_stack_in_psram_ ? "Yes" : "No");
}
void AudioFileMediaSource::setup() {
this->disable_loop();
this->event_group_ = xEventGroupCreate();
if (this->event_group_ == nullptr) {
ESP_LOGE(TAG, "Failed to create event group");
this->mark_failed();
return;
}
}
void AudioFileMediaSource::loop() {
EventBits_t event_bits = xEventGroupGetBits(this->event_group_);
if (event_bits & REQUEST_START) {
xEventGroupClearBits(this->event_group_, REQUEST_START);
this->decoding_state_ = AudioFileDecodingState::START_TASK;
}
switch (this->decoding_state_) {
case AudioFileDecodingState::START_TASK: {
if (!this->decode_task_.is_created()) {
xEventGroupClearBits(this->event_group_, ALL_BITS);
if (!this->decode_task_.create(decode_task, "AudioFileDec", DECODE_TASK_STACK_SIZE, this, 1,
this->task_stack_in_psram_)) {
ESP_LOGE(TAG, "Failed to create task");
this->status_momentary_error("task_create", 1000);
this->set_state_(media_source::MediaSourceState::ERROR);
this->decoding_state_ = AudioFileDecodingState::IDLE;
return;
}
}
this->decoding_state_ = AudioFileDecodingState::DECODING;
break;
}
case AudioFileDecodingState::DECODING: {
if (event_bits & TASK_STARTING) {
ESP_LOGD(TAG, "Starting");
xEventGroupClearBits(this->event_group_, TASK_STARTING);
}
if (event_bits & TASK_RUNNING) {
ESP_LOGV(TAG, "Started");
xEventGroupClearBits(this->event_group_, TASK_RUNNING);
this->set_state_(media_source::MediaSourceState::PLAYING);
}
if ((event_bits & TASK_PAUSED) && this->get_state() != media_source::MediaSourceState::PAUSED) {
this->set_state_(media_source::MediaSourceState::PAUSED);
} else if (!(event_bits & TASK_PAUSED) && this->get_state() == media_source::MediaSourceState::PAUSED) {
this->set_state_(media_source::MediaSourceState::PLAYING);
}
if (event_bits & TASK_STOPPING) {
ESP_LOGV(TAG, "Stopping");
xEventGroupClearBits(this->event_group_, TASK_STOPPING);
}
if (event_bits & TASK_ERROR) {
// Report error so the orchestrator knows playback failed; task will have already logged the specific error
this->set_state_(media_source::MediaSourceState::ERROR);
}
if (event_bits & TASK_STOPPED) {
ESP_LOGD(TAG, "Stopped");
xEventGroupClearBits(this->event_group_, ALL_BITS);
this->decode_task_.deallocate();
this->set_state_(media_source::MediaSourceState::IDLE);
this->decoding_state_ = AudioFileDecodingState::IDLE;
}
break;
}
case AudioFileDecodingState::IDLE: {
if (this->get_state() == media_source::MediaSourceState::ERROR && !this->status_has_error()) {
this->set_state_(media_source::MediaSourceState::IDLE);
}
break;
}
}
if ((this->decoding_state_ == AudioFileDecodingState::IDLE) &&
(this->get_state() == media_source::MediaSourceState::IDLE)) {
this->disable_loop();
}
}
// Called from the orchestrator's main loop, so no synchronization needed with loop()
bool AudioFileMediaSource::play_uri(const std::string &uri) {
if (!this->is_ready() || this->is_failed() || this->status_has_error() || !this->has_listener() ||
xEventGroupGetBits(this->event_group_) & REQUEST_START) {
return false;
}
// Check if source is already playing
if (this->get_state() != media_source::MediaSourceState::IDLE) {
ESP_LOGE(TAG, "Cannot play '%s': source is busy", uri.c_str());
return false;
}
// Validate URI starts with "audio-file://"
if (!uri.starts_with("audio-file://")) {
ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str());
return false;
}
// Strip "audio-file://" prefix and find the file
const char *file_id = uri.c_str() + 13; // "audio-file://" is 13 characters
for (const auto &named_file : get_named_audio_files()) {
if (strcmp(named_file.file_id, file_id) == 0) {
this->current_file_ = named_file.file;
xEventGroupSetBits(this->event_group_, EventGroupBits::REQUEST_START);
this->enable_loop();
return true;
}
}
ESP_LOGE(TAG, "Unknown file: '%s'", file_id);
return false;
}
// Called from the orchestrator's main loop, so no synchronization needed with loop()
void AudioFileMediaSource::handle_command(media_source::MediaSourceCommand command) {
if (this->decoding_state_ != AudioFileDecodingState::DECODING) {
return;
}
switch (command) {
case media_source::MediaSourceCommand::STOP:
xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_STOP);
break;
case media_source::MediaSourceCommand::PAUSE:
xEventGroupSetBits(this->event_group_, EventGroupBits::COMMAND_PAUSE);
break;
case media_source::MediaSourceCommand::PLAY:
xEventGroupClearBits(this->event_group_, EventGroupBits::COMMAND_PAUSE);
break;
default:
break;
}
}
void AudioFileMediaSource::decode_task(void *params) {
AudioFileMediaSource *this_source = static_cast<AudioFileMediaSource *>(params);
do { // do-while(false) ensures RAII objects are destroyed on all exit paths via break
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STARTING);
// 0 bytes for input transfer buffer makes it an inplace buffer
std::unique_ptr<audio::AudioDecoder> decoder = make_unique<audio::AudioDecoder>(0, 4096);
esp_err_t err = decoder->start(this_source->current_file_->file_type);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to start decoder: %s", esp_err_to_name(err));
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR | EventGroupBits::TASK_STOPPING);
break;
}
// Add the file as a const data source
decoder->add_source(this_source->current_file_->data, this_source->current_file_->length);
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_RUNNING);
AudioSinkAdapter audio_sink;
bool has_stream_info = false;
while (true) {
EventBits_t event_bits = xEventGroupGetBits(this_source->event_group_);
if (event_bits & EventGroupBits::COMMAND_STOP) {
break;
}
bool paused = event_bits & EventGroupBits::COMMAND_PAUSE;
decoder->set_pause_output_state(paused);
if (paused) {
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_PAUSED);
vTaskDelay(pdMS_TO_TICKS(20));
} else {
xEventGroupClearBits(this_source->event_group_, EventGroupBits::TASK_PAUSED);
}
// Will stop gracefully once finished with the current file
audio::AudioDecoderState decoder_state = decoder->decode(true);
if (decoder_state == audio::AudioDecoderState::FINISHED) {
break;
} else if (decoder_state == audio::AudioDecoderState::FAILED) {
ESP_LOGE(TAG, "Decoder failed");
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR);
break;
}
if (!has_stream_info && decoder->get_audio_stream_info().has_value()) {
has_stream_info = true;
audio::AudioStreamInfo stream_info = decoder->get_audio_stream_info().value();
ESP_LOGD(TAG, "Bits per sample: %d, Channels: %d, Sample rate: %d", stream_info.get_bits_per_sample(),
stream_info.get_channels(), stream_info.get_sample_rate());
if (stream_info.get_bits_per_sample() != 16 || stream_info.get_channels() > 2) {
ESP_LOGE(TAG, "Incompatible audio stream. Only 16 bits per sample and 1 or 2 channels are supported");
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR);
break;
}
audio_sink.source = this_source;
audio_sink.stream_info = stream_info;
esp_err_t err = decoder->add_sink(&audio_sink);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to add sink: %s", esp_err_to_name(err));
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_ERROR);
break;
}
}
}
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STOPPING);
} while (false);
// All RAII objects from the do-while block (decoder, audio_sink, etc.) are now destroyed.
xEventGroupSetBits(this_source->event_group_, EventGroupBits::TASK_STOPPED);
vTaskSuspend(nullptr); // Suspend this task indefinitely until the loop method deletes it
}
} // namespace esphome::audio_file
#endif // USE_ESP32
@@ -0,0 +1,50 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#include "esphome/components/audio/audio.h"
#include "esphome/components/audio_file/audio_file.h"
#include "esphome/components/media_source/media_source.h"
#include "esphome/core/component.h"
#include "esphome/core/static_task.h"
#include <freertos/FreeRTOS.h>
#include <freertos/event_groups.h>
namespace esphome::audio_file {
enum class AudioFileDecodingState : uint8_t {
START_TASK,
DECODING,
IDLE,
};
class AudioFileMediaSource : public Component, public media_source::MediaSource {
public:
void setup() override;
void loop() override;
void dump_config() override;
// MediaSource interface implementation
bool play_uri(const std::string &uri) override;
void handle_command(media_source::MediaSourceCommand command) override;
bool can_handle(const std::string &uri) const override { return uri.starts_with("audio-file://"); }
void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; }
protected:
static void decode_task(void *params);
audio::AudioFile *current_file_{nullptr};
AudioFileDecodingState decoding_state_{AudioFileDecodingState::IDLE};
EventGroupHandle_t event_group_{nullptr};
StaticTask decode_task_;
bool task_stack_in_psram_{false};
};
} // namespace esphome::audio_file
#endif // USE_ESP32
+20 -8
View File
@@ -1,4 +1,5 @@
#include "bedjet_codec.h"
#include <algorithm>
#include <cstdio>
#include <cstring>
@@ -68,6 +69,10 @@ BedjetPacket *BedjetCodec::get_set_runtime_remaining_request(const uint8_t hour,
/** Decodes the extra bytes that were received after being notified with a partial packet. */
void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) {
if (length < 5) {
ESP_LOGVV(TAG, "Received extra: %d bytes (too short)", length);
return;
}
ESP_LOGVV(TAG, "Received extra: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]);
uint8_t offset = this->last_buffer_size_;
if (offset > 0 && length + offset <= sizeof(BedjetStatusPacket)) {
@@ -90,14 +95,19 @@ void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) {
* @return `true` if the packet was decoded and represents a "partial" packet; `false` otherwise.
*/
bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) {
if (length < 5) {
ESP_LOGW(TAG, "Received short packet: %d bytes", length);
return false;
}
ESP_LOGV(TAG, "Received: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]);
if (data[1] == PACKET_FORMAT_V3_HOME && data[3] == PACKET_TYPE_STATUS) {
// Clear old buffer
memset(&this->buf_, 0, sizeof(BedjetStatusPacket));
// Copy new data into buffer
memcpy(&this->buf_, data, length);
this->last_buffer_size_ = length;
size_t copy_len = std::min(static_cast<size_t>(length), sizeof(BedjetStatusPacket));
memcpy(&this->buf_, data, copy_len);
this->last_buffer_size_ = copy_len;
// TODO: validate the packet checksum?
if (this->buf_.mode < 7 && this->buf_.target_temp_step >= 38 && this->buf_.target_temp_step <= 86 &&
@@ -113,13 +123,15 @@ bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) {
}
} else if (data[1] == PACKET_FORMAT_DEBUG || data[3] == PACKET_TYPE_DEBUG) {
// We don't actually know the packet format for this. Dump packets to log, in case a pattern presents itself.
ESP_LOGVV(TAG,
"received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, "
"[12]=%d, [-1]=%d",
bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8],
data[9], data[10], data[11], data[12], data[length - 1]);
if (length >= 13) {
ESP_LOGVV(TAG,
"received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF; [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, "
"[12]=%d, [-1]=%d",
bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8],
data[9], data[10], data[11], data[12], data[length - 1]);
}
if (this->has_status()) {
if (this->has_status() && length >= 7) {
this->status_packet_->ambient_temp_step = data[6];
}
} else {
+7 -3
View File
@@ -147,13 +147,17 @@ uint32_t CSE7761Component::read_(uint8_t reg, uint8_t size) {
}
uint32_t CSE7761Component::coefficient_by_unit_(uint32_t unit) {
uint32_t coeff = 0;
switch (unit) {
case RMS_UC:
return 0x400000 * 100 / this->data_.coefficient[RMS_UC];
coeff = this->data_.coefficient[RMS_UC];
return coeff ? 0x400000 * 100 / coeff : 0;
case RMS_IAC:
return (0x800000 * 100 / this->data_.coefficient[RMS_IAC]) * 10; // Stay within 32 bits
coeff = this->data_.coefficient[RMS_IAC];
return coeff ? (0x800000 * 100 / coeff) * 10 : 0; // Stay within 32 bits
case POWER_PAC:
return 0x80000000 / this->data_.coefficient[POWER_PAC];
coeff = this->data_.coefficient[POWER_PAC];
return coeff ? 0x80000000 / coeff : 0;
}
return 0;
}
+1
View File
@@ -260,6 +260,7 @@ void DFPlayer::loop() {
ESP_LOGV(TAG, "Playback finished (USB drive)");
this->is_playing_ = false;
this->on_finished_playback_callback_.call();
break;
case 0x3D:
ESP_LOGV(TAG, "Playback finished (SD card)");
this->is_playing_ = false;
@@ -30,11 +30,9 @@ class Command {
class ReadStateCommand : public Command {
public:
ReadStateCommand() { timeout_ms_ = 500; }
uint8_t execute(DfrobotSen0395Component *parent) override;
uint8_t on_message(std::string &message) override;
protected:
uint32_t timeout_ms_{500};
};
class PowerCommand : public Command {
@@ -99,12 +97,12 @@ class ResetSystemCommand : public Command {
class SaveCfgCommand : public Command {
public:
SaveCfgCommand() { cmd_ = "saveCfg 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89"; }
SaveCfgCommand() {
cmd_ = "saveCfg 0x45670123 0xCDEF89AB 0x956128C6 0xDF54AC89";
cmd_duration_ms_ = 3000;
timeout_ms_ = 3500;
}
uint8_t on_message(std::string &message) override;
protected:
uint32_t cmd_duration_ms_{3000};
uint32_t timeout_ms_{3500};
};
class LedModeCommand : public Command {
+3 -3
View File
@@ -22,9 +22,9 @@ class EE895Component : public PollingComponent, public i2c::I2CDevice {
void write_command_(uint16_t addr, uint16_t reg_cnt);
float read_float_();
uint16_t calc_crc16_(const uint8_t buf[], uint8_t len);
sensor::Sensor *co2_sensor_;
sensor::Sensor *temperature_sensor_;
sensor::Sensor *pressure_sensor_;
sensor::Sensor *co2_sensor_{nullptr};
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *pressure_sensor_{nullptr};
enum ErrorCode { NONE = 0, COMMUNICATION_FAILED, CRC_CHECK_FAILED } error_code_{NONE};
};
+1 -1
View File
@@ -72,7 +72,7 @@ void Emc2101Component::setup() {
config |= EMC2101_DAC_BIT;
}
if (this->inverted_) {
config |= EMC2101_POLARITY_BIT;
reg(EMC2101_REGISTER_FAN_CONFIG) |= EMC2101_POLARITY_BIT;
}
if (this->dac_mode_) { // DAC mode configurations
+2 -2
View File
@@ -2,7 +2,7 @@
#include "esphome/core/defines.h"
#ifdef USE_OTA
#include "esphome/components/ota/ota_backend.h"
#include "esphome/components/ota/ota_backend_factory.h"
#include "esphome/components/socket/socket.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -86,7 +86,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
socket::ListenSocket *server_{nullptr};
std::unique_ptr<socket::Socket> client_;
std::unique_ptr<ota::OTABackend> backend_;
ota::OTABackendPtr backend_;
uint32_t client_connect_time_{0};
uint16_t port_;
@@ -470,6 +470,7 @@ network::IPAddresses EthernetComponent::get_ip_addresses() {
uint8_t count = 0;
count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s);
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
assert(count < addresses.size());
for (int i = 0; i < count; i++) {
addresses[i + 1] = network::IPAddress(&if_ip6s[i]);
}
@@ -687,8 +688,6 @@ void EthernetComponent::start_connect_() {
this->status_set_warning();
}
bool EthernetComponent::is_connected() { return this->state_ == EthernetComponentState::CONNECTED; }
void EthernetComponent::dump_connect_params_() {
esp_netif_ip_info_t ip;
esp_netif_get_ip_info(this->eth_netif_, &ip);
@@ -76,7 +76,7 @@ class EthernetComponent : public Component {
void dump_config() override;
float get_setup_priority() const override;
void on_powerdown() override { powerdown(); }
bool is_connected();
bool is_connected() { return this->state_ == EthernetComponentState::CONNECTED; }
#ifdef USE_ETHERNET_SPI
void set_clk_pin(uint8_t clk_pin);
@@ -437,10 +437,15 @@ void FeedbackCover::recompute_position_() {
}
// check if we have an acceleration_wait_time, and remove from position computation
if (now > (this->start_dir_time_ + this->acceleration_wait_time_)) {
this->position +=
dir * (now - std::max(this->start_dir_time_ + this->acceleration_wait_time_, this->last_recompute_time_)) /
(action_dur - this->acceleration_wait_time_);
if (now - this->start_dir_time_ > this->acceleration_wait_time_) {
uint32_t accel_end_time = this->start_dir_time_ + this->acceleration_wait_time_;
uint32_t effective_start;
if (static_cast<int32_t>(accel_end_time - this->last_recompute_time_) >= 0) {
effective_start = accel_end_time;
} else {
effective_start = this->last_recompute_time_;
}
this->position += dir * (now - effective_start) / (action_dur - this->acceleration_wait_time_);
this->position = clamp(this->position, min_pos, max_pos);
}
this->last_recompute_time_ = now;
+1 -1
View File
@@ -34,7 +34,7 @@ AUTO_LOAD = ["sensor"]
CODEOWNERS = ["@coogle", "@ximex"]
gps_ns = cg.esphome_ns.namespace("gps")
GPS = gps_ns.class_("GPS", cg.Component, uart.UARTDevice)
GPS = gps_ns.class_("GPS", cg.PollingComponent, uart.UARTDevice)
GPSListener = gps_ns.class_("GPSListener")
MULTI_CONF = True
@@ -131,7 +131,7 @@ void GroveMotorDriveTB6612FNG::stepper_run(StepperModeTypeT mode, int16_t steps,
buffer_[4] = ms_per_step;
buffer_[5] = (ms_per_step >> 8);
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN, buffer_, 1) != i2c::ERROR_OK) {
if (this->write_register(GROVE_MOTOR_DRIVER_I2C_CMD_STEPPER_RUN, buffer_, 6) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Run stepper failed!");
this->status_set_warning();
return;
@@ -26,7 +26,7 @@ void GrowattSolar::update() {
}
// The bus might be slow, or there might be other devices, or other components might be talking to our device.
if (this->waiting_for_response()) {
if (!this->ready_for_immediate_send()) {
this->waiting_to_update_ = true;
return;
}
@@ -385,7 +385,7 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
}
haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
if (size < sizeof(smartair2_protocol::HaierStatus))
if (size != sizeof(smartair2_protocol::HaierStatus))
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
smartair2_protocol::HaierStatus packet;
memcpy(&packet, packet_buffer, size);
+10 -6
View File
@@ -24,12 +24,16 @@ void HC8Component::setup() {
}
void HC8Component::update() {
uint32_t now_ms = App.get_loop_component_start_time();
uint32_t warmup_ms = this->warmup_seconds_ * 1000;
if (now_ms < warmup_ms) {
ESP_LOGW(TAG, "HC8 warming up, %" PRIu32 " s left", (warmup_ms - now_ms) / 1000);
this->status_set_warning();
return;
if (!this->warmup_complete_) {
uint32_t now_ms = App.get_loop_component_start_time();
uint32_t warmup_ms = this->warmup_seconds_ * 1000;
if (now_ms < warmup_ms) {
ESP_LOGW(TAG, "HC8 warming up, %" PRIu32 " s left", (warmup_ms - now_ms) / 1000);
this->status_set_warning();
return;
}
this->warmup_complete_ = true;
this->status_clear_warning();
}
while (this->available())
+1
View File
@@ -23,6 +23,7 @@ class HC8Component : public PollingComponent, public uart::UARTDevice {
protected:
sensor::Sensor *co2_sensor_{nullptr};
uint32_t warmup_seconds_{0};
bool warmup_complete_{false};
};
template<typename... Ts> class HC8CalibrateAction : public Action<Ts...>, public Parented<HC8Component> {
+1 -1
View File
@@ -239,7 +239,7 @@ void HE60rCover::recompute_position_() {
return;
const uint32_t now = millis();
if (now > this->last_recompute_time_) {
if (now != this->last_recompute_time_) {
auto diff = (unsigned) (now - last_recompute_time_);
float delta;
switch (this->current_operation) {
@@ -8,10 +8,6 @@
#include "esphome/components/md5/md5.h"
#include "esphome/components/watchdog/watchdog.h"
#include "esphome/components/ota/ota_backend.h"
#include "esphome/components/ota/ota_backend_esp8266.h"
#include "esphome/components/ota/ota_backend_arduino_rp2040.h"
#include "esphome/components/ota/ota_backend_esp_idf.h"
namespace esphome {
namespace http_request {
@@ -69,8 +65,7 @@ void OtaHttpRequestComponent::flash() {
}
}
void OtaHttpRequestComponent::cleanup_(std::unique_ptr<ota::OTABackend> backend,
const std::shared_ptr<HttpContainer> &container) {
void OtaHttpRequestComponent::cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container) {
if (this->update_started_) {
ESP_LOGV(TAG, "Aborting OTA backend");
backend->abort();
@@ -1,6 +1,6 @@
#pragma once
#include "esphome/components/ota/ota_backend.h"
#include "esphome/components/ota/ota_backend_factory.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
@@ -39,7 +39,7 @@ class OtaHttpRequestComponent final : public ota::OTAComponent, public Parented<
void flash();
protected:
void cleanup_(std::unique_ptr<ota::OTABackend> backend, const std::shared_ptr<HttpContainer> &container);
void cleanup_(ota::OTABackendPtr backend, const std::shared_ptr<HttpContainer> &container);
uint8_t do_ota_();
std::string get_url_with_auth_(const std::string &url);
bool http_get_md5_();
@@ -136,7 +136,7 @@ void KamstrupKMPComponent::read_command_(uint16_t command) {
int timeout = 250; // ms
// Read the data from the UART
while (timeout > 0) {
while (timeout > 0 && buffer_len < static_cast<int>(sizeof(buffer))) {
if (this->available()) {
data = this->read();
if (data > -1) {
@@ -246,7 +246,7 @@ void KamstrupKMPComponent::parse_command_message_(uint16_t command, const uint8_
}
void KamstrupKMPComponent::set_sensor_value_(uint16_t command, float value, uint8_t unit_idx) {
const char *unit = UNITS[unit_idx];
const char *unit = unit_idx < sizeof(UNITS) / sizeof(UNITS[0]) ? UNITS[unit_idx] : "";
// Standard sensors
if (command == CMD_HEAT_ENERGY && this->heat_energy_sensor_ != nullptr) {
+1 -1
View File
@@ -99,7 +99,7 @@ void HOT LCDDisplay::display() {
this->send(this->buffer_[this->columns_ * 2 + i], true);
}
if (this->rows_ >= 1) {
if (this->rows_ >= 2) {
this->command_(LCD_DISPLAY_COMMAND_SET_DDRAM_ADDR | 0x40);
for (uint8_t i = 0; i < this->columns_; i++)
+4
View File
@@ -108,6 +108,10 @@ bool LwTx::lwtx_free() { return !this->tx_msg_active; }
Send a LightwaveRF message (10 nibbles in bytes)
**/
void LwTx::lwtx_send(const std::vector<uint8_t> &msg) {
if (msg.size() < TX_MSGLEN) {
ESP_LOGW("lightwaverf.sensor", "Message too short: %zu < %u", msg.size(), static_cast<unsigned>(TX_MSGLEN));
return;
}
if (this->tx_translate) {
for (uint8_t i = 0; i < TX_MSGLEN; i++) {
this->tx_buf[i] = TX_NIBBLE[msg[i] & 0xF];
@@ -61,7 +61,7 @@ void MatrixKeypad::loop() {
ESP_LOGD(TAG, "key @ row %d, col %d released", row, col);
for (auto &listener : this->listeners_)
listener->button_released(row, col);
if (!this->keys_.empty()) {
if (this->pressed_key_ < (int) this->keys_.size()) {
uint8_t keycode = this->keys_[this->pressed_key_];
ESP_LOGD(TAG, "key '%c' released", keycode);
for (auto &listener : this->listeners_)
@@ -84,7 +84,7 @@ void MatrixKeypad::loop() {
ESP_LOGD(TAG, "key @ row %d, col %d pressed", row, col);
for (auto &listener : this->listeners_)
listener->button_pressed(row, col);
if (!this->keys_.empty()) {
if (key < (int) this->keys_.size()) {
uint8_t keycode = this->keys_[key];
ESP_LOGD(TAG, "key '%c' pressed", keycode);
for (auto &trigger : this->key_triggers_)
+5
View File
@@ -20,6 +20,7 @@ MULTI_CONF = True
CONF_ROLE = "role"
CONF_MODBUS_ID = "modbus_id"
CONF_SEND_WAIT_TIME = "send_wait_time"
CONF_TURNAROUND_TIME = "turnaround_time"
ModbusRole = modbus_ns.enum("ModbusRole")
MODBUS_ROLES = {
@@ -36,6 +37,9 @@ CONFIG_SCHEMA = (
cv.Optional(
CONF_SEND_WAIT_TIME, default="250ms"
): cv.positive_time_period_milliseconds,
cv.Optional(
CONF_TURNAROUND_TIME, default="100ms"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_DISABLE_CRC, default=False): cv.boolean,
}
)
@@ -57,6 +61,7 @@ async def to_code(config):
cg.add(var.set_flow_control_pin(pin))
cg.add(var.set_send_wait_time(config[CONF_SEND_WAIT_TIME]))
cg.add(var.set_turnaround_time(config[CONF_TURNAROUND_TIME]))
cg.add(var.set_disable_crc(config[CONF_DISABLE_CRC]))
+199 -95
View File
@@ -15,10 +15,69 @@ void Modbus::setup() {
if (this->flow_control_pin_ != nullptr) {
this->flow_control_pin_->setup();
}
}
void Modbus::loop() {
const uint32_t now = App.get_loop_component_start_time();
this->frame_delay_ms_ =
std::max(2, // 1750us minimum per spec - rounded up to 2ms.
// 3.5 characters * 11 bits per character * 1000ms/sec / (bits/sec) (Standard modbus frame delay)
(uint16_t) (3.5 * 11 * 1000 / this->parent_->get_baud_rate()) + 1);
this->long_rx_buffer_delay_ms_ =
(this->parent_->get_rx_full_threshold() * 11 * 1000 / this->parent_->get_baud_rate()) + 1;
}
void Modbus::loop() {
// First process all available incoming data.
this->receive_and_parse_modbus_bytes_();
// If the response frame is finished (including interframe delay) - we timeout.
// The long_rx_buffer_delay accounts for long responses (larger than the UART rx_full_threshold) to avoid timeouts
// when the buffer is filling the back half of the response
const uint16_t timeout = std::max(
(uint16_t) this->frame_delay_ms_,
(uint16_t) (this->rx_buffer_.size() >= this->parent_->get_rx_full_threshold() ? this->long_rx_buffer_delay_ms_
: 0));
// We use millis() here and elsewhere instead of App.get_loop_component_start_time() to avoid stale timestamps
// It's critical in all timestamp comparisons that the left timestamp comes before the right one in time
// If we use a cached value in place of millis() and last_modbus_byte_ is updated inside our loop
// then the comparison is backwards (small negative which wraps to large positive) and will cause a false timeout
// So in this component we don't use any cached timestamp values to avoid these annoying bugs
if (millis() - this->last_modbus_byte_ > timeout) {
this->clear_rx_buffer_(LOG_STR("timeout after partial response"), true);
}
// If we're past the send_wait_time timeout and response buffer doesn't have the start of the expected response
if (this->waiting_for_response_ != 0 &&
millis() - this->last_send_ > this->last_send_tx_offset_ + this->send_wait_time_ &&
(this->rx_buffer_.empty() || this->rx_buffer_[0] != this->waiting_for_response_)) {
ESP_LOGW(TAG, "Stop waiting for response from %" PRIu8 " %" PRIu32 "ms after last send",
this->waiting_for_response_, millis() - this->last_send_);
this->waiting_for_response_ = 0;
}
// If there's no response pending and there's commands in the buffer
this->send_next_frame_();
}
bool Modbus::tx_blocked() {
const uint32_t now = millis();
// We block transmission in any of these case:
// 1. There are bytes in the UART Rx buffer
// 2. There are bytes in our Rx buffer
// 3. We're waiting for a response
// 4. The last sent byte isn't more than frame_delay ms ago (i.e. wait to tell receivers that our previous Tx is done)
// 5. The last received byte isn't more than frame_delay ms ago (i.e. wait to be sure there isn't more Rx coming)
// 6. If we're a client - also wait for the turnaround delay, to give the servers time to process the previous message
return this->available() || !this->rx_buffer_.empty() || (this->waiting_for_response_ != 0) ||
(now - this->last_send_ < this->last_send_tx_offset_ + this->frame_delay_ms_ +
(this->role == ModbusRole::CLIENT ? this->turnaround_delay_ms_ : 0)) ||
(now - this->last_modbus_byte_ <
this->frame_delay_ms_ + (this->role == ModbusRole::CLIENT ? this->turnaround_delay_ms_ : 0));
}
bool Modbus::tx_buffer_empty() { return this->tx_buffer_.empty(); }
void Modbus::receive_and_parse_modbus_bytes_() {
// Read all available bytes in batches to reduce UART call overhead.
size_t avail = this->available();
uint8_t buf[64];
@@ -28,33 +87,20 @@ void Modbus::loop() {
break;
}
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
if (this->parse_modbus_byte_(buf[i])) {
this->last_modbus_byte_ = now;
if (this->rx_buffer_.empty()) {
ESP_LOGV(TAG, "Received first byte %" PRIu8 " (0X%x) %" PRIu32 "ms after last send", buf[i], buf[i],
millis() - this->last_send_);
} else {
size_t at = this->rx_buffer_.size();
if (at > 0) {
ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse failed", at);
this->rx_buffer_.clear();
}
ESP_LOGVV(TAG, "Received byte %" PRIu8 " (0X%x) %" PRIu32 "ms after last send", buf[i], buf[i],
millis() - this->last_send_);
}
}
}
if (now - this->last_modbus_byte_ > 50) {
size_t at = this->rx_buffer_.size();
if (at > 0) {
ESP_LOGV(TAG, "Clearing buffer of %d bytes - timeout", at);
this->rx_buffer_.clear();
}
// stop blocking new send commands after sent_wait_time_ ms after response received
if (now - this->last_send_ > send_wait_time_) {
if (waiting_for_response > 0) {
ESP_LOGV(TAG, "Stop waiting for response from %d", waiting_for_response);
// If the bytes in the rx buffer do not parse, clear out the buffer
if (!this->parse_modbus_byte_(buf[i])) {
this->clear_rx_buffer_(LOG_STR("parse failed"), true);
}
waiting_for_response = 0;
this->last_modbus_byte_ = millis();
}
}
}
@@ -63,7 +109,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
size_t at = this->rx_buffer_.size();
this->rx_buffer_.push_back(byte);
const uint8_t *raw = &this->rx_buffer_[0];
ESP_LOGVV(TAG, "Modbus received Byte %d (0X%x)", byte, byte);
// Byte 0: modbus address (match all)
if (at == 0)
return true;
@@ -101,7 +147,7 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
if (computed_crc != remote_crc)
return true;
ESP_LOGD(TAG, "Modbus user-defined function %02X found", function_code);
ESP_LOGD(TAG, "User-defined function %02X found", function_code);
} else {
// data starts at 2 and length is 4 for read registers commands
@@ -152,9 +198,19 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
uint16_t remote_crc = uint16_t(raw[data_offset + data_len]) | (uint16_t(raw[data_offset + data_len + 1]) << 8);
if (computed_crc != remote_crc) {
if (this->disable_crc_) {
ESP_LOGD(TAG, "Modbus CRC Check failed, but ignored! %02X!=%02X", computed_crc, remote_crc);
ESP_LOGD(TAG, "CRC check failed %" PRIu32 "ms after last send; ignoring", millis() - this->last_send_);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
#endif
ESP_LOGVV(TAG, " (%02X != %02X) %s", computed_crc, remote_crc,
format_hex_pretty_to(hex_buf, this->rx_buffer_.data(), this->rx_buffer_.size()));
} else {
ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc);
ESP_LOGW(TAG, "CRC check failed %" PRIu32 "ms after last send", millis() - this->last_send_);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
#endif
ESP_LOGVV(TAG, " (%02X != %02X) %s", computed_crc, remote_crc,
format_hex_pretty_to(hex_buf, this->rx_buffer_.data(), this->rx_buffer_.size()));
return false;
}
}
@@ -164,52 +220,101 @@ bool Modbus::parse_modbus_byte_(uint8_t byte) {
for (auto *device : this->devices_) {
if (device->address_ == address) {
found = true;
// Is it an error response?
if ((function_code & FUNCTION_CODE_EXCEPTION_MASK) == FUNCTION_CODE_EXCEPTION_MASK) {
ESP_LOGD(TAG, "Modbus error function code: 0x%X exception: %d", function_code, raw[2]);
if (waiting_for_response != 0) {
device->on_modbus_error(function_code & FUNCTION_CODE_MASK, raw[2]);
} else {
// Ignore modbus exception not related to a pending command
ESP_LOGD(TAG, "Ignoring Modbus error - not expecting a response");
}
continue;
}
if (this->role == ModbusRole::SERVER) {
if (function_code == ModbusFunctionCode::READ_HOLDING_REGISTERS ||
function_code == ModbusFunctionCode::READ_INPUT_REGISTERS) {
device->on_modbus_read_registers(function_code, uint16_t(data[1]) | (uint16_t(data[0]) << 8),
uint16_t(data[3]) | (uint16_t(data[2]) << 8));
continue;
}
if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER ||
function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
} else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER ||
function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
device->on_modbus_write_registers(function_code, data);
continue;
}
} else { // We're a client
// Is it an error response?
if ((function_code & FUNCTION_CODE_EXCEPTION_MASK) == FUNCTION_CODE_EXCEPTION_MASK) {
uint8_t exception = raw[2];
ESP_LOGW(TAG,
"Error function code: 0x%X exception: %" PRIu8 ", address: %" PRIu8 ", %" PRIu32
"ms after last send",
function_code, exception, address, millis() - this->last_send_);
if (this->waiting_for_response_ == address) {
device->on_modbus_error(function_code & FUNCTION_CODE_MASK, exception);
} else {
// Ignore modbus exception not related to a pending command
ESP_LOGD(TAG, "Ignoring error - not expecting a response from %" PRIu8 "", address);
}
} else { // Not an error response
if (this->waiting_for_response_ == address) {
device->on_modbus_data(data);
} else {
// Ignore modbus response not related to a pending command
ESP_LOGW(TAG, "Ignoring response - not expecting a response from %" PRIu8 ", %" PRIu32 "ms after last send",
address, millis() - this->last_send_);
}
}
}
// fallthrough for other function codes
device->on_modbus_data(data);
}
}
waiting_for_response = 0;
if (!found) {
ESP_LOGW(TAG, "Got Modbus frame from unknown address 0x%02X! ", address);
if (!found && this->role == ModbusRole::CLIENT) {
ESP_LOGW(TAG, "Got frame from unknown address %" PRIu8 ", %" PRIu32 "ms after last send", address,
millis() - this->last_send_);
}
// reset buffer
ESP_LOGV(TAG, "Clearing buffer of %d bytes - parse succeeded", at);
this->rx_buffer_.clear();
this->clear_rx_buffer_(LOG_STR("parse succeeded"));
if (this->waiting_for_response_ == address)
this->waiting_for_response_ = 0;
return true;
}
void Modbus::send_next_frame_() {
if (this->tx_buffer_.empty())
return;
if (this->tx_blocked())
return;
const ModbusDeviceCommand &frame = this->tx_buffer_.front();
if (this->role == ModbusRole::CLIENT) {
this->waiting_for_response_ = frame.data.get()[0];
}
if (this->flow_control_pin_ != nullptr) {
this->flow_control_pin_->digital_write(true);
this->write_array(frame.data.get(), frame.size);
this->flush();
this->flow_control_pin_->digital_write(false);
this->last_send_tx_offset_ = 0;
} else {
this->write_array(frame.data.get(), frame.size);
this->last_send_tx_offset_ = frame.size * 11 * 1000 / this->parent_->get_baud_rate() + 1;
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
#endif
ESP_LOGV(TAG, "Write: %s %" PRIu32 "ms after last send", format_hex_pretty_to(hex_buf, frame.data.get(), frame.size),
millis() - this->last_send_);
this->last_send_ = millis();
this->tx_buffer_.pop_front();
if (!this->tx_buffer_.empty()) {
ESP_LOGV(TAG, "Write queue contains %" PRIu32 " items.", this->tx_buffer_.size());
}
}
void Modbus::dump_config() {
ESP_LOGCONFIG(TAG,
"Modbus:\n"
" Send Wait Time: %d ms\n"
" Turnaround Time: %d ms\n"
" Frame Delay: %d ms\n"
" Long Rx Buffer Delay: %d ms\n"
" CRC Disabled: %s",
this->send_wait_time_, YESNO(this->disable_crc_));
this->send_wait_time_, this->turnaround_delay_ms_, this->frame_delay_ms_,
this->long_rx_buffer_delay_ms_, YESNO(this->disable_crc_));
LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_);
}
float Modbus::get_setup_priority() const {
@@ -228,15 +333,6 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
return;
}
static constexpr size_t ADDR_SIZE = 1;
static constexpr size_t FC_SIZE = 1;
static constexpr size_t START_ADDR_SIZE = 2;
static constexpr size_t NUM_ENTITIES_SIZE = 2;
static constexpr size_t BYTE_COUNT_SIZE = 1;
static constexpr size_t MAX_PAYLOAD_SIZE = std::numeric_limits<uint8_t>::max();
static constexpr size_t CRC_SIZE = 2;
static constexpr size_t MAX_FRAME_SIZE =
ADDR_SIZE + FC_SIZE + START_ADDR_SIZE + NUM_ENTITIES_SIZE + BYTE_COUNT_SIZE + MAX_PAYLOAD_SIZE + CRC_SIZE;
uint8_t data[MAX_FRAME_SIZE];
size_t pos = 0;
@@ -259,29 +355,16 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
} else {
payload_len = 2; // Write single register or coil
}
if (payload_len + pos + 2 > MAX_FRAME_SIZE) { // Check if payload fits (accounting for CRC)
ESP_LOGE(TAG, "Payload too large to send: %d bytes", payload_len);
return;
}
for (int i = 0; i < payload_len; i++) {
data[pos++] = payload[i];
}
}
auto crc = crc16(data, pos);
data[pos++] = crc >> 0;
data[pos++] = crc >> 8;
if (this->flow_control_pin_ != nullptr)
this->flow_control_pin_->digital_write(true);
this->write_array(data, pos);
this->flush();
if (this->flow_control_pin_ != nullptr)
this->flow_control_pin_->digital_write(false);
waiting_for_response = address;
last_send_ = millis();
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
#endif
ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data, pos));
this->queue_raw_(data, pos);
}
// Helper function for lambdas
@@ -290,23 +373,44 @@ void Modbus::send_raw(const std::vector<uint8_t> &payload) {
if (payload.empty()) {
return;
}
// Frame size: payload + CRC(2)
if (payload.size() + 2 > MAX_FRAME_SIZE) {
ESP_LOGE(TAG, "Attempted to send frame larger than max frame size of %d bytes", MAX_FRAME_SIZE);
return;
}
// Use stack buffer - Modbus frames are small and bounded
uint8_t data[MAX_FRAME_SIZE];
if (this->flow_control_pin_ != nullptr)
this->flow_control_pin_->digital_write(true);
std::memcpy(data, payload.data(), payload.size());
auto crc = crc16(payload.data(), payload.size());
this->write_array(payload);
this->write_byte(crc & 0xFF);
this->write_byte((crc >> 8) & 0xFF);
this->flush();
if (this->flow_control_pin_ != nullptr)
this->flow_control_pin_->digital_write(false);
waiting_for_response = payload[0];
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
this->queue_raw_(data, payload.size());
}
// Assume data and length is valid and append CRC, then queue for sending. Used internally to avoid unnecessary copying
// of data into vectors
void Modbus::queue_raw_(const uint8_t *data, uint16_t len) {
if (this->tx_buffer_.size() < MODBUS_TX_BUFFER_SIZE) {
this->tx_buffer_.emplace_back(data, len);
} else {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_ERROR
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
#endif
ESP_LOGV(TAG, "Modbus write raw: %s", format_hex_pretty_to(hex_buf, payload.data(), payload.size()));
last_send_ = millis();
ESP_LOGE(TAG, "Write buffer full, dropped: %s", format_hex_pretty_to(hex_buf, data, len));
}
}
void Modbus::clear_rx_buffer_(const LogString *reason, bool warn) {
size_t at = this->rx_buffer_.size();
if (at > 0) {
if (warn) {
ESP_LOGW(TAG, "Clearing buffer of %" PRIu32 " bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason),
millis() - this->last_send_);
} else {
ESP_LOGV(TAG, "Clearing buffer of %" PRIu32 " bytes - %s %" PRIu32 "ms after last send", at, LOG_STR_ARG(reason),
millis() - this->last_send_);
}
this->rx_buffer_.clear();
}
}
} // namespace modbus
+46 -9
View File
@@ -5,11 +5,16 @@
#include "esphome/components/modbus/modbus_definitions.h"
#include <cstring>
#include <memory>
#include <vector>
#include <queue>
namespace esphome {
namespace modbus {
static constexpr uint16_t MODBUS_TX_BUFFER_SIZE = 15;
enum ModbusRole {
CLIENT,
SERVER,
@@ -17,6 +22,19 @@ enum ModbusRole {
class ModbusDevice;
struct ModbusDeviceCommand {
// Frame with exact-size allocation to avoid std::vector overhead
std::unique_ptr<uint8_t[]> data;
uint16_t size; // Modbus RTU max is 256 bytes
ModbusDeviceCommand(const uint8_t *src, uint16_t len) : data(std::make_unique<uint8_t[]>(len + 2)), size(len + 2) {
std::memcpy(this->data.get(), src, len);
auto crc = crc16(data.get(), len);
data[len + 0] = crc >> 0;
data[len + 1] = crc >> 8;
}
};
class Modbus : public uart::UARTDevice, public Component {
public:
Modbus() = default;
@@ -30,28 +48,45 @@ class Modbus : public uart::UARTDevice, public Component {
void register_device(ModbusDevice *device) { this->devices_.push_back(device); }
float get_setup_priority() const override;
bool tx_buffer_empty();
bool tx_blocked();
void send(uint8_t address, uint8_t function_code, uint16_t start_address, uint16_t number_of_entities,
uint8_t payload_len = 0, const uint8_t *payload = nullptr);
void send_raw(const std::vector<uint8_t> &payload);
void set_role(ModbusRole role) { this->role = role; }
void set_flow_control_pin(GPIOPin *flow_control_pin) { this->flow_control_pin_ = flow_control_pin; }
uint8_t waiting_for_response{0};
void set_send_wait_time(uint16_t time_in_ms) { send_wait_time_ = time_in_ms; }
void set_disable_crc(bool disable_crc) { disable_crc_ = disable_crc; }
void set_send_wait_time(uint16_t time_in_ms) { this->send_wait_time_ = time_in_ms; }
void set_turnaround_time(uint16_t time_in_ms) { this->turnaround_delay_ms_ = time_in_ms; }
void set_disable_crc(bool disable_crc) { this->disable_crc_ = disable_crc; }
ModbusRole role;
protected:
GPIOPin *flow_control_pin_{nullptr};
bool parse_modbus_byte_(uint8_t byte);
uint16_t send_wait_time_{250};
bool disable_crc_;
std::vector<uint8_t> rx_buffer_;
void receive_and_parse_modbus_bytes_();
void clear_rx_buffer_(const LogString *reason, bool warn = false);
void send_next_frame_();
void queue_raw_(const uint8_t *data, uint16_t len);
uint32_t last_modbus_byte_{0};
uint32_t last_send_{0};
uint32_t last_send_tx_offset_{0};
uint16_t frame_delay_ms_{5};
uint16_t long_rx_buffer_delay_ms_{0};
uint16_t send_wait_time_{250};
uint16_t turnaround_delay_ms_{100};
uint8_t waiting_for_response_{0};
bool disable_crc_{false};
GPIOPin *flow_control_pin_{nullptr};
std::vector<uint8_t> rx_buffer_;
std::vector<ModbusDevice *> devices_;
// std::deque is appropriate here since we need a FIFO buffer, and we can't know ahead of time how many
// requests will be queued. Each modbus component may queue multiple requests, and the sequence of scheduling
// may change at run time.
std::deque<ModbusDeviceCommand> tx_buffer_;
};
class ModbusDevice {
@@ -76,7 +111,9 @@ class ModbusDevice {
this->send_raw(error_response);
}
// If more than one device is connected block sending a new command before a response is received
bool waiting_for_response() { return parent_->waiting_for_response != 0; }
ESPDEPRECATED("Use ready_for_immediate_send() instead. Removed in 2026.9.0", "2026.3.0")
bool waiting_for_response() { return !ready_for_immediate_send(); }
bool ready_for_immediate_send() { return parent_->tx_buffer_empty() && !parent_->tx_blocked(); }
protected:
friend Modbus;
@@ -81,6 +81,8 @@ const uint8_t MAX_NUM_OF_REGISTERS_TO_WRITE = 123; // 0x7B
// 6.3 03 (0x03) Read Holding Registers
// 6.4 04 (0x04) Read Input Registers
const uint8_t MAX_NUM_OF_REGISTERS_TO_READ = 125; // 0x7D
static constexpr uint16_t MAX_FRAME_SIZE = 256;
/// End of Modbus definitions
} // namespace modbus
} // namespace esphome
@@ -48,6 +48,7 @@ CONF_SERVER_REGISTERS = "server_registers"
MULTI_CONF = True
modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller")
modbus_ns = cg.esphome_ns.namespace("modbus")
ModbusController = modbus_controller_ns.class_(
"ModbusController", cg.PollingComponent, modbus.ModbusDevice
)
@@ -56,7 +57,7 @@ SensorItem = modbus_controller_ns.struct("SensorItem")
ServerCourtesyResponse = modbus_controller_ns.struct("ServerCourtesyResponse")
ServerRegister = modbus_controller_ns.struct("ServerRegister")
ModbusFunctionCode_ns = modbus_controller_ns.namespace("ModbusFunctionCode")
ModbusFunctionCode_ns = modbus_ns.namespace("ModbusFunctionCode")
ModbusFunctionCode = ModbusFunctionCode_ns.enum("ModbusFunctionCode")
MODBUS_FUNCTION_CODE = {
"read_coils": ModbusFunctionCode.READ_COILS,
@@ -18,7 +18,7 @@ void ModbusController::setup() { this->create_register_ranges_(); }
bool ModbusController::send_next_command_() {
uint32_t last_send = millis() - this->last_command_timestamp_;
if ((last_send > this->command_throttle_) && !waiting_for_response() && !this->command_queue_.empty()) {
if ((last_send > this->command_throttle_) && this->ready_for_immediate_send() && !this->command_queue_.empty()) {
auto &command = this->command_queue_.front();
// remove from queue if command was sent too often
@@ -108,7 +108,7 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
}
// Get temperature of sensor
uint8_t temp_in_c = this->parse_temperature_(mopeka_data);
int8_t temp_in_c = this->parse_temperature_(mopeka_data);
if (this->temperature_ != nullptr) {
this->temperature_->publish_state(temp_in_c);
}
@@ -223,12 +223,12 @@ uint8_t MopekaStdCheck::parse_battery_level_(const mopeka_std_package *message)
return (uint8_t) percent;
}
uint8_t MopekaStdCheck::parse_temperature_(const mopeka_std_package *message) {
int8_t MopekaStdCheck::parse_temperature_(const mopeka_std_package *message) {
uint8_t tmp = message->raw_temp;
if (tmp == 0x0) {
return -40;
} else {
return (uint8_t) ((tmp - 25.0f) * 1.776964f);
return static_cast<int8_t>((tmp - 25.0f) * 1.776964f);
}
}
@@ -71,7 +71,7 @@ class MopekaStdCheck : public Component, public esp32_ble_tracker::ESPBTDeviceLi
float get_lpg_speed_of_sound_(float temperature);
uint8_t parse_battery_level_(const mopeka_std_package *message);
uint8_t parse_temperature_(const mopeka_std_package *message);
int8_t parse_temperature_(const mopeka_std_package *message);
};
} // namespace mopeka_std_check
+1 -1
View File
@@ -80,7 +80,7 @@ void MPU6886Component::setup() {
accel_config &= 0b11100111;
accel_config |= (MPU6886_RANGE_2G << 3);
ESP_LOGV(TAG, " Output accel_config: 0b" BYTE_TO_BINARY_PATTERN, BYTE_TO_BINARY(accel_config));
if (!this->write_byte(MPU6886_REGISTER_GYRO_CONFIG, gyro_config)) {
if (!this->write_byte(MPU6886_REGISTER_ACCEL_CONFIG, accel_config)) {
this->mark_failed();
return;
}
+1 -43
View File
@@ -1,53 +1,11 @@
#include "util.h"
#include "esphome/core/defines.h"
#ifdef USE_NETWORK
#ifdef USE_WIFI
#include "esphome/components/wifi/wifi_component.h"
#endif
#ifdef USE_ETHERNET
#include "esphome/components/ethernet/ethernet_component.h"
#endif
#ifdef USE_OPENTHREAD
#include "esphome/components/openthread/openthread.h"
#endif
#ifdef USE_MODEM
#include "esphome/components/modem/modem_component.h"
#endif
namespace esphome::network {
// The order of the components is important: WiFi should come after any possible main interfaces (it may be used as
// an AP that use a previous interface for NAT).
bool is_connected() {
#ifdef USE_ETHERNET
if (ethernet::global_eth_component != nullptr && ethernet::global_eth_component->is_connected())
return true;
#endif
#ifdef USE_MODEM
if (modem::global_modem_component != nullptr)
return modem::global_modem_component->is_connected();
#endif
#ifdef USE_WIFI
if (wifi::global_wifi_component != nullptr)
return wifi::global_wifi_component->is_connected();
#endif
#ifdef USE_OPENTHREAD
if (openthread::global_openthread_component != nullptr)
return openthread::global_openthread_component->is_connected();
#endif
#ifdef USE_HOST
return true; // Assume its connected
#endif
return false;
}
// an AP that uses a previous interface for NAT).
bool is_disabled() {
#ifdef USE_MODEM
+44 -1
View File
@@ -2,12 +2,55 @@
#include "esphome/core/defines.h"
#ifdef USE_NETWORK
#include <string>
#include "esphome/core/helpers.h"
#include "ip_address.h"
#ifdef USE_ETHERNET
#include "esphome/components/ethernet/ethernet_component.h"
#endif
#ifdef USE_MODEM
#include "esphome/components/modem/modem_component.h"
#endif
#ifdef USE_WIFI
#include "esphome/components/wifi/wifi_component.h"
#endif
#ifdef USE_OPENTHREAD
#include "esphome/components/openthread/openthread.h"
#endif
namespace esphome::network {
// The order of the components is important: WiFi should come after any possible main interfaces (it may be used as
// an AP that uses a previous interface for NAT).
/// Return whether the node is connected to the network (through wifi, eth, ...)
bool is_connected();
ESPHOME_ALWAYS_INLINE inline bool is_connected() {
#ifdef USE_ETHERNET
if (ethernet::global_eth_component != nullptr && ethernet::global_eth_component->is_connected())
return true;
#endif
#ifdef USE_MODEM
if (modem::global_modem_component != nullptr)
return modem::global_modem_component->is_connected();
#endif
#ifdef USE_WIFI
if (wifi::global_wifi_component != nullptr)
return wifi::global_wifi_component->is_connected();
#endif
#ifdef USE_OPENTHREAD
if (openthread::global_openthread_component != nullptr)
return openthread::global_openthread_component->is_connected();
#endif
#ifdef USE_HOST
return true; // Assume it's connected
#endif
return false;
}
/// Return whether the network is disabled (only wifi for now)
bool is_disabled();
/// Get the active network hostname
+3 -3
View File
@@ -337,7 +337,7 @@ void Nextion::loop() {
this->started_ms_ = App.get_loop_component_start_time();
if (this->startup_override_ms_ > 0 &&
this->started_ms_ + this->startup_override_ms_ < App.get_loop_component_start_time()) {
App.get_loop_component_start_time() - this->started_ms_ > this->startup_override_ms_) {
ESP_LOGV(TAG, "Manual ready set");
this->connection_state_.nextion_reports_is_setup_ = true;
}
@@ -853,10 +853,10 @@ void Nextion::process_nextion_commands_() {
const uint32_t ms = App.get_loop_component_start_time();
if (this->max_q_age_ms_ > 0 && !this->nextion_queue_.empty() &&
this->nextion_queue_.front()->queue_time + this->max_q_age_ms_ < ms) {
ms - this->nextion_queue_.front()->queue_time > this->max_q_age_ms_) {
for (size_t i = 0; i < this->nextion_queue_.size(); i++) {
NextionComponentBase *component = this->nextion_queue_[i]->component;
if (this->nextion_queue_[i]->queue_time + this->max_q_age_ms_ < ms) {
if (ms - this->nextion_queue_[i]->queue_time > this->max_q_age_ms_) {
if (this->nextion_queue_[i]->queue_time == 0) {
ESP_LOGD(TAG, "Remove old queue '%s':'%s' (t=0)", component->get_queue_type_string().c_str(),
component->get_variable_name().c_str());
+16 -2
View File
@@ -8,8 +8,14 @@ static const char *const TAG = "nfc.ndef_message";
NdefMessage::NdefMessage(std::vector<uint8_t> &data) {
ESP_LOGV(TAG, "Building NdefMessage with %zu bytes", data.size());
uint8_t index = 0;
while (index <= data.size()) {
size_t index = 0;
while (index < data.size()) {
// Minimum record: TNF byte + type length byte + payload length (1 or 4 bytes)
if (index + 2 >= data.size()) {
ESP_LOGE(TAG, "Truncated record header; aborting");
break;
}
uint8_t tnf_byte = data[index++];
bool me = tnf_byte & 0x40; // Message End bit (is set if this is the last record of the message)
bool sr = tnf_byte & 0x10; // Short record bit (is set if payload size is less or equal to 255 bytes)
@@ -23,6 +29,10 @@ NdefMessage::NdefMessage(std::vector<uint8_t> &data) {
if (sr) {
payload_length = data[index++];
} else {
if (index + 4 > data.size()) {
ESP_LOGE(TAG, "Truncated payload length; aborting");
break;
}
payload_length = (static_cast<uint32_t>(data[index]) << 24) | (static_cast<uint32_t>(data[index + 1]) << 16) |
(static_cast<uint32_t>(data[index + 2]) << 8) | static_cast<uint32_t>(data[index + 3]);
index += 4;
@@ -30,6 +40,10 @@ NdefMessage::NdefMessage(std::vector<uint8_t> &data) {
uint8_t id_length = 0;
if (il) {
if (index >= data.size()) {
ESP_LOGE(TAG, "Truncated ID length; aborting");
break;
}
id_length = data[index++];
}
+21
View File
@@ -14,9 +14,12 @@ import esphome.config_validation as cv
from esphome.const import (
CONF_CHANNEL,
CONF_ENABLE_IPV6,
CONF_FRAMEWORK,
CONF_ID,
CONF_LOG_LEVEL,
CONF_OUTPUT_POWER,
CONF_USE_ADDRESS,
PLATFORM_ESP32,
)
from esphome.core import CORE, TimePeriodMilliseconds
import esphome.final_validate as fv
@@ -46,6 +49,15 @@ AUTO_LOAD = ["network"]
CONFLICTS_WITH = ["wifi"]
DEPENDENCIES = ["esp32"]
IDF_TO_OT_LOG_LEVEL = {
"NONE": "NONE",
"ERROR": "CRIT",
"WARN": "WARN",
"INFO": "NOTE",
"DEBUG": "INFO",
"VERBOSE": "DEBG",
}
CONF_DEVICE_TYPES = [
"FTD",
"MTD",
@@ -198,6 +210,15 @@ def _final_validate(_):
"Please set `enable_ipv6: true` in the `network` configuration."
)
if (
(esp32_config := full_config.get(PLATFORM_ESP32)) is not None
and (fw_config := esp32_config.get(CONF_FRAMEWORK)) is not None
and (log_level := fw_config.get(CONF_LOG_LEVEL)) is not None
):
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_LOG_LEVEL_DYNAMIC", False)
ot_log_level = IDF_TO_OT_LOG_LEVEL.get(log_level, log_level)
add_idf_sdkconfig_option(f"CONFIG_OPENTHREAD_LOG_LEVEL_{ot_log_level}", True)
FINAL_VALIDATE_SCHEMA = _final_validate
+1 -1
View File
@@ -90,7 +90,7 @@ class InstanceLock {
otInstance *get_instance();
private:
// Use a private constructor in order to force thehandling
// Use a private constructor in order to force the handling
// of acquisition failure
InstanceLock() {}
};
@@ -197,6 +197,7 @@ network::IPAddresses OpenThreadComponent::get_ip_addresses() {
esp_netif_t *netif = esp_netif_get_default_netif();
count = esp_netif_get_all_ip6(netif, if_ip6s);
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
assert(count < addresses.size());
for (int i = 0; i < count; i++) {
addresses[i + 1] = network::IPAddress(&if_ip6s[i]);
}
-13
View File
@@ -49,17 +49,6 @@ enum OTAState {
OTA_ERROR,
};
class OTABackend {
public:
virtual ~OTABackend() = default;
virtual OTAResponseTypes begin(size_t image_size) = 0;
virtual void set_update_md5(const char *md5) = 0;
virtual OTAResponseTypes write(uint8_t *data, size_t len) = 0;
virtual OTAResponseTypes end() = 0;
virtual void abort() = 0;
virtual bool supports_compression() = 0;
};
/** Listener interface for OTA state changes.
*
* Components can implement this interface to receive OTA state updates
@@ -130,7 +119,5 @@ OTAGlobalCallback *get_global_ota_callback();
// - notify_state_deferred_() when in separate task (e.g., web_server OTA)
// This ensures proper listener execution in all contexts.
#endif
std::unique_ptr<ota::OTABackend> make_ota_backend();
} // namespace ota
} // namespace esphome
@@ -12,7 +12,7 @@ namespace ota {
static const char *const TAG = "ota.arduino_libretiny";
std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::ArduinoLibreTinyOTABackend>(); }
std::unique_ptr<ArduinoLibreTinyOTABackend> make_ota_backend() { return make_unique<ArduinoLibreTinyOTABackend>(); }
OTAResponseTypes ArduinoLibreTinyOTABackend::begin(size_t image_size) {
// Handle UPDATE_SIZE_UNKNOWN (0) which is used by web server OTA
@@ -7,19 +7,21 @@
namespace esphome {
namespace ota {
class ArduinoLibreTinyOTABackend final : public OTABackend {
class ArduinoLibreTinyOTABackend final {
public:
OTAResponseTypes begin(size_t image_size) override;
void set_update_md5(const char *md5) override;
OTAResponseTypes write(uint8_t *data, size_t len) override;
OTAResponseTypes end() override;
void abort() override;
bool supports_compression() override { return false; }
OTAResponseTypes begin(size_t image_size);
void set_update_md5(const char *md5);
OTAResponseTypes write(uint8_t *data, size_t len);
OTAResponseTypes end();
void abort();
bool supports_compression() { return false; }
private:
bool md5_set_{false};
};
std::unique_ptr<ArduinoLibreTinyOTABackend> make_ota_backend();
} // namespace ota
} // namespace esphome
@@ -14,7 +14,7 @@ namespace ota {
static const char *const TAG = "ota.arduino_rp2040";
std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::ArduinoRP2040OTABackend>(); }
std::unique_ptr<ArduinoRP2040OTABackend> make_ota_backend() { return make_unique<ArduinoRP2040OTABackend>(); }
OTAResponseTypes ArduinoRP2040OTABackend::begin(size_t image_size) {
// OTA size of 0 is not currently handled, but
@@ -9,19 +9,21 @@
namespace esphome {
namespace ota {
class ArduinoRP2040OTABackend final : public OTABackend {
class ArduinoRP2040OTABackend final {
public:
OTAResponseTypes begin(size_t image_size) override;
void set_update_md5(const char *md5) override;
OTAResponseTypes write(uint8_t *data, size_t len) override;
OTAResponseTypes end() override;
void abort() override;
bool supports_compression() override { return false; }
OTAResponseTypes begin(size_t image_size);
void set_update_md5(const char *md5);
OTAResponseTypes write(uint8_t *data, size_t len);
OTAResponseTypes end();
void abort();
bool supports_compression() { return false; }
private:
bool md5_set_{false};
};
std::unique_ptr<ArduinoRP2040OTABackend> make_ota_backend();
} // namespace ota
} // namespace esphome
@@ -48,7 +48,7 @@ namespace esphome::ota {
static const char *const TAG = "ota.esp8266";
std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::ESP8266OTABackend>(); }
std::unique_ptr<ESP8266OTABackend> make_ota_backend() { return make_unique<ESP8266OTABackend>(); }
OTAResponseTypes ESP8266OTABackend::begin(size_t image_size) {
// Handle UPDATE_SIZE_UNKNOWN (0) by calculating available space
+9 -7
View File
@@ -12,15 +12,15 @@ namespace esphome::ota {
/// OTA backend for ESP8266 using native SDK functions.
/// This implementation bypasses the Arduino Updater library to save ~228 bytes of RAM
/// by not having a global Update object in .bss.
class ESP8266OTABackend final : public OTABackend {
class ESP8266OTABackend final {
public:
OTAResponseTypes begin(size_t image_size) override;
void set_update_md5(const char *md5) override;
OTAResponseTypes write(uint8_t *data, size_t len) override;
OTAResponseTypes end() override;
void abort() override;
OTAResponseTypes begin(size_t image_size);
void set_update_md5(const char *md5);
OTAResponseTypes write(uint8_t *data, size_t len);
OTAResponseTypes end();
void abort();
// Compression supported in all ESP8266 Arduino versions ESPHome supports (>= 2.7.0)
bool supports_compression() override { return true; }
bool supports_compression() { return true; }
protected:
/// Erase flash sector if current address is at sector boundary
@@ -54,5 +54,7 @@ class ESP8266OTABackend final : public OTABackend {
bool md5_set_{false};
};
std::unique_ptr<ESP8266OTABackend> make_ota_backend();
} // namespace esphome::ota
#endif // USE_ESP8266
@@ -11,7 +11,7 @@
namespace esphome {
namespace ota {
std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::IDFOTABackend>(); }
std::unique_ptr<IDFOTABackend> make_ota_backend() { return make_unique<IDFOTABackend>(); }
OTAResponseTypes IDFOTABackend::begin(size_t image_size) {
#ifdef USE_OTA_ROLLBACK
+9 -7
View File
@@ -10,14 +10,14 @@
namespace esphome {
namespace ota {
class IDFOTABackend final : public OTABackend {
class IDFOTABackend final {
public:
OTAResponseTypes begin(size_t image_size) override;
void set_update_md5(const char *md5) override;
OTAResponseTypes write(uint8_t *data, size_t len) override;
OTAResponseTypes end() override;
void abort() override;
bool supports_compression() override { return false; }
OTAResponseTypes begin(size_t image_size);
void set_update_md5(const char *md5);
OTAResponseTypes write(uint8_t *data, size_t len);
OTAResponseTypes end();
void abort();
bool supports_compression() { return false; }
private:
esp_ota_handle_t update_handle_{0};
@@ -27,6 +27,8 @@ class IDFOTABackend final : public OTABackend {
bool md5_set_{false};
};
std::unique_ptr<IDFOTABackend> make_ota_backend();
} // namespace ota
} // namespace esphome
#endif // USE_ESP32
@@ -0,0 +1,27 @@
#pragma once
#include "ota_backend.h"
#include <memory>
#ifdef USE_ESP8266
#include "ota_backend_esp8266.h"
#elif defined(USE_ESP32)
#include "ota_backend_esp_idf.h"
#elif defined(USE_RP2040)
#include "ota_backend_arduino_rp2040.h"
#elif defined(USE_LIBRETINY)
#include "ota_backend_arduino_libretiny.h"
#elif defined(USE_HOST)
#include "ota_backend_host.h"
#else
// Stub for static analysis when no platform is defined
namespace esphome::ota {
struct StubOTABackend {};
std::unique_ptr<StubOTABackend> make_ota_backend();
} // namespace esphome::ota
#endif
namespace esphome::ota {
using OTABackendPtr = decltype(make_ota_backend());
} // namespace esphome::ota
+1 -1
View File
@@ -8,7 +8,7 @@ namespace esphome::ota {
// Stub implementation - OTA is not supported on host platform.
// All methods return error codes to allow compilation of configs with OTA triggers.
std::unique_ptr<ota::OTABackend> make_ota_backend() { return make_unique<ota::HostOTABackend>(); }
std::unique_ptr<HostOTABackend> make_ota_backend() { return make_unique<HostOTABackend>(); }
OTAResponseTypes HostOTABackend::begin(size_t image_size) { return OTA_RESPONSE_ERROR_UPDATE_PREPARE; }
+9 -7
View File
@@ -7,15 +7,17 @@ namespace esphome::ota {
/// Stub OTA backend for host platform - allows compilation but does not implement OTA.
/// All operations return error codes immediately. This enables configurations with
/// OTA triggers to compile for host platform during development.
class HostOTABackend final : public OTABackend {
class HostOTABackend final {
public:
OTAResponseTypes begin(size_t image_size) override;
void set_update_md5(const char *md5) override;
OTAResponseTypes write(uint8_t *data, size_t len) override;
OTAResponseTypes end() override;
void abort() override;
bool supports_compression() override { return false; }
OTAResponseTypes begin(size_t image_size);
void set_update_md5(const char *md5);
OTAResponseTypes write(uint8_t *data, size_t len);
OTAResponseTypes end();
void abort();
bool supports_compression() { return false; }
};
std::unique_ptr<HostOTABackend> make_ota_backend();
} // namespace esphome::ota
#endif
@@ -330,15 +330,16 @@ void PacketTransport::update() {
if (!this->ping_pong_enable_) {
return;
}
auto now = millis() / 1000;
if (this->last_key_time_ + this->ping_pong_recyle_time_ < now) {
uint32_t now = millis();
uint32_t ping_request_age = now - this->last_key_time_;
if (ping_request_age > this->ping_pong_recyle_time_ * 1000u) {
this->resend_ping_key_ = this->ping_pong_enable_;
ESP_LOGV(TAG, "Ping request, age %" PRIu32, now - this->last_key_time_);
ESP_LOGV(TAG, "Ping request, age %" PRIu32, ping_request_age);
this->last_key_time_ = now;
}
for (const auto &provider : this->providers_) {
uint32_t key_response_age = now - provider.second.last_key_response_time;
if (key_response_age > (this->ping_pong_recyle_time_ * 2u)) {
if (key_response_age > (this->ping_pong_recyle_time_ * 2000u)) {
#ifdef USE_STATUS_SENSOR
if (provider.second.status_sensor != nullptr && provider.second.status_sensor->state) {
ESP_LOGI(TAG, "Ping status for %s timeout at %" PRIu32 " with age %" PRIu32, provider.first.c_str(), now,
@@ -496,7 +497,7 @@ void PacketTransport::process_(std::span<const uint8_t> data) {
if (decoder.decode(PING_KEY, key) == DECODE_OK) {
if (key == this->ping_key_) {
ping_key_seen = true;
provider.last_key_response_time = millis() / 1000;
provider.last_key_response_time = millis();
ESP_LOGV(TAG, "Found good ping key %X at timestamp %" PRIu32, (unsigned) key, provider.last_key_response_time);
} else {
ESP_LOGV(TAG, "Unknown ping key %X", (unsigned) key);
+10 -4
View File
@@ -88,9 +88,10 @@ bool PN532Spi::read_response(uint8_t command, std::vector<uint8_t> &data) {
#endif
ESP_LOGV(TAG, "Header data: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), header.data(), header.size()));
if (header[0] != 0x00 && header[1] != 0x00 && header[2] != 0xFF) {
if (header[0] != 0x00 || header[1] != 0x00 || header[2] != 0xFF) {
// invalid packet
ESP_LOGV(TAG, "read data invalid preamble!");
this->disable();
return false;
}
@@ -100,15 +101,20 @@ bool PN532Spi::read_response(uint8_t command, std::vector<uint8_t> &data) {
if (!valid_header) {
ESP_LOGV(TAG, "read data invalid header!");
this->disable();
return false;
}
// full length of message, including command response
// full length of message, including command response (minimum 2: TFI + command response)
uint8_t full_len = header[3];
if (full_len < 2) {
ESP_LOGV(TAG, "read data has no payload");
this->disable();
return false;
}
// length of data, excluding command response
uint8_t len = full_len - 1;
if (full_len == 0)
len = 0;
ESP_LOGV(TAG, "Reading response of length %d", len);
@@ -175,7 +175,8 @@ void PulseCounterSensor::setup() {
void PulseCounterSensor::set_total_pulses(uint32_t pulses) {
this->current_total_ = pulses;
this->total_sensor_->publish_state(pulses);
if (this->total_sensor_ != nullptr)
this->total_sensor_->publish_state(pulses);
}
void PulseCounterSensor::dump_config() {
@@ -61,6 +61,10 @@ optional<ParseResult> PVVXMiThermometer::parse_header_(const esp32_ble_tracker::
}
auto raw = service_data.data;
if (raw.size() < 14) {
ESP_LOGVV(TAG, "parse_header_(): service data too short (%zu).", raw.size());
return {};
}
static uint8_t last_frame_count = 0;
if (last_frame_count == raw[13]) {
+1 -1
View File
@@ -251,7 +251,7 @@ void QMP6988Component::set_power_mode_(uint8_t power_mode) {
void QMP6988Component::write_filter_(QMP6988IIRFilter filter) {
uint8_t data;
data = (filter & 0x03);
data = (filter & QMP6988_CONFIG_REG_FILTER_MSK);
this->write_byte(QMP6988_CONFIG_REG, data);
delay(10);
}
+1
View File
@@ -169,6 +169,7 @@ void RC522::loop() {
default:
ESP_LOGE(TAG, "uid_idx_ invalid, uid_idx_ = %d", uid_idx_);
state_ = STATE_DONE;
return;
}
buffer_[1] = 32;
pcd_transceive_data_(2);
+3 -3
View File
@@ -91,7 +91,7 @@ def _parse_platform_version(value):
# The default/recommended arduino framework version
# - https://github.com/earlephilhower/arduino-pico/releases
# - https://api.registry.platformio.org/v3/packages/earlephilhower/tool/framework-arduinopico
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(5, 5, 0)
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(5, 5, 1)
# The raspberrypi platform version to use for arduino frameworks
# - https://github.com/maxgerhardt/platform-raspberrypi/tags
@@ -101,8 +101,8 @@ RECOMMENDED_ARDUINO_PLATFORM_VERSION = "v1.4.0-gcc14-arduinopico460"
def _arduino_check_versions(value):
value = value.copy()
lookups = {
"dev": (cv.Version(5, 5, 0), "https://github.com/earlephilhower/arduino-pico"),
"latest": (cv.Version(5, 5, 0), None),
"dev": (cv.Version(5, 5, 1), "https://github.com/earlephilhower/arduino-pico"),
"latest": (cv.Version(5, 5, 1), None),
"recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None),
}
@@ -9,21 +9,16 @@ namespace esphome {
namespace runtime_stats {
RuntimeStatsCollector::RuntimeStatsCollector() : log_interval_(60000), next_log_time_(0) {
RuntimeStatsCollector::RuntimeStatsCollector() : log_interval_(60000), next_log_time_(60000) {
global_runtime_stats = this;
}
void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_us, uint32_t current_time) {
void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_us) {
if (component == nullptr)
return;
// Record stats using component pointer as key
this->component_stats_[component].record_time(duration_us);
if (this->next_log_time_ == 0) {
this->next_log_time_ = current_time + this->log_interval_;
return;
}
}
void RuntimeStatsCollector::log_stats_() {
@@ -88,10 +83,7 @@ void RuntimeStatsCollector::log_stats_() {
}
void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) {
if (this->next_log_time_ == 0)
return;
if (current_time >= this->next_log_time_) {
if ((int32_t) (current_time - this->next_log_time_) >= 0) {
this->log_stats_();
this->reset_stats_();
this->next_log_time_ = current_time + this->log_interval_;
@@ -7,6 +7,7 @@
#include <map>
#include <cstdint>
#include <cstring>
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -80,10 +81,13 @@ class RuntimeStatsCollector {
public:
RuntimeStatsCollector();
void set_log_interval(uint32_t log_interval) { this->log_interval_ = log_interval; }
void set_log_interval(uint32_t log_interval) {
this->log_interval_ = log_interval;
this->next_log_time_ = millis() + log_interval;
}
uint32_t get_log_interval() const { return this->log_interval_; }
void record_component_time(Component *component, uint32_t duration_us, uint32_t current_time);
void record_component_time(Component *component, uint32_t duration_us);
// Process any pending stats printing (should be called after component loop)
void process_pending_stats(uint32_t current_time);
@@ -101,7 +105,7 @@ class RuntimeStatsCollector {
// We use Component* as the key since each component is unique
std::map<Component *, ComponentRuntimeStats> component_stats_;
uint32_t log_interval_;
uint32_t next_log_time_;
uint32_t next_log_time_{0};
};
} // namespace runtime_stats
+7 -4
View File
@@ -63,10 +63,13 @@ bool parse_ruuvi_data_byte(const esp32_ble_tracker::adv_data_t &adv_data, RuuviP
result.acceleration_x = data[6] == 0xFF && data[7] == 0xFF ? NAN : acceleration_x;
result.acceleration_y = data[8] == 0xFF && data[9] == 0xFF ? NAN : acceleration_y;
result.acceleration_z = data[10] == 0xFF && data[11] == 0xFF ? NAN : acceleration_z;
result.acceleration = result.acceleration_x == NAN || result.acceleration_y == NAN || result.acceleration_z == NAN
? NAN
: sqrtf(acceleration_x * acceleration_x + acceleration_y * acceleration_y +
acceleration_z * acceleration_z);
if ((data[6] != 0xFF || data[7] != 0xFF) && (data[8] != 0xFF || data[9] != 0xFF) &&
(data[10] != 0xFF || data[11] != 0xFF)) {
result.acceleration =
sqrtf(acceleration_x * acceleration_x + acceleration_y * acceleration_y + acceleration_z * acceleration_z);
} else {
result.acceleration = NAN;
}
result.battery_voltage = (power_info >> 5) == 0x7FF ? NAN : battery_voltage;
result.tx_power = (power_info & 0x1F) == 0x1F ? NAN : tx_power;
result.movement_counter = movement_counter;
+2 -1
View File
@@ -307,7 +307,7 @@ bool SCD4XComponent::start_measurement_() {
break;
}
static uint8_t remaining_retries = 3;
uint8_t remaining_retries = 3;
while (remaining_retries) {
if (!this->write_command(measurement_command)) {
ESP_LOGE(TAG, "Error starting measurements");
@@ -316,6 +316,7 @@ bool SCD4XComponent::start_measurement_() {
if (--remaining_retries == 0)
return false;
delay(50); // NOLINT wait 50 ms and try again
continue;
}
this->status_clear_warning();
return true;
@@ -177,10 +177,14 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c
uint16_t has_target_int = encode_uint16(data[1], data[0]);
this->has_target_binary_sensor_->publish_state(has_target_int);
if (has_target_int == 0) {
this->breath_rate_sensor_->publish_state(0.0);
this->heart_rate_sensor_->publish_state(0.0);
this->distance_sensor_->publish_state(0.0);
this->num_targets_sensor_->publish_state(0);
if (this->breath_rate_sensor_ != nullptr)
this->breath_rate_sensor_->publish_state(0.0);
if (this->heart_rate_sensor_ != nullptr)
this->heart_rate_sensor_->publish_state(0.0);
if (this->distance_sensor_ != nullptr)
this->distance_sensor_->publish_state(0.0);
if (this->num_targets_sensor_ != nullptr)
this->num_targets_sensor_->publish_state(0);
}
}
break;
+1 -1
View File
@@ -10,7 +10,7 @@ static const uint8_t MEASURECOMMANDS[] = {0xFD, 0xF6, 0xE0};
static const uint8_t SERIAL_NUMBER_COMMAND = 0x89;
void SHT4XComponent::start_heater_() {
uint8_t cmd[] = {MEASURECOMMANDS[this->heater_command_]};
uint8_t cmd[] = {this->heater_command_};
ESP_LOGD(TAG, "Heater turning on");
if (this->write(cmd, 1) != i2c::ERROR_OK) {
+3 -2
View File
@@ -196,7 +196,8 @@ void Sim800LComponent::parse_cmd_(std::string message) {
case STATE_CREG_WAIT: {
// Response: "+CREG: 0,1" -- the one there means registered ok
// "+CREG: -,-" means not registered ok
bool registered = message.compare(0, 6, "+CREG:") == 0 && (message[9] == '1' || message[9] == '5');
bool registered =
message.size() > 9 && message.compare(0, 6, "+CREG:") == 0 && (message[9] == '1' || message[9] == '5');
if (registered) {
if (!this->registered_) {
ESP_LOGD(TAG, "Registered OK");
@@ -205,7 +206,7 @@ void Sim800LComponent::parse_cmd_(std::string message) {
this->expect_ack_ = true;
} else {
ESP_LOGW(TAG, "Registration Fail");
if (message[7] == '0') { // Network registration is disable, enable it
if (message.size() > 7 && message[7] == '0') { // Network registration is disabled, enable it
send_cmd_("AT+CREG=1");
this->expect_ack_ = true;
this->state_ = STATE_SETUP_CMGF;
+2
View File
@@ -35,6 +35,8 @@ bool SmlFile::setup_node(SmlNode *node) {
// Check if we need additional length bytes
if (overlength) {
if (this->pos_ + 1 >= this->buffer_.size())
return false;
// Shift the current length to the higher nibble
// and add the lower nibble of the next byte to the length
length = (length << 4) + (this->buffer_[this->pos_ + 1] & 0x0f);
@@ -169,7 +169,7 @@ void HOT SSD1322::draw_absolute_pixel_internal(int x, int y, Color color) {
// ensure 'color4' is valid (only 4 bits aka 1 nibble) and shift the bits left when necessary
color4 = (color4 & SSD1322_COLORMASK) << shift;
// first mask off the nibble we must change...
this->buffer_[pos] &= (~SSD1322_COLORMASK >> shift);
this->buffer_[pos] &= (static_cast<uint8_t>(~SSD1322_COLORMASK) >> shift);
// ...then lay the new nibble back on top. done!
this->buffer_[pos] |= color4;
}
@@ -202,7 +202,7 @@ void HOT SSD1325::draw_absolute_pixel_internal(int x, int y, Color color) {
// ensure 'color4' is valid (only 4 bits aka 1 nibble) and shift the bits left when necessary
color4 = (color4 & SSD1325_COLORMASK) << shift;
// first mask off the nibble we must change...
this->buffer_[pos] &= (~SSD1325_COLORMASK >> shift);
this->buffer_[pos] &= (static_cast<uint8_t>(~SSD1325_COLORMASK) >> shift);
// ...then lay the new nibble back on top. done!
this->buffer_[pos] |= color4;
}
@@ -145,7 +145,7 @@ void HOT SSD1327::draw_absolute_pixel_internal(int x, int y, Color color) {
// ensure 'color4' is valid (only 4 bits aka 1 nibble) and shift the bits left when necessary
color4 = (color4 & SSD1327_COLORMASK) << shift;
// first mask off the nibble we must change...
this->buffer_[pos] &= (~SSD1327_COLORMASK >> shift);
this->buffer_[pos] &= (static_cast<uint8_t>(~SSD1327_COLORMASK) >> shift);
// ...then lay the new nibble back on top. done!
this->buffer_[pos] |= color4;
}
+1 -13
View File
@@ -466,7 +466,7 @@ void HOT ST7735::write_display_data_() {
}
void ST7735::spi_master_write_addr_(uint16_t addr1, uint16_t addr2) {
static uint8_t byte[4];
uint8_t byte[4];
byte[0] = (addr1 >> 8) & 0xFF;
byte[1] = addr1 & 0xFF;
byte[2] = (addr2 >> 8) & 0xFF;
@@ -476,17 +476,5 @@ void ST7735::spi_master_write_addr_(uint16_t addr1, uint16_t addr2) {
this->write_array(byte, 4);
}
void ST7735::spi_master_write_color_(uint16_t color, uint16_t size) {
static uint8_t byte[1024];
int index = 0;
for (int i = 0; i < size; i++) {
byte[index++] = (color >> 8) & 0xFF;
byte[index++] = color & 0xFF;
}
this->dc_pin_->digital_write(true);
write_array(byte, size * 2);
}
} // namespace st7735
} // namespace esphome
-1
View File
@@ -68,7 +68,6 @@ class ST7735 : public display::DisplayBuffer,
void set_addr_window_(uint16_t x, uint16_t y, uint16_t w, uint16_t h);
void draw_absolute_pixel_internal(int x, int y, Color color) override;
void spi_master_write_addr_(uint16_t addr1, uint16_t addr2);
void spi_master_write_color_(uint16_t color, uint16_t size);
int get_width_internal() override;
int get_height_internal() override;
+19 -10
View File
@@ -1,11 +1,16 @@
#include "st7789v.h"
#include "esphome/core/log.h"
#include <algorithm>
namespace esphome {
namespace st7789v {
static const char *const TAG = "st7789v";
static const size_t TEMP_BUFFER_SIZE = 128;
#ifdef USE_ESP32
static constexpr size_t TEMP_BUFFER_SIZE = 1024;
#else
static constexpr size_t TEMP_BUFFER_SIZE = 512;
#endif
void ST7789V::setup() {
#ifdef USE_POWER_SUPPLY
@@ -236,7 +241,7 @@ void ST7789V::write_data_(uint8_t value) {
}
void ST7789V::write_addr_(uint16_t addr1, uint16_t addr2) {
static uint8_t byte[4];
uint8_t byte[4];
byte[0] = (addr1 >> 8) & 0xFF;
byte[1] = addr1 & 0xFF;
byte[2] = (addr2 >> 8) & 0xFF;
@@ -247,15 +252,19 @@ void ST7789V::write_addr_(uint16_t addr1, uint16_t addr2) {
}
void ST7789V::write_color_(uint16_t color, uint16_t size) {
static uint8_t byte[1024];
int index = 0;
for (int i = 0; i < size; i++) {
byte[index++] = (color >> 8) & 0xFF;
byte[index++] = color & 0xFF;
}
uint8_t byte[TEMP_BUFFER_SIZE];
uint16_t remaining = size;
this->dc_pin_->digital_write(true);
write_array(byte, size * 2);
while (remaining > 0) {
uint16_t batch = std::min(remaining, static_cast<uint16_t>(sizeof(byte) / 2));
int index = 0;
for (int i = 0; i < batch; i++) {
byte[index++] = (color >> 8) & 0xFF;
byte[index++] = color & 0xFF;
}
this->write_array(byte, batch * 2);
remaining -= batch;
}
}
size_t ST7789V::get_buffer_length_() {
+9 -6
View File
@@ -72,16 +72,19 @@ void ST7920::goto_xy_(uint16_t x, uint16_t y) {
}
void HOT ST7920::write_display_data() {
uint8_t i, j, b;
for (j = 0; j < (uint8_t) (this->get_height_internal() / 2); j++) {
int i, j;
uint8_t b;
int width_bytes = this->get_width_internal() / 8;
int half_height = this->get_height_internal() / 2;
for (j = 0; j < half_height; j++) {
this->goto_xy_(0, j);
this->enable();
for (i = 0; i < 16; i++) { // 16 bytes from line #0+
b = this->buffer_[i + j * 16];
for (i = 0; i < width_bytes; i++) {
b = this->buffer_[i + j * width_bytes];
this->send_(LCD_DATA, b);
}
for (i = 0; i < 16; i++) { // 16 bytes from line #32+
b = this->buffer_[i + (j + 32) * 16];
for (i = 0; i < width_bytes; i++) {
b = this->buffer_[i + (j + half_height) * width_bytes];
this->send_(LCD_DATA, b);
}
this->disable();
+3 -3
View File
@@ -56,11 +56,11 @@ void SX1509Component::loop() {
return;
}
int row, col;
for (row = 0; row < 7; row++) {
for (row = 0; row < 8; row++) {
if (key_data & (1 << row))
break;
}
for (col = 8; col < 15; col++) {
for (col = 8; col < 16; col++) {
if (key_data & (1 << col))
break;
}
@@ -229,7 +229,7 @@ void SX1509Component::setup_keypad_() {
this->read_byte_16(REG_DIR_B, &this->ddr_mask_);
for (int i = 0; i < this->rows_; i++)
this->ddr_mask_ &= ~(1 << i);
for (int i = 8; i < (this->cols_ * 2); i++)
for (int i = 8; i < (8 + this->cols_); i++)
this->ddr_mask_ |= (1 << i);
this->write_byte_16(REG_DIR_B, this->ddr_mask_);
+2 -2
View File
@@ -118,8 +118,8 @@ void TMP1075Sensor::send_alert_limit_high_() {
}
static uint16_t temp2regvalue(const float temp) {
const uint16_t regvalue = temp / 0.0625f;
return regvalue << 4;
const int16_t regvalue = static_cast<int16_t>(temp / 0.0625f);
return static_cast<uint16_t>(regvalue << 4);
}
static float regvalue2temp(const uint16_t regvalue) {
@@ -183,6 +183,9 @@ void Tormatic::recompute_position_() {
duration = this->close_duration_;
}
if (duration == 0)
return;
auto delta = direction * diff / duration;
this->position = clamp(this->position + delta, COVER_CLOSED, COVER_OPEN);
+6 -1
View File
@@ -65,7 +65,12 @@ class Touchscreen : public PollingComponent {
void register_listener(TouchListener *listener) { this->touch_listeners_.push_back(listener); }
optional<TouchPoint> get_touch() { return this->touches_.begin()->second; }
optional<TouchPoint> get_touch() {
if (this->touches_.empty()) {
return {};
}
return this->touches_.begin()->second;
}
TouchPoints_t get_touches() {
TouchPoints_t touches;
+1 -1
View File
@@ -191,7 +191,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) {
arg->tx20_available = true;
return;
}
if (index <= MAX_BUFFER_SIZE) {
if (index < MAX_BUFFER_SIZE) {
arg->buffer[index] = delay;
}
arg->spent_time += delay;
+4 -4
View File
@@ -183,10 +183,10 @@ class UARTComponent {
virtual void check_logger_conflict() = 0;
bool check_read_timeout_(size_t len = 1);
InternalGPIOPin *tx_pin_;
InternalGPIOPin *rx_pin_;
InternalGPIOPin *flow_control_pin_;
size_t rx_buffer_size_;
InternalGPIOPin *tx_pin_{};
InternalGPIOPin *rx_pin_{};
InternalGPIOPin *flow_control_pin_{};
size_t rx_buffer_size_{};
size_t rx_full_threshold_{1};
size_t rx_timeout_{0};
uint32_t baud_rate_{0};
+1 -1
View File
@@ -8,7 +8,7 @@ static const char *const TAG = "ufire_ec";
void UFireECComponent::setup() {
uint8_t version;
if (!this->read_byte(REGISTER_VERSION, &version) && version != 0xFF) {
if (!this->read_byte(REGISTER_VERSION, &version) || version == 0xFF) {
this->mark_failed();
return;
}
+1 -1
View File
@@ -10,7 +10,7 @@ static const char *const TAG = "ufire_ise";
void UFireISEComponent::setup() {
uint8_t version;
if (!this->read_byte(REGISTER_VERSION, &version) && version != 0xFF) {
if (!this->read_byte(REGISTER_VERSION, &version) || version == 0xFF) {
this->mark_failed();
return;
}

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