Compare commits

...
Author SHA1 Message Date
J. Nick Koston ff50ffa154 Derive the registry archive path in one place 2026-09-05 15:19:57 +02:00
J. Nick Koston c31e02fa0c Prefer the landed archive while waiting, cap credited bytes, and give the registry wait a deadline 2026-09-05 14:59:01 +02:00
J. Nick Koston 562e5079d1 Share the lock wait with the ESP-IDF registry prefetch and trim the docstrings 2026-09-05 14:18:21 +02:00
J. Nick Koston a875016b1a Show the other downloader's progress while a prefetch job waits on its lock 2026-09-05 14:05:37 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d1829c495d Bump prek from 0.5.0 to 0.5.1 (#18977)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-04 19:05:36 -04:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ce87bf9b17 Bump platformdirs from 4.11.5 to 4.11.7 (#18976)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-04 19:05:26 -04:00
Jesse Hills 51ea97deff [esp32_ble] Reference count BLE advertising (#18943) 2026-09-05 08:38:55 +12:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ab800dc09d Bump filelock from 3.32.4 to 3.32.5 (#18963)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-03 17:19:29 -04:00
J. Nick Kostonandpre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> f65ab5629e [esp8266] Drop Arduino framework versions before 3.0.0 (#18917) to
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-09-03 15:16:36 -04:00
esphome[bot]esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>Jonathan Swoboda
b84532d254 Bump bundled esphome-device-builder to 1.14.0 (#18960)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-09-03 12:15:06 +00:00
Keith Burzinski 6b11636491 [remote_transmitter] Fix BK7231N build by limiting the PWM path to BK7238 (#18958) 2026-09-03 08:12:36 -04:00
Jesse Hills 2bb98f2d64 Merge branch 'beta' into dev 2026-09-03 14:07:41 +12:00
Jesse Hills 25c29fd9e0 Merge pull request #18957 from esphome/bump-2026.9.0b1
2026.9.0b1
2026-09-03 14:07:19 +12:00
Jesse Hills f3c786c784 Bump version to 2026.10.0-dev 2026-09-03 13:07:21 +12:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 2250430999 Bump zeroconf from 0.151.2 to 0.151.3 (#18951)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-02 20:56:51 -04:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d1068d582f Bump ninja from 1.13.0 to 1.13.2 (#18952)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-02 20:56:42 -04:00
Jesse Hills 567a981078 Bump version to 2026.9.0b1 2026-09-03 12:16:25 +12:00
Jesse Hills 81ecb87253 Bump version to 2026.10.0-dev 2026-09-03 12:11:56 +12:00
f0e2eb96bd [snapshot][SDL] Display headless mode and snapshots (#17917)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-09-03 09:39:32 +12:00
Keith Burzinskiandpuddly 567f7f9196 [serial_proxy] Skip no-op reconfigure requests (#18953)
Co-authored-by: puddly <32534428+puddly@users.noreply.github.com>
2026-09-02 15:01:23 -05:00
Jesse Hills ecbde8ddf4 [uart] Migrate check_uart_settings to final validation (#18940) 2026-09-03 07:25:08 +12:00
146 changed files with 2826 additions and 627 deletions
+13 -2
View File
@@ -374,8 +374,9 @@ jobs:
- name: Install apt packages (cached) - name: Install apt packages (cached)
# ccache speeds up the host compiles. A cache hit never touches apt # ccache speeds up the host compiles. A cache hit never touches apt
# (mirror outages cannot hang the job); the timeout bounds the cold # (mirror outages cannot hang the job); the timeout bounds the cold
# path. Packages and version must match seed-apt-cache exactly; # path. Packages and version must match seed-apt-cache exactly.
# libsdl2-dev is unused here and carried only for cache-key parity. # libsdl2-dev is needed by the headless display tests, which capture
# screenshots.
timeout-minutes: 10 timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3 uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with: with:
@@ -438,6 +439,16 @@ jobs:
echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests" echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests"
pytest -vv --no-cov --tb=native --durations=30 -n auto --dist worksteal \ pytest -vv --no-cov --tb=native --durations=30 -n auto --dist worksteal \
--junitxml=junit-integration.xml "${test_files[@]}" --junitxml=junit-integration.xml "${test_files[@]}"
- name: Upload test artifacts
# Tests that compare rendered output write the image they actually got here, so a
# failure can be looked at without reproducing the whole build locally.
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: integration-test-artifacts-${{ matrix.bucket.name }}
path: test_artifacts/
if-no-files-found: ignore
retention-days: 7
- name: Upload junit timings - name: Upload junit timings
# Consumed by sync-integration-durations.yml through # Consumed by sync-integration-durations.yml through
# script/update_integration_test_durations.py; only full matrix dev # script/update_integration_test_durations.py; only full matrix dev
+2
View File
@@ -137,6 +137,8 @@ config/
!tests/component_tests/**/config/ !tests/component_tests/**/config/
tests/build/ tests/build/
tests/.esphome/ tests/.esphome/
# Output kept by failing tests for inspection; uploaded by CI
test_artifacts/
/.temp-clang-tidy.cpp /.temp-clang-tidy.cpp
/.temp/ /.temp/
.pio/ .pio/
+1
View File
@@ -496,6 +496,7 @@ esphome/components/sm2335/* @Cossid
esphome/components/sml/* @alengwenus esphome/components/sml/* @alengwenus
esphome/components/smt100/* @piechade esphome/components/smt100/* @piechade
esphome/components/sn74hc165/* @jesserockz esphome/components/sn74hc165/* @jesserockz
esphome/components/snapshot/* @clydebarrow
esphome/components/socket/* @esphome/core esphome/components/socket/* @esphome/core
esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/sound_level/* @kahrendt esphome/components/sound_level/* @kahrendt
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # control system is used.
PROJECT_NUMBER = 2026.9.0-dev PROJECT_NUMBER = 2026.10.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description # Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a # for a project that appears at the top of each page and should give viewer a
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt -r /requirements.txt
# Install the ESPHome Device Builder dashboard. # Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.13.1 RUN uv pip install --no-cache-dir esphome-device-builder==1.14.0
RUN \ RUN \
platformio settings set enable_telemetry No \ platformio settings set enable_telemetry No \
+4 -9
View File
@@ -44,8 +44,7 @@ def get_arduino8266_tools_path() -> Path:
return tools_cache_path(*ARDUINO8266_TOOLS_CACHE) return tools_cache_path(*ARDUINO8266_TOOLS_CACHE)
# 3.1.1 rather than 3.1.0: the registry has no package for 3.1.0, and the # 3.1.1 rather than 3.1.0: the registry has no packages for 3.0.0, 3.0.1 or 3.1.0
# encoder below cannot name 3.0.0/3.0.1 either (see its docstring)
MIN_FRAMEWORK_VERSION = Version(3, 1, 1) MIN_FRAMEWORK_VERSION = Version(3, 1, 1)
@@ -53,20 +52,16 @@ def framework_package_version(ver: Version) -> str:
"""Map an Arduino core version to its registry package version (3.1.2 -> """Map an Arduino core version to its registry package version (3.1.2 ->
3.30102.0; the leading 3 is the package major). 3.30102.0; the leading 3 is the package major).
Exact registry names only for cores > 2.6.2 and >= 3.0.2; callers floor Exact registry names for 3.x cores; callers floor at MIN_FRAMEWORK_VERSION.
at MIN_FRAMEWORK_VERSION.
""" """
if ver.major > 3: if ver.major > 3:
raise EsphomeError( raise EsphomeError(
f"Arduino core {ver} is not supported yet; " f"Arduino core {ver} is not supported yet; "
"the newest known core series is 3.x" "the newest known core series is 3.x"
) )
if ver <= Version(2, 6, 2): if ver.major < 3:
# Cores <= 2.6.2 use the older 1.x/2.x package-major encodings (same
# boundary as _format_framework_arduino_version's era guard)
raise EsphomeError( raise EsphomeError(
f"Arduino core {ver} uses an older package encoding than this " f"Arduino core {ver} is not supported; ESPHome requires core 3.x"
"helper implements (newer than 2.6.2)"
) )
return f"3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" return f"3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
+2 -2
View File
@@ -368,8 +368,8 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() {
} }
void Climate::save_state_(const ClimateTraits &traits) { void Climate::save_state_(const ClimateTraits &traits) {
#if (defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \ #if (defined(USE_ESP32) || defined(USE_ESP8266)) && !defined(CLANG_TIDY)
!defined(CLANG_TIDY) #pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wclass-memaccess" #pragma GCC diagnostic ignored "-Wclass-memaccess"
#define TEMP_IGNORE_MEMACCESS #define TEMP_IGNORE_MEMACCESS
#endif #endif
-1
View File
@@ -100,7 +100,6 @@ bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t comma
void CM1106Component::dump_config() { void CM1106Component::dump_config() {
ESP_LOGCONFIG(TAG, "CM1106:"); ESP_LOGCONFIG(TAG, "CM1106:");
LOG_SENSOR(" ", "CO2", this->co2_sensor_); LOG_SENSOR(" ", "CO2", this->co2_sensor_);
this->check_uart_settings(9600);
if (this->is_failed()) { if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
} }
+8
View File
@@ -46,6 +46,14 @@ CONFIG_SCHEMA = (
.extend(uart.UART_DEVICE_SCHEMA) .extend(uart.UART_DEVICE_SCHEMA)
) )
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"cm1106",
baud_rate=9600,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None: async def to_code(config: ConfigType) -> None:
"""Code generation entry point.""" """Code generation entry point."""
-1
View File
@@ -58,7 +58,6 @@ void CSE7761Component::dump_config() {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
} }
LOG_UPDATE_INTERVAL(this); LOG_UPDATE_INTERVAL(this);
this->check_uart_settings(38400, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
} }
void CSE7761Component::update() { void CSE7761Component::update() {
+7 -1
View File
@@ -68,7 +68,13 @@ CONFIG_SCHEMA = (
) )
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"cse7761", baud_rate=38400, require_rx=True, require_tx=True "cse7761",
baud_rate=38400,
require_rx=True,
require_tx=True,
data_bits=8,
parity="EVEN",
stop_bits=1,
) )
-1
View File
@@ -255,7 +255,6 @@ void CSE7766Component::dump_config() {
LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_); LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_);
LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_); LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_);
LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_); LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_);
this->check_uart_settings(4800, 1, uart::UART_CONFIG_PARITY_EVEN);
} }
} // namespace esphome::cse7766 } // namespace esphome::cse7766
+6 -1
View File
@@ -84,7 +84,12 @@ CONFIG_SCHEMA = (
.extend(cv.COMPONENT_SCHEMA) .extend(cv.COMPONENT_SCHEMA)
) )
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"cse7766", baud_rate=4800, parity="EVEN", require_rx=True "cse7766",
baud_rate=4800,
require_rx=True,
data_bits=8,
parity="EVEN",
stop_bits=1,
) )
+8
View File
@@ -26,6 +26,14 @@ CONFIG_SCHEMA = (
.extend(cv.polling_component_schema("30s")) .extend(cv.polling_component_schema("30s"))
) )
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"daly_bms",
baud_rate=9600,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None: async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
+1 -4
View File
@@ -22,10 +22,7 @@ static const uint8_t DALY_REQUEST_TEMPERATURE = 0x96;
void DalyBmsComponent::setup() { this->next_request_ = 1; } void DalyBmsComponent::setup() { this->next_request_ = 1; }
void DalyBmsComponent::dump_config() { void DalyBmsComponent::dump_config() { ESP_LOGCONFIG(TAG, "Daly BMS:"); }
ESP_LOGCONFIG(TAG, "Daly BMS:");
this->check_uart_settings(9600);
}
void DalyBmsComponent::update() { void DalyBmsComponent::update() {
this->trigger_next_ = true; this->trigger_next_ = true;
+2 -2
View File
@@ -22,9 +22,9 @@ void DebugComponent::dump_config() {
LOG_SENSOR(" ", "Free space on heap", this->free_sensor_); LOG_SENSOR(" ", "Free space on heap", this->free_sensor_);
LOG_SENSOR(" ", "Largest free heap block", this->block_sensor_); LOG_SENSOR(" ", "Largest free heap block", this->block_sensor_);
LOG_SENSOR(" ", "CPU frequency", this->cpu_frequency_sensor_); LOG_SENSOR(" ", "CPU frequency", this->cpu_frequency_sensor_);
#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) #ifdef USE_ESP8266
LOG_SENSOR(" ", "Heap fragmentation", this->fragmentation_sensor_); LOG_SENSOR(" ", "Heap fragmentation", this->fragmentation_sensor_);
#endif // defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) #endif // USE_ESP8266
#endif // USE_SENSOR #endif // USE_SENSOR
char device_info_buffer[DEVICE_INFO_BUFFER_SIZE]; char device_info_buffer[DEVICE_INFO_BUFFER_SIZE];
+2 -2
View File
@@ -35,7 +35,7 @@ class DebugComponent final : public PollingComponent {
#ifdef USE_SENSOR #ifdef USE_SENSOR
void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; } void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; }
void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; } void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; }
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32) #if defined(USE_ESP8266) || defined(USE_ESP32)
void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; } void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; }
#endif #endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_LIBRETINY)
@@ -61,7 +61,7 @@ class DebugComponent final : public PollingComponent {
sensor::Sensor *free_sensor_{nullptr}; sensor::Sensor *free_sensor_{nullptr};
sensor::Sensor *block_sensor_{nullptr}; sensor::Sensor *block_sensor_{nullptr};
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32) #if defined(USE_ESP8266) || defined(USE_ESP32)
sensor::Sensor *fragmentation_sensor_{nullptr}; sensor::Sensor *fragmentation_sensor_{nullptr};
#endif #endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_LIBRETINY)
@@ -159,12 +159,10 @@ void DebugComponent::update_platform_() {
// NOLINTNEXTLINE(readability-static-accessed-through-instance) // NOLINTNEXTLINE(readability-static-accessed-through-instance)
this->block_sensor_->publish_state(ESP.getMaxFreeBlockSize()); this->block_sensor_->publish_state(ESP.getMaxFreeBlockSize());
} }
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
if (this->fragmentation_sensor_ != nullptr) { if (this->fragmentation_sensor_ != nullptr) {
// NOLINTNEXTLINE(readability-static-accessed-through-instance) // NOLINTNEXTLINE(readability-static-accessed-through-instance)
this->fragmentation_sensor_->publish_state(ESP.getHeapFragmentation()); this->fragmentation_sensor_->publish_state(ESP.getHeapFragmentation());
} }
#endif
#endif #endif
} }
+2 -5
View File
@@ -52,12 +52,9 @@ CONFIG_SCHEMA = {
), ),
cv.Optional(CONF_FRAGMENTATION): cv.All( cv.Optional(CONF_FRAGMENTATION): cv.All(
cv.Any( cv.Any(
cv.All( cv.only_on_esp8266,
cv.only_on_esp8266,
cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)),
),
cv.only_on_esp32, cv.only_on_esp32,
msg="This feature is only available on ESP8266 (Arduino 2.5.2+) and ESP32", msg="This feature is only available on ESP8266 and ESP32",
), ),
sensor.sensor_schema( sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT, unit_of_measurement=UNIT_PERCENT,
+6 -1
View File
@@ -60,7 +60,12 @@ CONFIG_SCHEMA = cv.All(
).extend(uart.UART_DEVICE_SCHEMA) ).extend(uart.UART_DEVICE_SCHEMA)
) )
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"dfplayer", baud_rate=9600, require_tx=True "dfplayer",
baud_rate=9600,
require_tx=True,
data_bits=8,
parity="NONE",
stop_bits=1,
) )
+1 -4
View File
@@ -277,9 +277,6 @@ void DFPlayer::loop() {
} }
} }
} }
void DFPlayer::dump_config() { void DFPlayer::dump_config() { ESP_LOGCONFIG(TAG, "DFPlayer:"); }
ESP_LOGCONFIG(TAG, "DFPlayer:");
this->check_uart_settings(9600);
}
} // namespace esphome::dfplayer } // namespace esphome::dfplayer
+28 -7
View File
@@ -100,21 +100,38 @@ void ESP32BLE::disable() {
#ifdef USE_ESP32_BLE_ADVERTISING #ifdef USE_ESP32_BLE_ADVERTISING
void ESP32BLE::advertising_start() { void ESP32BLE::advertising_start() {
this->advertising_init_(); this->advertising_init_();
if (!this->is_active()) this->advertising_ref_count_++;
this->advertising_refresh();
}
void ESP32BLE::advertising_stop() {
if (this->advertising_ref_count_ == 0)
return; return;
this->advertising_->start(); this->advertising_ref_count_--;
this->advertising_refresh();
}
void ESP32BLE::advertising_refresh() {
if (this->advertising_ == nullptr || !this->is_active())
return;
// Advertise while any component still needs it, otherwise stop
if (this->advertising_ref_count_ == 0) {
this->advertising_->stop();
} else {
this->advertising_->start();
}
} }
void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) { void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
this->advertising_init_(); this->advertising_init_();
this->advertising_->set_service_data(data); this->advertising_->set_service_data(data);
this->advertising_start(); this->advertising_refresh();
} }
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) { void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
this->advertising_init_(); this->advertising_init_();
this->advertising_->set_manufacturer_data(data); this->advertising_->set_manufacturer_data(data);
this->advertising_start(); this->advertising_refresh();
} }
void ESP32BLE::advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name) { void ESP32BLE::advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name) {
@@ -136,7 +153,7 @@ void ESP32BLE::advertising_set_service_data_and_name(std::span<const uint8_t> da
this->advertising_->set_service_data(data); this->advertising_->set_service_data(data);
} }
this->advertising_start(); this->advertising_refresh();
} }
void ESP32BLE::advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback) { void ESP32BLE::advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback) {
@@ -147,13 +164,13 @@ void ESP32BLE::advertising_register_raw_advertisement_callback(std::function<voi
void ESP32BLE::advertising_add_service_uuid(ESPBTUUID uuid) { void ESP32BLE::advertising_add_service_uuid(ESPBTUUID uuid) {
this->advertising_init_(); this->advertising_init_();
this->advertising_->add_service_uuid(uuid); this->advertising_->add_service_uuid(uuid);
this->advertising_start(); this->advertising_refresh();
} }
void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) { void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) {
this->advertising_init_(); this->advertising_init_();
this->advertising_->remove_service_uuid(uuid); this->advertising_->remove_service_uuid(uuid);
this->advertising_start(); this->advertising_refresh();
} }
#endif #endif
@@ -575,6 +592,10 @@ void ESP32BLE::loop_handle_state_transition_not_active_() {
} }
this->state_ = BLE_COMPONENT_STATE_ACTIVE; this->state_ = BLE_COMPONENT_STATE_ACTIVE;
#ifdef USE_ESP32_BLE_ADVERTISING
// Requests made before the stack was up (or before it was re-enabled) take effect now
this->advertising_refresh();
#endif
} }
} }
+13
View File
@@ -114,7 +114,17 @@ class ESP32BLE final : public Component {
void set_name(const char *name) { this->name_ = name; } void set_name(const char *name) { this->name_ = name; }
#ifdef USE_ESP32_BLE_ADVERTISING #ifdef USE_ESP32_BLE_ADVERTISING
/** Request advertising on behalf of a component.
*
* Requests are reference counted: advertising runs until every component that called
* advertising_start() has released it again with advertising_stop(). Each component must
* pair its calls, so nothing advertises until something actually asks for it.
*/
void advertising_start(); void advertising_start();
/// Release a request made with advertising_start(); advertising stops at the last release.
void advertising_stop();
/// Apply the current payload and request count: advertise while requested, otherwise stop.
void advertising_refresh();
void advertising_set_service_data(const std::vector<uint8_t> &data); void advertising_set_service_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data); void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; } void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
@@ -226,6 +236,9 @@ class ESP32BLE final : public Component {
// 1-byte aligned members (grouped together to minimize padding) // 1-byte aligned members (grouped together to minimize padding)
BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum) BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum)
bool enable_on_boot_{}; // 1 byte bool enable_on_boot_{}; // 1 byte
#ifdef USE_ESP32_BLE_ADVERTISING
uint8_t advertising_ref_count_{0}; // 1 byte, number of components requesting advertising
#endif
#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS #ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
optional<esp_ble_auth_req_t> auth_req_mode_; optional<esp_ble_auth_req_t> auth_req_mode_;
@@ -67,6 +67,8 @@ void ESP32BLEBeacon::setup() {
this->on_advertise_(); this->on_advertise_();
} }
}); });
// A beacon always needs the device to advertise, and never releases the request
global_ble->advertising_start();
} }
void ESP32BLEBeacon::on_advertise_() { void ESP32BLEBeacon::on_advertise_() {
@@ -596,6 +596,18 @@ async def to_code(config):
cg.add(var.set_parent(parent)) cg.add(var.set_parent(parent))
cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE])) cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE]))
cg.add(var.set_max_clients(config[CONF_MAX_CLIENTS])) cg.add(var.set_max_clients(config[CONF_MAX_CLIENTS]))
# Only advertise for the server itself when the configuration gives clients something to
# find. A server that is auto-loaded purely to host a runtime service (esp32_improv) stays
# silent until that service asks for advertising.
cg.add(
var.set_advertising_required(
CONF_MANUFACTURER_DATA in config
or any(
not uuid_is(service_config[CONF_UUID], DEVICE_INFORMATION_SERVICE_UUID)
for service_config in config[CONF_SERVICES]
)
)
)
if CONF_MANUFACTURER_DATA in config: if CONF_MANUFACTURER_DATA in config:
cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA])) cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA]))
for service_config in config[CONF_SERVICES]: for service_config in config[CONF_SERVICES]:
@@ -81,6 +81,7 @@ void BLEServer::loop() {
if (this->device_information_service_->is_running()) { if (this->device_information_service_->is_running()) {
this->state_ = RUNNING; this->state_ = RUNNING;
this->restart_advertising_(); this->restart_advertising_();
this->request_advertising_();
ESP_LOGD(TAG, "BLE server setup successfully"); ESP_LOGD(TAG, "BLE server setup successfully");
} else if (this->device_information_service_->is_created()) { } else if (this->device_information_service_->is_created()) {
this->device_information_service_->start(); this->device_information_service_->start();
@@ -98,6 +99,20 @@ void BLEServer::restart_advertising_() {
} }
} }
void BLEServer::request_advertising_() {
if (!this->advertising_required_ || this->advertising_requested_)
return;
this->advertising_requested_ = true;
this->parent_->advertising_start();
}
void BLEServer::release_advertising_() {
if (!this->advertising_requested_)
return;
this->advertising_requested_ = false;
this->parent_->advertising_stop();
}
BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles) { BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[esp32_ble::UUID_STR_LEN]; char uuid_buf[esp32_ble::UUID_STR_LEN];
@@ -170,7 +185,7 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga
this->add_client_(param->connect.conn_id); this->add_client_(param->connect.conn_id);
// Resume advertising so additional clients can discover and connect // Resume advertising so additional clients can discover and connect
if (this->client_count_ < this->max_clients_) { if (this->client_count_ < this->max_clients_) {
this->parent_->advertising_start(); this->parent_->advertising_refresh();
} }
this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id); this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id);
break; break;
@@ -178,7 +193,7 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga
case ESP_GATTS_DISCONNECT_EVT: { case ESP_GATTS_DISCONNECT_EVT: {
ESP_LOGD(TAG, "BLE Client disconnected"); ESP_LOGD(TAG, "BLE Client disconnected");
this->remove_client_(param->disconnect.conn_id); this->remove_client_(param->disconnect.conn_id);
this->parent_->advertising_start(); this->parent_->advertising_refresh();
this->dispatch_callbacks_(CallbackType::ON_DISCONNECT, param->disconnect.conn_id); this->dispatch_callbacks_(CallbackType::ON_DISCONNECT, param->disconnect.conn_id);
break; break;
} }
@@ -226,6 +241,8 @@ void BLEServer::remove_client_(uint16_t conn_id) {
} }
void BLEServer::ble_before_disabled_event_handler() { void BLEServer::ble_before_disabled_event_handler() {
// Advertising is re-requested once the server is running again after BLE is re-enabled
this->release_advertising_();
// Delete all clients // Delete all clients
this->client_count_ = 0; this->client_count_ = 0;
// Delete all services // Delete all services
@@ -38,6 +38,13 @@ class BLEServer final : public Component, public Parented<ESP32BLE> {
this->restart_advertising_(); this->restart_advertising_();
} }
/** Whether this server needs the device to advertise so clients can find and connect to it.
*
* False for a server that only hosts services created at runtime (e.g. esp32_improv), which
* request advertising themselves for as long as they need it.
*/
void set_advertising_required(bool required) { this->advertising_required_ = required; }
void set_max_clients(uint8_t max_clients) { this->max_clients_ = max_clients; } void set_max_clients(uint8_t max_clients) { this->max_clients_ = max_clients; }
uint8_t get_max_clients() const { return this->max_clients_; } uint8_t get_max_clients() const { return this->max_clients_; }
@@ -82,6 +89,8 @@ class BLEServer final : public Component, public Parented<ESP32BLE> {
}; };
void restart_advertising_(); void restart_advertising_();
void request_advertising_();
void release_advertising_();
int8_t find_client_index_(uint16_t conn_id) const; int8_t find_client_index_(uint16_t conn_id) const;
void add_client_(uint16_t conn_id); void add_client_(uint16_t conn_id);
@@ -93,6 +102,8 @@ class BLEServer final : public Component, public Parented<ESP32BLE> {
std::vector<uint8_t> manufacturer_data_{}; std::vector<uint8_t> manufacturer_data_{};
esp_gatt_if_t gatts_if_{0}; esp_gatt_if_t gatts_if_{0};
bool registered_{false}; bool registered_{false};
bool advertising_required_{true};
bool advertising_requested_{false};
uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{}; uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{};
uint8_t client_count_{0}; uint8_t client_count_{0};
@@ -112,6 +112,7 @@ void ESP32ImprovComponent::loop() {
this->state_callback_.call(this->state_, this->error_state_); this->state_callback_.call(this->state_, this->error_state_);
#endif #endif
} }
this->release_advertising_();
this->incoming_data_.clear(); this->incoming_data_.clear();
return; return;
} }
@@ -143,8 +144,9 @@ void ESP32ImprovComponent::loop() {
ESP_LOGV(TAG, "Starting with device name advertising"); ESP_LOGV(TAG, "Starting with device name advertising");
this->advertising_device_name_ = true; this->advertising_device_name_ = true;
this->last_name_adv_time_ = App.get_loop_component_start_time(); this->last_name_adv_time_ = App.get_loop_component_start_time();
// Set the payload before requesting, so advertising starts exactly once
esp32_ble::global_ble->advertising_set_service_data_and_name(std::span<const uint8_t>{}, true); esp32_ble::global_ble->advertising_set_service_data_and_name(std::span<const uint8_t>{}, true);
esp32_ble::global_ble->advertising_start(); this->request_advertising_();
// Set initial state based on whether we have an authorizer // Set initial state based on whether we have an authorizer
this->set_state_(this->get_initial_state_(), false); this->set_state_(this->get_initial_state_(), false);
@@ -326,6 +328,8 @@ void ESP32ImprovComponent::stop() {
this->set_timeout("end-service", STOP_ADVERTISING_DELAY, [this] { this->set_timeout("end-service", STOP_ADVERTISING_DELAY, [this] {
if (this->state_ == improv::STATE_STOPPED || this->service_ == nullptr) if (this->state_ == improv::STATE_STOPPED || this->service_ == nullptr)
return; return;
// Release first so removing the service UUID does not restart advertising on the way out
this->release_advertising_();
this->service_->stop(); this->service_->stop();
this->set_state_(improv::STATE_STOPPED); this->set_state_(improv::STATE_STOPPED);
}); });
@@ -520,6 +524,20 @@ void ESP32ImprovComponent::update_advertising_type_() {
} }
} }
void ESP32ImprovComponent::request_advertising_() {
if (this->advertising_requested_)
return;
this->advertising_requested_ = true;
esp32_ble::global_ble->advertising_start();
}
void ESP32ImprovComponent::release_advertising_() {
if (!this->advertising_requested_)
return;
this->advertising_requested_ = false;
esp32_ble::global_ble->advertising_stop();
}
improv::State ESP32ImprovComponent::get_initial_state_() const { improv::State ESP32ImprovComponent::get_initial_state_() const {
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
// If we have an authorizer, start in awaiting authorization state // If we have an authorizer, start in awaiting authorization state
@@ -104,8 +104,11 @@ class ESP32ImprovComponent final : public Component, public improv_base::ImprovB
bool status_indicator_state_{false}; bool status_indicator_state_{false};
uint32_t last_name_adv_time_{0}; uint32_t last_name_adv_time_{0};
bool advertising_device_name_{false}; bool advertising_device_name_{false};
bool advertising_requested_{false};
void set_status_indicator_state_(bool state); void set_status_indicator_state_(bool state);
void update_advertising_type_(); void update_advertising_type_();
void request_advertising_();
void release_advertising_();
void set_state_(improv::State state, bool update_advertising = true); void set_state_(improv::State state, bool update_advertising = true);
void set_error_(improv::Error error); void set_error_(improv::Error error);
+17 -40
View File
@@ -35,7 +35,7 @@ from esphome.platformio.toolchain import copy_ccache_script
from esphome.storage_json import StorageJSON from esphome.storage_json import StorageJSON
from esphome.types import ConfigType from esphome.types import ConfigType
from .boards import BOARDS, ESP8266_LD_SCRIPTS, board_ld_script from .boards import BOARDS, board_ld_script
from .const import ( from .const import (
CONF_EARLY_PIN_INIT, CONF_EARLY_PIN_INIT,
CONF_ENABLE_SERIAL, CONF_ENABLE_SERIAL,
@@ -43,8 +43,6 @@ from .const import (
CONF_RESTORE_FROM_FLASH, CONF_RESTORE_FROM_FLASH,
KEY_BOARD, KEY_BOARD,
KEY_ESP8266, KEY_ESP8266,
KEY_FLASH_SIZE,
KEY_LDSCRIPT,
KEY_PIN_INITIAL_STATES, KEY_PIN_INITIAL_STATES,
KEY_SERIAL1_REQUIRED, KEY_SERIAL1_REQUIRED,
KEY_SERIAL_REQUIRED, KEY_SERIAL_REQUIRED,
@@ -133,10 +131,6 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
# format the given arduino (https://github.com/esp8266/Arduino/releases) version to # format the given arduino (https://github.com/esp8266/Arduino/releases) version to
# a PIO platformio/framework-arduinoespressif8266 value # a PIO platformio/framework-arduinoespressif8266 value
# List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266 # List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266
if ver <= cv.Version(2, 4, 1):
return f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
if ver <= cv.Version(2, 6, 2):
return f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
# Same encoding the native toolchain uses for its package download, so a # Same encoding the native toolchain uses for its package download, so a
# version bump cannot drift between the two paths. # version bump cannot drift between the two paths.
from esphome.arduino8266.framework import framework_package_version from esphome.arduino8266.framework import framework_package_version
@@ -159,11 +153,9 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
# - https://github.com/esp8266/Arduino/releases # - https://github.com/esp8266/Arduino/releases
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266 # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 2) RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 2)
# The platformio/espressif8266 version to use for arduino 2 framework versions # The platformio/espressif8266 version to use for arduino 3 framework versions
# - https://github.com/platformio/platform-espressif8266/releases # - https://github.com/platformio/platform-espressif8266/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif8266 # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif8266
ARDUINO_2_PLATFORM_VERSION = cv.Version(2, 6, 3)
# for arduino 3 framework versions
ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0) ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0)
# for arduino 4 framework versions # for arduino 4 framework versions
ARDUINO_4_PLATFORM_VERSION = cv.Version(4, 2, 1) ARDUINO_4_PLATFORM_VERSION = cv.Version(4, 2, 1)
@@ -188,6 +180,14 @@ def _arduino_check_versions(value: ConfigType) -> ConfigType:
version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
source = value.get(CONF_SOURCE, None) source = value.get(CONF_SOURCE, None)
if version < cv.Version(3, 0, 0):
raise cv.Invalid(
f"Arduino framework {version} is no longer supported; ESPHome requires "
f"C++20, which needs Arduino core 3.x. Use the recommended version "
f"({RECOMMENDED_ARDUINO_FRAMEWORK_VERSION}).",
path=[CONF_VERSION],
)
value[CONF_VERSION] = str(version) value[CONF_VERSION] = str(version)
value[CONF_SOURCE] = source or _format_framework_arduino_version(version) value[CONF_SOURCE] = source or _format_framework_arduino_version(version)
@@ -195,12 +195,8 @@ def _arduino_check_versions(value: ConfigType) -> ConfigType:
if platform_version is None: if platform_version is None:
if version >= cv.Version(3, 1, 0): if version >= cv.Version(3, 1, 0):
platform_version = _parse_platform_version(str(ARDUINO_4_PLATFORM_VERSION)) platform_version = _parse_platform_version(str(ARDUINO_4_PLATFORM_VERSION))
elif version >= cv.Version(3, 0, 0):
platform_version = _parse_platform_version(str(ARDUINO_3_PLATFORM_VERSION))
elif version >= cv.Version(2, 5, 0):
platform_version = _parse_platform_version(str(ARDUINO_2_PLATFORM_VERSION))
else: else:
platform_version = _parse_platform_version(str(cv.Version(1, 8, 0))) platform_version = _parse_platform_version(str(ARDUINO_3_PLATFORM_VERSION))
value[CONF_PLATFORM_VERSION] = platform_version value[CONF_PLATFORM_VERSION] = platform_version
if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION:
@@ -289,29 +285,11 @@ def check_rosetta() -> None:
) )
def _choose_ld_script(board: str, ver: cv.Version) -> str | None: def _choose_ld_script(board: str) -> str:
"""The flash ld to pin for this board and core, or None for cores """The flash ld to pin for this board."""
without ld-script support."""
board_data = BOARDS[board]
ld_scripts = ESP8266_LD_SCRIPTS[board_data[KEY_FLASH_SIZE]]
if ver <= cv.Version(2, 3, 0):
# No ld script support
return None
if ver <= cv.Version(2, 4, 2):
# Old ld script path; the modern per-board override names do not
# exist in this core's SDK, so the override cannot be honored.
# Substituting the size default would move _FS_end and the
# preferences sector, wiping flash-backed state on flash.
if KEY_LDSCRIPT in board_data:
raise EsphomeError(
f"Board {board} requires its {board_data[KEY_LDSCRIPT]} "
f"flash layout, which Arduino core {ver} cannot honor; "
"use a core newer than 2.4.2"
)
return ld_scripts[0]
# A per-board override preserves a layout the board shipped with # A per-board override preserves a layout the board shipped with
# (see d1_wroom_02 in boards.py) # (see d1_wroom_02 in boards.py)
return board_ld_script(board_data) return board_ld_script(BOARDS[board])
@coroutine_with_priority(CoroPriority.PLATFORM) @coroutine_with_priority(CoroPriority.PLATFORM)
@@ -435,10 +413,9 @@ async def to_code(config: ConfigType) -> None:
) )
if config[CONF_BOARD] in BOARDS: if config[CONF_BOARD] in BOARDS:
ld_script = _choose_ld_script(config[CONF_BOARD], ver) cg.add_platformio_option(
"board_build.ldscript", _choose_ld_script(config[CONF_BOARD])
if ld_script is not None: )
cg.add_platformio_option("board_build.ldscript", ld_script)
CORE.add_job(add_pin_initial_states_array) CORE.add_job(add_pin_initial_states_array)
CORE.add_job(finalize_waveform_config) CORE.add_job(finalize_waveform_config)
-1
View File
@@ -96,7 +96,6 @@ void HC8Component::dump_config() {
" Warmup time: %" PRIu32 " s", " Warmup time: %" PRIu32 " s",
this->warmup_seconds_); this->warmup_seconds_);
LOG_SENSOR(" ", "CO2", this->co2_sensor_); LOG_SENSOR(" ", "CO2", this->co2_sensor_);
this->check_uart_settings(9600);
} }
} // namespace esphome::hc8 } // namespace esphome::hc8
+3
View File
@@ -47,6 +47,9 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
baud_rate=9600, baud_rate=9600,
require_rx=True, require_rx=True,
require_tx=True, require_tx=True,
data_bits=8,
parity="NONE",
stop_bits=1,
) )
-1
View File
@@ -38,7 +38,6 @@ CoverTraits HE60rCover::get_traits() {
void HE60rCover::dump_config() { void HE60rCover::dump_config() {
LOG_COVER("", "HE60R Cover", this); LOG_COVER("", "HE60R Cover", this);
this->check_uart_settings(1200, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
" Open Duration: %.1fs\n" " Open Duration: %.1fs\n"
" Close Duration: %.1fs", " Close Duration: %.1fs",
@@ -68,8 +68,6 @@ void HrxlMaxsonarWrComponent::check_buffer_() {
void HrxlMaxsonarWrComponent::dump_config() { void HrxlMaxsonarWrComponent::dump_config() {
ESP_LOGCONFIG(TAG, "HRXL MaxSonar WR Sensor:"); ESP_LOGCONFIG(TAG, "HRXL MaxSonar WR Sensor:");
LOG_SENSOR(" ", "Distance", this); LOG_SENSOR(" ", "Distance", this);
// As specified in the sensor's data sheet
this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
} }
} // namespace esphome::hrxl_maxsonar_wr } // namespace esphome::hrxl_maxsonar_wr
@@ -23,6 +23,14 @@ CONFIG_SCHEMA = sensor.sensor_schema(
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
).extend(uart.UART_DEVICE_SCHEMA) ).extend(uart.UART_DEVICE_SCHEMA)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"hrxl_maxsonar_wr",
baud_rate=9600,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None: async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config) var = await sensor.new_sensor(config)
@@ -11,7 +11,6 @@ static const char *const PROTOCOL_NAMES[] = {HYDREON_RGXX_PROTOCOL_LIST(, HYDREO
static const char *const IGNORE_STRINGS[] = {HYDREON_RGXX_IGNORE_LIST(, HYDREON_RGXX_COMMA)}; static const char *const IGNORE_STRINGS[] = {HYDREON_RGXX_IGNORE_LIST(, HYDREON_RGXX_COMMA)};
void HydreonRGxxComponent::dump_config() { void HydreonRGxxComponent::dump_config() {
this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
ESP_LOGCONFIG(TAG, "hydreon_rgxx:"); ESP_LOGCONFIG(TAG, "hydreon_rgxx:");
if (this->is_failed()) { if (this->is_failed()) {
ESP_LOGE(TAG, "Connection with hydreon_rgxx failed!"); ESP_LOGE(TAG, "Connection with hydreon_rgxx failed!");
@@ -130,6 +130,14 @@ CONFIG_SCHEMA = cv.All(
_validate, _validate,
) )
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"hydreon_rgxx",
baud_rate=9600,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None: async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
@@ -26,8 +26,6 @@ void KamstrupKMPComponent::dump_config() {
LOG_SENSOR(" ", "Custom Sensor", this->custom_sensors_[i]); LOG_SENSOR(" ", "Custom Sensor", this->custom_sensors_[i]);
ESP_LOGCONFIG(TAG, " Command: 0x%04X", this->custom_commands_[i]); ESP_LOGCONFIG(TAG, " Command: 0x%04X", this->custom_commands_[i]);
} }
this->check_uart_settings(1200, 2, uart::UART_CONFIG_PARITY_NONE, 8);
} }
void KamstrupKMPComponent::update() { void KamstrupKMPComponent::update() {
+7 -1
View File
@@ -102,7 +102,13 @@ CONFIG_SCHEMA = (
) )
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"kamstrup_kmp", baud_rate=1200, require_rx=True, require_tx=True "kamstrup_kmp",
baud_rate=1200,
require_rx=True,
require_tx=True,
data_bits=8,
parity="NONE",
stop_bits=2,
) )
-2
View File
@@ -143,8 +143,6 @@ void MHZ19Component::dump_config() {
ESP_LOGCONFIG(TAG, "MH-Z19:"); ESP_LOGCONFIG(TAG, "MH-Z19:");
LOG_SENSOR(" ", "CO2", this->co2_sensor_); LOG_SENSOR(" ", "CO2", this->co2_sensor_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
this->check_uart_settings(9600);
if (this->abc_boot_logic_ == MHZ19_ABC_ENABLED) { if (this->abc_boot_logic_ == MHZ19_ABC_ENABLED) {
ESP_LOGCONFIG(TAG, " Automatic baseline calibration enabled on boot"); ESP_LOGCONFIG(TAG, " Automatic baseline calibration enabled on boot");
} else if (this->abc_boot_logic_ == MHZ19_ABC_DISABLED) { } else if (this->abc_boot_logic_ == MHZ19_ABC_DISABLED) {
+8
View File
@@ -80,6 +80,14 @@ CONFIG_SCHEMA = (
.extend(uart.UART_DEVICE_SCHEMA) .extend(uart.UART_DEVICE_SCHEMA)
) )
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"mhz19",
baud_rate=9600,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None: async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
@@ -163,10 +163,7 @@ void Mk2PVRouter::publish_value_(const char *tag, const char *val) {
#endif #endif
} }
void Mk2PVRouter::dump_config() { void Mk2PVRouter::dump_config() { ESP_LOGCONFIG(TAG, "Mk2PVRouter:"); }
ESP_LOGCONFIG(TAG, "Mk2PVRouter:");
this->check_uart_settings(BAUD_RATE, 1, uart::UART_CONFIG_PARITY_EVEN, 7);
}
#ifdef MK2PVROUTER_LISTENER_COUNT #ifdef MK2PVROUTER_LISTENER_COUNT
void Mk2PVRouter::register_mk2pvrouter_listener(Mk2PVRouterListener *listener) { void Mk2PVRouter::register_mk2pvrouter_listener(Mk2PVRouterListener *listener) {
@@ -43,7 +43,6 @@ class Mk2PVRouter final : public Component, public uart::UARTDevice {
protected: protected:
static constexpr size_t CRC_SUFFIX_LEN = 1; static constexpr size_t CRC_SUFFIX_LEN = 1;
static constexpr uint32_t BAUD_RATE = 9600;
enum class State : uint8_t { enum class State : uint8_t {
WAITING_FOR_START, WAITING_FOR_START,
@@ -209,14 +209,8 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
http_client.setTimeout(this->tft_upload_http_timeout_); http_client.setTimeout(this->tft_upload_http_timeout_);
bool begin_status = false; bool begin_status = false;
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0)
http_client.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); http_client.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
#elif USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0)
http_client.setFollowRedirects(true);
#endif
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0)
http_client.setRedirectLimit(3); http_client.setRedirectLimit(3);
#endif
begin_status = http_client.begin(*this->get_wifi_client_(), this->tft_url_.c_str()); begin_status = http_client.begin(*this->get_wifi_client_(), this->tft_url_.c_str());
if (!begin_status) { if (!begin_status) {
this->connection_state_.is_updating_ = false; this->connection_state_.is_updating_ = false;
-1
View File
@@ -16,7 +16,6 @@ void PM1006Component::dump_config() {
ESP_LOGCONFIG(TAG, "PM1006:"); ESP_LOGCONFIG(TAG, "PM1006:");
LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_);
LOG_UPDATE_INTERVAL(this); LOG_UPDATE_INTERVAL(this);
this->check_uart_settings(9600);
} }
void PM1006Component::update() { void PM1006Component::update() {
+3
View File
@@ -48,6 +48,9 @@ def validate_interval_uart(config: ConfigType) -> None:
baud_rate=9600, baud_rate=9600,
require_rx=True, require_rx=True,
require_tx=interval.total_milliseconds != SCHEDULER_DONT_RUN, require_tx=interval.total_milliseconds != SCHEDULER_DONT_RUN,
data_bits=8,
parity="NONE",
stop_bits=1,
)(config) )(config)
-2
View File
@@ -46,8 +46,6 @@ void PMSX003Component::dump_config() {
} else { } else {
ESP_LOGCONFIG(TAG, " Mode: passive with sleep/wake cycles"); ESP_LOGCONFIG(TAG, " Mode: passive with sleep/wake cycles");
} }
this->check_uart_settings(9600);
} }
void PMSX003Component::loop() { void PMSX003Component::loop() {
+7 -1
View File
@@ -302,7 +302,13 @@ CONFIG_SCHEMA = cv.All(
def final_validate(config: ConfigType) -> None: def final_validate(config: ConfigType) -> None:
require_tx = config[CONF_UPDATE_INTERVAL] > cv.time_period("0s") require_tx = config[CONF_UPDATE_INTERVAL] > cv.time_period("0s")
schema = uart.final_validate_device_schema( schema = uart.final_validate_device_schema(
"pmsx003", baud_rate=9600, require_rx=True, require_tx=require_tx "pmsx003",
baud_rate=9600,
require_rx=True,
require_tx=require_tx,
data_bits=8,
parity="NONE",
stop_bits=1,
) )
schema(config) schema(config)
+8
View File
@@ -41,6 +41,14 @@ CONFIG_SCHEMA = cv.All(
.extend(uart.UART_DEVICE_SCHEMA) .extend(uart.UART_DEVICE_SCHEMA)
) )
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"pylontech",
baud_rate=115200,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None: async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
@@ -33,7 +33,6 @@ static const uint8_t ASCII_LF = 0x0A;
PylontechComponent::PylontechComponent() {} PylontechComponent::PylontechComponent() {}
void PylontechComponent::dump_config() { void PylontechComponent::dump_config() {
this->check_uart_settings(115200, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
ESP_LOGCONFIG(TAG, "pylontech:"); ESP_LOGCONFIG(TAG, "pylontech:");
if (this->is_failed()) { if (this->is_failed()) {
ESP_LOGE(TAG, "Connection with pylontech failed!"); ESP_LOGE(TAG, "Connection with pylontech failed!");
@@ -4,11 +4,7 @@ from esphome import automation, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32, esp32_rmt, remote_base from esphome.components import esp32, esp32_rmt, remote_base
from esphome.components.libretiny import get_libretiny_family from esphome.components.libretiny import get_libretiny_family
from esphome.components.libretiny.const import ( from esphome.components.libretiny.const import FAMILY_BK7238, FAMILY_RTL8720C
FAMILY_BK7231N,
FAMILY_BK7238,
FAMILY_RTL8720C,
)
from esphome.config_helpers import filter_source_files_from_platform from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@@ -49,7 +45,9 @@ DigitalWriteAction = remote_transmitter_ns.class_(
) )
_NON_BLOCKING_LIBRETINY_FAMILIES = (FAMILY_RTL8720C, FAMILY_BK7231N, FAMILY_BK7238) # Keep in sync with the USE_LIBRETINY_VARIANT_RTL8720C / REMOTE_TRANSMITTER_BK_PWM gates in
# remote_transmitter.h, which decide where set_non_blocking() is declared
_NON_BLOCKING_LIBRETINY_FAMILIES = (FAMILY_RTL8720C, FAMILY_BK7238)
def _validate_non_blocking_platform(value: bool) -> bool: def _validate_non_blocking_platform(value: bool) -> bool:
@@ -59,9 +57,7 @@ def _validate_non_blocking_platform(value: bool) -> bool:
return cv.boolean(value) return cv.boolean(value)
if CORE.is_libretiny and get_libretiny_family() in _NON_BLOCKING_LIBRETINY_FAMILIES: if CORE.is_libretiny and get_libretiny_family() in _NON_BLOCKING_LIBRETINY_FAMILIES:
return cv.boolean(value) return cv.boolean(value)
raise cv.Invalid( raise cv.Invalid("non_blocking is only supported on ESP32, RTL8720C and BK7238")
"non_blocking is only supported on ESP32, RTL8720C, BK7231N and BK7238"
)
MULTI_CONF = True MULTI_CONF = True
@@ -12,10 +12,11 @@
#endif // SOC_RMT_SUPPORTED #endif // SOC_RMT_SUPPORTED
#endif // USE_ESP32 #endif // USE_ESP32
// The BK7231N-style PWM block (hardware shadow-load duty updates) enables the ISR-driven // Enables the ISR-driven transmitter on Beken. Gated on BK7238 alone: the shadow-load PWM
// transmitter on these families; family-level proxy for the SDK's CFG_SOC_NAME gate. // block is shared with BK7231N, but LibreTiny builds that family against an older BDK whose
// See remote_transmitter_bk72xx.cpp. // PWM driver has no pwm_init_param()/pwm_start(). See remote_transmitter_bk72xx.cpp.
#if defined(USE_LIBRETINY_VARIANT_BK7231N) || defined(USE_LIBRETINY_VARIANT_BK7238) // Keep in sync with _NON_BLOCKING_LIBRETINY_FAMILIES in __init__.py.
#ifdef USE_LIBRETINY_VARIANT_BK7238
#define REMOTE_TRANSMITTER_BK_PWM #define REMOTE_TRANSMITTER_BK_PWM
#endif #endif
@@ -9,10 +9,13 @@
// with the core's fixes for type-name collisions between the two // with the core's fixes for type-name collisions between the two
#include <ArduinoPrivate.h> #include <ArduinoPrivate.h>
// Only the BK7231N-style PWM block (shadow registers with a hardware CFG_UPDATA load bit) // Needs the BK7231N-style PWM block (shadow registers with a hardware CFG_UPDATA load bit)
// supports glitch-free per-edge duty updates; older SoCs compile the generic bit-bang // for glitch-free per-edge duty updates, and an SDK exposing pwm_init_param()/pwm_start().
// implementation (remote_transmitter.cpp) instead, and this file compiles to nothing. // BK7231N has the block but LibreTiny builds it against an older BDK offering only the
// REMOTE_TRANSMITTER_BK_PWM is set per-family in remote_transmitter.h. // sddev_control API (CMD_PWM_INIT_PARAM), so it stays on the generic bit-bang path until
// someone can add and validate that path on real hardware. Every other Beken SoC lacks the
// block. REMOTE_TRANSMITTER_BK_PWM is set per-family in remote_transmitter.h; when it is
// unset this file compiles to nothing and remote_transmitter.cpp is used instead.
namespace esphome::remote_transmitter { namespace esphome::remote_transmitter {
@@ -3,11 +3,11 @@
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
// Envelope chain shared by the LibreTiny families that pace transmission from a hardware // Envelope chain shared by the LibreTiny families that pace transmission from a hardware timer
// timer interrupt: RTL8720C (gtimer) and the BK7231N-style PWM block (BKTIMER1). Everything // interrupt: RTL8720C (gtimer) and BK7238 (BKTIMER1). Everything platform-specific sits behind
// platform-specific sits behind five hooks implemented in the per-family files -- carrier // five hooks implemented in the per-family files -- carrier setup, duty writes, one-shot arming
// setup, duty writes, one-shot arming and timer stop. Families without a usable timer keep // and timer stop. Families without a usable timer keep the generic bit-bang implementation and
// the generic bit-bang implementation and compile none of this. // compile none of this.
#if defined(USE_LIBRETINY_VARIANT_RTL8720C) || defined(REMOTE_TRANSMITTER_BK_PWM) #if defined(USE_LIBRETINY_VARIANT_RTL8720C) || defined(REMOTE_TRANSMITTER_BK_PWM)
namespace esphome::remote_transmitter { namespace esphome::remote_transmitter {
+253
View File
@@ -1 +1,254 @@
import esphome.codegen as cg
CODEOWNERS = ["@clydebarrow"] CODEOWNERS = ["@clydebarrow"]
SDL_KeyCode = cg.global_ns.enum("SDL_KeyCode")
SDL_KEYS = (
"SDLK_UNKNOWN",
"SDLK_RETURN",
"SDLK_ESCAPE",
"SDLK_BACKSPACE",
"SDLK_TAB",
"SDLK_SPACE",
"SDLK_EXCLAIM",
"SDLK_QUOTEDBL",
"SDLK_HASH",
"SDLK_PERCENT",
"SDLK_DOLLAR",
"SDLK_AMPERSAND",
"SDLK_QUOTE",
"SDLK_LEFTPAREN",
"SDLK_RIGHTPAREN",
"SDLK_ASTERISK",
"SDLK_PLUS",
"SDLK_COMMA",
"SDLK_MINUS",
"SDLK_PERIOD",
"SDLK_SLASH",
"SDLK_0",
"SDLK_1",
"SDLK_2",
"SDLK_3",
"SDLK_4",
"SDLK_5",
"SDLK_6",
"SDLK_7",
"SDLK_8",
"SDLK_9",
"SDLK_COLON",
"SDLK_SEMICOLON",
"SDLK_LESS",
"SDLK_EQUALS",
"SDLK_GREATER",
"SDLK_QUESTION",
"SDLK_AT",
"SDLK_LEFTBRACKET",
"SDLK_BACKSLASH",
"SDLK_RIGHTBRACKET",
"SDLK_CARET",
"SDLK_UNDERSCORE",
"SDLK_BACKQUOTE",
"SDLK_a",
"SDLK_b",
"SDLK_c",
"SDLK_d",
"SDLK_e",
"SDLK_f",
"SDLK_g",
"SDLK_h",
"SDLK_i",
"SDLK_j",
"SDLK_k",
"SDLK_l",
"SDLK_m",
"SDLK_n",
"SDLK_o",
"SDLK_p",
"SDLK_q",
"SDLK_r",
"SDLK_s",
"SDLK_t",
"SDLK_u",
"SDLK_v",
"SDLK_w",
"SDLK_x",
"SDLK_y",
"SDLK_z",
"SDLK_CAPSLOCK",
"SDLK_F1",
"SDLK_F2",
"SDLK_F3",
"SDLK_F4",
"SDLK_F5",
"SDLK_F6",
"SDLK_F7",
"SDLK_F8",
"SDLK_F9",
"SDLK_F10",
"SDLK_F11",
"SDLK_F12",
"SDLK_PRINTSCREEN",
"SDLK_SCROLLLOCK",
"SDLK_PAUSE",
"SDLK_INSERT",
"SDLK_HOME",
"SDLK_PAGEUP",
"SDLK_DELETE",
"SDLK_END",
"SDLK_PAGEDOWN",
"SDLK_RIGHT",
"SDLK_LEFT",
"SDLK_DOWN",
"SDLK_UP",
"SDLK_NUMLOCKCLEAR",
"SDLK_KP_DIVIDE",
"SDLK_KP_MULTIPLY",
"SDLK_KP_MINUS",
"SDLK_KP_PLUS",
"SDLK_KP_ENTER",
"SDLK_KP_1",
"SDLK_KP_2",
"SDLK_KP_3",
"SDLK_KP_4",
"SDLK_KP_5",
"SDLK_KP_6",
"SDLK_KP_7",
"SDLK_KP_8",
"SDLK_KP_9",
"SDLK_KP_0",
"SDLK_KP_PERIOD",
"SDLK_APPLICATION",
"SDLK_POWER",
"SDLK_KP_EQUALS",
"SDLK_F13",
"SDLK_F14",
"SDLK_F15",
"SDLK_F16",
"SDLK_F17",
"SDLK_F18",
"SDLK_F19",
"SDLK_F20",
"SDLK_F21",
"SDLK_F22",
"SDLK_F23",
"SDLK_F24",
"SDLK_EXECUTE",
"SDLK_HELP",
"SDLK_MENU",
"SDLK_SELECT",
"SDLK_STOP",
"SDLK_AGAIN",
"SDLK_UNDO",
"SDLK_CUT",
"SDLK_COPY",
"SDLK_PASTE",
"SDLK_FIND",
"SDLK_MUTE",
"SDLK_VOLUMEUP",
"SDLK_VOLUMEDOWN",
"SDLK_KP_COMMA",
"SDLK_KP_EQUALSAS400",
"SDLK_ALTERASE",
"SDLK_SYSREQ",
"SDLK_CANCEL",
"SDLK_CLEAR",
"SDLK_PRIOR",
"SDLK_RETURN2",
"SDLK_SEPARATOR",
"SDLK_OUT",
"SDLK_OPER",
"SDLK_CLEARAGAIN",
"SDLK_CRSEL",
"SDLK_EXSEL",
"SDLK_KP_00",
"SDLK_KP_000",
"SDLK_THOUSANDSSEPARATOR",
"SDLK_DECIMALSEPARATOR",
"SDLK_CURRENCYUNIT",
"SDLK_CURRENCYSUBUNIT",
"SDLK_KP_LEFTPAREN",
"SDLK_KP_RIGHTPAREN",
"SDLK_KP_LEFTBRACE",
"SDLK_KP_RIGHTBRACE",
"SDLK_KP_TAB",
"SDLK_KP_BACKSPACE",
"SDLK_KP_A",
"SDLK_KP_B",
"SDLK_KP_C",
"SDLK_KP_D",
"SDLK_KP_E",
"SDLK_KP_F",
"SDLK_KP_XOR",
"SDLK_KP_POWER",
"SDLK_KP_PERCENT",
"SDLK_KP_LESS",
"SDLK_KP_GREATER",
"SDLK_KP_AMPERSAND",
"SDLK_KP_DBLAMPERSAND",
"SDLK_KP_VERTICALBAR",
"SDLK_KP_DBLVERTICALBAR",
"SDLK_KP_COLON",
"SDLK_KP_HASH",
"SDLK_KP_SPACE",
"SDLK_KP_AT",
"SDLK_KP_EXCLAM",
"SDLK_KP_MEMSTORE",
"SDLK_KP_MEMRECALL",
"SDLK_KP_MEMCLEAR",
"SDLK_KP_MEMADD",
"SDLK_KP_MEMSUBTRACT",
"SDLK_KP_MEMMULTIPLY",
"SDLK_KP_MEMDIVIDE",
"SDLK_KP_PLUSMINUS",
"SDLK_KP_CLEAR",
"SDLK_KP_CLEARENTRY",
"SDLK_KP_BINARY",
"SDLK_KP_OCTAL",
"SDLK_KP_DECIMAL",
"SDLK_KP_HEXADECIMAL",
"SDLK_LCTRL",
"SDLK_LSHIFT",
"SDLK_LALT",
"SDLK_LGUI",
"SDLK_RCTRL",
"SDLK_RSHIFT",
"SDLK_RALT",
"SDLK_RGUI",
"SDLK_MODE",
"SDLK_AUDIONEXT",
"SDLK_AUDIOPREV",
"SDLK_AUDIOSTOP",
"SDLK_AUDIOPLAY",
"SDLK_AUDIOMUTE",
"SDLK_MEDIASELECT",
"SDLK_WWW",
"SDLK_MAIL",
"SDLK_CALCULATOR",
"SDLK_COMPUTER",
"SDLK_AC_SEARCH",
"SDLK_AC_HOME",
"SDLK_AC_BACK",
"SDLK_AC_FORWARD",
"SDLK_AC_STOP",
"SDLK_AC_REFRESH",
"SDLK_AC_BOOKMARKS",
"SDLK_BRIGHTNESSDOWN",
"SDLK_BRIGHTNESSUP",
"SDLK_DISPLAYSWITCH",
"SDLK_KBDILLUMTOGGLE",
"SDLK_KBDILLUMDOWN",
"SDLK_KBDILLUMUP",
"SDLK_EJECT",
"SDLK_SLEEP",
"SDLK_APP1",
"SDLK_APP2",
"SDLK_AUDIOREWIND",
"SDLK_AUDIOFASTFORWARD",
"SDLK_SOFTLEFT",
"SDLK_SOFTRIGHT",
"SDLK_CALL",
"SDLK_ENDCALL",
)
SDL_KEYMAP = {key: getattr(SDL_KeyCode, key) for key in SDL_KEYS}
+3 -250
View File
@@ -7,262 +7,15 @@ from esphome.core import Lambda
from esphome.cpp_generator import ExpressionStatement, RawExpression from esphome.cpp_generator import ExpressionStatement, RawExpression
from esphome.types import ConfigType from esphome.types import ConfigType
from .display import CONF_SDL_ID, Sdl from . import SDL_KEYMAP
from .display import CONF_SDL_ID, Sdl, headless_final_validate
CODEOWNERS = ["@bdm310"] CODEOWNERS = ["@bdm310"]
STATE_ARG = "state" STATE_ARG = "state"
SDL_KeyCode = cg.global_ns.enum("SDL_KeyCode") FINAL_VALIDATE_SCHEMA = headless_final_validate("binary_sensor")
SDL_KEYS = (
"SDLK_UNKNOWN",
"SDLK_RETURN",
"SDLK_ESCAPE",
"SDLK_BACKSPACE",
"SDLK_TAB",
"SDLK_SPACE",
"SDLK_EXCLAIM",
"SDLK_QUOTEDBL",
"SDLK_HASH",
"SDLK_PERCENT",
"SDLK_DOLLAR",
"SDLK_AMPERSAND",
"SDLK_QUOTE",
"SDLK_LEFTPAREN",
"SDLK_RIGHTPAREN",
"SDLK_ASTERISK",
"SDLK_PLUS",
"SDLK_COMMA",
"SDLK_MINUS",
"SDLK_PERIOD",
"SDLK_SLASH",
"SDLK_0",
"SDLK_1",
"SDLK_2",
"SDLK_3",
"SDLK_4",
"SDLK_5",
"SDLK_6",
"SDLK_7",
"SDLK_8",
"SDLK_9",
"SDLK_COLON",
"SDLK_SEMICOLON",
"SDLK_LESS",
"SDLK_EQUALS",
"SDLK_GREATER",
"SDLK_QUESTION",
"SDLK_AT",
"SDLK_LEFTBRACKET",
"SDLK_BACKSLASH",
"SDLK_RIGHTBRACKET",
"SDLK_CARET",
"SDLK_UNDERSCORE",
"SDLK_BACKQUOTE",
"SDLK_a",
"SDLK_b",
"SDLK_c",
"SDLK_d",
"SDLK_e",
"SDLK_f",
"SDLK_g",
"SDLK_h",
"SDLK_i",
"SDLK_j",
"SDLK_k",
"SDLK_l",
"SDLK_m",
"SDLK_n",
"SDLK_o",
"SDLK_p",
"SDLK_q",
"SDLK_r",
"SDLK_s",
"SDLK_t",
"SDLK_u",
"SDLK_v",
"SDLK_w",
"SDLK_x",
"SDLK_y",
"SDLK_z",
"SDLK_CAPSLOCK",
"SDLK_F1",
"SDLK_F2",
"SDLK_F3",
"SDLK_F4",
"SDLK_F5",
"SDLK_F6",
"SDLK_F7",
"SDLK_F8",
"SDLK_F9",
"SDLK_F10",
"SDLK_F11",
"SDLK_F12",
"SDLK_PRINTSCREEN",
"SDLK_SCROLLLOCK",
"SDLK_PAUSE",
"SDLK_INSERT",
"SDLK_HOME",
"SDLK_PAGEUP",
"SDLK_DELETE",
"SDLK_END",
"SDLK_PAGEDOWN",
"SDLK_RIGHT",
"SDLK_LEFT",
"SDLK_DOWN",
"SDLK_UP",
"SDLK_NUMLOCKCLEAR",
"SDLK_KP_DIVIDE",
"SDLK_KP_MULTIPLY",
"SDLK_KP_MINUS",
"SDLK_KP_PLUS",
"SDLK_KP_ENTER",
"SDLK_KP_1",
"SDLK_KP_2",
"SDLK_KP_3",
"SDLK_KP_4",
"SDLK_KP_5",
"SDLK_KP_6",
"SDLK_KP_7",
"SDLK_KP_8",
"SDLK_KP_9",
"SDLK_KP_0",
"SDLK_KP_PERIOD",
"SDLK_APPLICATION",
"SDLK_POWER",
"SDLK_KP_EQUALS",
"SDLK_F13",
"SDLK_F14",
"SDLK_F15",
"SDLK_F16",
"SDLK_F17",
"SDLK_F18",
"SDLK_F19",
"SDLK_F20",
"SDLK_F21",
"SDLK_F22",
"SDLK_F23",
"SDLK_F24",
"SDLK_EXECUTE",
"SDLK_HELP",
"SDLK_MENU",
"SDLK_SELECT",
"SDLK_STOP",
"SDLK_AGAIN",
"SDLK_UNDO",
"SDLK_CUT",
"SDLK_COPY",
"SDLK_PASTE",
"SDLK_FIND",
"SDLK_MUTE",
"SDLK_VOLUMEUP",
"SDLK_VOLUMEDOWN",
"SDLK_KP_COMMA",
"SDLK_KP_EQUALSAS400",
"SDLK_ALTERASE",
"SDLK_SYSREQ",
"SDLK_CANCEL",
"SDLK_CLEAR",
"SDLK_PRIOR",
"SDLK_RETURN2",
"SDLK_SEPARATOR",
"SDLK_OUT",
"SDLK_OPER",
"SDLK_CLEARAGAIN",
"SDLK_CRSEL",
"SDLK_EXSEL",
"SDLK_KP_00",
"SDLK_KP_000",
"SDLK_THOUSANDSSEPARATOR",
"SDLK_DECIMALSEPARATOR",
"SDLK_CURRENCYUNIT",
"SDLK_CURRENCYSUBUNIT",
"SDLK_KP_LEFTPAREN",
"SDLK_KP_RIGHTPAREN",
"SDLK_KP_LEFTBRACE",
"SDLK_KP_RIGHTBRACE",
"SDLK_KP_TAB",
"SDLK_KP_BACKSPACE",
"SDLK_KP_A",
"SDLK_KP_B",
"SDLK_KP_C",
"SDLK_KP_D",
"SDLK_KP_E",
"SDLK_KP_F",
"SDLK_KP_XOR",
"SDLK_KP_POWER",
"SDLK_KP_PERCENT",
"SDLK_KP_LESS",
"SDLK_KP_GREATER",
"SDLK_KP_AMPERSAND",
"SDLK_KP_DBLAMPERSAND",
"SDLK_KP_VERTICALBAR",
"SDLK_KP_DBLVERTICALBAR",
"SDLK_KP_COLON",
"SDLK_KP_HASH",
"SDLK_KP_SPACE",
"SDLK_KP_AT",
"SDLK_KP_EXCLAM",
"SDLK_KP_MEMSTORE",
"SDLK_KP_MEMRECALL",
"SDLK_KP_MEMCLEAR",
"SDLK_KP_MEMADD",
"SDLK_KP_MEMSUBTRACT",
"SDLK_KP_MEMMULTIPLY",
"SDLK_KP_MEMDIVIDE",
"SDLK_KP_PLUSMINUS",
"SDLK_KP_CLEAR",
"SDLK_KP_CLEARENTRY",
"SDLK_KP_BINARY",
"SDLK_KP_OCTAL",
"SDLK_KP_DECIMAL",
"SDLK_KP_HEXADECIMAL",
"SDLK_LCTRL",
"SDLK_LSHIFT",
"SDLK_LALT",
"SDLK_LGUI",
"SDLK_RCTRL",
"SDLK_RSHIFT",
"SDLK_RALT",
"SDLK_RGUI",
"SDLK_MODE",
"SDLK_AUDIONEXT",
"SDLK_AUDIOPREV",
"SDLK_AUDIOSTOP",
"SDLK_AUDIOPLAY",
"SDLK_AUDIOMUTE",
"SDLK_MEDIASELECT",
"SDLK_WWW",
"SDLK_MAIL",
"SDLK_CALCULATOR",
"SDLK_COMPUTER",
"SDLK_AC_SEARCH",
"SDLK_AC_HOME",
"SDLK_AC_BACK",
"SDLK_AC_FORWARD",
"SDLK_AC_STOP",
"SDLK_AC_REFRESH",
"SDLK_AC_BOOKMARKS",
"SDLK_BRIGHTNESSDOWN",
"SDLK_BRIGHTNESSUP",
"SDLK_DISPLAYSWITCH",
"SDLK_KBDILLUMTOGGLE",
"SDLK_KBDILLUMDOWN",
"SDLK_KBDILLUMUP",
"SDLK_EJECT",
"SDLK_SLEEP",
"SDLK_APP1",
"SDLK_APP2",
"SDLK_AUDIOREWIND",
"SDLK_AUDIOFASTFORWARD",
"SDLK_SOFTLEFT",
"SDLK_SOFTRIGHT",
"SDLK_CALL",
"SDLK_ENDCALL",
)
SDL_KEYMAP = {key: getattr(SDL_KeyCode, key) for key in SDL_KEYS}
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
binary_sensor.binary_sensor_schema(BinarySensor) binary_sensor.binary_sensor_schema(BinarySensor)
+52 -1
View File
@@ -4,6 +4,7 @@ from typing import Any
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import display from esphome.components import display
from esphome.components.snapshot import Snapshot, register_snapshot
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_DIMENSIONS, CONF_DIMENSIONS,
@@ -16,14 +17,21 @@ from esphome.const import (
CONF_Y, CONF_Y,
PLATFORM_HOST, PLATFORM_HOST,
) )
import esphome.final_validate as fv
from esphome.types import ConfigType from esphome.types import ConfigType
from . import SDL_KEYMAP
AUTO_LOAD = ["snapshot"]
sdl_ns = cg.esphome_ns.namespace("sdl") sdl_ns = cg.esphome_ns.namespace("sdl")
Sdl = sdl_ns.class_("Sdl", display.Display, cg.Component) Sdl = sdl_ns.class_("Sdl", display.Display, cg.Component, Snapshot)
sdl_window_flags = cg.global_ns.enum("SDL_WindowFlags") sdl_window_flags = cg.global_ns.enum("SDL_WindowFlags")
CONF_CENTERED_ON_DISPLAY = "centered_on_display" CONF_CENTERED_ON_DISPLAY = "centered_on_display"
CONF_HEADLESS = "headless"
CONF_SNAPSHOT_KEY = "snapshot_key"
CONF_SDL_OPTIONS = "sdl_options" CONF_SDL_OPTIONS = "sdl_options"
CONF_SDL_ID = "sdl_id" CONF_SDL_ID = "sdl_id"
CONF_WINDOW_OPTIONS = "window_options" CONF_WINDOW_OPTIONS = "window_options"
@@ -67,12 +75,29 @@ def _validate_position(config: dict) -> dict:
raise cv.Invalid("Must specify either 'x' and 'y' or 'centered_on_display'") raise cv.Invalid("Must specify either 'x' and 'y' or 'centered_on_display'")
def _validate_headless(config: ConfigType) -> ConfigType:
if not config[CONF_HEADLESS]:
return config
if CONF_WINDOW_OPTIONS in config:
raise cv.Invalid(
f"'{CONF_WINDOW_OPTIONS}' has no effect when '{CONF_HEADLESS}' is set - there is no window"
)
if CONF_SNAPSHOT_KEY in config:
raise cv.Invalid(
f"'{CONF_SNAPSHOT_KEY}' cannot be used when '{CONF_HEADLESS}' is set - "
f"there is no keyboard. Use the 'snapshot.take' action instead"
)
return config
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
display.FULL_DISPLAY_SCHEMA.extend( display.FULL_DISPLAY_SCHEMA.extend(
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(Sdl), cv.GenerateID(): cv.declare_id(Sdl),
cv.Optional(CONF_SDL_OPTIONS, default=""): get_sdl_options, cv.Optional(CONF_SDL_OPTIONS, default=""): get_sdl_options,
cv.Optional(CONF_HEADLESS, default=False): cv.boolean,
cv.Optional(CONF_SNAPSHOT_KEY): cv.enum(SDL_KEYMAP),
cv.Required(CONF_DIMENSIONS): cv.Any( cv.Required(CONF_DIMENSIONS): cv.Any(
cv.dimensions, cv.dimensions,
cv.Schema( cv.Schema(
@@ -99,16 +124,42 @@ CONFIG_SCHEMA = cv.All(
} }
) )
), ),
_validate_headless,
cv.only_on(PLATFORM_HOST), cv.only_on(PLATFORM_HOST),
) )
def headless_final_validate(platform: str) -> cv.Schema:
"""Build a FINAL_VALIDATE_SCHEMA rejecting a platform whose sdl display is headless.
Mouse and keyboard platforms are driven by window events, so under a headless display they
would never report anything.
"""
def validate_display(display_config: ConfigType) -> ConfigType:
if display_config.get(CONF_HEADLESS):
raise cv.Invalid(
f"The sdl {platform} platform needs a window, but its display has "
f"'{CONF_HEADLESS}' set"
)
return display_config
return cv.Schema(
{cv.Required(CONF_SDL_ID): fv.id_declaration_match_schema(validate_display)},
extra=cv.ALLOW_EXTRA,
)
async def to_code(config: ConfigType) -> None: async def to_code(config: ConfigType) -> None:
for option in config[CONF_SDL_OPTIONS].split(): for option in config[CONF_SDL_OPTIONS].split():
cg.add_build_flag(option) cg.add_build_flag(option)
cg.add_build_flag("-DSDL_BYTEORDER=4321") cg.add_build_flag("-DSDL_BYTEORDER=4321")
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config) await display.register_display(var, config)
await register_snapshot(var, config)
cg.add(var.set_headless(config[CONF_HEADLESS]))
if (key := config.get(CONF_SNAPSHOT_KEY)) is not None:
cg.add(var.set_snapshot_key(key))
dimensions = config[CONF_DIMENSIONS] dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict): if isinstance(dimensions, dict):
+228 -46
View File
@@ -2,8 +2,17 @@
#include "sdl_esphome.h" #include "sdl_esphome.h"
#include "esphome/components/display/display_color_utils.h" #include "esphome/components/display/display_color_utils.h"
#include <cstdlib>
namespace esphome::sdl { namespace esphome::sdl {
namespace {
// Key under which each window keeps a pointer back to its Sdl instance.
constexpr const char *const WINDOW_DATA_KEY = "esphome_sdl";
} // namespace
int Sdl::get_width() { int Sdl::get_width() {
switch (this->rotation_) { switch (this->rotation_) {
case display::DISPLAY_ROTATION_90_DEGREES: case display::DISPLAY_ROTATION_90_DEGREES:
@@ -28,17 +37,96 @@ int Sdl::get_height() {
} }
} }
void Sdl::setup() { void Sdl::destroy_renderer_() {
SDL_Init(SDL_INIT_VIDEO); // Reverse order of creation: the renderer refers to the window or surface it was made from.
this->window_ = SDL_CreateWindow(App.get_name().c_str(), this->pos_x_, this->pos_y_, this->width_, this->height_, if (this->shot_target_ != nullptr) {
this->window_options_); SDL_DestroyTexture(this->shot_target_);
this->renderer_ = SDL_CreateRenderer(this->window_, -1, SDL_RENDERER_SOFTWARE); this->shot_target_ = nullptr;
SDL_RenderSetLogicalSize(this->renderer_, this->width_, this->height_); }
if (this->texture_ != nullptr) {
SDL_DestroyTexture(this->texture_);
this->texture_ = nullptr;
}
if (this->renderer_ != nullptr) {
SDL_DestroyRenderer(this->renderer_);
this->renderer_ = nullptr;
}
if (this->window_ != nullptr) {
SDL_DestroyWindow(this->window_);
this->window_ = nullptr;
}
if (this->surface_ != nullptr) {
SDL_FreeSurface(this->surface_);
this->surface_ = nullptr;
}
}
bool Sdl::setup_failed_(const char *what) {
ESP_LOGE(TAG, "%s: %s", what, SDL_GetError());
// Give back whatever was created before the failure. Without this a half set up display leaves an
// empty window on screen for the life of the process, still registered as an event target.
this->destroy_renderer_();
return false;
}
bool Sdl::setup_renderer_() {
SDL_SetMainReady();
if (this->headless_) {
// SDL_INIT_VIDEO is deliberately not requested: a software renderer bound to a surface needs no
// video device, so this works on a machine with no display server at all.
if (SDL_Init(0) != 0)
return this->setup_failed_("SDL_Init failed");
this->surface_ = SDL_CreateRGBSurfaceWithFormat(0, this->width_, this->height_, 16, SDL_PIXELFORMAT_RGB565);
if (this->surface_ == nullptr)
return this->setup_failed_("Could not create offscreen surface");
this->renderer_ = SDL_CreateSoftwareRenderer(this->surface_);
} else {
if (SDL_Init(SDL_INIT_VIDEO) != 0)
return this->setup_failed_("SDL_Init failed");
this->window_ = SDL_CreateWindow(App.get_name().c_str(), this->pos_x_, this->pos_y_, this->width_, this->height_,
this->window_options_);
if (this->window_ == nullptr)
return this->setup_failed_("Could not create window");
// Lets loop() find the display an event belongs to, so one display does not act on another's
// input when several windows are open.
SDL_SetWindowData(this->window_, WINDOW_DATA_KEY, this);
this->renderer_ = SDL_CreateRenderer(this->window_, -1, SDL_RENDERER_SOFTWARE);
}
if (this->renderer_ == nullptr)
return this->setup_failed_("Could not create renderer");
if (SDL_RenderSetLogicalSize(this->renderer_, this->width_, this->height_) != 0)
return this->setup_failed_("Could not set renderer logical size");
this->texture_ = this->texture_ =
SDL_CreateTexture(this->renderer_, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STATIC, this->width_, this->height_); SDL_CreateTexture(this->renderer_, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STATIC, this->width_, this->height_);
SDL_SetTextureBlendMode(this->texture_, SDL_BLENDMODE_BLEND); if (this->texture_ == nullptr)
return this->setup_failed_("Could not create texture");
// The texture has no alpha channel, so blending is pointless. Headless it would also force a
// different software blit path onto the 16 bit target surface.
if (SDL_SetTextureBlendMode(this->texture_, this->headless_ ? SDL_BLENDMODE_NONE : SDL_BLENDMODE_BLEND) != 0)
return this->setup_failed_("Could not set texture blend mode");
return true;
} }
void Sdl::setup() {
if (!this->setup_renderer_()) {
this->mark_failed();
return;
}
if (this->headless_) {
// Nothing generates events, so there is nothing for loop() to do.
this->disable_loop();
} else if (this->snapshot_key_ != 0) {
this->add_key_listener(this->snapshot_key_, [this](bool down) {
if (down && !this->take_snapshot(nullptr)) {
ESP_LOGW(TAG, "snapshot key did not write a file");
}
});
}
}
void Sdl::update() { void Sdl::update() {
if (this->texture_ == nullptr)
return;
this->do_update_(); this->do_update_();
if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_))
return; return;
@@ -51,12 +139,19 @@ void Sdl::update() {
} }
void Sdl::redraw_(SDL_Rect &rect) { void Sdl::redraw_(SDL_Rect &rect) {
// Nothing to present when headless - a snapshot blits the whole texture when it needs it, so
// doing it here as well would just burn CPU. draw_pixels_at() calls this on every partial
// update, so it is worth skipping.
if (this->headless_)
return;
SDL_RenderCopy(this->renderer_, this->texture_, &rect, &rect); SDL_RenderCopy(this->renderer_, this->texture_, &rect, &rect);
SDL_RenderPresent(this->renderer_); SDL_RenderPresent(this->renderer_);
} }
void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) { display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
if (this->texture_ == nullptr)
return;
SDL_Rect rect{x_start, y_start, w, h}; SDL_Rect rect{x_start, y_start, w, h};
if (this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || bitness != display::COLOR_BITNESS_565 || big_endian) { if (this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || bitness != display::COLOR_BITNESS_565 || big_endian) {
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
@@ -69,7 +164,7 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *
} }
void Sdl::draw_pixel_at(int x, int y, Color color) { void Sdl::draw_pixel_at(int x, int y, Color color) {
if (!this->get_clipping().inside(x, y)) if (this->texture_ == nullptr || !this->get_clipping().inside(x, y))
return; return;
if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) { if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) {
@@ -104,61 +199,148 @@ void Sdl::process_key(uint32_t keycode, bool down) {
callback->second(down); callback->second(down);
} }
Sdl *Sdl::instance_for_window_(uint32_t window_id) {
SDL_Window *window = SDL_GetWindowFromID(window_id);
if (window == nullptr)
return nullptr;
return static_cast<Sdl *>(SDL_GetWindowData(window, WINDOW_DATA_KEY));
}
void Sdl::handle_event_(const SDL_Event &event) {
switch (event.type) {
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
if (event.button.button == 1) {
this->mouse_x = event.button.x;
this->mouse_y = event.button.y;
this->mouse_down = event.button.state != 0;
}
break;
case SDL_MOUSEMOTION:
if (event.motion.state & 1) {
this->mouse_x = event.motion.x;
this->mouse_y = event.motion.y;
this->mouse_down = true;
} else {
this->mouse_down = false;
}
break;
case SDL_KEYDOWN:
// Ignore auto-repeat, otherwise holding a key floods the listeners.
if (event.key.repeat != 0)
break;
ESP_LOGD(TAG, "keydown %d", event.key.keysym.sym);
this->process_key(event.key.keysym.sym, true);
break;
case SDL_KEYUP:
ESP_LOGD(TAG, "keyup %d", event.key.keysym.sym);
this->process_key(event.key.keysym.sym, false);
break;
case SDL_WINDOWEVENT:
switch (event.window.event) {
case SDL_WINDOWEVENT_SIZE_CHANGED:
case SDL_WINDOWEVENT_EXPOSED:
case SDL_WINDOWEVENT_RESIZED: {
SDL_Rect rect{0, 0, this->width_, this->height_};
this->redraw_(rect);
break;
}
default:
break;
}
break;
default:
break;
}
}
void Sdl::loop() { void Sdl::loop() {
SDL_Event e; SDL_Event e;
if (SDL_PollEvent(&e)) { // Take everything that is waiting, not one event per loop. A touch drag produces a burst of
switch (e.type) { // motion events, and consuming them one at a time lets the queue grow without bound, so the
case SDL_QUIT: // pointer ends up acting on input from further and further in the past. Draining collapses a
exit(0); // burst to the position it ended at, which is the one the user is asking for anyway.
while (SDL_PollEvent(&e)) {
if (e.type == SDL_QUIT)
exit(0);
// Events carry the window they happened in, so send each one to the display that owns it.
uint32_t window_id;
switch (e.type) {
case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP: case SDL_MOUSEBUTTONUP:
if (e.button.button == 1) { window_id = e.button.windowID;
this->mouse_x = e.button.x;
this->mouse_y = e.button.y;
this->mouse_down = e.button.state != 0;
}
break; break;
case SDL_MOUSEMOTION: case SDL_MOUSEMOTION:
if (e.motion.state & 1) { window_id = e.motion.windowID;
this->mouse_x = e.button.x;
this->mouse_y = e.button.y;
this->mouse_down = true;
} else {
this->mouse_down = false;
}
break; break;
case SDL_KEYDOWN: case SDL_KEYDOWN:
ESP_LOGD(TAG, "keydown %d", e.key.keysym.sym);
this->process_key(e.key.keysym.sym, true);
break;
case SDL_KEYUP: case SDL_KEYUP:
ESP_LOGD(TAG, "keyup %d", e.key.keysym.sym); window_id = e.key.windowID;
this->process_key(e.key.keysym.sym, false);
break; break;
case SDL_WINDOWEVENT: case SDL_WINDOWEVENT:
switch (e.window.event) { window_id = e.window.windowID;
case SDL_WINDOWEVENT_SIZE_CHANGED:
case SDL_WINDOWEVENT_EXPOSED:
case SDL_WINDOWEVENT_RESIZED: {
SDL_Rect rect{0, 0, this->width_, this->height_};
this->redraw_(rect);
break;
}
default:
break;
}
break; break;
default: default:
// Anything else, including the touch events SDL reports alongside the mouse events it
// synthesises from them, is not used here.
ESP_LOGV(TAG, "Event %d", e.type); ESP_LOGV(TAG, "Event %d", e.type);
break; continue;
}
Sdl *target = instance_for_window_(window_id);
if (target == nullptr) {
// Nothing to route this to: the window has gone, or it is not one of ours. Say so, otherwise
// input that stops working leaves no trace at all.
ESP_LOGV(TAG, "Event %d for unknown window %u", e.type, window_id);
continue;
}
target->handle_event_(e);
}
}
bool Sdl::capture_bgr(uint8_t *dest, size_t row_stride) {
if (this->texture_ == nullptr || this->renderer_ == nullptr) {
ESP_LOGE(TAG, "Snapshot requested but SDL is not set up");
return false;
}
if (this->shot_target_ == nullptr) {
this->shot_target_ = SDL_CreateTexture(this->renderer_, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_TARGET,
this->width_, this->height_);
if (this->shot_target_ == nullptr) {
ESP_LOGE(TAG, "Could not create capture texture: %s", SDL_GetError());
return false;
}
SDL_SetTextureBlendMode(this->shot_target_, SDL_BLENDMODE_NONE);
}
// Render into an offscreen target first. SDL_RenderReadPixels works in physical output pixels and
// ignores the logical size, so reading straight off a resizable window would read more pixels than
// there is room for.
// Every step is checked: a failed clear or copy would otherwise be read back as a blank or stale
// picture, written out, and reported as a snapshot that worked.
bool ok = false;
if (SDL_SetRenderTarget(this->renderer_, this->shot_target_) == 0) {
ok = SDL_SetRenderDrawColor(this->renderer_, 0, 0, 0, SDL_ALPHA_OPAQUE) == 0 &&
SDL_RenderClear(this->renderer_) == 0 &&
SDL_RenderCopy(this->renderer_, this->texture_, nullptr, nullptr) == 0 &&
SDL_RenderReadPixels(this->renderer_, nullptr, SDL_PIXELFORMAT_BGR24, dest, static_cast<int>(row_stride)) == 0;
if (SDL_SetRenderTarget(this->renderer_, nullptr) != 0) {
// Stuck rendering into shot_target_ from here on, so there's no point continuing.
ESP_LOGE(TAG, "Could not restore the render target: %s", SDL_GetError());
this->mark_failed();
return false;
} }
} }
if (!ok) {
ESP_LOGE(TAG, "Could not capture the screen: %s", SDL_GetError());
}
return ok;
} }
} // namespace esphome::sdl } // namespace esphome::sdl
+30 -5
View File
@@ -1,10 +1,12 @@
#pragma once #pragma once
#ifdef USE_HOST #ifdef USE_HOST
#include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/components/display/display.h" #include "esphome/components/display/display.h"
#include "esphome/components/snapshot/snapshot.h"
#define SDL_MAIN_HANDLED #define SDL_MAIN_HANDLED
#include "SDL.h" #include "SDL.h"
#include <map> #include <map>
@@ -13,7 +15,7 @@ namespace esphome::sdl {
constexpr static const char *const TAG = "sdl"; constexpr static const char *const TAG = "sdl";
class Sdl final : public display::Display { class Sdl final : public display::Display, public snapshot::Snapshot {
public: public:
display::DisplayType get_display_type() override { return display::DISPLAY_TYPE_COLOR; } display::DisplayType get_display_type() override { return display::DISPLAY_TYPE_COLOR; }
void update() override; void update() override;
@@ -32,6 +34,9 @@ class Sdl final : public display::Display {
this->pos_x_ = pos_x; this->pos_x_ = pos_x;
this->pos_y_ = pos_y; this->pos_y_ = pos_y;
} }
void set_headless(bool headless) { this->headless_ = headless; }
void set_snapshot_key(int32_t keycode) { this->snapshot_key_ = keycode; }
int get_width() override; int get_width() override;
int get_height() override; int get_height() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; } float get_setup_priority() const override { return setup_priority::HARDWARE; }
@@ -51,20 +56,40 @@ class Sdl final : public display::Display {
int get_width_internal() override { return this->width_; } int get_width_internal() override { return this->width_; }
int get_height_internal() override { return this->height_; } int get_height_internal() override { return this->height_; }
void redraw_(SDL_Rect &rect); void redraw_(SDL_Rect &rect);
bool setup_renderer_();
/// Release the window, surface, renderer and textures, and forget them.
void destroy_renderer_();
/// Log an SDL failure during setup, release anything already created, and return false.
bool setup_failed_(const char *what);
int snapshot_width() override { return this->width_; }
int snapshot_height() override { return this->height_; }
bool capture_bgr(uint8_t *dest, size_t row_stride) override;
void handle_event_(const SDL_Event &event);
/// The display owning the given window, or nullptr if it is not one of ours.
static Sdl *instance_for_window_(uint32_t window_id);
SDL_Renderer *renderer_{};
SDL_Window *window_{};
SDL_Texture *texture_{};
// Offscreen render target used when headless. SDL_CreateSoftwareRenderer only borrows the
// surface, and the renderer goes back to using it as its output whenever the capture target is
// released, so it has to stay alive as long as the renderer does.
SDL_Surface *surface_{};
// Capture target, created on first snapshot.
SDL_Texture *shot_target_{};
std::map<int32_t, CallbackManager<void(bool)>> key_callbacks_{};
int width_{}; int width_{};
int height_{}; int height_{};
uint32_t window_options_{0}; uint32_t window_options_{0};
int32_t pos_x_{SDL_WINDOWPOS_UNDEFINED}; int32_t pos_x_{SDL_WINDOWPOS_UNDEFINED};
int32_t pos_y_{SDL_WINDOWPOS_UNDEFINED}; int32_t pos_y_{SDL_WINDOWPOS_UNDEFINED};
SDL_Renderer *renderer_{}; int32_t snapshot_key_{0};
SDL_Window *window_{};
SDL_Texture *texture_{};
uint16_t x_low_{0}; uint16_t x_low_{0};
uint16_t y_low_{0}; uint16_t y_low_{0};
uint16_t x_high_{0}; uint16_t x_high_{0};
uint16_t y_high_{0}; uint16_t y_high_{0};
std::map<int32_t, CallbackManager<void(bool)>> key_callbacks_{}; bool headless_{false};
}; };
} // namespace esphome::sdl } // namespace esphome::sdl
#endif #endif
@@ -4,10 +4,12 @@ import esphome.config_validation as cv
from esphome.const import CONF_ID from esphome.const import CONF_ID
from esphome.types import ConfigType from esphome.types import ConfigType
from ..display import CONF_SDL_ID, Sdl, sdl_ns from ..display import CONF_SDL_ID, Sdl, headless_final_validate, sdl_ns
SdlTouchscreen = sdl_ns.class_("SdlTouchscreen", touchscreen.Touchscreen) SdlTouchscreen = sdl_ns.class_("SdlTouchscreen", touchscreen.Touchscreen)
FINAL_VALIDATE_SCHEMA = headless_final_validate("touchscreen")
CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
{ {
@@ -31,6 +31,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
require_tx=True, require_tx=True,
require_rx=True, require_rx=True,
baud_rate=115200, baud_rate=115200,
data_bits=8,
parity="NONE", parity="NONE",
stop_bits=1, stop_bits=1,
) )
@@ -33,8 +33,6 @@ void MR60FDA2Component::dump_config() {
// Initialisation functions // Initialisation functions
void MR60FDA2Component::setup() { void MR60FDA2Component::setup() {
this->check_uart_settings(115200);
this->current_frame_locate_ = LOCATE_FRAME_HEADER; this->current_frame_locate_ = LOCATE_FRAME_HEADER;
this->current_frame_id_ = 0; this->current_frame_id_ = 0;
this->current_frame_len_ = 0; this->current_frame_len_ = 0;
@@ -130,17 +130,26 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin
return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED; return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED;
} }
// Apply validated parameters // Skip a no-op reconfigure. Clients routinely re-send identical settings on every
uart_comp->set_baud_rate(baudrate); // port open, and on a USB UART each apply is a CDC SET_LINE_CODING control transfer.
uart_comp->set_stop_bits(stop_bits); // Some bridges watch line-coding changes as a signalling channel (a magic baud
uart_comp->set_data_bits(data_size); // sequence to enter a bootloader, say), so redundant applies are not harmless.
// Map parity value to UARTParityOptions
static const uart::UARTParityOptions PARITY_MAP[] = { static const uart::UARTParityOptions PARITY_MAP[] = {
uart::UART_CONFIG_PARITY_NONE, uart::UART_CONFIG_PARITY_NONE,
uart::UART_CONFIG_PARITY_EVEN, uart::UART_CONFIG_PARITY_EVEN,
uart::UART_CONFIG_PARITY_ODD, uart::UART_CONFIG_PARITY_ODD,
}; };
if (uart_comp->get_baud_rate() == baudrate && uart_comp->get_stop_bits() == stop_bits &&
uart_comp->get_data_bits() == data_size && uart_comp->get_parity() == PARITY_MAP[parity]) {
ESP_LOGV(TAG, "Settings unchanged, skipping reconfigure [%" PRIu32 "]", this->instance_index_);
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
}
// Apply validated parameters
uart_comp->set_baud_rate(baudrate);
uart_comp->set_stop_bits(stop_bits);
uart_comp->set_data_bits(data_size);
uart_comp->set_parity(PARITY_MAP[parity]); uart_comp->set_parity(PARITY_MAP[parity]);
// load_settings() is available on ESP8266 and ESP32 platforms // load_settings() is available on ESP8266 and ESP32 platforms
+7 -1
View File
@@ -68,7 +68,13 @@ CONFIG_SCHEMA = (
) )
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"smt100", baud_rate=9600, require_rx=True, require_tx=True "smt100",
baud_rate=9600,
require_rx=True,
require_tx=True,
data_bits=8,
parity="NONE",
stop_bits=1,
) )
-1
View File
@@ -65,7 +65,6 @@ void SMT100Component::dump_config() {
LOG_SENSOR(TAG, "Temperature", this->temperature_sensor_); LOG_SENSOR(TAG, "Temperature", this->temperature_sensor_);
LOG_SENSOR(TAG, "Moisture", this->moisture_sensor_); LOG_SENSOR(TAG, "Moisture", this->moisture_sensor_);
LOG_UPDATE_INTERVAL(this); LOG_UPDATE_INTERVAL(this);
this->check_uart_settings(9600);
} }
int SMT100Component::readline_(int readch, char *buffer, int len) { int SMT100Component::readline_(int readch, char *buffer, int len) {
+76
View File
@@ -0,0 +1,76 @@
"""Shared support for writing what a display is showing out to an image file.
The component itself has no configuration. It provides the ``snapshot.take`` action and the C++
base class behind it, so any display that can hand over its pixels - the in memory display in this
component, or an SDL window - saves files the same way, under the same directory, with the same
rules about names.
"""
from dataclasses import dataclass
from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.core import CORE, ID
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType, TemplateArgsType
CODEOWNERS = ["@clydebarrow"]
DOMAIN = "snapshot"
CONF_FILENAME = "filename"
snapshot_ns = cg.esphome_ns.namespace("snapshot")
Snapshot = snapshot_ns.class_("Snapshot")
SnapshotAction = snapshot_ns.class_("SnapshotAction", automation.Action)
@automation.register_action(
"snapshot.take",
SnapshotAction,
automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(Snapshot),
cv.Optional(CONF_FILENAME): cv.templatable(cv.string),
}
),
synchronous=True,
)
async def snapshot_take_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
if (filename := config.get(CONF_FILENAME)) is not None:
cg.add(var.set_filename(await cg.templatable(filename, args, cg.std_string)))
return var
@dataclass
class SnapshotData:
directory_defined: bool = False
def _get_data() -> SnapshotData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = SnapshotData()
return CORE.data[DOMAIN]
async def register_snapshot(var: MockObj, config: ConfigType) -> None:
"""Set up a component so that the snapshot action can write its picture to a file."""
data = _get_data()
# Only once, however many displays there are: two defines that say the same thing do not
# compare equal, so asking for this per display repeats the line in defines.h.
if not data.directory_defined:
data.directory_defined = True
cg.add_define(
"ESPHOME_SNAPSHOT_DIR",
(CORE.data_dir / "snapshots" / CORE.name).as_posix(),
)
cg.add(var.set_snapshot_prefix(str(config[CONF_ID])))
@@ -0,0 +1,61 @@
import esphome.codegen as cg
from esphome.components import display
import esphome.config_validation as cv
from esphome.const import (
CONF_DIMENSIONS,
CONF_HEIGHT,
CONF_ID,
CONF_LAMBDA,
CONF_WIDTH,
PLATFORM_HOST,
)
from esphome.types import ConfigType
from .. import Snapshot, register_snapshot, snapshot_ns
# The base class and the file writing live in the parent component, which nothing else in a
# configuration using only this platform would pull in.
AUTO_LOAD = ["snapshot"]
SnapshotDisplay = snapshot_ns.class_(
"SnapshotDisplay", display.DisplayBuffer, cg.Component, Snapshot
)
CONFIG_SCHEMA = cv.All(
display.FULL_DISPLAY_SCHEMA.extend(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SnapshotDisplay),
cv.Required(CONF_DIMENSIONS): cv.Any(
cv.dimensions,
cv.Schema(
{
cv.Required(CONF_WIDTH): cv.positive_not_null_int,
cv.Required(CONF_HEIGHT): cv.positive_not_null_int,
}
),
),
}
)
),
cv.only_on(PLATFORM_HOST),
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await display.register_display(var, config)
await register_snapshot(var, config)
dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict):
cg.add(var.set_dimensions(dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT]))
else:
(width, height) = dimensions
cg.add(var.set_dimensions(width, height))
if lamb := config.get(CONF_LAMBDA):
lambda_ = await cg.process_lambda(
lamb, [(display.DisplayRef, "it")], return_type=cg.void
)
cg.add(var.set_writer(lambda_))
@@ -0,0 +1,80 @@
#ifdef USE_HOST
#include "snapshot_display.h"
#include "esphome/components/display/display_color_utils.h"
#include "esphome/core/log.h"
#include <cstring>
namespace esphome::snapshot {
static const char *const TAG = "snapshot.display";
namespace {
/// Spread a channel that only goes up to `max` over the whole 0 to 255 range, so that the
/// brightest value stays the brightest. This is the same arithmetic SDL uses, which is what makes
/// a picture taken here come out identical to the same picture taken from an SDL window.
constexpr uint8_t expand_channel(uint16_t value, uint16_t max) { return static_cast<uint8_t>(value * 255 / max); }
constexpr uint16_t RED_MAX = 0x1F;
constexpr uint16_t GREEN_MAX = 0x3F;
constexpr uint16_t BLUE_MAX = 0x1F;
} // namespace
void SnapshotDisplay::setup() {
this->init_internal_(static_cast<uint32_t>(this->width_) * this->height_ * 2);
if (this->buffer_ == nullptr) {
this->mark_failed(LOG_STR("Could not allocate display buffer"));
}
}
void SnapshotDisplay::dump_config() { LOG_DISPLAY("", "Snapshot", this); }
void SnapshotDisplay::draw_absolute_pixel_internal(int x, int y, Color color) {
if (this->buffer_ == nullptr || x < 0 || x >= this->width_ || y < 0 || y >= this->height_)
return;
this->pixels_()[y * this->width_ + x] = display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB);
}
void SnapshotDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr,
display::ColorOrder order, display::ColorBitness bitness, bool big_endian,
int x_offset, int y_offset, int x_pad) {
if (this->buffer_ == nullptr)
return;
// Anything that is not already laid out the way the buffer is, or that would reach outside it,
// goes through the base class, which turns it into one call per pixel with the bounds checked.
const bool copyable = this->rotation_ == display::DISPLAY_ROTATION_0_DEGREES &&
bitness == display::COLOR_BITNESS_565 && !big_endian && x_start >= 0 && y_start >= 0 &&
x_start + w <= this->width_ && y_start + h <= this->height_;
if (!copyable) {
DisplayBuffer::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
return;
}
const size_t stride = static_cast<size_t>(x_offset) + w + x_pad;
const uint8_t *src = ptr + (stride * y_offset + x_offset) * 2;
for (int y = 0; y != h; y++) {
memcpy(&this->pixels_()[(y_start + y) * this->width_ + x_start], src + y * stride * 2, w * 2);
}
}
bool SnapshotDisplay::capture_bgr(uint8_t *dest, size_t row_stride) {
if (this->buffer_ == nullptr) {
ESP_LOGE(TAG, "Snapshot requested but there is no buffer to read");
return false;
}
const uint16_t *src = this->pixels_();
for (int y = 0; y != this->height_; y++) {
uint8_t *out = dest + y * row_stride;
for (int x = 0; x != this->width_; x++) {
const uint16_t pixel = *src++;
*out++ = expand_channel(pixel & BLUE_MAX, BLUE_MAX);
*out++ = expand_channel((pixel >> 5) & GREEN_MAX, GREEN_MAX);
*out++ = expand_channel(pixel >> 11, RED_MAX);
}
}
return true;
}
} // namespace esphome::snapshot
#endif
@@ -0,0 +1,48 @@
#pragma once
#ifdef USE_HOST
#include "esphome/components/display/display_buffer.h"
#include "esphome/components/snapshot/snapshot.h"
#include "esphome/core/component.h"
namespace esphome::snapshot {
/// A display with nowhere to show anything: it keeps the picture in memory, where the snapshot
/// action can pick it up. That makes it a way to see what a configuration draws on a machine with
/// no screen, and to check the result in a test.
class SnapshotDisplay final : public display::DisplayBuffer, public Snapshot {
public:
void setup() override;
void update() override { this->do_update_(); }
void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
display::DisplayType get_display_type() override { return display::DISPLAY_TYPE_COLOR; }
void set_dimensions(uint16_t width, uint16_t height) {
this->width_ = width;
this->height_ = height;
}
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
protected:
void draw_absolute_pixel_internal(int x, int y, Color color) override;
int get_width_internal() override { return this->width_; }
int get_height_internal() override { return this->height_; }
int snapshot_width() override { return this->width_; }
int snapshot_height() override { return this->height_; }
bool capture_bgr(uint8_t *dest, size_t row_stride) override;
/// The picture, one 16 bit RGB565 value per pixel, topmost row first. Owned by DisplayBuffer as
/// a byte pointer; this is the same memory seen as what is actually stored in it.
uint16_t *pixels_() { return reinterpret_cast<uint16_t *>(this->buffer_); }
int width_{};
int height_{};
};
} // namespace esphome::snapshot
#endif
+248
View File
@@ -0,0 +1,248 @@
#ifdef USE_HOST
#include "snapshot.h"
#include "esphome/core/log.h"
#include <fcntl.h>
#include <strings.h>
#include <unistd.h>
#include <cctype>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <filesystem>
#include <memory>
namespace esphome::snapshot {
namespace {
constexpr const char *const TAG = "snapshot";
// Longest name we will build a path from. NAME_MAX is 255 and we may append a collision suffix.
constexpr size_t MAX_NAME_LENGTH = 200;
// Give up rather than spin forever if every candidate name is taken.
constexpr unsigned MAX_NAME_ATTEMPTS = 1000;
// A BMP file header followed by a BITMAPINFOHEADER, which is where the pixels start.
constexpr size_t BMP_HEADER_SIZE = 54;
constexpr size_t BMP_INFO_HEADER_SIZE = 40;
constexpr int BMP_BITS_PER_PIXEL = 24;
/// True if the name already ends in ".bmp". The comparison ignores case, so "shot.BMP" is left
/// alone rather than turned into "shot.BMP.bmp".
bool has_bmp_suffix(const std::string &name) {
return name.size() >= 4 && strcasecmp(name.c_str() + name.size() - 4, ".bmp") == 0;
}
/// Reduce a user supplied name to a single safe path component. Everything outside the allowed set
/// is replaced, so "..", "/" and absolute paths cannot escape the snapshot directory.
/// Returns an empty string if nothing usable is left.
std::string sanitise_filename(const char *const name, bool *name_changed) {
std::string result;
bool all_dots = true;
bool changed = false;
for (const char *p = name; *p != '\0'; p++) {
if (result.size() >= MAX_NAME_LENGTH) {
changed = true;
break;
}
char c = *p;
if (!(std::isalnum(static_cast<unsigned char>(c)) || c == '.' || c == '_' || c == '-')) {
c = '_';
changed = true;
}
if (c != '.')
all_dots = false;
result.push_back(c);
}
if (all_dots) {
*name_changed = true;
return "";
}
if (!has_bmp_suffix(result))
result += ".bmp";
*name_changed = changed;
return result;
}
/// Insert "-<attempt>" before the file extension, e.g. "shot.bmp" -> "shot-1.bmp".
std::string add_suffix(const std::string &name, unsigned attempt) {
char suffix[12];
snprintf(suffix, sizeof(suffix), "-%u", attempt);
auto dot = name.rfind('.');
if (dot == std::string::npos)
return name + suffix;
return name.substr(0, dot) + suffix + name.substr(dot);
}
/// Directory snapshots are written to. The environment variable lets a test redirect output
/// without rebuilding, matching how the host platform handles ESPHOME_PREFDIR.
const char *snapshot_dir() {
const char *dir = getenv("ESPHOME_SNAPSHOT_DIR"); // NOLINT(concurrency-mt-unsafe)
return dir != nullptr && dir[0] != '\0' ? dir : ESPHOME_SNAPSHOT_DIR;
}
/// Store a value in as many bytes, least significant first, and step the pointer past it.
/// BMP is a little endian format whatever the machine writing it uses.
void put_le(uint8_t *&dest, uint32_t value, size_t bytes) {
for (size_t i = 0; i != bytes; i++)
*dest++ = static_cast<uint8_t>(value >> (8 * i));
}
/// The number of bytes one row of `width` pixels takes up in the file. Rows are padded out to a
/// multiple of four bytes.
size_t bmp_row_size(int width) { return (static_cast<size_t>(width) * 3 + 3) & ~size_t{3}; }
/// Write pixels out as a 24 bit BMP. The rows given start with the topmost and are `row_stride`
/// bytes apart, which must leave room for a whole padded row; a BMP holds its rows the other way
/// up, so they go out last first.
bool write_bmp(FILE *file, const uint8_t *pixels, int width, int height, size_t row_stride) {
const size_t row_size = bmp_row_size(width);
const size_t pixel_bytes = row_size * height;
uint8_t header[BMP_HEADER_SIZE];
uint8_t *pos = header;
*pos++ = 'B';
*pos++ = 'M';
put_le(pos, static_cast<uint32_t>(BMP_HEADER_SIZE + pixel_bytes), 4);
put_le(pos, 0, 4); // reserved
put_le(pos, BMP_HEADER_SIZE, 4);
put_le(pos, BMP_INFO_HEADER_SIZE, 4);
put_le(pos, static_cast<uint32_t>(width), 4);
put_le(pos, static_cast<uint32_t>(height), 4);
put_le(pos, 1, 2); // one plane
put_le(pos, BMP_BITS_PER_PIXEL, 2);
put_le(pos, 0, 4); // not compressed
put_le(pos, static_cast<uint32_t>(pixel_bytes), 4);
put_le(pos, 0, 4); // pixels per metre across, unspecified
put_le(pos, 0, 4); // pixels per metre down, unspecified
put_le(pos, 0, 4); // no palette
put_le(pos, 0, 4); // so no palette entry matters more than another
if (fwrite(header, 1, sizeof(header), file) != sizeof(header))
return false;
for (int y = height - 1; y >= 0; y--) {
if (fwrite(pixels + static_cast<size_t>(y) * row_stride, 1, row_size, file) != row_size)
return false;
}
return true;
}
/// Reserve a name in the snapshot directory and write the picture to it.
/// With `exact` set the given name is the only one tried; otherwise a number is added on
/// collision. Returns true if a file was written.
bool write_snapshot_file(const uint8_t *pixels, int width, int height, size_t row_stride, const std::string &name,
bool exact) {
const std::string dir = snapshot_dir();
std::error_code ec;
std::filesystem::create_directories(dir, ec);
if (ec) {
ESP_LOGE(TAG, "Could not create snapshot directory %s: %s", dir.c_str(), ec.message().c_str());
return false;
}
// O_EXCL guarantees we never write over a file that is already there.
std::string path;
int fd = -1;
for (unsigned attempt = 0; attempt < MAX_NAME_ATTEMPTS; attempt++) {
path = dir + "/" + (attempt == 0 ? name : add_suffix(name, attempt));
fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0644);
if (fd >= 0)
break;
if (errno != EEXIST) {
ESP_LOGE(TAG, "Could not create %s: %s", path.c_str(), strerror(errno));
return false;
}
if (exact) {
// The caller asked for this exact name, so silently writing somewhere else would be worse
// than failing - a test asserting on the path would pick up a stale file.
ESP_LOGE(TAG, "Snapshot %s already exists, not overwriting", path.c_str());
return false;
}
}
if (fd < 0) {
ESP_LOGE(TAG, "Could not find an unused name for %s in %s", name.c_str(), dir.c_str());
return false;
}
FILE *file = fdopen(fd, "wb");
if (file == nullptr) {
ESP_LOGE(TAG, "Could not open %s: %s", path.c_str(), strerror(errno));
::close(fd);
::unlink(path.c_str());
return false;
}
bool ok = write_bmp(file, pixels, width, height, row_stride);
int saved_errno = ok ? 0 : errno;
// Closing can fail in its own right - the last of the data is still on its way out.
if (fclose(file) != 0) {
if (ok)
saved_errno = errno;
ok = false;
}
if (!ok) {
ESP_LOGE(TAG, "Could not write %s: %s", path.c_str(), strerror(saved_errno));
// Leave no truncated file behind - it would block a retry under the same name.
::unlink(path.c_str());
return false;
}
ESP_LOGI(TAG, "Snapshot written to %s", path.c_str());
return true;
}
} // namespace
// helper function since ESP_LOGW is disallowed in a header file
void Snapshot::log_action_failed() { ESP_LOGW(TAG, "snapshot.take did not write a file"); }
bool Snapshot::take_snapshot(const char *filename) {
const int width = this->snapshot_width();
const int height = this->snapshot_height();
if (width <= 0 || height <= 0) {
ESP_LOGE(TAG, "Snapshot requested but the display is %dx%d", width, height);
return false;
}
std::string name;
bool exact = false;
if (filename != nullptr) {
bool name_changed = false;
name = sanitise_filename(filename, &name_changed);
exact = !name.empty();
if (name_changed) {
ESP_LOGW(TAG, "Requested snapshot name '%s' is not an acceptable file name, using '%s' instead", filename,
name.empty() ? "a name made from the time" : name.c_str());
}
}
if (name.empty()) {
struct timespec now {};
if (clock_gettime(CLOCK_REALTIME, &now) != 0)
now = {};
struct tm tm_buf {};
if (localtime_r(&now.tv_sec, &tm_buf) == nullptr)
tm_buf = {};
char stamp[32]{};
// ::strftime to be sure of the one from <ctime>; display has an unrelated member of that name
if (::strftime(stamp, sizeof(stamp), "%Y%m%d-%H%M%S", &tm_buf) == 0)
snprintf(stamp, sizeof(stamp), "unknown-time");
char buffer[MAX_NAME_LENGTH];
int written =
snprintf(buffer, sizeof(buffer), "%s-%s-%03ld.bmp", this->snapshot_prefix_, stamp, now.tv_nsec / 1000000);
if (written < 0 || static_cast<size_t>(written) >= sizeof(buffer)) {
ESP_LOGW(TAG, "Could not build a timestamped snapshot name, using a fallback");
snprintf(buffer, sizeof(buffer), "snapshot.bmp");
}
name = buffer;
}
// Rows are padded out to a multiple of four bytes, as the file wants them, so each one can be
// written straight from the buffer. Zeroed on allocation, which is what the padding must be.
const size_t row_stride = bmp_row_size(width);
auto pixels = std::make_unique<uint8_t[]>(row_stride * height);
if (!this->capture_bgr(pixels.get(), row_stride))
return false;
return write_snapshot_file(pixels.get(), width, height, row_stride, name, exact);
}
} // namespace esphome::snapshot
#endif
+72
View File
@@ -0,0 +1,72 @@
#pragma once
#ifdef USE_HOST
#include "esphome/core/automation.h"
#include <cstddef>
#include <cstdint>
#include <string>
// Directory snapshots are written to. Normally set by codegen to a folder under .esphome; the
// fallback keeps the component compiling for static analysis, where no defines.h is generated.
#ifndef ESPHOME_SNAPSHOT_DIR
#define ESPHOME_SNAPSHOT_DIR "."
#endif
namespace esphome::snapshot {
/// Base for anything that can hand over the picture it is showing so it can be written to a file.
///
/// A subclass says how big the picture is and fills in the pixels. Everything else - picking a
/// name, staying inside the snapshot directory, not writing over anything, and encoding the file -
/// is done here, so every component that can take a snapshot behaves the same way.
class Snapshot {
public:
virtual ~Snapshot() = default;
/// Set the word generated names start with. Codegen passes the component id, so with more than
/// one display in a device it is clear which one a file came from.
void set_snapshot_prefix(const char *prefix) { this->snapshot_prefix_ = prefix; }
/// Write the current picture to a BMP file in the snapshot directory.
///
/// Pass nullptr to have a name made up from the prefix and the current time. A file that is
/// already there is never written over. Returns true if a file was written.
bool take_snapshot(const char *filename);
/// Log that an action-triggered snapshot did not write a file.
static void log_action_failed();
protected:
/// Width of the picture in pixels.
virtual int snapshot_width() = 0;
/// Height of the picture in pixels.
virtual int snapshot_height() = 0;
/// Fill in the picture: three bytes per pixel in blue, green, red order, topmost row first, with
/// `row_stride` bytes from the start of one row to the start of the next. Returns false, having
/// logged why, if the picture could not be read.
virtual bool capture_bgr(uint8_t *dest, size_t row_stride) = 0;
const char *snapshot_prefix_{"snapshot"};
};
template<typename... Ts> class SnapshotAction final : public Action<Ts...>, public Parented<Snapshot> {
public:
TEMPLATABLE_VALUE(std::string, filename)
protected:
void play(const Ts &...x) override {
bool ok;
if (this->filename_.has_value()) {
ok = this->parent_->take_snapshot(this->filename_.value(x...).c_str());
} else {
ok = this->parent_->take_snapshot(nullptr);
}
if (!ok)
this->parent_->log_action_failed();
}
};
} // namespace esphome::snapshot
#endif
+7 -1
View File
@@ -33,7 +33,13 @@ CONFIG_SCHEMA = (
) )
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"t6615", baud_rate=19200, require_rx=True, require_tx=True "t6615",
baud_rate=19200,
require_rx=True,
require_tx=True,
data_bits=8,
parity="NONE",
stop_bits=1,
) )
-1
View File
@@ -88,7 +88,6 @@ void T6615Component::query_ppm_() {
void T6615Component::dump_config() { void T6615Component::dump_config() {
ESP_LOGCONFIG(TAG, "T6615:"); ESP_LOGCONFIG(TAG, "T6615:");
LOG_SENSOR(" ", "CO2", this->co2_sensor_); LOG_SENSOR(" ", "CO2", this->co2_sensor_);
this->check_uart_settings(19200);
} }
} // namespace esphome::t6615 } // namespace esphome::t6615
+16
View File
@@ -35,6 +35,22 @@ CONFIG_SCHEMA = (
) )
def _final_validate(config: ConfigType) -> ConfigType:
# Historical mode runs at 1200 baud, standard mode at 9600 baud.
baud_rate = 1200 if config[CONF_HISTORICAL_MODE] else 9600
uart.final_validate_device_schema(
"teleinfo",
baud_rate=baud_rate,
data_bits=7,
parity="EVEN",
stop_bits=1,
)(config)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None: async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID], config[CONF_HISTORICAL_MODE]) var = cg.new_Pvariable(config[CONF_ID], config[CONF_HISTORICAL_MODE])
await cg.register_component(var, config) await cg.register_component(var, config)
+1 -6
View File
@@ -184,10 +184,7 @@ void TeleInfo::publish_value_(const std::string &tag, const std::string &val) {
element->publish_val(val); element->publish_val(val);
} }
} }
void TeleInfo::dump_config() { void TeleInfo::dump_config() { ESP_LOGCONFIG(TAG, "TeleInfo:"); }
ESP_LOGCONFIG(TAG, "TeleInfo:");
this->check_uart_settings(baud_rate_, 1, uart::UART_CONFIG_PARITY_EVEN, 7);
}
TeleInfo::TeleInfo(bool historical_mode) { TeleInfo::TeleInfo(bool historical_mode) {
if (historical_mode) { if (historical_mode) {
/* /*
@@ -195,11 +192,9 @@ TeleInfo::TeleInfo(bool historical_mode) {
*/ */
checksum_area_end_ = 2; checksum_area_end_ = 2;
separator_ = 0x20; separator_ = 0x20;
baud_rate_ = 1200;
} else { } else {
checksum_area_end_ = 1; checksum_area_end_ = 1;
separator_ = 0x9; separator_ = 0x9;
baud_rate_ = 9600;
} }
} }
void TeleInfo::register_teleinfo_listener(TeleInfoListener *listener) { teleinfo_listeners_.push_back(listener); } void TeleInfo::register_teleinfo_listener(TeleInfoListener *listener) { teleinfo_listeners_.push_back(listener); }
-1
View File
@@ -31,7 +31,6 @@ class TeleInfo final : public PollingComponent, public uart::UARTDevice {
std::vector<TeleInfoListener *> teleinfo_listeners_{}; std::vector<TeleInfoListener *> teleinfo_listeners_{};
protected: protected:
uint32_t baud_rate_;
int checksum_area_end_; int checksum_area_end_;
int separator_; int separator_;
char buf_[MAX_BUF_SIZE]; char buf_[MAX_BUF_SIZE];
@@ -36,8 +36,6 @@ cover::CoverTraits Tormatic::get_traits() {
void Tormatic::dump_config() { void Tormatic::dump_config() {
LOG_COVER("", "Tormatic Cover", this); LOG_COVER("", "Tormatic Cover", this);
this->check_uart_settings(9600, 1, uart::UART_CONFIG_PARITY_NONE, 8);
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
" Open Duration: %.1fs\n" " Open Duration: %.1fs\n"
" Close Duration: %.1fs", " Close Duration: %.1fs",
+2
View File
@@ -3,6 +3,7 @@
#include <vector> #include <vector>
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "uart_component.h" #include "uart_component.h"
@@ -66,6 +67,7 @@ class UARTDevice {
} }
/// Check that the configuration of the UART bus matches the provided values and otherwise print a warning /// Check that the configuration of the UART bus matches the provided values and otherwise print a warning
ESPDEPRECATED("Use uart.final_validate_device_schema() in Python instead. Removed in 2027.3.0", "2026.9.0")
void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits = 1, void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits = 1,
UARTParityOptions parity = UART_CONFIG_PARITY_NONE, uint8_t data_bits = 8); UARTParityOptions parity = UART_CONFIG_PARITY_NONE, uint8_t data_bits = 8);
+1
View File
@@ -30,6 +30,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
require_tx=True, require_tx=True,
require_rx=True, require_rx=True,
baud_rate=2400, baud_rate=2400,
data_bits=8,
parity="EVEN", parity="EVEN",
stop_bits=1, stop_bits=1,
) )
-1
View File
@@ -213,7 +213,6 @@ void UFM01Component::dump_config() {
LOG_BINARY_SENSOR(" ", "Empty Tube", this->empty_tube_binary_sensor_); LOG_BINARY_SENSOR(" ", "Empty Tube", this->empty_tube_binary_sensor_);
LOG_BINARY_SENSOR(" ", "Flow Rate Out Of Range", this->flow_rate_out_of_range_binary_sensor_); LOG_BINARY_SENSOR(" ", "Flow Rate Out Of Range", this->flow_rate_out_of_range_binary_sensor_);
#endif #endif
this->check_uart_settings(2400, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
} }
void UFM01Component::on_active_frame_(uint8_t data[FRAME_SIZE]) { void UFM01Component::on_active_frame_(uint8_t data[FRAME_SIZE]) {
@@ -50,7 +50,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
require_tx=True, require_tx=True,
require_rx=True, require_rx=True,
data_bits=8, data_bits=8,
parity=None, parity="NONE",
stop_bits=1, stop_bits=1,
) )
@@ -29,8 +29,6 @@ void UponorSmatrixComponent::dump_config() {
} }
#endif #endif
this->check_uart_settings(19200);
if (!this->unknown_devices_.empty()) { if (!this->unknown_devices_.empty()) {
ESP_LOGCONFIG(TAG, " Detected unknown device addresses:"); ESP_LOGCONFIG(TAG, " Detected unknown device addresses:");
for (auto device_address : this->unknown_devices_) { for (auto device_address : this->unknown_devices_) {
+8
View File
@@ -29,6 +29,14 @@ CONFIG_SCHEMA = uart.UART_DEVICE_SCHEMA.extend(
} }
) )
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"vbus",
baud_rate=9600,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None: async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
+1 -4
View File
@@ -11,10 +11,7 @@ static const char *const TAG = "vbus";
// Maximum bytes to log in verbose hex output (16 frames * 4 bytes = 64 bytes typical) // Maximum bytes to log in verbose hex output (16 frames * 4 bytes = 64 bytes typical)
static constexpr size_t VBUS_MAX_LOG_BYTES = 64; static constexpr size_t VBUS_MAX_LOG_BYTES = 64;
void VBus::dump_config() { void VBus::dump_config() { ESP_LOGCONFIG(TAG, "VBus:"); }
ESP_LOGCONFIG(TAG, "VBus:");
check_uart_settings(9600);
}
static void septet_spread(uint8_t *data, int start, int count, uint8_t septet) { static void septet_spread(uint8_t *data, int start, int count, uint8_t septet) {
for (int i = 0; i < count; i++, septet >>= 1) { for (int i = 0; i < count; i++, septet >>= 1) {
-5
View File
@@ -40,11 +40,6 @@
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
#include <ESP8266WiFiType.h> #include <ESP8266WiFiType.h>
#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0)
extern "C" {
#include <user_interface.h>
};
#endif
#endif #endif
#ifdef USE_RP2 #ifdef USE_RP2
@@ -21,7 +21,6 @@ extern "C" {
#include "lwip/apps/sntp.h" #include "lwip/apps/sntp.h"
#include "lwip/netif.h" // struct netif #include "lwip/netif.h" // struct netif
#include <AddrList.h> #include <AddrList.h>
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0)
#include "LwipDhcpServer.h" #include "LwipDhcpServer.h"
#if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0) #if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0)
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
@@ -30,7 +29,6 @@ extern "C" {
#define wifi_softap_set_dhcps_lease_time(time) dhcpSoftAP.set_dhcps_lease_time(time) #define wifi_softap_set_dhcps_lease_time(time) dhcpSoftAP.set_dhcps_lease_time(time)
#define wifi_softap_set_dhcps_offer_option(offer, mode) dhcpSoftAP.set_dhcps_offer_option(offer, mode) #define wifi_softap_set_dhcps_offer_option(offer, mode) dhcpSoftAP.set_dhcps_offer_option(offer, mode)
#endif #endif
#endif
} }
#include "esphome/core/application.h" #include "esphome/core/application.h"
@@ -293,7 +291,6 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
conf.bssid_set = 0; conf.bssid_set = 0;
} }
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
if (ap.password_.empty()) { if (ap.password_.empty()) {
conf.threshold.authmode = AUTH_OPEN; conf.threshold.authmode = AUTH_OPEN;
} else { } else {
@@ -310,7 +307,6 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
} }
} }
conf.threshold.rssi = -127; conf.threshold.rssi = -127;
#endif
ETS_UART_INTR_DISABLE(); ETS_UART_INTR_DISABLE();
bool ret = wifi_station_set_config_current(&conf); bool ret = wifi_station_set_config_current(&conf);
@@ -602,7 +598,6 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
#endif #endif
break; break;
} }
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
case EVENT_OPMODE_CHANGED: { case EVENT_OPMODE_CHANGED: {
auto it = event->event_info.opmode_changed; auto it = event->event_info.opmode_changed;
ESP_LOGV(TAG, "Changed Mode old=%s new=%s", LOG_STR_ARG(get_op_mode_str(it.old_opmode)), ESP_LOGV(TAG, "Changed Mode old=%s new=%s", LOG_STR_ARG(get_op_mode_str(it.old_opmode)),
@@ -620,7 +615,6 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
#endif #endif
break; break;
} }
#endif
default: default:
break; break;
} }
@@ -705,7 +699,6 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
config.bssid = nullptr; config.bssid = nullptr;
config.channel = 0; config.channel = 0;
config.show_hidden = 1; config.show_hidden = 1;
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE; config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE;
// Use shorter dwell times for roaming scans - we only need to detect strong // Use shorter dwell times for roaming scans - we only need to detect strong
// nearby APs, not do a thorough survey. This also reduces off-channel time // nearby APs, not do a thorough survey. This also reduces off-channel time
@@ -724,7 +717,6 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
config.scan_time.active.min = roaming ? SCAN_ACTIVE_MIN_ROAMING_MS : SCAN_ACTIVE_MIN_DEFAULT_MS; config.scan_time.active.min = roaming ? SCAN_ACTIVE_MIN_ROAMING_MS : SCAN_ACTIVE_MIN_DEFAULT_MS;
config.scan_time.active.max = roaming ? SCAN_ACTIVE_MAX_ROAMING_MS : SCAN_ACTIVE_MAX_DEFAULT_MS; config.scan_time.active.max = roaming ? SCAN_ACTIVE_MAX_ROAMING_MS : SCAN_ACTIVE_MAX_DEFAULT_MS;
} }
#endif
bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback); bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback);
if (!ret) { if (!ret) {
ESP_LOGV(TAG, "wifi_station_scan failed"); ESP_LOGV(TAG, "wifi_station_scan failed");
@@ -830,7 +822,7 @@ bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
return false; return false;
} }
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0) && USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0) #if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0)
dhcpSoftAP.begin(&info); dhcpSoftAP.begin(&info);
#endif #endif
+8
View File
@@ -21,6 +21,14 @@ CONFIG_SCHEMA = (
.extend(uart.UART_DEVICE_SCHEMA) .extend(uart.UART_DEVICE_SCHEMA)
) )
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"wl_134",
baud_rate=9600,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None: async def to_code(config: ConfigType) -> None:
var = await text_sensor.new_text_sensor(config) var = await text_sensor.new_text_sensor(config)
-2
View File
@@ -110,7 +110,5 @@ uint64_t Wl134Component::hex_lsb_ascii_to_uint64_(const uint8_t *text, uint8_t t
void Wl134Component::dump_config() { void Wl134Component::dump_config() {
ESP_LOGCONFIG(TAG, "WL-134 Sensor:"); ESP_LOGCONFIG(TAG, "WL-134 Sensor:");
LOG_TEXT_SENSOR("", "Tag", this); LOG_TEXT_SENSOR("", "Tag", this);
// As specified in the sensor's data sheet
this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
} }
} // namespace esphome::wl_134 } // namespace esphome::wl_134
+1 -1
View File
@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum from esphome.enum import StrEnum
__version__ = "2026.9.0-dev" __version__ = "2026.10.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = ( VALID_SUBSTITUTIONS_CHARACTERS = (
+1
View File
@@ -13,6 +13,7 @@
#define ESPHOME_PROJECT_VERSION "v2" #define ESPHOME_PROJECT_VERSION "v2"
#define ESPHOME_PROJECT_VERSION_30 "v2" #define ESPHOME_PROJECT_VERSION_30 "v2"
#define ESPHOME_VARIANT "ESP32" #define ESPHOME_VARIANT "ESP32"
#define ESPHOME_SNAPSHOT_DIR "."
#define ESPHOME_NAME_ADD_MAC_SUFFIX #define ESPHOME_NAME_ADD_MAC_SUFFIX
#define ESPHOME_DEBUG_SCHEDULER #define ESPHOME_DEBUG_SCHEDULER
#define ESPHOME_DEBUG_API #define ESPHOME_DEBUG_API
-14
View File
@@ -18,7 +18,6 @@
#ifdef USE_STORE_LOG_STR_IN_FLASH #ifdef USE_STORE_LOG_STR_IN_FLASH
#include "WString.h" #include "WString.h"
#include "esphome/core/defines.h" // for USE_ARDUINO_VERSION_CODE
#endif #endif
// Include ESP-IDF/Arduino based logging methods here so they don't undefine ours later // Include ESP-IDF/Arduino based logging methods here so they don't undefine ours later
@@ -177,20 +176,7 @@ struct LogString;
#include <pgmspace.h> #include <pgmspace.h>
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 0)
#define LOG_STR_ARG(s) ((PGM_P) (s)) #define LOG_STR_ARG(s) ((PGM_P) (s))
#else
// Pre-Arduino 2.5, we can't pass a PSTR() to printf(). Emulate support by copying the message to a
// local buffer first. String length is limited to 63 characters.
// https://github.com/esp8266/Arduino/commit/6280e98b0360f85fdac2b8f10707fffb4f6e6e31
#define LOG_STR_ARG(s) \
({ \
char __buf[64]; \
__buf[63] = '\0'; \
strncpy_P(__buf, (PGM_P) (s), 63); \
__buf; \
})
#endif
#define LOG_STR(s) (reinterpret_cast<const LogString *>(PSTR(s))) #define LOG_STR(s) (reinterpret_cast<const LogString *>(PSTR(s)))
#define LOG_STR_LITERAL(s) LOG_STR_ARG(LOG_STR(s)) #define LOG_STR_LITERAL(s) LOG_STR_ARG(LOG_STR(s))
+57 -4
View File
@@ -23,6 +23,7 @@ from esphome.net_retry import (
) )
if TYPE_CHECKING: if TYPE_CHECKING:
from filelock import FileLock
import requests import requests
PathType = str | os.PathLike PathType = str | os.PathLike
@@ -909,6 +910,61 @@ def _part_path(dest: Path) -> Path:
return dest.with_name(dest.name + ".part") return dest.with_name(dest.name + ".part")
def downloaded_bytes(dest: Path, size: int | None = None) -> int:
"""Bytes of ``dest`` on disk (its ``.part`` while streaming), capped at ``size``."""
done = 0
for candidate in (_part_path(dest), dest):
try:
done = candidate.stat().st_size
break
except FileNotFoundError:
continue
return done if size is None else min(done, size)
# Short lock-acquire slices so a waiting worker still observes Ctrl-C
_DOWNLOAD_LOCK_POLL = 1
# Waiting on another process's download; past this the caller leaves the
# file to its holder (the later sequential install waits on the same lock)
DOWNLOAD_LOCK_TIMEOUT = 60
class DownloadLockUnavailable(OSError):
"""The lock file cannot be used at all (a lock-less filesystem)."""
def wait_for_download_lock(
lock: "FileLock",
tracker: Callable[[int], None],
on_disk: Callable[[], int],
name: str,
) -> None:
"""Acquire ``lock``, reporting ``on_disk()`` to ``tracker`` each poll so the
bar follows the holder's download. Raises filelock's ``Timeout`` once
``DOWNLOAD_LOCK_TIMEOUT`` seconds pass."""
from filelock import Timeout
deadline = time.monotonic() + DOWNLOAD_LOCK_TIMEOUT
waiting = False
while True:
try:
lock.acquire(timeout=_DOWNLOAD_LOCK_POLL)
return
except Timeout:
pass
except OSError as err:
# Distinct from an OSError out of on_disk(), which must not
# read as "locks unsupported"
raise DownloadLockUnavailable(*err.args) from err
if not waiting:
waiting = True
_LOGGER.info("Waiting for another process downloading %s", name)
tracker(on_disk()) # raises when the batch is cancelled
if time.monotonic() >= deadline:
raise Timeout(lock.lock_file)
def discard_partial_download(dest: Path) -> None: def discard_partial_download(dest: Path) -> None:
"""Remove ``dest`` and the resume sidecars of an abandoned download.""" """Remove ``dest`` and the resume sidecars of an abandoned download."""
part = _part_path(dest) part = _part_path(dest)
@@ -1319,10 +1375,7 @@ def download_from_mirrors(
) )
# Tick with the bytes already on disk so a combined bar holds # Tick with the bytes already on disk so a combined bar holds
# steady during the backoff instead of rewinding to zero # steady during the backoff instead of rewinding to zero
done = 0 done = downloaded_bytes(path_target) if progress is not None else 0
if progress is not None:
part = _part_path(path_target)
done = part.stat().st_size if part.is_file() else 0
_cancellable_sleep(delay, progress, done) _cancellable_sleep(delay, progress, done)
# 3. Report every attempted URL if all mirrors failed. failures spans # 3. Report every attempted URL if all mirrors failed. failures spans
+44 -43
View File
@@ -33,11 +33,14 @@ import time
from typing import Any, NamedTuple from typing import Any, NamedTuple
from esphome.framework_helpers import ( from esphome.framework_helpers import (
DownloadLockUnavailable,
content_length, content_length,
discard_partial_download, discard_partial_download,
downloaded_bytes,
failure_reason, failure_reason,
resume_fetch_job, resume_fetch_job,
run_batch_downloads, run_batch_downloads,
wait_for_download_lock,
warn_prefetch_failures, warn_prefetch_failures,
) )
from esphome.helpers import get_bool_env, get_usable_cpu_count, rmtree from esphome.helpers import get_bool_env, get_usable_cpu_count, rmtree
@@ -61,16 +64,10 @@ _RESOLVE_WORKERS = 8
# A hung child must not block the build; downloads resume on the next run # A hung child must not block the build; downloads resume on the next run
_PREFETCH_TIMEOUT = 20 * 60 _PREFETCH_TIMEOUT = 20 * 60
# Waiting on another process's URL download; past this, leave it to pio
_DOWNLOAD_LOCK_TIMEOUT = 60
# Child exit for a handled, already-warned failure; 1 would collide with # Child exit for a handled, already-warned failure; 1 would collide with
# the interpreter's own import-failure exit # the interpreter's own import-failure exit
_EXIT_HANDLED = 3 _EXIT_HANDLED = 3
# Short lock-acquire slices so a waiting worker still observes Ctrl-C
_URI_LOCK_POLL = 1
# Resolution errored (vs a clean skip); suppresses the warm sentinel # Resolution errored (vs a clean skip); suppresses the warm sentinel
_RESOLVE_FAILED = object() _RESOLVE_FAILED = object()
@@ -462,51 +459,54 @@ def _uri_jobs(
def _serialized_fetch_job( def _serialized_fetch_job(
dl_path: Path, lock_path: str, body: Any, unlocked_ok: bool = True dl_path: Path,
lock_path: str,
body: Any,
size: int,
stream_dest: Path | None = None,
unlocked_ok: bool = True,
) -> Any: ) -> Any:
"""Wrap ``body`` so the shared destination is single-writer. """Wrap ``body`` so the shared destination is single-writer (interleaved
writers truncate each other's ``.part``, see registry.py). A blown deadline
Interleaved writers truncate each other's ``.part`` bytes (see is a clean skip. On a lock-less filesystem a sha256-verified body runs
registry.py). The bounded poll observes Ctrl-C via the tracker; a unlocked with one warning; a checksum-less one (``unlocked_ok=False``) fails.
blown deadline is a clean skip (the holder's copy is what the build
needs). On a lock-less filesystem a sha256-verified body runs
unlocked with one warning; a checksum-less one
(``unlocked_ok=False``) is a counted failure instead.
""" """
def on_disk() -> int:
# A URL job's holder streams beside the staging path until it
# promotes; after that only dl_path is left
done = downloaded_bytes(dl_path, size)
if not done and stream_dest is not None:
done = downloaded_bytes(stream_dest, size)
return done
def run(tracker: Any) -> None: def run(tracker: Any) -> None:
from filelock import FileLock, Timeout from filelock import FileLock, Timeout
# fallback_to_soft would leave a stale marker on lock-less # fallback_to_soft would leave a stale marker on lock-less
# filesystems that blocks every later build (see git.py) # filesystems that blocks every later build (see git.py)
lock = FileLock(lock_path, fallback_to_soft=False) lock = FileLock(lock_path, fallback_to_soft=False)
deadline = time.monotonic() + _DOWNLOAD_LOCK_TIMEOUT try:
while True: wait_for_download_lock(lock, tracker, on_disk, dl_path.name)
try: except Timeout:
lock.acquire(timeout=_URI_LOCK_POLL) # The holder's copy is what the build needs (a large
break # framework archive can outlast this deadline)
except Timeout: _LOGGER.debug("Leaving %s to its current downloader", dl_path.name)
tracker(0) # raises when the batch is cancelled return
if time.monotonic() >= deadline: except DownloadLockUnavailable as err:
# Another process is fetching this same file; its copy if not unlocked_ok:
# is what the build needs (a large framework archive # A body with no checksum to catch interleaved corruption
# can hold the lock far longer than this deadline) raise
_LOGGER.debug("Leaving %s to its current downloader", dl_path.name) lock = None
return _LOGGER.warning(
except OSError as err: "Could not lock %s (%s); downloading unlocked",
if not unlocked_ok: dl_path.name,
# A body with no checksum to catch interleaved corruption err,
raise )
lock = None
_LOGGER.warning(
"Could not lock %s (%s); downloading unlocked",
dl_path.name,
err,
)
break
try: try:
if dl_path.is_file(): if dl_path.is_file():
return # another process finished it while we waited tracker(size) # another process finished it while we waited
return
body(tracker) body(tracker)
finally: finally:
if lock is not None: if lock is not None:
@@ -540,6 +540,7 @@ def _registry_fetch_job(
dl_path, dl_path,
f"{dl_path}.esphome.lock", f"{dl_path}.esphome.lock",
resume_fetch_job(url, dl_path, sha256=checksum, size=size), resume_fetch_job(url, dl_path, sha256=checksum, size=size),
size,
) )
def run(tracker: Any) -> None: def run(tracker: Any) -> None:
@@ -571,9 +572,9 @@ def _uri_fetch_job(manager: Any, url: str, dl_path: Path, size: int) -> Any:
tmp.replace(dl_path) tmp.replace(dl_path)
def run(tracker: Any) -> None: def run(tracker: Any) -> None:
_serialized_fetch_job(dl_path, f"{tmp}.lock", promote, unlocked_ok=False)( _serialized_fetch_job(
tracker dl_path, f"{tmp}.lock", promote, size, tmp, unlocked_ok=False
) )(tracker)
if dl_path.is_file(): if dl_path.is_file():
# Won or lost, the race is over; staging files left behind # Won or lost, the race is over; staging files left behind
# are dead weight PlatformIO's cache never prunes # are dead weight PlatformIO's cache never prunes
+45 -22
View File
@@ -17,8 +17,10 @@ from esphome.framework_helpers import (
archive_extract_all, archive_extract_all,
download_from_mirrors, download_from_mirrors,
download_with_resume, download_with_resume,
downloaded_bytes,
rmdir, rmdir,
run_batch_downloads, run_batch_downloads,
wait_for_download_lock,
) )
from esphome.net_retry import fetch_with_retry, http_request from esphome.net_retry import fetch_with_retry, http_request
@@ -164,11 +166,17 @@ class _PendingArchive(NamedTuple):
name: str name: str
version: str version: str
dest: Path dest: Path
archive: Path
url: str url: str
sha256: str sha256: str
size: int size: int
def _archive_path(downloads_dir: Path, name: str, version: str) -> Path:
"""The one archive path the prefetch and the sequential install share."""
return downloads_dir / f"{name}-{version}"
def _already_installed(dest: Path) -> bool: def _already_installed(dest: Path) -> bool:
"""Whether ``dest`` holds a completed install (extraction marker).""" """Whether ``dest`` holds a completed install (extraction marker)."""
return (dest / ".esphome_extracted").is_file() return (dest / ".esphome_extracted").is_file()
@@ -187,18 +195,18 @@ def prefetch_packages(
lock as ``install_package``: the archive's ``.part`` file is shared, and lock as ``install_package``: the archive's ``.part`` file is shared, and
two concurrent writers would truncate each other's bytes. two concurrent writers would truncate each other's bytes.
""" """
from filelock import FileLock from filelock import FileLock, Timeout
pending: list[_PendingArchive] = [] pending: list[_PendingArchive] = []
seen: set[str] = set() seen: set[Path] = set()
for name, version, dest, mirrors in packages: for name, version, dest, mirrors in packages:
if mirrors or (dest / ".esphome_extracted").is_file(): if mirrors or (dest / ".esphome_extracted").is_file():
continue continue
archive_name = f"{name}-{version}" archive = _archive_path(downloads_dir, name, version)
if archive_name in seen: if archive in seen:
# A duplicate entry would race itself between two workers # A duplicate entry would race itself between two workers
continue continue
seen.add(archive_name) seen.add(archive)
try: try:
url, sha256, size = registry_download(name, version) url, sha256, size = registry_download(name, version)
except EsphomeError as err: except EsphomeError as err:
@@ -207,10 +215,9 @@ def prefetch_packages(
continue continue
if not size: if not size:
continue continue
archive = downloads_dir / archive_name
if archive.is_file() and archive.stat().st_size == size: if archive.is_file() and archive.stat().st_size == size:
continue continue
pending.append(_PendingArchive(name, version, dest, url, sha256, size)) pending.append(_PendingArchive(name, version, dest, archive, url, sha256, size))
if len(pending) < 2: if len(pending) < 2:
return return
downloads_dir.mkdir(parents=True, exist_ok=True) downloads_dir.mkdir(parents=True, exist_ok=True)
@@ -222,20 +229,36 @@ def prefetch_packages(
def _fetch(entry: _PendingArchive, tracker: Callable[[int], None]) -> None: def _fetch(entry: _PendingArchive, tracker: Callable[[int], None]) -> None:
entry.dest.parent.mkdir(parents=True, exist_ok=True) entry.dest.parent.mkdir(parents=True, exist_ok=True)
with FileLock(f"{entry.dest}.lock", fallback_to_soft=False):
# Marker re-check: a concurrent build may have installed (and def on_disk() -> int:
# deleted the archive of) this package while we waited; if done := downloaded_bytes(entry.archive, entry.size):
# re-downloading would orphan a fresh copy in downloads_dir return done
# no branch: the thread tracer misses the skip edge; both # The holder deletes the archive once it has installed it
# arms of _already_installed are pinned directly return entry.size if _already_installed(entry.dest) else 0
if not _already_installed(entry.dest): # pragma: no branch
download_with_resume( lock = FileLock(f"{entry.dest}.lock", fallback_to_soft=False)
entry.url, try:
downloads_dir / f"{entry.name}-{entry.version}", wait_for_download_lock(lock, tracker, on_disk, entry.name)
sha256=entry.sha256, except Timeout:
size=entry.size, # install_package waits on this same lock and verifies the
progress=tracker, # holder's copy
) _LOGGER.debug("Leaving %s to its current downloader", entry.name)
return
try:
if _already_installed(entry.dest):
# A concurrent build installed it while we waited; a
# re-download would orphan a fresh copy in downloads_dir
tracker(entry.size)
return
download_with_resume(
entry.url,
entry.archive,
sha256=entry.sha256,
size=entry.size,
progress=tracker,
)
finally:
lock.release()
failures = run_batch_downloads( failures = run_batch_downloads(
"Downloading packages", "Downloading packages",
@@ -288,7 +311,7 @@ def install_package(
rmdir(dest, msg=f"Clean up incomplete {name} install") rmdir(dest, msg=f"Clean up incomplete {name} install")
# Persistent location so an interrupted download resumes across runs. # Persistent location so an interrupted download resumes across runs.
downloads_dir.mkdir(parents=True, exist_ok=True) downloads_dir.mkdir(parents=True, exist_ok=True)
archive = downloads_dir / f"{name}-{version}" archive = _archive_path(downloads_dir, name, version)
_LOGGER.info("Downloading %s %s ...", name, version) _LOGGER.info("Downloading %s %s ...", name, version)
if mirrors: if mirrors:
_LOGGER.warning( _LOGGER.warning(
+4 -4
View File
@@ -14,7 +14,7 @@ esptool==5.3.1
click==8.3.3 click==8.3.3
aioesphomeapi==46.3.0 aioesphomeapi==46.3.0
aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi
zeroconf==0.151.2 zeroconf==0.151.3
puremagic==2.2.0 puremagic==2.2.0
ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml==0.19.1 # dashboard_import
ruamel.yaml.clib==0.2.15 # dashboard_import ruamel.yaml.clib==0.2.15 # dashboard_import
@@ -27,9 +27,9 @@ bleak==3.0.2
smpclient==7.2.0 smpclient==7.2.0
requests==2.34.2 requests==2.34.2
py7zr==1.1.3 py7zr==1.1.3
platformdirs==4.11.5 # native esp-idf toolchain global cache dir platformdirs==4.11.7 # native esp-idf toolchain global cache dir
ninja==1.13.0 # native esp8266 arduino toolchain build driver ninja==1.13.2 # native esp8266 arduino toolchain build driver
filelock==3.32.4 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg filelock==3.32.5 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg
# esp-idf >= 5.0 requires this # esp-idf >= 5.0 requires this
pyparsing >= 3.3.2 pyparsing >= 3.3.2
+1 -1
View File
@@ -2,7 +2,7 @@ pylint==4.0.8
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.16.5 # also change in .pre-commit-config.yaml when updating ruff==0.16.5 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
prek==0.5.0 # also change in .github/workflows/ci.yml when updating prek==0.5.1 # also change in .github/workflows/ci.yml when updating
# Unit tests # Unit tests
pytest==9.1.1 pytest==9.1.1
@@ -0,0 +1,13 @@
esphome:
name: test
esp32:
variant: esp32
wifi:
ssid: MySSID
password: password1
# esp32_ble_server is only auto-loaded here, so it has no services of its own.
esp32_improv:
authorizer: none
@@ -0,0 +1,9 @@
esphome:
name: test
esp32:
variant: esp32
esp32_ble_server:
id: ble_server
manufacturer_data: [0x72, 0x04, 0x00, 0x23]
@@ -0,0 +1,14 @@
esphome:
name: test
esp32:
variant: esp32
esp32_ble_server:
id: ble_server
services:
- uuid: 2a24b789-7aab-4535-af3e-ee76a35cc12d
characteristics:
- uuid: cad48e28-7fbe-41cf-bae9-d77a6c233423
read: true
value: [1, 2, 3, 4]
@@ -1,5 +1,10 @@
"""Tests for esp32_ble_server configuration helpers.""" """Tests for esp32_ble_server configuration helpers."""
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
import pytest import pytest
from esphome.components.esp32_ble_server import ( from esphome.components.esp32_ble_server import (
@@ -45,3 +50,26 @@ def test_uuid_is_matches_descriptor_short_strings(uuid16) -> None:
assert uuid_is(uuid16, uuid16) assert uuid_is(uuid16, uuid16)
assert uuid_is(f"{uuid16:04X}", uuid16) assert uuid_is(f"{uuid16:04X}", uuid16)
assert uuid_is(f"{uuid16:08X}", uuid16) assert uuid_is(f"{uuid16:08X}", uuid16)
@pytest.mark.parametrize(
("config_file", "required"),
[
# Auto-loaded by esp32_improv only: nothing to find until Improv asks for it
("improv_only.yaml", False),
# The configuration defines a service clients are meant to connect to
("own_service.yaml", True),
# Manufacturer data is only useful if it is actually broadcast
("manufacturer_data_only.yaml", True),
],
)
def test_advertising_required(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
config_file: str,
required: bool,
) -> None:
"""The server only requests advertising when the configuration needs it."""
main_cpp = generate_main(component_config_path(config_file))
assert f"set_advertising_required({str(required).lower()})" in main_cpp

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