mirror of
https://github.com/esphome/esphome.git
synced 2026-09-16 17:48:40 +00:00
Merge branch 'dev' into rp2040-socket-wake
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
||||
b97e16a84153b2a4cfc51137cd6121db3c32374504b2bea55144413b3e573052
|
||||
b6f8c16c1ddd222134bf4a71910b4c832e764e23caf49f9bce3280b079955fcf
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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_();
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -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_)
|
||||
|
||||
@@ -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]))
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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++];
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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_);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user